Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1e1fedc3f | ||
|
|
84dc631d80 | ||
|
|
6a3072fe04 | ||
|
|
98d943f39d | ||
|
|
bcf724273d | ||
|
|
8703918a8c | ||
|
|
7e88f3ba29 | ||
|
|
d9a2f221db | ||
|
|
a4b3a257ed | ||
|
|
10397d0847 | ||
|
|
db442b2746 | ||
|
|
8cb3c69b78 | ||
|
|
967f21cc23 | ||
|
|
3ecd73f430 | ||
|
|
ca7f6b86d7 | ||
|
|
00cee81812 | ||
|
|
25f5db82dc | ||
|
|
9e5c5d5e69 | ||
|
|
2cfd1784d8 | ||
|
|
bec1b95ad3 | ||
|
|
e26f500ad0 | ||
|
|
9c33f007a1 | ||
|
|
902256d461 | ||
|
|
6051bd6d06 | ||
|
|
c82f4e50d2 | ||
|
|
94e785c75c | ||
|
|
8b8daa7b4c | ||
|
|
c95e63014f | ||
|
|
32bf42cbb9 | ||
|
|
175ec98947 | ||
|
|
0abd9343a9 | ||
|
|
c9976382a9 | ||
|
|
d38e93ac7e | ||
|
|
e51f1d20c0 | ||
|
|
df595f4835 | ||
|
|
63e4d2f686 | ||
|
|
971580def8 | ||
|
|
ffd32426b5 | ||
|
|
d2d26cc822 | ||
|
|
a373b0b6eb | ||
|
|
f515fa1443 | ||
|
|
e32e83d45e | ||
|
|
7be3cdeb65 | ||
|
|
b234b9166d | ||
|
|
2c485b5efb | ||
|
|
b7d7e1a1af | ||
|
|
01be6ae70a | ||
|
|
445eaadac3 | ||
|
|
d5b1dfddee | ||
|
|
c68ea04f06 | ||
|
|
9abc30b60c | ||
|
|
1f7561298c | ||
|
|
611c5757e0 | ||
|
|
ab56e82173 | ||
|
|
34350fadb6 | ||
|
|
77786da53f | ||
|
|
f794ca5426 | ||
|
|
a2010e6d1d | ||
|
|
4ce6e9bfd7 | ||
|
|
9a3794073b | ||
|
|
d6197d6d21 | ||
|
|
1f321cf6bc | ||
|
|
5c6d3f4078 | ||
|
|
6b8b95e4ca | ||
|
|
ae08d48641 | ||
|
|
d1ce5566cf | ||
|
|
5f027ebc79 | ||
|
|
8cf83f8338 | ||
|
|
b96e2c1fe0 | ||
|
|
4ad1379773 | ||
|
|
ef0883f732 | ||
|
|
a2076b4e2d | ||
|
|
0a3998530e | ||
|
|
ed2ec56a44 | ||
|
|
87473bdf92 | ||
|
|
8186a6841a | ||
|
|
0a0b5b6612 | ||
|
|
72704f9dc9 | ||
|
|
06ad23d904 | ||
|
|
fbd1c55f44 | ||
|
|
9668a04a1a | ||
|
|
24af375a8e | ||
|
|
a32c973ab8 | ||
|
|
50beb913de | ||
|
|
05f1ec7b34 | ||
|
|
9271b107b6 | ||
|
|
e7208dd7d2 | ||
|
|
e5dfb34082 | ||
|
|
2ba5c4e706 | ||
|
|
27bcc5f4f8 | ||
|
|
d884bd539b | ||
|
|
580a56727c | ||
|
|
ac3163d061 | ||
|
|
8bc7a6c3e1 | ||
|
|
31d368979e |
6
.github/FUNDING.yml
vendored
@@ -1 +1,5 @@
|
||||
github: clash-verge-rev
|
||||
custom:
|
||||
[
|
||||
"https://t.me/tribute/app?startapp=dtfk",
|
||||
"https://t.me/tribute/app?startapp=dtLE",
|
||||
]
|
||||
|
||||
3
.github/workflows/autobuild.yml
vendored
@@ -193,6 +193,9 @@ jobs:
|
||||
- os: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_NET_RETRY: "5"
|
||||
CARGO_HTTP_CHECK_REVOKE: "false"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
3
.github/workflows/clippy.yml
vendored
@@ -17,6 +17,9 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_NET_RETRY: "5"
|
||||
CARGO_HTTP_CHECK_REVOKE: "false"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
3
.github/workflows/dev.yml
vendored
@@ -28,6 +28,9 @@ jobs:
|
||||
bundle: dmg
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_NET_RETRY: "5"
|
||||
CARGO_HTTP_CHECK_REVOKE: "false"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
3
.github/workflows/fmt.yml
vendored
@@ -10,6 +10,9 @@ on:
|
||||
jobs:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_NET_RETRY: "5"
|
||||
CARGO_HTTP_CHECK_REVOKE: "false"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
254
.github/workflows/release.yml
vendored
@@ -40,9 +40,91 @@ jobs:
|
||||
fi
|
||||
echo "Tag and package.json version are consistent."
|
||||
|
||||
create_release_notes:
|
||||
name: Create Release Notes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: |
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
if [ -n "$UPDATE_LOGS" ]; then
|
||||
echo "Found update logs"
|
||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No update sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Get Version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
|
||||
|
||||
- run: |
|
||||
if [ -z "$UPDATE_LOGS" ]; then
|
||||
echo "No update logs found, using default message"
|
||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found update logs"
|
||||
fi
|
||||
|
||||
cat > release.txt << EOF
|
||||
$UPDATE_LOGS
|
||||
|
||||
## Which version should I download?
|
||||
|
||||
### macOS
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_aarch64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Apple%20Silicon"></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**
|
||||
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>
|
||||
|
||||
### Linux
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.x86_64.rpm"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=fedora&label=RPM"> </a>
|
||||
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64.deb"><img src="https://img.shields.io/badge/arm64-default?style=flat&logo=debian&label=DEB"> </a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.aarch64.rpm"><img src="https://img.shields.io/badge/aarch64-default?style=flat&logo=fedora&label=RPM"> </a>
|
||||
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_armhf.deb"><img src="https://img.shields.io/badge/armhf-default?style=flat&logo=debian&label=DEB"> </a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.armhfp.rpm"><img src="https://img.shields.io/badge/armhfp-default?style=flat&logo=fedora&label=RPM"> </a>
|
||||
|
||||
### Windows (Win7 is no longer supported)
|
||||
#### Normal version (recommended)
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
|
||||
#### Portable version is no longer available with many problems
|
||||
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64_fixed_webview2-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_arm64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{env.VERSION}}
|
||||
name: "Koala Clash v${{env.VERSION}}"
|
||||
body_path: release.txt
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release:
|
||||
name: Release Build
|
||||
needs: check_tag_version
|
||||
needs: [check_tag_version, create_release_notes]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -96,20 +178,77 @@ jobs:
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Create .p8 file
|
||||
run: |
|
||||
mkdir -p ~/.appstoreconnect/private_keys
|
||||
echo "${{ secrets.APPLE_API_KEY_CONTENT }}" > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8
|
||||
|
||||
- name: Tauri build
|
||||
id: build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_PATH: "~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8"
|
||||
with:
|
||||
tagName: v__VERSION__
|
||||
releaseName: "Clash Verge Rev Lite v__VERSION__"
|
||||
releaseBody: "More new features are now supported."
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename Artifact (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
- name: Rename Artifact (Linux/macOS)
|
||||
if: runner.os == 'Linux' || runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
|
||||
|
||||
if [ ! -d "$TARGET_DIR" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
|
||||
dir_path=$(dirname "$old_path")
|
||||
old_filename=$(basename "$old_path")
|
||||
new_filename=$(echo "$old_filename" \
|
||||
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
|
||||
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
|
||||
)
|
||||
new_path="${dir_path}/${new_filename}"
|
||||
if [ "$old_path" != "$new_path" ]; then
|
||||
echo " - '$old_filename' -> '$new_filename'"
|
||||
mv "$old_path" "$new_path"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{steps.build.outputs.appVersion}}
|
||||
name: "Koala Clash v${{steps.build.outputs.appVersion}}"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig
|
||||
|
||||
release-for-linux-arm:
|
||||
name: Release Build for Linux ARM
|
||||
strategy:
|
||||
@@ -219,14 +358,36 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
|
||||
|
||||
- name: Rename
|
||||
shell: bash
|
||||
run: |
|
||||
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
|
||||
|
||||
if [ ! -d "$TARGET_DIR" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
|
||||
dir_path=$(dirname "$old_path")
|
||||
old_filename=$(basename "$old_path")
|
||||
new_filename=$(echo "$old_filename" \
|
||||
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
|
||||
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
|
||||
)
|
||||
new_path="${dir_path}/${new_filename}"
|
||||
if [ "$old_path" != "$new_path" ]; then
|
||||
echo " - '$old_filename' -> '$new_filename'"
|
||||
mv "$old_path" "$new_path"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{env.VERSION}}
|
||||
name: "Clash Verge Rev Lite v${{env.VERSION}}"
|
||||
body: "More new features are now supported."
|
||||
name: "Koala Clash v${{env.VERSION}}"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
@@ -275,8 +436,8 @@ jobs:
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
|
||||
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
|
||||
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
|
||||
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
|
||||
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||
|
||||
@@ -296,19 +457,19 @@ jobs:
|
||||
run: |
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
@@ -316,8 +477,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{steps.build.outputs.appVersion}}
|
||||
name: "Clash Verge Rev Lite v${{steps.build.outputs.appVersion}}"
|
||||
body: "More new features are now supported."
|
||||
name: "Koala Clash v${{steps.build.outputs.appVersion}}"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
|
||||
@@ -376,3 +536,69 @@ jobs:
|
||||
run: pnpm updater-fixed-webview2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
push-notify-to-telegram:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-update, release-update-for-fixed-webview2]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: |
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
if [ -n "$UPDATE_LOGS" ]; then
|
||||
echo "Found update logs"
|
||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No update sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Get Version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
|
||||
|
||||
- run: |
|
||||
if [ -z "$UPDATE_LOGS" ]; then
|
||||
echo "No update logs found, using default message"
|
||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found update logs"
|
||||
UPDATE_LOGS=$(echo "$UPDATE_LOGS" | sed 's/^## \(v.*\)/\*\1\*/')
|
||||
fi
|
||||
|
||||
cat > release.txt << EOF
|
||||
Вышло обновление!
|
||||
|
||||
$UPDATE_LOGS
|
||||
|
||||
[Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest)
|
||||
|
||||
EOF
|
||||
|
||||
- name: notify to channel
|
||||
uses: appleboy/telegram-action@master
|
||||
with:
|
||||
to: ${{ secrets.TELEGRAM_TO_CHANNEL }}
|
||||
token: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
message_file: release.txt
|
||||
format: markdown
|
||||
|
||||
- name: notify to group
|
||||
uses: appleboy/telegram-action@master
|
||||
with:
|
||||
to: ${{ secrets.TELEGRAM_TO_GROUP }}
|
||||
token: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
message_file: release.txt
|
||||
format: markdown
|
||||
|
||||
1
.gitignore
vendored
@@ -10,3 +10,4 @@ scripts/_env.sh
|
||||
.tool-versions
|
||||
.idea
|
||||
.old
|
||||
bun.lock
|
||||
58
UPDATELOG.md
@@ -1,3 +1,61 @@
|
||||
## v0.2.7
|
||||
- fixed bug in proxy groups menu
|
||||
- added message about global mode enabled on main screen
|
||||
- fixed minor bugs
|
||||
- updated Mihomo core to v1.19.14
|
||||
|
||||
## v0.2.6
|
||||
|
||||
- fixed deep links
|
||||
- removed AliDNS, replaced with Cloudflare and Google DNS servers
|
||||
- improved proxy selector view
|
||||
- added some animations
|
||||
- fixed an issue with saving the profile when changing advanced settings
|
||||
- fixed DNS leak, strict routing now default
|
||||
- logs translated into English
|
||||
- table on the connections page corrected
|
||||
- fixed issue with deleting profiles with long names
|
||||
- glass effect added to components
|
||||
- icon background fixed
|
||||
- fixed tun settings override
|
||||
- added support for brotli, gzip, zstd
|
||||
|
||||
## v0.2.5
|
||||
|
||||
- new main page
|
||||
- fixed issue with opening via shortcut
|
||||
- fixed logo in sidebar
|
||||
- fixed issue with changing tray settings
|
||||
- name changed to koala clash
|
||||
- added signing for installer on macOS
|
||||
|
||||
## v0.2.4
|
||||
|
||||
- added auto-scaling and scaling via key combination
|
||||
- direct was removed, and the translation for rules and global was replaced
|
||||
- added icons for proxy groups on main page
|
||||
- fixed log color when dark theme is enabled
|
||||
- the alphabetical index has been removed, and additional information about proxies is now hidden by default
|
||||
- notification of exceeding the number of devices in the subscription
|
||||
- support for vless:// links with templates by @legiz-ru
|
||||
- started the process of renaming to Koala Clash, replaced icons
|
||||
- traffic information has been reworked on profile page
|
||||
|
||||
## v0.2.3
|
||||
|
||||
- fixed problem with profile inactivation after adding via deeplink on windows
|
||||
- corrected layout on the proxy page, now all cards are the same size
|
||||
- corrected announe transposition by \n
|
||||
- corrected side menu in compressed window
|
||||
- added check at the main toggle switch, now it cannot be enabled if there are no profiles.
|
||||
|
||||
## v0.2.1
|
||||
|
||||
- added headers "announce-url", "update-always"
|
||||
- added a check for the presence of a profile, if it already exists, an update will be performed
|
||||
- fixed processing of links for displaying telegram icon on the main page
|
||||
- added profile update button on the main page
|
||||
|
||||
## v0.2
|
||||
|
||||
- added handlers for "Announe", "Support-Url", "New-Sub-Domain", "Profile-Title" headers:
|
||||
|
||||
21
hooks/use-mobile.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
102
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "0.2.0",
|
||||
"name": "koala-clash",
|
||||
"version": "0.2.7",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
@@ -29,53 +29,55 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/lab": "7.0.0-beta.13",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-data-grid": "^8.5.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@mui/x-data-grid": "^8.11.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tauri-apps/api": "2.5.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
||||
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2.4.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.1",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.1",
|
||||
"@tauri-apps/plugin-updater": "2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@tauri-apps/plugin-window-state": "^2.4.0",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.5",
|
||||
"axios": "^1.9.0",
|
||||
"chart.js": "^4.4.9",
|
||||
"ahooks": "^3.9.5",
|
||||
"axios": "^1.12.2",
|
||||
"chart.js": "^4.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cli-color": "^2.0.4",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.45",
|
||||
"glob": "^11.0.2",
|
||||
"i18next": "^25.2.1",
|
||||
"js-base64": "^3.7.7",
|
||||
"foxact": "^0.2.49",
|
||||
"framer-motion": "^12.23.16",
|
||||
"glob": "^11.0.3",
|
||||
"i18next": "^25.5.2",
|
||||
"js-base64": "^3.7.8",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.514.0",
|
||||
@@ -83,26 +85,26 @@
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"peggy": "^5.0.3",
|
||||
"peggy": "^5.0.6",
|
||||
"react": "19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-hook-form": "^7.63.0",
|
||||
"react-i18next": "15.5.2",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.58.0",
|
||||
"react-router-dom": "7.6.2",
|
||||
"react-virtuoso": "^4.12.8",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"sockette": "^2.0.6",
|
||||
"sonner": "^2.0.5",
|
||||
"swr": "^2.3.3",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zod": "^3.25.67",
|
||||
"zustand": "^5.0.5"
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
@@ -110,30 +112,30 @@
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "19.1.6",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@vitejs/plugin-legacy": "^6.1.1",
|
||||
"@vitejs/plugin-react": "4.5.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"commander": "^14.0.0",
|
||||
"commander": "^14.0.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"meta-json-schema": "^1.19.10",
|
||||
"meta-json-schema": "^1.19.13",
|
||||
"node-fetch": "^3.3.2",
|
||||
"postcss": "^8.5.4",
|
||||
"prettier": "^3.5.3",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"sass": "^1.89.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"terser": "^5.41.0",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"sass": "^1.93.0",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"terser": "^5.44.0",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
@@ -143,4 +145,4 @@
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.13.2"
|
||||
}
|
||||
}
|
||||
2684
pnpm-lock.yaml
generated
@@ -42,16 +42,16 @@ async function resolvePortable() {
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "Koala Clash.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
|
||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
zip.addLocalFolder(
|
||||
path.join(
|
||||
releaseDir,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
||||
),
|
||||
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
||||
);
|
||||
zip.addLocalFolder(configDir, ".config");
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ async function resolvePortable() {
|
||||
}
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "clash-verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-clash.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
|
||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
zip.addLocalFolder(configDir, ".config");
|
||||
|
||||
|
||||
@@ -175,8 +175,8 @@ function clashMetaAlpha() {
|
||||
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo-alpha",
|
||||
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
name: "koala-mihomo-alpha",
|
||||
targetFile: `koala-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
@@ -192,8 +192,8 @@ function clashMeta() {
|
||||
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo",
|
||||
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
name: "koala-mihomo",
|
||||
targetFile: `koala-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
@@ -381,7 +381,7 @@ const resolvePlugin = async () => {
|
||||
// service chmod
|
||||
const resolveServicePermission = async () => {
|
||||
const serviceExecutables = [
|
||||
"clash-verge-service*",
|
||||
"koala-clash-service*",
|
||||
"install-service*",
|
||||
"uninstall-service*",
|
||||
];
|
||||
@@ -429,14 +429,14 @@ async function resolveLocales() {
|
||||
/**
|
||||
* main
|
||||
*/
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
|
||||
const SERVICE_URL = `https://github.com/coolcoala/koala-clash-service/releases/download/${SIDECAR_HOST}`;
|
||||
|
||||
const resolveService = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
resolveResource({
|
||||
file: "clash-verge-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||
file: "koala-clash-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/koala-clash-service${ext}`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -489,13 +489,13 @@ const resolveWinSysproxy = () =>
|
||||
const tasks = [
|
||||
// { name: "clash", func: resolveClash, retry: 5 },
|
||||
{
|
||||
name: "verge-mihomo-alpha",
|
||||
name: "koala-mihomo-alpha",
|
||||
func: () =>
|
||||
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||
retry: 5,
|
||||
},
|
||||
{
|
||||
name: "verge-mihomo",
|
||||
name: "koala-mihomo",
|
||||
func: () =>
|
||||
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
|
||||
retry: 5,
|
||||
|
||||
2131
src-tauri/Cargo.lock
generated
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "0.2.0"
|
||||
description = "clash verge"
|
||||
name = "koala-clash"
|
||||
version = "0.2.7"
|
||||
description = "koala clash"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/coolcoala/clash-verge-rev-lite.git"
|
||||
default-run = "clash-verge"
|
||||
default-run = "koala-clash"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
identifier = "io.github.koala-clash"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.3.0", features = [] }
|
||||
@@ -18,7 +18,7 @@ tauri-build = { version = "2.3.0", features = [] }
|
||||
[dependencies]
|
||||
url = "2.5.4"
|
||||
os_info = "3.0"
|
||||
machine-uid = "0.2"
|
||||
machine-uid = "0.5.3"
|
||||
warp = "0.3.7"
|
||||
anyhow = "1.0.98"
|
||||
dirs = "6.0"
|
||||
@@ -28,7 +28,7 @@ dunce = "1.0.5"
|
||||
log4rs = "1.3.0"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.41"
|
||||
sysinfo = "0.35.2"
|
||||
sysinfo = "0.36.1"
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34-deprecated"
|
||||
@@ -45,7 +45,7 @@ tokio = { version = "1.45.1", features = [
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
|
||||
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies", "brotli", "gzip", "zstd"] }
|
||||
regex = "1.11.1"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
image = "0.25.6"
|
||||
@@ -63,7 +63,6 @@ tauri-plugin-dialog = "2.3.0"
|
||||
tauri-plugin-fs = "2.4.0"
|
||||
tauri-plugin-process = "2.3.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.0"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-window-state = "2.3.0"
|
||||
zip = "4.2.0"
|
||||
@@ -85,6 +84,7 @@ sha2 = "0.10.9"
|
||||
hex = "0.4.3"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.3.0"
|
||||
tauri-plugin-deep-link = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
@@ -110,6 +110,7 @@ users = "0.11.0"
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
tauri-plugin-global-shortcut = "2.3.0"
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"core:window:allow-set-theme",
|
||||
"notification:default"
|
||||
"notification:default",
|
||||
"core:webview:allow-set-webview-zoom"
|
||||
]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
chmod +x /usr/bin/install-service
|
||||
chmod +x /usr/bin/uninstall-service
|
||||
chmod +x /usr/bin/clash-verge-service
|
||||
chmod +x /usr/bin/koala-clash-service
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<false/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>io.github.clash-verge-rev.clash-verge-rev</string>
|
||||
<string>io.github.koala-clash</string>
|
||||
</array>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
|
||||
@@ -427,52 +427,52 @@ Function .onInit
|
||||
!endif
|
||||
FunctionEnd
|
||||
|
||||
!macro CheckAllVergeProcesses
|
||||
; Check if clash-verge-service.exe is running
|
||||
!macro CheckAllKoalaProcesses
|
||||
; Check if koala-clash-service.exe is running
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "clash-verge-service.exe"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "koala-clash-service.exe"
|
||||
!else
|
||||
nsis_tauri_utils::FindProcess "clash-verge-service.exe"
|
||||
nsis_tauri_utils::FindProcess "koala-clash-service.exe"
|
||||
!endif
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
DetailPrint "Kill clash-verge-service.exe..."
|
||||
DetailPrint "Kill koala-clash-service.exe..."
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "koala-clash-service.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "clash-verge-service.exe"
|
||||
nsis_tauri_utils::KillProcess "koala-clash-service.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
; Check if verge-mihomo-alpha.exe is running
|
||||
; Check if koala-mihomo-alpha.exe is running
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo-alpha.exe"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo-alpha.exe"
|
||||
!else
|
||||
nsis_tauri_utils::FindProcess "verge-mihomo-alpha.exe"
|
||||
nsis_tauri_utils::FindProcess "koala-mihomo-alpha.exe"
|
||||
!endif
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
DetailPrint "Kill verge-mihomo-alpha.exe..."
|
||||
DetailPrint "Kill koala-mihomo-alpha.exe..."
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo-alpha.exe"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo-alpha.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "verge-mihomo-alpha.exe"
|
||||
nsis_tauri_utils::KillProcess "koala-mihomo-alpha.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
; Check if verge-mihomo.exe is running
|
||||
; Check if koala-mihomo.exe is running
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo.exe"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo.exe"
|
||||
!else
|
||||
nsis_tauri_utils::FindProcess "verge-mihomo.exe"
|
||||
nsis_tauri_utils::FindProcess "koala-mihomo.exe"
|
||||
!endif
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
DetailPrint "Kill verge-mihomo.exe..."
|
||||
DetailPrint "Kill koala-mihomo.exe..."
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo.exe"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "verge-mihomo.exe"
|
||||
nsis_tauri_utils::KillProcess "koala-mihomo.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
@@ -509,22 +509,22 @@ FunctionEnd
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro StartVergeService
|
||||
!macro StartKoalaService
|
||||
; Check if the service exists
|
||||
SimpleSC::ExistsService "clash_verge_service"
|
||||
SimpleSC::ExistsService "koala_clash_service"
|
||||
Pop $0 ; 0:service exists;other: service not exists
|
||||
; Service exists
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
; Check if the service is running
|
||||
SimpleSC::ServiceIsRunning "clash_verge_service"
|
||||
SimpleSC::ServiceIsRunning "koala_clash_service"
|
||||
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
${If} $1 == 0
|
||||
DetailPrint "Restart Clash Verge Service..."
|
||||
SimpleSC::StartService "clash_verge_service" "" 30
|
||||
DetailPrint "Restart Koala Clash Service..."
|
||||
SimpleSC::StartService "koala_clash_service" "" 30
|
||||
${EndIf}
|
||||
${ElseIf} $0 != 0
|
||||
Push $0
|
||||
@@ -535,35 +535,35 @@ FunctionEnd
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro RemoveVergeService
|
||||
!macro RemoveKoalaService
|
||||
; Check if the service exists
|
||||
SimpleSC::ExistsService "clash_verge_service"
|
||||
SimpleSC::ExistsService "koala_clash_service"
|
||||
Pop $0 ; 0:service exists;other: service not exists
|
||||
; Service exists
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
; Check if the service is running
|
||||
SimpleSC::ServiceIsRunning "clash_verge_service"
|
||||
SimpleSC::ServiceIsRunning "koala_clash_service"
|
||||
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
${If} $1 == 1
|
||||
DetailPrint "Stop Clash Verge Service..."
|
||||
SimpleSC::StopService "clash_verge_service" 1 30
|
||||
DetailPrint "Stop Koala Clash Service..."
|
||||
SimpleSC::StopService "koala_clash_service" 1 30
|
||||
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||
${If} $0 == 0
|
||||
DetailPrint "Removing Clash Verge Service..."
|
||||
SimpleSC::RemoveService "clash_verge_service"
|
||||
DetailPrint "Removing Koala Clash Service..."
|
||||
SimpleSC::RemoveService "koala_clash_service"
|
||||
${ElseIf} $0 != 0
|
||||
Push $0
|
||||
SimpleSC::GetErrorMessage
|
||||
Pop $0
|
||||
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
|
||||
MessageBox MB_OK|MB_ICONSTOP "Koala Clash Service Stop Error ($0)"
|
||||
${EndIf}
|
||||
${ElseIf} $1 == 0
|
||||
DetailPrint "Removing Clash Verge Service..."
|
||||
SimpleSC::RemoveService "clash_verge_service"
|
||||
DetailPrint "Removing Koala Clash Service..."
|
||||
SimpleSC::RemoveService "koala_clash_service"
|
||||
${EndIf}
|
||||
${ElseIf} $0 != 0
|
||||
Push $0
|
||||
@@ -764,7 +764,7 @@ Section Install
|
||||
SetOutPath $INSTDIR
|
||||
nsExec::Exec 'netsh int tcp res'
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
!insertmacro CheckAllKoalaProcesses
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
@@ -772,32 +772,32 @@ Section Install
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
; 清理旧版本的注册表项 (Koala Clash)
|
||||
ReadRegStr $R2 HKCU "$R1" "Koala Clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
DeleteRegValue HKCU "$R1" "Koala Clash"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
ReadRegStr $R2 HKLM "$R1" "Koala Clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
DeleteRegValue HKLM "$R1" "Koala Clash"
|
||||
${EndIf}
|
||||
|
||||
; 清理新版本的注册表项 (clash-verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "clash-verge"
|
||||
; 清理新版本的注册表项 (koala-clash)
|
||||
ReadRegStr $R2 HKCU "$R1" "koala-clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
DeleteRegValue HKCU "$R1" "koala-clash"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
ReadRegStr $R2 HKLM "$R1" "koala-clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
DeleteRegValue HKLM "$R1" "koala-clash"
|
||||
${EndIf}
|
||||
|
||||
; Delete old files before installation
|
||||
; Delete clash-verge.desktop
|
||||
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
|
||||
Delete "$INSTDIR\Clash Verge.exe"
|
||||
; Delete koala-clash.desktop
|
||||
IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2
|
||||
Delete "$INSTDIR\Koala Clash.exe"
|
||||
|
||||
; Copy main executable
|
||||
File "${MAINBINARYSRCPATH}"
|
||||
@@ -815,7 +815,7 @@ Section Install
|
||||
File /a "/oname={{this}}" "{{@key}}"
|
||||
{{/each}}
|
||||
|
||||
!insertmacro StartVergeService
|
||||
!insertmacro StartKoalaService
|
||||
|
||||
; Create uninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
@@ -918,11 +918,11 @@ FunctionEnd
|
||||
Section Uninstall
|
||||
;删除 window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
Delete "$APPDATA\io.github.koala-clash\window-state.json"
|
||||
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
!insertmacro RemoveVergeService
|
||||
!insertmacro CheckAllKoalaProcesses
|
||||
!insertmacro RemoveKoalaService
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
@@ -930,26 +930,26 @@ Section Uninstall
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
; 清理旧版本的注册表项 (Koala Clash)
|
||||
ReadRegStr $R2 HKCU "$R1" "Koala Clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
DeleteRegValue HKCU "$R1" "Koala Clash"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
ReadRegStr $R2 HKLM "$R1" "Koala Clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
DeleteRegValue HKLM "$R1" "Koala Clash"
|
||||
${EndIf}
|
||||
|
||||
; 清理新版本的注册表项 (clash-verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "clash-verge"
|
||||
; 清理新版本的注册表项 (koala-clash)
|
||||
ReadRegStr $R2 HKCU "$R1" "koala-clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
DeleteRegValue HKCU "$R1" "koala-clash"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
ReadRegStr $R2 HKLM "$R1" "koala-clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
DeleteRegValue HKLM "$R1" "koala-clash"
|
||||
${EndIf}
|
||||
|
||||
; Delete the app directory and its content from disk
|
||||
@@ -966,9 +966,9 @@ Section Uninstall
|
||||
Delete "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
|
||||
; Delete clash-verge.desktop
|
||||
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
|
||||
Delete "$INSTDIR\Clash Verge.exe"
|
||||
; Delete koala-clash.desktop
|
||||
IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2
|
||||
Delete "$INSTDIR\Koala Clash.exe"
|
||||
|
||||
; Delete uninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
@@ -982,20 +982,20 @@ Section Uninstall
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\koala-clash.lnk"
|
||||
|
||||
; Remove start menu shortcut
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk"
|
||||
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
|
||||
; Remove desktop shortcuts
|
||||
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
Delete "$DESKTOP\clash-verge.lnk"
|
||||
Delete "$DESKTOP\koala-clash.lnk"
|
||||
|
||||
; Remove registry information for add/remove programs
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
@@ -1017,7 +1017,7 @@ Section Uninstall
|
||||
|
||||
;删除 window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
Delete "$APPDATA\io.github.koala-clash\window-state.json"
|
||||
|
||||
${GetOptions} $CMDLINE "/P" $R0
|
||||
IfErrors +2 0
|
||||
|
||||
@@ -147,7 +147,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
Err(format!("下载的内容不是有效图片: {url}"))
|
||||
Err(format!("Downloaded content is not a valid image: {url}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,15 +209,17 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
/// 通知UI已准备就绪
|
||||
#[tauri::command]
|
||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
log::info!(target: "app", "前端UI已准备就绪");
|
||||
log::info!(target: "app", "Frontend UI is 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(())
|
||||
}
|
||||
|
||||
/// UI加载阶段
|
||||
#[tauri::command]
|
||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
log::info!(target: "app", "UI加载阶段更新: {stage}");
|
||||
log::info!(target: "app", "UI loading stage updated: {stage}");
|
||||
|
||||
use crate::utils::resolve::UiReadyStage;
|
||||
|
||||
@@ -228,8 +230,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
||||
"Ready" => UiReadyStage::Ready,
|
||||
_ => {
|
||||
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
|
||||
return Err(format!("未知的UI加载阶段: {stage}"));
|
||||
log::warn!(target: "app", "Unknown UI loading stage: {stage}");
|
||||
return Err(format!("Unknown UI loading stage: {stage}"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -240,7 +242,7 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
/// 重置UI就绪状态
|
||||
#[tauri::command]
|
||||
pub fn reset_ui_ready_state() -> CmdResult<()> {
|
||||
log::info!(target: "app", "重置UI就绪状态");
|
||||
log::info!(target: "app", "Reset UI ready state");
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde_yaml::Mapping;
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
log::debug!(target: "app", "异步获取系统代理配置");
|
||||
log::debug!(target: "app", "Asynchronously getting system proxy configuration");
|
||||
|
||||
let current = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
@@ -19,14 +19,14 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
|
||||
log::debug!(target: "app", "Return system proxy configuration: enable={}, {}:{}", current.enable, current.host, current.port);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 获取自动代理配置
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
|
||||
log::debug!(target: "app", "Start retrieving auto proxy configuration (event-driven)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
@@ -40,7 +40,7 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.clone().into());
|
||||
|
||||
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
|
||||
log::debug!(target: "app", "Return auto proxy configuration (cached): enable={}, url={}", current.enable, current.url);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
state.proxies = Box::new(proxies);
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "proxies刷新成功");
|
||||
log::debug!(target: "app", "Proxies refreshed successfully");
|
||||
}
|
||||
|
||||
let proxies = {
|
||||
@@ -50,7 +50,7 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
log::debug!(target: "app", "强制刷新代理缓存");
|
||||
log::debug!(target: "app", "Force refresh proxy cache");
|
||||
|
||||
let proxies = manager.get_refresh_proxies().await?;
|
||||
|
||||
@@ -61,7 +61,7 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
state.last_refresh_time = Instant::now();
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "强制刷新代理缓存完成");
|
||||
log::debug!(target: "app", "Force refresh proxy cache completed");
|
||||
Ok(proxies)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
state.providers_proxies = Box::new(providers);
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "providers_proxies刷新成功");
|
||||
log::debug!(target: "app", "providers_proxies refreshed successfully");
|
||||
}
|
||||
|
||||
let providers_proxies = {
|
||||
|
||||
@@ -84,7 +84,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
// 发送合并文件专用错误通知
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "Merge config file");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -133,17 +133,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
{
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||
log::info!(target: "app", "[cmd config save] YAML config file validation failed, sending notification");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML config file");
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
|
||||
log::info!(target: "app", "[cmd config save] Script file validation failed, sending notification");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||
log::info!(target: "app", "[cmd config save] Other validation failure type, sending general notification");
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 验证过程发生错误: {}",
|
||||
"[cmd config save] Error occurred during validation: {}",
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"{} 验证失败: {}",
|
||||
"{} validation failed: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
@@ -43,14 +43,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
/// 验证指定脚本文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
|
||||
logging!(info, Type::Config, true, "Validating script file: {}", file_path);
|
||||
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path, None)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
handle_script_validation_notice(&result, "脚本文件");
|
||||
handle_script_validation_notice(&result, "Script file");
|
||||
Ok(result.0) // 返回验证结果布尔值
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -129,7 +129,7 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[通知] 发送通知: status={}, msg={}",
|
||||
"[Notice] Sending notice: status={}, msg={}",
|
||||
status,
|
||||
error_msg
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ impl IClashTemp {
|
||||
tun.insert("enable".into(), false.into());
|
||||
tun.insert("stack".into(), "gvisor".into());
|
||||
tun.insert("auto-route".into(), true.into());
|
||||
tun.insert("strict-route".into(), false.into());
|
||||
tun.insert("strict-route".into(), true.into());
|
||||
tun.insert("auto-detect-interface".into(), true.into());
|
||||
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -129,7 +129,7 @@ impl IClashTemp {
|
||||
help::save_yaml(
|
||||
&dirs::clash_path()?,
|
||||
&self.0,
|
||||
Some("# Generated by Clash Verge"),
|
||||
Some("# Generated by Koala Clash"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ use once_cell::sync::OnceCell;
|
||||
use std::path::PathBuf;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
pub const RUNTIME_CONFIG: &str = "koala-clash.yaml";
|
||||
pub const CHECK_CONFIG: &str = "koala-clash-check.yaml";
|
||||
|
||||
pub struct Config {
|
||||
clash_config: Draft<Box<IClashTemp>>,
|
||||
@@ -69,9 +69,9 @@ impl Config {
|
||||
}
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate().await {
|
||||
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
|
||||
logging!(error, Type::Config, true, "Failed to generate runtime config: {}", err);
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "生成运行时配置成功");
|
||||
logging!(info, Type::Config, true, "Runtime config generated successfully");
|
||||
}
|
||||
|
||||
// 生成运行时配置文件并验证
|
||||
@@ -79,7 +79,7 @@ impl Config {
|
||||
|
||||
let validation_result = if config_result.is_ok() {
|
||||
// 验证配置文件
|
||||
logging!(info, Type::Config, true, "开始验证配置");
|
||||
logging!(info, Type::Config, true, "Starting config validation");
|
||||
|
||||
match CoreManager::global().validate_config().await {
|
||||
Ok((is_valid, error_msg)) => {
|
||||
@@ -88,7 +88,7 @@ impl Config {
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
||||
"[First launch] Config validation failed, starting with minimal default config: {}",
|
||||
error_msg
|
||||
);
|
||||
CoreManager::global()
|
||||
@@ -96,12 +96,12 @@ impl Config {
|
||||
.await?;
|
||||
Some(("config_validate::boot_error", error_msg))
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "配置验证成功");
|
||||
logging!(info, Type::Config, true, "Config validation succeeded");
|
||||
Some(("config_validate::success", String::new()))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
||||
logging!(warn, Type::Config, true, "Validation process execution failed: {}", err);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::process_terminated", "")
|
||||
.await?;
|
||||
@@ -109,7 +109,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||
logging!(warn, Type::Config, true, "Failed to generate config file; using default config");
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
@@ -141,7 +141,7 @@ impl Config {
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("failed to get runtime config"))?;
|
||||
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
|
||||
help::save_yaml(&path, &config, Some("# Generated by Koala Clash"))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ macro_rules! draft_define {
|
||||
|
||||
impl Draft<Box<$id>> {
|
||||
#[allow(unused)]
|
||||
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
pub fn data(&self) -> MappedMutexGuard<'_, Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||
}
|
||||
|
||||
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
pub fn latest(&self) -> MappedMutexGuard<'_, Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
&mut inner.0
|
||||
@@ -33,7 +33,7 @@ macro_rules! draft_define {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
pub fn draft(&self) -> MappedMutexGuard<'_, Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
inner.1 = Some(inner.0.clone());
|
||||
|
||||
@@ -4,11 +4,11 @@ use crate::utils::{
|
||||
tmpl,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, time::Duration};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use url::Url;
|
||||
|
||||
use super::Config;
|
||||
@@ -63,6 +63,10 @@ pub struct PrfItem {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub announce: Option<String>,
|
||||
|
||||
/// profile announce url
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub announce_url: Option<String>,
|
||||
|
||||
/// the file data
|
||||
#[serde(skip)]
|
||||
pub file_data: Option<String>,
|
||||
@@ -126,6 +130,9 @@ pub struct PrfOption {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_hwid: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub update_always: Option<bool>,
|
||||
}
|
||||
|
||||
impl PrfOption {
|
||||
@@ -146,6 +153,7 @@ impl PrfOption {
|
||||
a.groups = b.groups.or(a.groups);
|
||||
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
||||
a.use_hwid = b.use_hwid.or(a.use_hwid);
|
||||
a.update_always = b.update_always.or(a.update_always);
|
||||
Some(a)
|
||||
}
|
||||
t => t.0.or(t.1),
|
||||
@@ -246,6 +254,7 @@ impl PrfItem {
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||
})
|
||||
@@ -267,7 +276,7 @@ impl PrfItem {
|
||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
||||
let use_hwid = opt_ref.is_some_and(|o| o.use_hwid.unwrap_or(true));
|
||||
let use_hwid = Config::verge().latest().enable_send_hwid.unwrap_or(true);
|
||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||
let mut script = opt_ref.and_then(|o| o.script.clone());
|
||||
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
||||
@@ -317,7 +326,7 @@ impl PrfItem {
|
||||
if let Ok(mut parsed_url) = Url::parse(url) {
|
||||
if parsed_url.set_host(Some(new_domain)).is_ok() {
|
||||
final_url = parsed_url.to_string();
|
||||
log::info!(target: "app", "URL host updated to -> {}", final_url);
|
||||
log::info!(target: "app", "URL host updated to -> {final_url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,6 +382,11 @@ impl PrfItem {
|
||||
},
|
||||
};
|
||||
|
||||
let update_always = match header.get("update-always") {
|
||||
Some(value) => value.to_str().unwrap_or("false").parse::<bool>().ok(),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let home = match header.get("profile-web-page-url") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
@@ -393,7 +407,8 @@ impl PrfItem {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
||||
STANDARD.decode(b64_data)
|
||||
STANDARD
|
||||
.decode(b64_data)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
} else {
|
||||
@@ -403,11 +418,27 @@ impl PrfItem {
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some(announce_msg) = &announce {
|
||||
let lower_msg = announce_msg.to_lowercase();
|
||||
if lower_msg.contains("device") || lower_msg.contains("устройств") {
|
||||
bail!(announce_msg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let announce_url = match header.get("announce-url") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
Some(str_value.to_string())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let profile_title = match header.get("profile-title") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
||||
STANDARD.decode(b64_data)
|
||||
STANDARD
|
||||
.decode(b64_data)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
} else {
|
||||
@@ -419,7 +450,9 @@ impl PrfItem {
|
||||
|
||||
let uid = help::get_uid("R");
|
||||
let file = format!("{uid}.yaml");
|
||||
let name = name.or(profile_title).unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||
let name = name
|
||||
.or(profile_title)
|
||||
.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||
let data = resp.text_with_charset("utf-8").await?;
|
||||
|
||||
// process the charset "UTF-8 with BOM"
|
||||
@@ -471,17 +504,29 @@ impl PrfItem {
|
||||
selected: None,
|
||||
extra,
|
||||
option: Some(PrfOption {
|
||||
user_agent: user_agent.clone(),
|
||||
with_proxy: if with_proxy { Some(true) } else { None },
|
||||
self_proxy: if self_proxy { Some(true) } else { None },
|
||||
update_interval,
|
||||
update_always,
|
||||
timeout_seconds: Some(timeout),
|
||||
danger_accept_invalid_certs: if accept_invalid_certs {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
merge,
|
||||
script,
|
||||
rules,
|
||||
proxies,
|
||||
groups,
|
||||
use_hwid: Some(use_hwid),
|
||||
..PrfOption::default()
|
||||
}),
|
||||
home,
|
||||
support_url,
|
||||
announce,
|
||||
announce_url,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(data.into()),
|
||||
})
|
||||
@@ -511,6 +556,7 @@ impl PrfItem {
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(template),
|
||||
})
|
||||
@@ -535,6 +581,7 @@ impl PrfItem {
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
@@ -558,6 +605,7 @@ impl PrfItem {
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
@@ -581,6 +629,7 @@ impl PrfItem {
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
@@ -604,6 +653,7 @@ impl PrfItem {
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
|
||||
@@ -66,7 +66,7 @@ impl IProfiles {
|
||||
help::save_yaml(
|
||||
&dirs::profiles_path()?,
|
||||
self,
|
||||
Some("# Profiles Config for Clash Verge"),
|
||||
Some("# Profiles Config for Koala Clash"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -136,10 +136,9 @@ impl IProfiles {
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
if self.current.is_none()
|
||||
&& (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()))
|
||||
{
|
||||
self.current = uid;
|
||||
if item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()) {
|
||||
// Always switch current to the newly created remote/local profile
|
||||
self.current = uid.clone();
|
||||
}
|
||||
|
||||
if self.items.is_none() {
|
||||
@@ -221,6 +220,7 @@ impl IProfiles {
|
||||
each.updated = item.updated;
|
||||
each.home = item.home;
|
||||
each.announce = item.announce;
|
||||
each.announce_url = item.announce_url;
|
||||
each.support_url = item.support_url;
|
||||
each.name = item.name;
|
||||
each.url = item.url;
|
||||
@@ -535,7 +535,7 @@ impl IProfiles {
|
||||
if Self::is_profile_file(file_name) {
|
||||
// 检查是否为全局扩展文件
|
||||
if protected_files.contains(file_name) {
|
||||
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
||||
log::debug!(target: "app", "Protect global extension config file: {file_name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -544,11 +544,11 @@ impl IProfiles {
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(_) => {
|
||||
deleted_files.push(file_name.to_string());
|
||||
log::info!(target: "app", "已清理冗余文件: {file_name}");
|
||||
log::info!(target: "app", "Cleaned up redundant file: {file_name}");
|
||||
}
|
||||
Err(e) => {
|
||||
failed_deletions.push(format!("{file_name}: {e}"));
|
||||
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
|
||||
log::warn!(target: "app", "Failed to clean file: {file_name} - {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,14 +678,14 @@ impl IProfiles {
|
||||
if !result.deleted_files.is_empty() {
|
||||
log::info!(
|
||||
target: "app",
|
||||
"自动清理完成,删除了 {} 个冗余文件",
|
||||
"Auto cleanup completed, deleted {} redundant files",
|
||||
result.deleted_files.len()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "自动清理失败: {e}");
|
||||
log::warn!(target: "app", "Auto cleanup failed: {e}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ pub struct IVerge {
|
||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
|
||||
pub enable_send_hwid: Option<bool>,
|
||||
|
||||
pub primary_action: Option<String>,
|
||||
|
||||
/// always use default bypass
|
||||
@@ -236,7 +238,7 @@ pub struct IVergeTheme {
|
||||
|
||||
impl IVerge {
|
||||
/// 有效的clash核心名称
|
||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["koala-mihomo", "koala-mihomo-alpha"];
|
||||
|
||||
/// 验证并修正配置文件中的clash_core值
|
||||
pub fn validate_and_fix_config() -> Result<()> {
|
||||
@@ -255,10 +257,10 @@ impl IVerge {
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
|
||||
"Invalid clash_core config detected at startup: '{}', auto-fixing to 'koala-mihomo'",
|
||||
core
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".to_string());
|
||||
config.clash_core = Some("koala-mihomo".to_string());
|
||||
needs_fix = true;
|
||||
}
|
||||
} else {
|
||||
@@ -266,21 +268,21 @@ impl IVerge {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
|
||||
"clash_core not configured at startup; setting default to 'koala-mihomo'"
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".to_string());
|
||||
config.clash_core = Some("koala-mihomo".to_string());
|
||||
needs_fix = true;
|
||||
}
|
||||
|
||||
// 修正后保存配置
|
||||
if needs_fix {
|
||||
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
|
||||
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
|
||||
logging!(info, Type::Config, true, "Saving fixed configuration file...");
|
||||
help::save_yaml(&config_path, &config, Some("# Koala Clash Config"))?;
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"配置文件修正完成,需要重新加载配置"
|
||||
"Configuration file fixed; reloading config required"
|
||||
);
|
||||
|
||||
Self::reload_config_after_fix(config)?;
|
||||
@@ -289,7 +291,7 @@ impl IVerge {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"clash_core配置验证通过: {:?}",
|
||||
"clash_core config validation passed: {:?}",
|
||||
config.clash_core
|
||||
);
|
||||
}
|
||||
@@ -319,7 +321,7 @@ impl IVerge {
|
||||
pub fn get_valid_clash_core(&self) -> String {
|
||||
self.clash_core
|
||||
.clone()
|
||||
.unwrap_or_else(|| "verge-mihomo".to_string())
|
||||
.unwrap_or_else(|| "koala-mihomo".to_string())
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
@@ -338,18 +340,17 @@ impl IVerge {
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
dirs::verge_path()
|
||||
.and_then(|path| help::read_yaml::<IVerge>(&path))
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
clash_core: Some("verge-mihomo".into()),
|
||||
clash_core: Some("koala-mihomo".into()),
|
||||
language: Some(Self::get_system_language()),
|
||||
theme_mode: Some("system".into()),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -403,6 +404,7 @@ impl IVerge {
|
||||
enable_auto_light_weight_mode: Some(false),
|
||||
auto_light_weight_minutes: Some(10),
|
||||
enable_dns_settings: Some(false),
|
||||
enable_send_hwid: Some(true),
|
||||
primary_action: Some("tun-mode".into()),
|
||||
home_cards: None,
|
||||
service_state: None,
|
||||
@@ -412,7 +414,7 @@ impl IVerge {
|
||||
|
||||
/// Save IVerge App Config
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Koala Clash Config"))
|
||||
}
|
||||
|
||||
/// patch verge config
|
||||
@@ -492,6 +494,7 @@ impl IVerge {
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(enable_send_hwid);
|
||||
patch!(primary_action);
|
||||
patch!(home_cards);
|
||||
patch!(service_state);
|
||||
@@ -588,6 +591,7 @@ pub struct IVergeResponse {
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub enable_send_hwid: Option<bool>,
|
||||
pub primary_action: Option<String>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
@@ -661,6 +665,7 @@ impl From<IVerge> for IVergeResponse {
|
||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
enable_send_hwid: verge.enable_send_hwid,
|
||||
primary_action: verge.primary_action,
|
||||
home_cards: verge.home_cards,
|
||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||
|
||||
@@ -39,15 +39,15 @@ impl AsyncProxyQuery {
|
||||
pub async fn get_auto_proxy() -> AsyncAutoproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
|
||||
log::debug!(target: "app", "Async auto proxy fetch succeeded: enable={}, url={}", proxy.enable, proxy.url);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "异步获取自动代理失败: {e}");
|
||||
log::warn!(target: "app", "Async auto proxy fetch failed: {e}");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "异步获取自动代理超时");
|
||||
log::warn!(target: "app", "Async auto proxy fetch timed out");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
}
|
||||
@@ -57,15 +57,15 @@ impl AsyncProxyQuery {
|
||||
pub async fn get_system_proxy() -> AsyncSysproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
|
||||
log::debug!(target: "app", "Async system proxy fetch succeeded: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "异步获取系统代理失败: {e}");
|
||||
log::warn!(target: "app", "Async system proxy fetch failed: {e}");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "异步获取系统代理超时");
|
||||
log::warn!(target: "app", "Async system proxy fetch timed out");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ impl AsyncProxyQuery {
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||
|
||||
if result != 0 {
|
||||
log::debug!(target: "app", "无法打开注册表项");
|
||||
log::debug!(target: "app", "Unable to open registry key");
|
||||
return Ok(AsyncAutoproxy::default());
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ impl AsyncProxyQuery {
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(url_buffer.len());
|
||||
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
|
||||
log::debug!(target: "app", "从注册表读取到PAC URL: {}", pac_url);
|
||||
log::debug!(target: "app", "Read PAC URL from registry: {}", pac_url);
|
||||
}
|
||||
|
||||
// 2. 检查自动检测设置是否启用
|
||||
@@ -148,7 +148,7 @@ impl AsyncProxyQuery {
|
||||
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
|
||||
|
||||
if pac_enabled {
|
||||
log::debug!(target: "app", "PAC配置启用: URL={}, AutoDetect={}", pac_url, auto_detect);
|
||||
log::debug!(target: "app", "PAC configuration enabled: URL={}, AutoDetect={}", pac_url, auto_detect);
|
||||
|
||||
if pac_url.is_empty() && auto_detect != 0 {
|
||||
pac_url = "auto-detect".to_string();
|
||||
@@ -159,7 +159,7 @@ impl AsyncProxyQuery {
|
||||
url: pac_url,
|
||||
})
|
||||
} else {
|
||||
log::debug!(target: "app", "PAC配置未启用");
|
||||
log::debug!(target: "app", "PAC configuration not enabled");
|
||||
Ok(AsyncAutoproxy::default())
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ impl AsyncProxyQuery {
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
|
||||
log::debug!(target: "app", "Parse result: pac_enabled={pac_enabled}, pac_url={pac_url}");
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: pac_enabled && !pac_url.is_empty(),
|
||||
@@ -361,7 +361,7 @@ impl AsyncProxyQuery {
|
||||
(proxy_server, 8080)
|
||||
};
|
||||
|
||||
log::debug!(target: "app", "从注册表读取到代理设置: {}:{}, bypass: {}", host, port, bypass_list);
|
||||
log::debug!(target: "app", "Read proxy settings from registry: {}:{}, bypass: {}", host, port, bypass_list);
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
@@ -518,7 +518,7 @@ impl AsyncProxyQuery {
|
||||
};
|
||||
|
||||
if host.is_empty() {
|
||||
return Err(anyhow!("无效的代理URL"));
|
||||
return Err(anyhow!("Invalid proxy URL"));
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
|
||||
@@ -108,11 +108,11 @@ impl WebDavClient {
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(op.timeout()))
|
||||
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||
.user_agent(format!("koala-clash/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||
// 允许所有请求类型的重定向,包括PUT
|
||||
if attempt.previous().len() >= 5 {
|
||||
attempt.error("重定向次数过多")
|
||||
attempt.error("Too many redirects")
|
||||
} else {
|
||||
attempt.follow()
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法读取文件以检测类型: {}, 错误: {}",
|
||||
"Failed to read file to detect type: {}, error: {}",
|
||||
path,
|
||||
err
|
||||
);
|
||||
@@ -130,7 +130,7 @@ impl CoreManager {
|
||||
debug,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法确定文件类型,默认当作YAML处理: {}",
|
||||
"Unable to determine file type, defaulting to YAML handling: {}",
|
||||
path
|
||||
);
|
||||
Ok(false)
|
||||
@@ -146,14 +146,19 @@ impl CoreManager {
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
Some("# Koala Clash Runtime"),
|
||||
)?;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
Ok(())
|
||||
}
|
||||
/// 验证运行时配置
|
||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||
logging!(info, Type::Config, true, "生成临时配置文件用于验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Generate temporary config file for validation"
|
||||
);
|
||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
self.validate_config_internal(config_path).await
|
||||
@@ -166,7 +171,12 @@ impl CoreManager {
|
||||
) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"App is exiting, skipping validation"
|
||||
);
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
@@ -183,7 +193,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"检测到Merge文件,仅进行语法检查: {}",
|
||||
"Detected merge file, performing syntax check only: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_file_syntax(config_path).await;
|
||||
@@ -201,7 +211,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法确定文件类型: {}, 错误: {}",
|
||||
"Unable to determine file type: {}, error: {}",
|
||||
config_path,
|
||||
err
|
||||
);
|
||||
@@ -215,7 +225,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"检测到脚本文件,使用JavaScript验证: {}",
|
||||
"Detected script file, validating with JavaScript: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_script_file(config_path).await;
|
||||
@@ -226,7 +236,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"使用Clash内核验证配置文件: {}",
|
||||
"Validating config file with Clash core: {}",
|
||||
config_path
|
||||
);
|
||||
self.validate_config_internal(config_path).await
|
||||
@@ -235,7 +245,12 @@ impl CoreManager {
|
||||
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"App is exiting, skipping validation"
|
||||
);
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
@@ -243,17 +258,23 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"开始验证配置文件: {}",
|
||||
"Starting validation for config file: {}",
|
||||
config_path
|
||||
);
|
||||
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
||||
logging!(info, Type::Config, true, "Using core: {}", clash_core);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let app_dir_str = dirs::path_to_str(&app_dir)?;
|
||||
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Validation directory: {}",
|
||||
app_dir_str
|
||||
);
|
||||
|
||||
// 使用子进程运行clash验证配置
|
||||
let output = app_handle
|
||||
@@ -271,56 +292,84 @@ impl CoreManager {
|
||||
let has_error =
|
||||
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
|
||||
logging!(info, Type::Config, true, "-------- 验证结果 --------");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"-------- Validation Result --------"
|
||||
);
|
||||
|
||||
if !stderr.is_empty() {
|
||||
logging!(info, Type::Config, true, "stderr输出:\n{}", stderr);
|
||||
logging!(info, Type::Config, true, "stderr output:\n{}", stderr);
|
||||
}
|
||||
|
||||
if has_error {
|
||||
logging!(info, Type::Config, true, "发现错误,开始处理错误信息");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Errors found, processing error details"
|
||||
);
|
||||
let error_msg = if !stdout.is_empty() {
|
||||
stdout.to_string()
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.to_string()
|
||||
} else if let Some(code) = output.status.code() {
|
||||
format!("验证进程异常退出,退出码: {code}")
|
||||
format!("Validation process exited abnormally, exit code: {code}")
|
||||
} else {
|
||||
"验证进程被终止".to_string()
|
||||
"Validation process was terminated".to_string()
|
||||
};
|
||||
|
||||
logging!(info, Type::Config, true, "-------- 验证结束 --------");
|
||||
logging!(info, Type::Config, true, "-------- Validation End --------");
|
||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "验证成功");
|
||||
logging!(info, Type::Config, true, "-------- 验证结束 --------");
|
||||
logging!(info, Type::Config, true, "Validation succeeded");
|
||||
logging!(info, Type::Config, true, "-------- Validation End --------");
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
}
|
||||
/// 只进行文件语法检查,不进行完整验证
|
||||
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Starting file check: {}",
|
||||
config_path
|
||||
);
|
||||
|
||||
// 读取文件内容
|
||||
let content = match std::fs::read_to_string(config_path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read file: {err}");
|
||||
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"Failed to read file: {}",
|
||||
error_msg
|
||||
);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
};
|
||||
// 对YAML文件尝试解析,只检查语法正确性
|
||||
logging!(info, Type::Config, true, "进行YAML语法检查");
|
||||
logging!(info, Type::Config, true, "Performing YAML syntax check");
|
||||
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Config, true, "YAML语法检查通过");
|
||||
logging!(info, Type::Config, true, "YAML syntax check passed");
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
Err(err) => {
|
||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||
let error_msg = format!("YAML syntax error: {err}");
|
||||
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"YAML syntax error: {}",
|
||||
error_msg
|
||||
);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
}
|
||||
@@ -332,13 +381,19 @@ impl CoreManager {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read script file: {err}");
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
logging!(warn, Type::Config, true, "Script syntax error: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
};
|
||||
|
||||
logging!(debug, Type::Config, true, "验证脚本文件: {}", path);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Config,
|
||||
true,
|
||||
"Validating script file: {}",
|
||||
path
|
||||
);
|
||||
|
||||
// 使用boa引擎进行基本语法检查
|
||||
use boa_engine::{Context, Source};
|
||||
@@ -348,7 +403,13 @@ impl CoreManager {
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
logging!(debug, Type::Config, true, "脚本语法验证通过: {}", path);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Config,
|
||||
true,
|
||||
"Script syntax validation passed: {}",
|
||||
path
|
||||
);
|
||||
|
||||
// 检查脚本是否包含main函数
|
||||
if !content.contains("function main")
|
||||
@@ -356,7 +417,13 @@ impl CoreManager {
|
||||
&& !content.contains("let main")
|
||||
{
|
||||
let error_msg = "Script must contain a main function";
|
||||
logging!(warn, Type::Config, true, "脚本缺少main函数: {}", path);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"Script missing main function: {}",
|
||||
path
|
||||
);
|
||||
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
|
||||
return Ok((false, error_msg.to_string()));
|
||||
}
|
||||
@@ -365,7 +432,7 @@ impl CoreManager {
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Script syntax error: {err}");
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
logging!(warn, Type::Config, true, "Script syntax error: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
@@ -375,33 +442,55 @@ impl CoreManager {
|
||||
pub async fn update_config(&self) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过完整验证流程
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Config, true, "应用正在退出,跳过验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"App is exiting, skipping validation"
|
||||
);
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
logging!(info, Type::Config, true, "开始更新配置");
|
||||
logging!(info, Type::Config, true, "Starting config update");
|
||||
|
||||
// 1. 先生成新的配置内容
|
||||
logging!(info, Type::Config, true, "生成新的配置内容");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Generating new configuration content"
|
||||
);
|
||||
Config::generate().await?;
|
||||
|
||||
// 2. 验证配置
|
||||
match self.validate_config().await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, true, "配置验证通过");
|
||||
logging!(info, Type::Config, true, "Configuration validation passed");
|
||||
// 4. 验证通过后,生成正式的运行时配置
|
||||
logging!(info, Type::Config, true, "生成运行时配置");
|
||||
logging!(info, Type::Config, true, "Generating runtime configuration");
|
||||
let run_path = Config::generate_file(ConfigType::Run)?;
|
||||
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
|
||||
Ok((true, "something".into()))
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"Configuration validation failed: {}",
|
||||
error_msg
|
||||
);
|
||||
Config::runtime().discard();
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"Error occurred during validation: {}",
|
||||
e
|
||||
);
|
||||
Config::runtime().discard();
|
||||
Err(e)
|
||||
}
|
||||
@@ -435,7 +524,12 @@ impl CoreManager {
|
||||
impl CoreManager {
|
||||
/// 清理多余的 mihomo 进程
|
||||
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, true, "开始清理多余的 mihomo 进程");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Starting cleanup of orphaned mihomo processes"
|
||||
);
|
||||
|
||||
// 获取当前管理的进程 PID
|
||||
let current_pid = {
|
||||
@@ -443,7 +537,7 @@ impl CoreManager {
|
||||
child_guard.as_ref().map(|child| child.pid())
|
||||
};
|
||||
|
||||
let target_processes = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||
let target_processes = ["koala-mihomo", "koala-mihomo-alpha"];
|
||||
|
||||
// 并行查找所有目标进程
|
||||
let mut process_futures = Vec::new();
|
||||
@@ -471,7 +565,7 @@ impl CoreManager {
|
||||
debug,
|
||||
Type::Core,
|
||||
true,
|
||||
"跳过当前管理的进程: {} (PID: {})",
|
||||
"Skipping currently managed process: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
@@ -482,13 +576,24 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(debug, Type::Core, true, "查找进程时发生错误: {}", e);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
true,
|
||||
"Error occurred while finding processes: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pids_to_kill.is_empty() {
|
||||
logging!(debug, Type::Core, true, "未发现多余的 mihomo 进程");
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
true,
|
||||
"No orphaned mihomo processes found"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -506,7 +611,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"清理完成,共终止了 {} 个多余的 mihomo 进程",
|
||||
"Cleanup complete, a total of {} redundant mihomo processes terminated",
|
||||
killed_count
|
||||
);
|
||||
}
|
||||
@@ -615,7 +720,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"尝试终止进程: {} (PID: {})",
|
||||
"Attempt to terminate process: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
@@ -662,7 +767,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"进程 {} (PID: {}) 终止命令成功但进程仍在运行",
|
||||
"Process {} (PID: {}) Termination command successful, but process still running",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
@@ -672,7 +777,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"成功终止进程: {} (PID: {})",
|
||||
"Successfully terminated process: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
@@ -683,7 +788,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"无法终止进程: {} (PID: {})",
|
||||
"Unable to terminate process: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
@@ -837,19 +942,29 @@ impl CoreManager {
|
||||
// 当服务安装失败时的回退逻辑
|
||||
async fn attempt_service_init(&self) -> Result<()> {
|
||||
if service::check_service_needs_reinstall().await {
|
||||
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service version mismatch or abnormal status, performing reinstallation"
|
||||
);
|
||||
if let Err(e) = service::reinstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务重装失败 during attempt_service_init: {}",
|
||||
"Service reinstallation failed during attempt_service_init: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
// 如果重装成功,还需要尝试启动服务
|
||||
logging!(info, Type::Core, true, "服务重装成功,尝试启动服务");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service reinstalled successfully, attempting to start"
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = self.start_core_by_service().await {
|
||||
@@ -857,20 +972,20 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"通过服务启动核心失败 during attempt_service_init: {}",
|
||||
"Failed to start core via service during attempt_service_init: {}",
|
||||
e
|
||||
);
|
||||
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
|
||||
let mut state = service::ServiceState::get();
|
||||
if !state.prefer_sidecar {
|
||||
state.prefer_sidecar = true;
|
||||
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
|
||||
state.last_error = Some(format!("Failed to start core via service: {e}"));
|
||||
if let Err(save_err) = state.save() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Core,
|
||||
true,
|
||||
"保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}",
|
||||
"Failed to save ServiceState (in attempt_service_init/start_core_by_service): {}",
|
||||
save_err
|
||||
);
|
||||
}
|
||||
@@ -889,7 +1004,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"应用初始化时清理多余 mihomo 进程失败: {}",
|
||||
"Failed to clean up unnecessary mihomo processes during application initialization: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
@@ -901,11 +1016,16 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务当前可用或看似可用,尝试通过服务模式启动/重装"
|
||||
"Service currently available or appears available; attempting to start/reinstall via service mode"
|
||||
);
|
||||
match self.attempt_service_init().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Core, true, "服务模式成功启动核心");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service mode successfully started core"
|
||||
);
|
||||
core_started_successfully = true;
|
||||
}
|
||||
Err(_err) => {
|
||||
@@ -913,7 +1033,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务模式启动或重装失败。将尝试Sidecar模式回退。"
|
||||
"Service mode start or reinstall failed. Will attempt Sidecar fallback."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -922,7 +1042,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务初始不可用 (is_service_available 调用失败)"
|
||||
"Service initially unavailable (is_service_available call failed)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -931,7 +1051,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑"
|
||||
"Core not started via service mode; performing Sidecar fallback or first-time install logic"
|
||||
);
|
||||
|
||||
let service_state = service::ServiceState::get();
|
||||
@@ -941,7 +1061,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
|
||||
"User prefers Sidecar mode or previous service start failed; starting with Sidecar mode"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
|
||||
@@ -953,26 +1073,41 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
|
||||
"No service installation record (first run or state reset); attempting to install service"
|
||||
);
|
||||
match service::install_service().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service installed successfully (first attempt)"
|
||||
);
|
||||
let mut new_state = service::ServiceState::default();
|
||||
new_state.record_install();
|
||||
new_state.prefer_sidecar = false;
|
||||
new_state.save()?;
|
||||
|
||||
if service::is_service_available().await.is_ok() {
|
||||
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Newly installed service available; attempting to start"
|
||||
);
|
||||
if self.start_core_by_service().await.is_ok() {
|
||||
logging!(info, Type::Core, true, "新安装的服务启动成功");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Newly installed service started successfully"
|
||||
);
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"新安装的服务启动失败,回退到Sidecar模式"
|
||||
"Newly installed service failed to start; falling back to Sidecar mode"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
final_state.prefer_sidecar = true;
|
||||
@@ -986,7 +1121,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
|
||||
"Service installed successfully but not connectable/immediately available; falling back to Sidecar mode"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
final_state.prefer_sidecar = true;
|
||||
@@ -999,7 +1134,13 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Core, true, "服务首次安装失败: {}", err);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service first-time installation failed: {}",
|
||||
err
|
||||
);
|
||||
let new_state = service::ServiceState {
|
||||
last_error: Some(err.to_string()),
|
||||
prefer_sidecar: true,
|
||||
@@ -1017,7 +1158,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
|
||||
"There is a service installation record, but the service is unavailable/not started. Force switch to Sidecar mode"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
if !final_state.prefer_sidecar {
|
||||
@@ -1025,7 +1166,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"prefer_sidecar 为 false,因服务启动失败或不可用而强制设置为 true"
|
||||
"prefer_sidecar is false, but is forced to true due to service startup failure or unavailability"
|
||||
);
|
||||
final_state.prefer_sidecar = true;
|
||||
final_state.last_error =
|
||||
@@ -1062,7 +1203,12 @@ impl CoreManager {
|
||||
if service::check_service_needs_reinstall().await {
|
||||
service::reinstall_service().await?;
|
||||
}
|
||||
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service available; starting in service mode"
|
||||
);
|
||||
self.start_core_by_service().await?;
|
||||
} else {
|
||||
// 服务不可用,检查用户偏好
|
||||
@@ -1072,11 +1218,16 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务不可用,根据用户偏好使用Sidecar模式"
|
||||
"Service unavailable; starting in Sidecar mode per user preference"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
} else {
|
||||
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service unavailable; starting in Sidecar mode"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ struct QueryRequest {
|
||||
response_tx: oneshot::Sender<Autoproxy>,
|
||||
}
|
||||
|
||||
// 配置结构体移到外部
|
||||
// Configuration structure moved to external
|
||||
struct ProxyConfig {
|
||||
sys_enabled: bool,
|
||||
pac_enabled: bool,
|
||||
@@ -106,59 +106,59 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取自动代理配置(缓存)
|
||||
/// Get automatic proxy configuration (cached)
|
||||
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
|
||||
self.state.read().auto_proxy.clone()
|
||||
}
|
||||
|
||||
/// 异步获取最新的自动代理配置
|
||||
/// Asynchronously get the latest automatic proxy configuration
|
||||
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let query = QueryRequest { response_tx: tx };
|
||||
|
||||
if self.query_sender.send(query).is_err() {
|
||||
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
|
||||
log::error!(target: "app", "Failed to send query request, returning cached data");
|
||||
return self.get_auto_proxy_cached();
|
||||
}
|
||||
|
||||
match timeout(Duration::from_secs(5), rx).await {
|
||||
Ok(Ok(result)) => result,
|
||||
_ => {
|
||||
log::warn!(target: "app", "查询超时,返回缓存数据");
|
||||
log::warn!(target: "app", "Query timed out, returning cached data");
|
||||
self.get_auto_proxy_cached()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知配置变更
|
||||
/// Notify configuration changed
|
||||
pub fn notify_config_changed(&self) {
|
||||
self.send_event(ProxyEvent::ConfigChanged);
|
||||
}
|
||||
|
||||
/// 通知应用启动
|
||||
/// Notify application started
|
||||
pub fn notify_app_started(&self) {
|
||||
self.send_event(ProxyEvent::AppStarted);
|
||||
}
|
||||
|
||||
/// 通知应用即将关闭
|
||||
/// Notify application stopping
|
||||
#[allow(dead_code)]
|
||||
pub fn notify_app_stopping(&self) {
|
||||
self.send_event(ProxyEvent::AppStopping);
|
||||
}
|
||||
|
||||
/// 启用系统代理
|
||||
/// Enable system proxy
|
||||
#[allow(dead_code)]
|
||||
pub fn enable_proxy(&self) {
|
||||
self.send_event(ProxyEvent::EnableProxy);
|
||||
}
|
||||
|
||||
/// 禁用系统代理
|
||||
/// Disable system proxy
|
||||
#[allow(dead_code)]
|
||||
pub fn disable_proxy(&self) {
|
||||
self.send_event(ProxyEvent::DisableProxy);
|
||||
}
|
||||
|
||||
/// 强制检查代理状态
|
||||
/// Force check proxy status
|
||||
#[allow(dead_code)]
|
||||
pub fn force_check(&self) {
|
||||
self.send_event(ProxyEvent::ForceCheck);
|
||||
@@ -166,7 +166,7 @@ impl EventDrivenProxyManager {
|
||||
|
||||
fn send_event(&self, event: ProxyEvent) {
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
log::error!(target: "app", "发送代理事件失败: {e}");
|
||||
log::error!(target: "app", "Failed to send proxy event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,18 +176,18 @@ impl EventDrivenProxyManager {
|
||||
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
log::info!(target: "app", "事件驱动代理管理器启动");
|
||||
log::info!(target: "app", "Event-driven proxy manager started");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = event_rx.recv() => {
|
||||
match event {
|
||||
Some(event) => {
|
||||
log::debug!(target: "app", "处理代理事件: {event:?}");
|
||||
log::debug!(target: "app", "Handling proxy event: {event:?}");
|
||||
Self::handle_event(&state, event).await;
|
||||
}
|
||||
None => {
|
||||
log::info!(target: "app", "事件通道关闭,代理管理器停止");
|
||||
log::info!(target: "app", "Event channel closed, proxy manager stopped");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,7 @@ impl EventDrivenProxyManager {
|
||||
let _ = query.response_tx.send(result);
|
||||
}
|
||||
None => {
|
||||
log::info!(target: "app", "查询通道关闭");
|
||||
log::info!(target: "app", "Query channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ impl EventDrivenProxyManager {
|
||||
Self::initialize_proxy_state(state).await;
|
||||
}
|
||||
ProxyEvent::AppStopping => {
|
||||
log::info!(target: "app", "清理代理状态");
|
||||
log::info!(target: "app", "Cleaning up proxy state");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,7 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
|
||||
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "初始化代理状态");
|
||||
log::info!(target: "app", "Initializing proxy state");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||
@@ -260,11 +260,11 @@ impl EventDrivenProxyManager {
|
||||
s.is_healthy = true;
|
||||
});
|
||||
|
||||
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||
log::info!(target: "app", "Proxy state initialized: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||
}
|
||||
|
||||
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::debug!(target: "app", "更新代理配置");
|
||||
log::debug!(target: "app", "Updating proxy configuration");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
|
||||
@@ -288,7 +288,7 @@ impl EventDrivenProxyManager {
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "检查代理状态");
|
||||
log::debug!(target: "app", "Checking proxy status");
|
||||
|
||||
if pac_enabled {
|
||||
Self::check_and_restore_pac_proxy(state).await;
|
||||
@@ -306,7 +306,7 @@ impl EventDrivenProxyManager {
|
||||
});
|
||||
|
||||
if !current.enable || current.url != expected.url {
|
||||
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
||||
log::info!(target: "app", "PAC proxy setting abnormal, recovering...");
|
||||
Self::restore_pac_proxy(&expected.url).await;
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
@@ -328,7 +328,7 @@ impl EventDrivenProxyManager {
|
||||
});
|
||||
|
||||
if !current.enable || current.host != expected.host || current.port != expected.port {
|
||||
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
||||
log::info!(target: "app", "System proxy setting abnormal, recovering...");
|
||||
Self::restore_sys_proxy(&expected).await;
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
@@ -344,7 +344,7 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
|
||||
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "启用系统代理");
|
||||
log::info!(target: "app", "Enabling system proxy");
|
||||
|
||||
let pac_enabled = state.read().pac_enabled;
|
||||
|
||||
@@ -360,7 +360,7 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
|
||||
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "禁用系统代理");
|
||||
log::info!(target: "app", "Disabling system proxy");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
@@ -373,7 +373,7 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
|
||||
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
|
||||
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
|
||||
log::info!(target: "app", "Switching to {} mode", if to_pac { "PAC" } else { "HTTP Proxy" });
|
||||
|
||||
if to_pac {
|
||||
let disabled_sys = Sysproxy::default();
|
||||
@@ -396,7 +396,7 @@ impl EventDrivenProxyManager {
|
||||
async fn get_auto_proxy_with_timeout() -> Autoproxy {
|
||||
let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
|
||||
|
||||
// 转换为兼容的结构
|
||||
// Convert to compatible structure
|
||||
Autoproxy {
|
||||
enable: async_proxy.enable,
|
||||
url: async_proxy.url,
|
||||
@@ -406,7 +406,7 @@ impl EventDrivenProxyManager {
|
||||
async fn get_sys_proxy_with_timeout() -> Sysproxy {
|
||||
let async_proxy = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
// 转换为兼容的结构
|
||||
// Convert to compatible structure
|
||||
Sysproxy {
|
||||
enable: async_proxy.enable,
|
||||
host: async_proxy.host,
|
||||
@@ -415,7 +415,7 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的状态更新方法
|
||||
// Unified state update method
|
||||
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
|
||||
where
|
||||
F: FnOnce(&mut ProxyState),
|
||||
@@ -534,14 +534,14 @@ impl EventDrivenProxyManager {
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "获取服务路径失败: {}", e);
|
||||
log::error!(target: "app", "Failed to get service path: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe 不存在");
|
||||
log::error!(target: "app", "sysproxy.exe does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -554,17 +554,17 @@ impl EventDrivenProxyManager {
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "执行sysproxy命令失败: {:?}", args);
|
||||
log::error!(target: "app", "Failed to execute sysproxy command: {:?}", args);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.is_empty() {
|
||||
log::error!(target: "app", "sysproxy错误输出: {}", stderr);
|
||||
log::error!(target: "app", "sysproxy stderr: {}", stderr);
|
||||
}
|
||||
} else {
|
||||
log::debug!(target: "app", "成功执行sysproxy命令: {:?}", args);
|
||||
log::debug!(target: "app", "Successfully executed sysproxy command: {:?}", args);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "执行sysproxy命令出错: {}", e);
|
||||
log::error!(target: "app", "Error executing sysproxy command: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +258,8 @@ pub struct Handle {
|
||||
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||
startup_completed: Arc<RwLock<bool>>,
|
||||
notification_system: Arc<RwLock<Option<NotificationSystem>>>,
|
||||
/// Messages that should be emitted only after UI is really ready
|
||||
ui_pending_messages: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||
}
|
||||
|
||||
impl Default for Handle {
|
||||
@@ -268,6 +270,7 @@ impl Default for Handle {
|
||||
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
||||
startup_completed: Arc::new(RwLock::new(false)),
|
||||
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
|
||||
ui_pending_messages: Arc::new(RwLock::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +298,10 @@ impl Handle {
|
||||
}
|
||||
|
||||
pub fn get_window(&self) -> Option<WebviewWindow> {
|
||||
// If we are in lightweight mode, treat as no window (webview may be destroyed)
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
return None;
|
||||
}
|
||||
let app_handle = self.app_handle()?;
|
||||
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
||||
if window.is_none() {
|
||||
@@ -411,12 +418,13 @@ impl Handle {
|
||||
let status_str = status.into();
|
||||
let msg_str = msg.into();
|
||||
|
||||
// If startup not completed, buffer messages (existing behavior)
|
||||
if !*handle.startup_completed.read() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Frontend,
|
||||
true,
|
||||
"启动过程中发现错误,加入消息队列: {} - {}",
|
||||
"Error found during startup; queued: {} - {}",
|
||||
status_str,
|
||||
msg_str
|
||||
);
|
||||
@@ -429,6 +437,23 @@ impl Handle {
|
||||
return;
|
||||
}
|
||||
|
||||
// If UI is not yet ready (e.g., window re-created from tray or lightweight mode),
|
||||
// buffer messages to emit after UI signals readiness.
|
||||
if !crate::utils::resolve::is_ui_ready() {
|
||||
log::debug!(
|
||||
target: "app",
|
||||
"UI not ready, queue notice message: {} - {}",
|
||||
status_str,
|
||||
msg_str
|
||||
);
|
||||
let mut pendings = handle.ui_pending_messages.write();
|
||||
pendings.push(ErrorMessage {
|
||||
status: status_str,
|
||||
message: msg_str,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if handle.is_exiting() {
|
||||
return;
|
||||
}
|
||||
@@ -442,6 +467,34 @@ impl Handle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush messages buffered while UI was not ready
|
||||
pub fn flush_ui_pending_messages(&self) {
|
||||
let pending = {
|
||||
let mut msgs = self.ui_pending_messages.write();
|
||||
std::mem::take(&mut *msgs)
|
||||
};
|
||||
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
let system_opt = self.notification_system.read();
|
||||
if let Some(system) = system_opt.as_ref() {
|
||||
for msg in pending {
|
||||
system.send_event(FrontendEvent::NoticeMessage {
|
||||
status: msg.status,
|
||||
message: msg.message,
|
||||
});
|
||||
// small pacing to avoid flooding immediately on resume
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_startup_completed(&self) {
|
||||
{
|
||||
let mut completed = self.startup_completed.write();
|
||||
@@ -466,7 +519,7 @@ impl Handle {
|
||||
info,
|
||||
Type::Frontend,
|
||||
true,
|
||||
"发送{}条启动时累积的错误消息",
|
||||
"Sending {} accumulated startup error messages",
|
||||
errors.len()
|
||||
);
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -477,7 +477,12 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
|
||||
/// 检查服务状态 - 使用IPC通信
|
||||
pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
logging!(info, Type::Service, true, "开始检查服务状态 (IPC)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Starting service status check (IPC)"
|
||||
);
|
||||
|
||||
// 使用IPC通信
|
||||
let payload = serde_json::json!({});
|
||||
@@ -495,8 +500,16 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
); */
|
||||
|
||||
if !response.success {
|
||||
let err_msg = response.error.unwrap_or_else(|| "未知服务错误".to_string());
|
||||
logging!(error, Type::Service, true, "服务响应错误: {}", err_msg);
|
||||
let err_msg = response
|
||||
.error
|
||||
.unwrap_or_else(|| "Unknown service error".to_string());
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service response error: {}",
|
||||
err_msg
|
||||
);
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
@@ -516,7 +529,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"解析嵌套的ResponseBody失败: {}; 尝试其他方式",
|
||||
"Failed to parse nested ResponseBody: {}; trying alternative",
|
||||
e
|
||||
);
|
||||
None
|
||||
@@ -536,7 +549,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"服务检测成功: code={}, msg={}, data存在={}",
|
||||
"Service check succeeded: code={}, msg={}, data_present={}",
|
||||
json_response.code,
|
||||
json_response.msg,
|
||||
json_response.data.is_some()
|
||||
@@ -550,7 +563,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"服务检测成功: code={}, msg={}",
|
||||
"Service check succeeded: code={}, msg={}",
|
||||
json_response.code,
|
||||
json_response.msg
|
||||
);
|
||||
@@ -561,31 +574,42 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"解析服务响应失败: {}; 原始数据: {:?}",
|
||||
"Failed to parse service response: {}; raw data: {:?}",
|
||||
e,
|
||||
data
|
||||
);
|
||||
bail!("无法解析服务响应数据: {}", e)
|
||||
bail!("Unable to parse service response data: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
logging!(error, Type::Service, true, "服务响应中没有数据");
|
||||
bail!("服务响应中没有数据")
|
||||
logging!(error, Type::Service, true, "No data in service response");
|
||||
bail!("No data in service response")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "IPC通信失败: {}", e);
|
||||
bail!("无法连接到Clash Verge Service: {}", e)
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"IPC communication failed: {}",
|
||||
e
|
||||
);
|
||||
bail!("Unable to connect to Koala Clash Service: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查服务版本 - 使用IPC通信
|
||||
pub async fn check_service_version() -> Result<String> {
|
||||
logging!(info, Type::Service, true, "开始检查服务版本 (IPC)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Starting service version check (IPC)"
|
||||
);
|
||||
|
||||
let payload = serde_json::json!({});
|
||||
// logging!(debug, Type::Service, true, "发送GetVersion请求");
|
||||
@@ -604,8 +628,14 @@ pub async fn check_service_version() -> Result<String> {
|
||||
if !response.success {
|
||||
let err_msg = response
|
||||
.error
|
||||
.unwrap_or_else(|| "获取服务版本失败".to_string());
|
||||
logging!(error, Type::Service, true, "获取版本错误: {}", err_msg);
|
||||
.unwrap_or_else(|| "Failed to get service version".to_string());
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to get service version: {}",
|
||||
err_msg
|
||||
);
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
@@ -618,7 +648,7 @@ pub async fn check_service_version() -> Result<String> {
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"获取到服务版本: {}",
|
||||
"Service version: {}",
|
||||
version_str
|
||||
);
|
||||
return Ok(version_str.to_string());
|
||||
@@ -628,7 +658,7 @@ pub async fn check_service_version() -> Result<String> {
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"嵌套数据中没有version字段: {:?}",
|
||||
"Nested data does not contain version field: {:?}",
|
||||
nested_data
|
||||
);
|
||||
} else {
|
||||
@@ -639,7 +669,7 @@ pub async fn check_service_version() -> Result<String> {
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"获取到服务版本: {}",
|
||||
"Received service version: {}",
|
||||
version_response.version
|
||||
);
|
||||
return Ok(version_response.version);
|
||||
@@ -649,44 +679,55 @@ pub async fn check_service_version() -> Result<String> {
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"解析版本响应失败: {}; 原始数据: {:?}",
|
||||
"Failed to parse version response: {}; raw data: {:?}",
|
||||
e,
|
||||
data
|
||||
);
|
||||
bail!("无法解析服务版本数据: {}", e)
|
||||
bail!("Unable to parse service version data: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("响应中未找到有效的版本信息")
|
||||
bail!("No valid version information found in response")
|
||||
}
|
||||
None => {
|
||||
logging!(error, Type::Service, true, "版本响应中没有数据");
|
||||
bail!("服务版本响应中没有数据")
|
||||
logging!(error, Type::Service, true, "No data in version response");
|
||||
bail!("No data in service version response")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "IPC通信失败: {}", e);
|
||||
bail!("无法连接到Clash Verge Service: {}", e)
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"IPC communication failed: {}",
|
||||
e
|
||||
);
|
||||
bail!("Unable to connect to Koala Clash Service: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查服务是否需要重装
|
||||
pub async fn check_service_needs_reinstall() -> bool {
|
||||
logging!(info, Type::Service, true, "开始检查服务是否需要重装");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Checking whether service needs reinstallation"
|
||||
);
|
||||
|
||||
let service_state = ServiceState::get();
|
||||
|
||||
if !service_state.can_reinstall() {
|
||||
log::info!(target: "app", "服务重装检查: 处于冷却期或已达最大尝试次数");
|
||||
log::info!(target: "app", "Service reinstall check: in cooldown period or max attempts reached");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查版本和可用性
|
||||
match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
|
||||
log::info!(target: "app", "Service version check: current={version}, required={REQUIRED_SERVICE_VERSION}");
|
||||
/* logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
@@ -698,25 +739,36 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
|
||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||
if needs_reinstall {
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
|
||||
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
|
||||
log::warn!(target: "app", "Service version mismatch detected, reinstallation required! current={version}, required={REQUIRED_SERVICE_VERSION}");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service version mismatch, reinstallation required"
|
||||
);
|
||||
|
||||
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||
// log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配,无需重装");
|
||||
log::info!(target: "app", "Service version matches, no reinstallation needed");
|
||||
// logging!(info, Type::Service, true, "服务版本匹配,无需重装");
|
||||
}
|
||||
|
||||
needs_reinstall
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Service, true, "检查服务版本失败: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to check service version: {}",
|
||||
err
|
||||
);
|
||||
|
||||
// 检查服务是否可用
|
||||
match is_service_available().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务正在运行但版本检查失败: {err}");
|
||||
log::info!(target: "app", "Service is running but version check failed: {err}");
|
||||
/* logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
@@ -727,7 +779,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
log::info!(target: "app", "服务不可用或未运行,需要重装");
|
||||
log::info!(target: "app", "Service unavailable or not running, reinstallation needed");
|
||||
// logging!(info, Type::Service, true, "服务不可用或未运行,需要重装");
|
||||
true
|
||||
}
|
||||
@@ -738,7 +790,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
|
||||
/// 尝试使用服务启动core
|
||||
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
|
||||
log::info!(target:"app", "Attempting to start core with existing service (IPC)");
|
||||
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
|
||||
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
@@ -781,8 +833,16 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
); */
|
||||
|
||||
if !response.success {
|
||||
let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string());
|
||||
logging!(error, Type::Service, true, "启动核心失败: {}", err_msg);
|
||||
let err_msg = response
|
||||
.error
|
||||
.unwrap_or_else(|| "Failed to start core".to_string());
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to start core: {}",
|
||||
err_msg
|
||||
);
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
@@ -793,127 +853,140 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
let msg = data
|
||||
.get("msg")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("未知错误");
|
||||
.unwrap_or("Unknown error");
|
||||
|
||||
if code_value != 0 {
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"启动核心返回错误: code={}, msg={}",
|
||||
"Start core returned error: code={}, msg={}",
|
||||
code_value,
|
||||
msg
|
||||
);
|
||||
bail!("启动核心失败: {}", msg);
|
||||
bail!("Failed to start core: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logging!(info, Type::Service, true, "服务成功启动核心");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service successfully started core"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "启动核心IPC通信失败: {}", e);
|
||||
bail!("无法连接到Clash Verge Service: {}", e)
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to start core via IPC: {}",
|
||||
e
|
||||
);
|
||||
bail!("Unable to connect to Koala Clash Service: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 以服务启动core
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target: "app", "正在尝试通过服务启动核心");
|
||||
log::info!(target: "app", "Attempting to start core via service");
|
||||
|
||||
// 先检查服务版本,不受冷却期限制
|
||||
let version_check = match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}");
|
||||
log::info!(target: "app", "Detected service version: {version}, required: {REQUIRED_SERVICE_VERSION}");
|
||||
|
||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||
log::warn!(target: "app", "Service version mismatch, reinstallation required");
|
||||
false
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配");
|
||||
log::info!(target: "app", "Service version matches");
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "无法获取服务版本: {err}");
|
||||
log::warn!(target: "app", "Failed to get service version: {err}");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if version_check && is_service_available().await.is_ok() {
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
log::info!(target: "app", "Service is running and version matches, attempting to use it");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
if !version_check {
|
||||
log::info!(target: "app", "服务版本不匹配,尝试重装");
|
||||
log::info!(target: "app", "Service version mismatch, attempting reinstallation");
|
||||
|
||||
let service_state = ServiceState::get();
|
||||
if !service_state.can_reinstall() {
|
||||
log::warn!(target: "app", "由于限制无法重装服务");
|
||||
log::warn!(target: "app", "Cannot reinstall service due to limitations");
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
|
||||
log::info!(target: "app", "Service started successfully despite version mismatch");
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("服务版本不匹配且无法重装,启动失败");
|
||||
bail!("Service version mismatch and cannot reinstall; startup failed");
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(target: "app", "开始重装服务");
|
||||
log::info!(target: "app", "Starting service reinstallation");
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {err}");
|
||||
log::warn!(target: "app", "Service reinstallation failed: {err}");
|
||||
|
||||
log::info!(target: "app", "尝试使用现有服务");
|
||||
log::info!(target: "app", "Attempting to use existing service");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
log::info!(target: "app", "服务重装成功,尝试启动");
|
||||
log::info!(target: "app", "Service reinstalled successfully, attempting to start");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
// 检查服务状态
|
||||
// Check service status
|
||||
match check_ipc_service_status().await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "服务可用但未运行核心,尝试启动");
|
||||
log::info!(target: "app", "Service available but core not running, attempting to start");
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "服务检查失败: {err}");
|
||||
log::warn!(target: "app", "Service check failed: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
// 服务不可用或启动失败,检查是否需要重装
|
||||
// Service unavailable or startup failed, check if reinstallation is needed
|
||||
if check_service_needs_reinstall().await {
|
||||
log::info!(target: "app", "服务需要重装");
|
||||
log::info!(target: "app", "Service needs reinstallation");
|
||||
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {err}");
|
||||
log::warn!(target: "app", "Service reinstallation failed: {err}");
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
|
||||
log::info!(target: "app", "服务重装完成,尝试启动核心");
|
||||
log::info!(target: "app", "Service reinstallation completed, attempting to start core");
|
||||
start_with_existing_service(config_file).await
|
||||
} else {
|
||||
log::warn!(target: "app", "服务不可用且无法重装");
|
||||
log::warn!(target: "app", "Service unavailable and cannot be reinstalled");
|
||||
bail!("Service is not available and cannot be reinstalled at this time")
|
||||
}
|
||||
}
|
||||
|
||||
/// 通过服务停止core
|
||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "通过服务停止核心 (IPC)");
|
||||
logging!(info, Type::Service, true, "Stopping core via service (IPC)");
|
||||
|
||||
let payload = serde_json::json!({});
|
||||
let response = send_ipc_request(IpcCommand::StopClash, payload)
|
||||
.await
|
||||
.context("无法连接到Clash Verge Service")?;
|
||||
.context("Unable to connect to Koala Clash Service")?;
|
||||
|
||||
if !response.success {
|
||||
bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string()));
|
||||
bail!(response
|
||||
.error
|
||||
.unwrap_or_else(|| "Failed to stop core".to_string()));
|
||||
}
|
||||
|
||||
if let Some(data) = &response.data {
|
||||
@@ -922,18 +995,18 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
let msg = data
|
||||
.get("msg")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("未知错误");
|
||||
.unwrap_or("Unknown error");
|
||||
|
||||
if code_value != 0 {
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"停止核心返回错误: code={}, msg={}",
|
||||
"Stop core returned error: code={}, msg={}",
|
||||
code_value,
|
||||
msg
|
||||
);
|
||||
bail!("停止核心失败: {}", msg);
|
||||
bail!("Failed to stop core: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -943,19 +1016,24 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
|
||||
/// 检查服务是否正在运行
|
||||
pub async fn is_service_available() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "开始检查服务是否正在运行");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Checking whether service is running"
|
||||
);
|
||||
|
||||
match check_ipc_service_status().await {
|
||||
Ok(resp) => {
|
||||
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||
logging!(info, Type::Service, true, "服务正在运行");
|
||||
logging!(info, Type::Service, true, "Service is running");
|
||||
Ok(())
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"服务未正常运行: code={}, msg={}",
|
||||
"Service not running normally: code={}, msg={}",
|
||||
resp.code,
|
||||
resp.msg
|
||||
);
|
||||
@@ -963,7 +1041,13 @@ pub async fn is_service_available() -> Result<()> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Service, true, "检查服务运行状态失败: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to check service running status: {}",
|
||||
err
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
@@ -971,21 +1055,21 @@ pub async fn is_service_available() -> Result<()> {
|
||||
|
||||
/// 强制重装服务(UI修复按钮)
|
||||
pub async fn force_reinstall_service() -> Result<()> {
|
||||
log::info!(target: "app", "用户请求强制重装服务");
|
||||
log::info!(target: "app", "User requested forced service reinstallation");
|
||||
|
||||
let service_state = ServiceState::default();
|
||||
service_state.save()?;
|
||||
|
||||
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
||||
log::info!(target: "app", "Service state reset, starting reinstallation");
|
||||
|
||||
match reinstall_service().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务重装成功");
|
||||
log::info!(target: "app", "Service reinstalled successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "强制重装服务失败: {err}");
|
||||
bail!("强制重装服务失败: {}", err)
|
||||
log::error!(target: "app", "Forced service reinstallation failed: {err}");
|
||||
bail!("Forced service reinstallation failed: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ use sha2::{Digest, Sha256};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
|
||||
r"\\.\pipe\clash-verge-service"
|
||||
r"\\.\pipe\koala-clash-service"
|
||||
} else {
|
||||
"/tmp/clash-verge-service.sock"
|
||||
"/tmp/koala-clash-service.sock"
|
||||
};
|
||||
|
||||
// 定义命令类型
|
||||
@@ -43,7 +43,7 @@ pub struct IpcResponse {
|
||||
fn derive_secret_key() -> Vec<u8> {
|
||||
// to do
|
||||
// 从系统安全存储中获取或从程序安装时生成的密钥文件中读取
|
||||
let unique_app_id = "clash-verge-app-secret-fuck-me-until-daylight";
|
||||
let unique_app_id = "koala-clash-app-secret-fuck-me-until-daylight";
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(unique_app_id.as_bytes());
|
||||
hasher.finalize().to_vec()
|
||||
@@ -85,7 +85,7 @@ fn sign_message(message: &str) -> Result<String> {
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
let secret_key = derive_secret_key();
|
||||
let mut mac = HmacSha256::new_from_slice(&secret_key).context("HMAC初始化失败")?;
|
||||
let mut mac = HmacSha256::new_from_slice(&secret_key).context("Failed to initialize HMAC")?;
|
||||
|
||||
mac.update(message.as_bytes());
|
||||
let result = mac.finalize();
|
||||
@@ -129,14 +129,25 @@ pub async fn send_ipc_request(
|
||||
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
|
||||
};
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Connecting to service (Windows)..."
|
||||
);
|
||||
|
||||
let command_type = format!("{:?}", command);
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to create signed request: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
@@ -147,8 +158,14 @@ pub async fn send_ipc_request(
|
||||
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
|
||||
Ok(name) => name,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "创建CString失败: {}", e);
|
||||
return Err(anyhow::anyhow!("创建CString失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to create CString: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to create CString: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,64 +187,110 @@ pub async fn send_ipc_request(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"连接到服务命名管道失败: {}",
|
||||
"Failed to connect to service named pipe: {}",
|
||||
error
|
||||
);
|
||||
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error));
|
||||
return Err(anyhow::anyhow!("Unable to connect to service named pipe: {}", error));
|
||||
}
|
||||
|
||||
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
|
||||
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service connection successful (Windows)"
|
||||
);
|
||||
|
||||
let request_bytes = request_json.as_bytes();
|
||||
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
||||
|
||||
if let Err(e) = pipe.write_all(&len_bytes) {
|
||||
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
||||
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
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) {
|
||||
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
||||
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
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() {
|
||||
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
|
||||
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
|
||||
logging!(error, Type::Service, true, "Failed to flush pipe: {}", e);
|
||||
return Err(anyhow::anyhow!("Failed to flush pipe: {}", e));
|
||||
}
|
||||
|
||||
let mut response_len_bytes = [0u8; 4];
|
||||
if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
|
||||
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
||||
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
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 mut response_bytes = vec![0u8; response_len];
|
||||
if let Err(e) = pipe.read_exact(&mut response_bytes) {
|
||||
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
||||
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
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) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "服务响应解析失败: {}", e);
|
||||
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to parse service response: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to parse response: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
match verify_response_signature(&response) {
|
||||
Ok(valid) => {
|
||||
if !valid {
|
||||
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
||||
bail!("服务响应签名验证失败");
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service response signature verification failed"
|
||||
);
|
||||
bail!("Service response signature verification failed");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Error verifying response signature: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@@ -236,7 +299,7 @@ pub async fn send_ipc_request(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"IPC请求完成: 命令={}, 成功={}",
|
||||
"IPC request completed: command={}, success={}",
|
||||
command_type,
|
||||
response.success
|
||||
);
|
||||
@@ -255,14 +318,14 @@ pub async fn send_ipc_request(
|
||||
) -> Result<IpcResponse> {
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
||||
logging!(info, Type::Service, true, "Connecting to service (Unix)...");
|
||||
|
||||
let command_type = format!("{command:?}");
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
||||
logging!(error, Type::Service, true, "Failed to create signed request: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
@@ -271,12 +334,23 @@ pub async fn send_ipc_request(
|
||||
|
||||
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
|
||||
Ok(s) => {
|
||||
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service connection successful (Unix)"
|
||||
);
|
||||
s
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
|
||||
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to connect to Unix socket: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Unable to connect to service Unix socket: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -284,46 +358,58 @@ pub async fn send_ipc_request(
|
||||
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
||||
|
||||
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
|
||||
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
||||
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
||||
logging!(error, Type::Service, true, "Failed to write request length: {}", e);
|
||||
return Err(anyhow::anyhow!("Failed to write request length: {}", e));
|
||||
}
|
||||
|
||||
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
|
||||
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
||||
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
||||
logging!(error, Type::Service, true, "Failed to write request body: {}", e);
|
||||
return Err(anyhow::anyhow!("Failed to write request body: {}", e));
|
||||
}
|
||||
|
||||
let mut response_len_bytes = [0u8; 4];
|
||||
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
|
||||
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
||||
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
||||
logging!(error, 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 mut response_bytes = vec![0u8; response_len];
|
||||
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
|
||||
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
||||
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
||||
logging!(error, 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) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
|
||||
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to parse service response: {}",
|
||||
e,
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to parse response: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
match verify_response_signature(&response) {
|
||||
Ok(valid) => {
|
||||
if !valid {
|
||||
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
||||
bail!("服务响应签名验证失败");
|
||||
logging!(error, Type::Service, true, "Service response signature verification failed");
|
||||
bail!("Service response signature verification failed");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Error verifying response signature: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@@ -332,7 +418,7 @@ pub async fn send_ipc_request(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"IPC请求完成: 命令={}, 成功={}",
|
||||
"IPC request completed: command={}, success={}",
|
||||
command_type,
|
||||
response.success
|
||||
);
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Sysopt {
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_app_started();
|
||||
|
||||
log::info!(target: "app", "已启用事件驱动代理守卫");
|
||||
log::info!(target: "app", "Event-driven proxy guard enabled");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ impl Sysopt {
|
||||
let mut autoproxy = match Autoproxy::get_auto_proxy() {
|
||||
Ok(ap) => ap,
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
|
||||
log::warn!(target: "app", "Failed to get auto proxy config while resetting: {e}, using default config");
|
||||
Autoproxy {
|
||||
enable: false,
|
||||
url: "".to_string(),
|
||||
@@ -248,14 +248,14 @@ impl Sysopt {
|
||||
{
|
||||
if is_enable {
|
||||
if let Err(e) = startup_shortcut::create_shortcut() {
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "Failed to create startup shortcut: {}", e);
|
||||
// 如果快捷方式创建失败,回退到原来的方法
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "Failed to remove startup shortcut: {}", e);
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
@@ -290,11 +290,11 @@ impl Sysopt {
|
||||
{
|
||||
match startup_shortcut::is_shortcut_enabled() {
|
||||
Ok(enabled) => {
|
||||
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
|
||||
log::info!(target: "app", "Shortcut auto-launch state: {}", enabled);
|
||||
return Ok(enabled);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
|
||||
log::error!(target: "app", "Failed to check shortcut, falling back to original method: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"已注册的定时任务数量: {}",
|
||||
"Registered timer task count: {}",
|
||||
timer_map.len()
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"注册了定时任务 - uid={}, interval={}min, task_id={}",
|
||||
"Registered timer task - uid={}, interval={}min, task_id={}",
|
||||
uid,
|
||||
task.interval_minutes,
|
||||
task.task_id
|
||||
@@ -100,7 +100,12 @@ impl Timer {
|
||||
let uid = item.uid.as_ref()?;
|
||||
|
||||
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
||||
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"Profile requires immediate update: uid={}",
|
||||
uid
|
||||
);
|
||||
Some(uid.clone())
|
||||
} else {
|
||||
None
|
||||
@@ -116,7 +121,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"需要立即更新的配置数量: {}",
|
||||
"Number of profiles requiring immediate update: {}",
|
||||
profiles_to_update.len()
|
||||
);
|
||||
let timer_map = self.timer_map.read();
|
||||
@@ -124,7 +129,7 @@ impl Timer {
|
||||
|
||||
for uid in profiles_to_update {
|
||||
if let Some(task) = timer_map.get(&uid) {
|
||||
logging!(info, Type::Timer, "立即执行任务: uid={}", uid);
|
||||
logging!(info, Type::Timer, "Executing task immediately: uid={}", uid);
|
||||
if let Err(e) = delay_timer.advance_task(task.task_id) {
|
||||
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
|
||||
}
|
||||
@@ -237,7 +242,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"找到定时更新配置: uid={}, interval={}min",
|
||||
"Found scheduled update config: uid={}, interval={}min",
|
||||
uid,
|
||||
interval
|
||||
);
|
||||
@@ -251,7 +256,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"生成的定时更新配置数量: {}",
|
||||
"Generated scheduled update config count: {}",
|
||||
new_map.len()
|
||||
);
|
||||
new_map
|
||||
@@ -267,7 +272,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"当前 timer_map 大小: {}",
|
||||
"Current timer_map size: {}",
|
||||
timer_map.len()
|
||||
);
|
||||
|
||||
@@ -279,7 +284,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"定时任务间隔变更: uid={}, 旧={}, 新={}",
|
||||
"Timer task interval changed: uid={}, old={}, new={}",
|
||||
uid,
|
||||
task.interval_minutes,
|
||||
interval
|
||||
@@ -288,12 +293,12 @@ impl Timer {
|
||||
}
|
||||
None => {
|
||||
// Task no longer needed
|
||||
logging!(debug, Type::Timer, "定时任务已删除: uid={}", uid);
|
||||
logging!(debug, Type::Timer, "Timer task removed: uid={}", uid);
|
||||
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
|
||||
}
|
||||
_ => {
|
||||
// Task exists with same interval, no change needed
|
||||
logging!(debug, Type::Timer, "定时任务保持不变: uid={}", uid);
|
||||
logging!(debug, Type::Timer, "Timer task unchanged: uid={}", uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,7 +311,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"新增定时任务: uid={}, interval={}min",
|
||||
"Added timer task: uid={}, interval={}min",
|
||||
uid,
|
||||
interval
|
||||
);
|
||||
@@ -320,7 +325,13 @@ impl Timer {
|
||||
*self.timer_count.lock() = next_id;
|
||||
}
|
||||
|
||||
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
|
||||
logging!(debug, Type::Timer, "Number of scheduled task changes: {}", diff_map.len());
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"Number of timer task changes: {}",
|
||||
diff_map.len()
|
||||
);
|
||||
diff_map
|
||||
}
|
||||
|
||||
@@ -363,13 +374,18 @@ impl Timer {
|
||||
|
||||
/// Get next update time for a profile
|
||||
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> {
|
||||
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid);
|
||||
logging!(info, Type::Timer, "Getting next update time, uid={}", uid);
|
||||
|
||||
let timer_map = self.timer_map.read();
|
||||
let task = match timer_map.get(uid) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"Corresponding timer task not found, uid={}",
|
||||
uid
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -380,7 +396,7 @@ impl Timer {
|
||||
let items = match profiles.get_items() {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "获取配置列表失败");
|
||||
logging!(warn, Type::Timer, "Failed to get profile list");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -388,7 +404,12 @@ impl Timer {
|
||||
let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "找不到对应的配置,uid={}", uid);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"Corresponding profile not found, uid={}",
|
||||
uid
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -401,7 +422,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"计算得到下次更新时间: {}, uid={}",
|
||||
"Calculated next update time: {}, uid={}",
|
||||
next_time,
|
||||
uid
|
||||
);
|
||||
@@ -410,7 +431,7 @@ impl Timer {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"更新时间或间隔无效,updated={}, interval={}",
|
||||
"Invalid update time or interval, updated={}, interval={}",
|
||||
updated,
|
||||
task.interval_minutes
|
||||
);
|
||||
@@ -442,7 +463,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"配置 {} 是否为当前激活配置: {}",
|
||||
"Is profile {} currently active: {}",
|
||||
uid,
|
||||
is_current
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ use tauri::tray::TrayIconBuilder;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::{
|
||||
cmd,
|
||||
config::Config,
|
||||
feat, logging,
|
||||
module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
|
||||
@@ -46,7 +45,7 @@ fn should_handle_tray_click() -> bool {
|
||||
*last_click = now;
|
||||
true
|
||||
} else {
|
||||
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
|
||||
log::debug!(target: "app", "Tray click ignored by debounce; time since last click: {:?}ms",
|
||||
now.duration_since(*last_click).as_millis());
|
||||
false
|
||||
}
|
||||
@@ -231,7 +230,7 @@ impl Tray {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
|
||||
log::warn!(target: "app", "Failed to update tray menu: app_handle not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -279,11 +278,11 @@ impl Tray {
|
||||
profile_uid_and_name,
|
||||
is_lightweight_mode,
|
||||
)?));
|
||||
log::debug!(target: "app", "托盘菜单更新成功");
|
||||
log::debug!(target: "app", "Tray menu updated successfully");
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
|
||||
log::warn!(target: "app", "Failed to update tray menu: tray not found");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -295,7 +294,7 @@ impl Tray {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
||||
log::warn!(target: "app", "Failed to update tray icon: app_handle not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -303,7 +302,7 @@ impl Tray {
|
||||
let tray = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
||||
log::warn!(target: "app", "Failed to update tray icon: tray not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -332,7 +331,7 @@ impl Tray {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
||||
log::warn!(target: "app", "Failed to update tray icon: app_handle not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -340,7 +339,7 @@ impl Tray {
|
||||
let tray = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
||||
log::warn!(target: "app", "Failed to update tray icon: tray not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -376,7 +375,7 @@ impl Tray {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘提示失败: app_handle不存在");
|
||||
log::warn!(target: "app", "Failed to update tray tooltip: app_handle not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -384,7 +383,7 @@ impl Tray {
|
||||
let version = match VERSION.get() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在");
|
||||
log::warn!(target: "app", "Failed to update tray tooltip: version info not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -414,7 +413,7 @@ impl Tray {
|
||||
|
||||
if let Some(tray) = app_handle.tray_by_id("main") {
|
||||
let _ = tray.set_tooltip(Some(&format!(
|
||||
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||
"Koala Clash {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||
t("SysProxy"),
|
||||
switch_map[system_proxy],
|
||||
t("TUN"),
|
||||
@@ -423,7 +422,7 @@ impl Tray {
|
||||
current_profile_name
|
||||
)));
|
||||
} else {
|
||||
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
|
||||
log::warn!(target: "app", "Failed to update tray tooltip: tray not found");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -443,7 +442,7 @@ impl Tray {
|
||||
pub fn unsubscribe_traffic(&self) {}
|
||||
|
||||
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
||||
log::info!(target: "app", "Creating system tray from AppHandle");
|
||||
|
||||
// 获取图标
|
||||
let icon_bytes = TrayState::get_common_tray_icon().1;
|
||||
@@ -491,20 +490,20 @@ impl Tray {
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"main_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "Tray点击事件: 显示主窗口");
|
||||
log::info!(target: "app", "Tray click: show main window");
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
log::info!(target: "app", "Window show result: {result:?}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
log::info!(target: "app", "系统托盘创建成功");
|
||||
log::info!(target: "app", "System tray created successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -601,16 +600,6 @@ fn create_tray_menu(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let direct_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"direct_mode",
|
||||
t("Direct Mode"),
|
||||
true,
|
||||
mode == "direct",
|
||||
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"profiles",
|
||||
@@ -650,45 +639,6 @@ fn create_tray_menu(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let copy_env =
|
||||
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
||||
|
||||
let open_app_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_app_dir",
|
||||
t("Conf Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_core_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_core_dir",
|
||||
t("Core Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_logs_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_logs_dir",
|
||||
t("Logs Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_dir = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"open_dir",
|
||||
t("Open Dir"),
|
||||
true,
|
||||
&[open_app_dir, open_core_dir, open_logs_dir],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let restart_clash = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_clash",
|
||||
@@ -736,7 +686,6 @@ fn create_tray_menu(
|
||||
separator,
|
||||
rule_mode,
|
||||
global_mode,
|
||||
direct_mode,
|
||||
separator,
|
||||
profiles,
|
||||
separator,
|
||||
@@ -744,8 +693,6 @@ fn create_tray_menu(
|
||||
tun_mode,
|
||||
separator,
|
||||
lighteweight_mode,
|
||||
copy_env,
|
||||
open_dir,
|
||||
more,
|
||||
separator,
|
||||
quit,
|
||||
@@ -770,18 +717,18 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
"open_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||
log::info!(target: "app", "Tray menu click: open window");
|
||||
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
log::info!(target: "app", "Window show result: {result:?}");
|
||||
}
|
||||
"system_proxy" => {
|
||||
feat::toggle_system_proxy();
|
||||
@@ -789,16 +736,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
"tun_mode" => {
|
||||
feat::toggle_tun_mode(None);
|
||||
}
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => {
|
||||
let _ = cmd::open_app_dir();
|
||||
}
|
||||
"open_core_dir" => {
|
||||
let _ = cmd::open_core_dir();
|
||||
}
|
||||
"open_logs_dir" => {
|
||||
let _ = cmd::open_logs_dir();
|
||||
}
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => {
|
||||
@@ -816,7 +753,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
if was_lightweight {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
||||
log::info!(target: "app", "Show main window after exiting lightweight mode: {result:?}");
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
@@ -830,6 +767,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_all_states() {
|
||||
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
||||
log::warn!(target: "app", "Failed to update tray state: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,8 @@ impl ChainSupport {
|
||||
(self, core.as_str()),
|
||||
(ChainSupport::All, _)
|
||||
| (ChainSupport::Clash, "clash")
|
||||
| (ChainSupport::ClashMeta, "verge-mihomo")
|
||||
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
| (ChainSupport::ClashMeta, "koala-mihomo")
|
||||
| (ChainSupport::ClashMetaAlpha, "koala-mihomo-alpha")
|
||||
),
|
||||
None => true,
|
||||
}
|
||||
|
||||
@@ -202,7 +202,9 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
});
|
||||
let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
|
||||
for (key, value) in patch_tun.into_iter() {
|
||||
tun.insert(key, value);
|
||||
if !tun.contains_key(&key) {
|
||||
tun.insert(key, value);
|
||||
}
|
||||
}
|
||||
config.insert("tun".into(), tun.into());
|
||||
} else {
|
||||
@@ -239,7 +241,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||
.map(|(_, c)| c)
|
||||
.for_each(|item| {
|
||||
log::debug!(target: "app", "run builtin script {}", item.uid);
|
||||
log::debug!(target: "app", "run builtin script {0}", item.uid);
|
||||
if let ChainType::Script(script) = item.data {
|
||||
match use_script(script, config.to_owned(), "".to_string()) {
|
||||
Ok((res_config, _)) => {
|
||||
|
||||
@@ -141,8 +141,8 @@ fn test_script() {
|
||||
fn test_escape_unescape() {
|
||||
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
||||
let escaped = escape_js_string_for_single_quote(test_string);
|
||||
println!("Original: {}", test_string);
|
||||
println!("Escaped: {}", escaped);
|
||||
println!("Original: {test_string}");
|
||||
println!("Escaped: {escaped}");
|
||||
|
||||
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
||||
let parsed = parse_json_safely(json_str).unwrap();
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||
crate::utils::resolve::set_public_dns("8.8.8.8".to_string()).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,13 @@ pub async fn update_profile(
|
||||
option: Option<PrfOption>,
|
||||
auto_refresh: Option<bool>,
|
||||
) -> Result<()> {
|
||||
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[Subscription Update] Start updating subscription {}",
|
||||
uid
|
||||
);
|
||||
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
||||
|
||||
let url_opt = {
|
||||
@@ -41,14 +47,14 @@ pub async fn update_profile(
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
|
||||
log::info!(target: "app", "[Subscription Update] {uid} is not a remote subscription, skipping update");
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新");
|
||||
log::warn!(target: "app", "[Subscription Update] {uid} is missing URL, cannot update");
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
log::info!(target: "app",
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
"[Subscription Update] {} is a remote subscription, URL: {}",
|
||||
uid,
|
||||
item.url.clone().unwrap()
|
||||
);
|
||||
@@ -58,24 +64,24 @@ pub async fn update_profile(
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
|
||||
log::info!(target: "app", "[Subscription Update] Start downloading new subscription content");
|
||||
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
|
||||
|
||||
// 尝试使用正常设置更新
|
||||
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||
Ok(item) => {
|
||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||
log::info!(target: "app", "[Subscription Update] Subscription config updated successfully");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
log::info!(target: "app", "[Subscription Update] Is current active subscription: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(err) => {
|
||||
// 首次更新失败,尝试使用Clash代理
|
||||
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err},尝试使用Clash代理更新");
|
||||
log::warn!(target: "app", "[Subscription Update] Normal update failed: {err}, trying to update via Clash proxy");
|
||||
|
||||
// 发送通知
|
||||
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||
@@ -92,7 +98,7 @@ pub async fn update_profile(
|
||||
// 使用Clash代理重试
|
||||
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
|
||||
Ok(mut item) => {
|
||||
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
|
||||
log::info!(target: "app", "[Subscription Update] Update via Clash proxy succeeded");
|
||||
|
||||
// 恢复原始代理设置到item
|
||||
if let Some(option) = item.option.as_mut() {
|
||||
@@ -112,11 +118,11 @@ pub async fn update_profile(
|
||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
log::info!(target: "app", "[Subscription Update] Is current active subscription: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(retry_err) => {
|
||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
|
||||
log::error!(target: "app", "[Subscription Update] Update via Clash proxy still failed: {retry_err}");
|
||||
handle::Handle::notice_message(
|
||||
"update_failed_even_with_clash",
|
||||
format!("{retry_err}"),
|
||||
@@ -131,14 +137,30 @@ pub async fn update_profile(
|
||||
};
|
||||
|
||||
if should_update {
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[Subscription Update] Update core configuration"
|
||||
);
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[Subscription Update] Update succeeded"
|
||||
);
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[Subscription Update] Update failed: {}",
|
||||
err
|
||||
);
|
||||
handle::Handle::notice_message("update_failed", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
|
||||
log::info!(target: "app", "Attempting to open/close dashboard (bypass debounce: {bypass_debounce})");
|
||||
|
||||
// 热键调用调度到主线程执行,避免 WebView 创建死锁
|
||||
if bypass_debounce {
|
||||
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
|
||||
log::info!(target: "app", "Hotkey invoked, dispatching window operation to main thread");
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
log::info!(target: "app", "主线程中执行热键窗口操作");
|
||||
log::info!(target: "app", "Executing hotkey window operation on main thread");
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||
@@ -64,7 +64,7 @@ fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
||||
/// 异步优化的应用退出函数
|
||||
pub fn quit() {
|
||||
use crate::process::AsyncHandler;
|
||||
logging!(debug, Type::System, true, "启动退出流程");
|
||||
logging!(debug, Type::System, true, "Start exit process");
|
||||
|
||||
// 获取应用句柄并设置退出标志
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
@@ -73,19 +73,24 @@ pub fn quit() {
|
||||
// 优先关闭窗口,提供立即反馈
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
log::info!(target: "app", "窗口已隐藏");
|
||||
log::info!(target: "app", "Window hidden");
|
||||
}
|
||||
|
||||
// 使用异步任务处理资源清理,避免阻塞
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始异步清理资源");
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"Start asynchronous resource cleanup"
|
||||
);
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"资源清理完成,退出代码: {}",
|
||||
"Resource cleanup completed, exit code: {}",
|
||||
if cleanup_result { 0 } else { 1 }
|
||||
);
|
||||
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
||||
@@ -95,7 +100,12 @@ pub fn quit() {
|
||||
async fn clean_async() -> bool {
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
logging!(info, Type::System, true, "开始执行异步清理操作...");
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"Start executing asynchronous cleanup..."
|
||||
);
|
||||
|
||||
// 1. 处理TUN模式
|
||||
let tun_task = async {
|
||||
@@ -112,11 +122,11 @@ async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "TUN模式已禁用");
|
||||
log::info!(target: "app", "TUN mode disabled");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "禁用TUN模式超时");
|
||||
log::warn!(target: "app", "Timeout disabling TUN mode");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -134,11 +144,11 @@ async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "系统代理已重置");
|
||||
log::info!(target: "app", "System proxy reset");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "重置系统代理超时");
|
||||
log::warn!(target: "app", "Timeout resetting system proxy");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -148,11 +158,11 @@ async fn clean_async() -> bool {
|
||||
let core_task = async {
|
||||
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "核心服务已停止");
|
||||
log::info!(target: "app", "Core service stopped");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "停止核心服务超时");
|
||||
log::warn!(target: "app", "Timeout stopping core service");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -168,11 +178,11 @@ async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "DNS设置已恢复");
|
||||
log::info!(target: "app", "DNS settings restored");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "恢复DNS设置超时");
|
||||
log::warn!(target: "app", "Timeout restoring DNS settings");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -192,7 +202,7 @@ async fn clean_async() -> bool {
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
|
||||
"Asynchronous cleanup completed - TUN: {}, Proxy: {}, Core: {}, DNS: {}, Overall: {}",
|
||||
tun_success,
|
||||
proxy_success,
|
||||
core_success,
|
||||
@@ -209,7 +219,7 @@ pub fn clean() -> bool {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始执行清理操作...");
|
||||
logging!(info, Type::System, true, "Start executing cleanup...");
|
||||
|
||||
// 使用已有的异步清理函数
|
||||
let cleanup_result = clean_async().await;
|
||||
@@ -220,7 +230,13 @@ pub fn clean() -> bool {
|
||||
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
|
||||
Ok(result) => {
|
||||
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"Cleanup completed, result: {}",
|
||||
result
|
||||
);
|
||||
result
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -228,7 +244,7 @@ pub fn clean() -> bool {
|
||||
warn,
|
||||
Type::System,
|
||||
true,
|
||||
"清理操作超时,返回成功状态避免阻塞"
|
||||
"Cleanup timed out, returning success to avoid blocking"
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ mod module;
|
||||
mod process;
|
||||
mod state;
|
||||
mod utils;
|
||||
use crate::{
|
||||
core::hotkey,
|
||||
process::AsyncHandler,
|
||||
utils::{resolve, resolve::resolve_scheme, server},
|
||||
};
|
||||
use crate::{core::hotkey, process::AsyncHandler, utils::resolve};
|
||||
use config::Config;
|
||||
use std::sync::{Mutex, Once};
|
||||
use tauri::AppHandle;
|
||||
@@ -86,85 +82,75 @@ impl AppHandleManager {
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
pub fn run() {
|
||||
utils::network::NetworkManager::global().init();
|
||||
// Capture early deep link before any async setup (cold start on macOS)
|
||||
utils::resolve::capture_early_deep_link_from_args();
|
||||
|
||||
utils::network::NetworkManager::global().init();
|
||||
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
// 异步单例检测
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||
match timeout(Duration::from_secs(3), server::check_singleton()).await {
|
||||
Ok(result) => {
|
||||
if result.is_err() {
|
||||
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||
if let Some(app_handle) = AppHandleManager::global().get() {
|
||||
app_handle.exit(0);
|
||||
} else {
|
||||
std::process::exit(0);
|
||||
}
|
||||
} else {
|
||||
logging!(info, Type::Setup, true, "未检测到其他应用实例");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"单例检查超时,假定没有其他实例运行"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let devtools = tauri_plugin_devtools::init();
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.setup(|app| {
|
||||
logging!(info, Type::Setup, true, "开始应用初始化...");
|
||||
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
auto_start_plugin_builder = auto_start_plugin_builder
|
||||
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||
.app_name(app.config().identifier.clone());
|
||||
}
|
||||
let _ = app.handle().plugin(auto_start_plugin_builder.build());
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| {
|
||||
// Handle deep link when a second instance is invoked: forward URL to the running instance
|
||||
if let Some(url) = argv
|
||||
.iter()
|
||||
.find(|a| a.starts_with("clash://") || a.starts_with("koala-clash://"))
|
||||
.cloned()
|
||||
{
|
||||
// Robust scheduling avoids races with lightweight/window
|
||||
resolve::schedule_handle_deep_link(url);
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.setup(|app| {
|
||||
logging!(info, Type::Setup, true, "Starting app initialization...");
|
||||
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
logging!(info, Type::Setup, true, "注册深层链接...");
|
||||
logging_error!(Type::System, true, app.deep_link().register_all());
|
||||
}
|
||||
// Register deep link handler as early as possible to not miss cold-start events (macOS)
|
||||
app.deep_link().on_open_url(|event| {
|
||||
let urls: Vec<String> = event.urls().iter().map(|u| u.to_string()).collect();
|
||||
logging!(info, Type::Setup, true, "on_open_url received: {:?}", urls);
|
||||
if let Some(url) = urls.first().cloned() {
|
||||
resolve::schedule_handle_deep_link(url);
|
||||
}
|
||||
});
|
||||
|
||||
app.deep_link().on_open_url(|event| {
|
||||
AsyncHandler::spawn(move || {
|
||||
let url = event.urls().first().map(|u| u.to_string());
|
||||
async move {
|
||||
if let Some(url) = url {
|
||||
logging_error!(Type::Setup, true, resolve_scheme(url).await);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
auto_start_plugin_builder = auto_start_plugin_builder
|
||||
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||
.app_name(app.config().identifier.clone());
|
||||
}
|
||||
let _ = app.handle().plugin(auto_start_plugin_builder.build());
|
||||
|
||||
// Ensure URL schemes are registered with the OS (all platforms)
|
||||
logging!(info, Type::Setup, true, "Registering deep links with OS...");
|
||||
logging_error!(Type::System, true, app.deep_link().register_all());
|
||||
|
||||
// Deep link handler will be registered AFTER core handle init to ensure window creation works
|
||||
|
||||
// 窗口管理
|
||||
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Initializing window state management..."
|
||||
);
|
||||
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
||||
.with_filename("window_state.json")
|
||||
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
||||
@@ -174,7 +160,12 @@ pub fn run() {
|
||||
// 异步处理
|
||||
let app_handle = app.handle().clone();
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "异步执行应用设置...");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Executing app setup asynchronously..."
|
||||
);
|
||||
match timeout(
|
||||
Duration::from_secs(30),
|
||||
resolve::resolve_setup_async(&app_handle),
|
||||
@@ -182,41 +173,81 @@ pub fn run() {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Setup, true, "应用设置成功完成");
|
||||
logging!(info, Type::Setup, true, "App setup completed successfully");
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"应用设置超时(30秒),继续执行后续流程"
|
||||
"App setup timed out (30s), continuing with subsequent steps"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Executing main setup operations..."
|
||||
);
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
|
||||
logging!(info, Type::Setup, true, "Initializing AppHandleManager...");
|
||||
AppHandleManager::global().init(app.handle().clone());
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化核心句柄...");
|
||||
logging!(info, Type::Setup, true, "Initializing core handle...");
|
||||
core::handle::Handle::global().init(app.handle());
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化配置...");
|
||||
logging!(info, Type::Setup, true, "Initializing config...");
|
||||
if let Err(e) = utils::init::init_config() {
|
||||
logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to initialize config: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化资源...");
|
||||
logging!(info, Type::Setup, true, "Initializing resources...");
|
||||
if let Err(e) = utils::init::init_resources() {
|
||||
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to initialize resources: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
|
||||
app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||
// If an early deep link was captured from argv, schedule it now (after core and window can be created)
|
||||
utils::resolve::replay_early_deep_link();
|
||||
|
||||
// (deep link handler already registered above)
|
||||
|
||||
tauri::async_runtime::spawn(async {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"Running profile updates at startup..."
|
||||
);
|
||||
if let Err(e) = crate::cmd::update_profiles_on_startup().await {
|
||||
log::error!("Failed to update profiles on startup: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Initialization completed, continuing"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -295,6 +326,8 @@ pub fn run() {
|
||||
cmd::read_profile_file,
|
||||
cmd::save_profile_file,
|
||||
cmd::get_next_update_time,
|
||||
cmd::update_profiles_on_startup,
|
||||
cmd::create_profile_from_share_link,
|
||||
// script validation
|
||||
cmd::script_validate_notice,
|
||||
cmd::validate_script_file,
|
||||
@@ -334,7 +367,7 @@ pub fn run() {
|
||||
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||
logging!(info, Type::System, true, "应用就绪或恢复");
|
||||
logging!(info, Type::System, true, "App ready or resumed");
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -342,8 +375,8 @@ pub fn run() {
|
||||
.get_handle()
|
||||
.get_webview_window("main")
|
||||
{
|
||||
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
||||
let _ = window.set_title("Clash Verge Rev Lite");
|
||||
logging!(info, Type::Window, true, "Setting macOS window title");
|
||||
let _ = window.set_title("Koala Clash");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,6 +387,10 @@ pub fn run() {
|
||||
} => {
|
||||
if !has_visible_windows {
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
}
|
||||
@@ -383,7 +420,12 @@ pub fn run() {
|
||||
if let Some(window) = core::handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"Tried to hide window but it does not exist"
|
||||
);
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Focused(true) => {
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn run_once_auto_lightweight() {
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
|
||||
"Silent start detected: create window, then attach auto lightweight-mode listener"
|
||||
);
|
||||
set_lightweight_mode(false);
|
||||
enable_auto_light_weight_mode();
|
||||
@@ -70,7 +70,7 @@ pub fn auto_lightweight_mode_init() {
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||
"Non-silent start: directly attach auto lightweight-mode listener"
|
||||
);
|
||||
set_lightweight_mode(true);
|
||||
enable_auto_light_weight_mode();
|
||||
@@ -102,26 +102,26 @@ pub fn set_lightweight_mode(value: bool) {
|
||||
|
||||
pub fn enable_auto_light_weight_mode() {
|
||||
Timer::global().init().unwrap();
|
||||
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
|
||||
logging!(info, Type::Lightweight, true, "Enable auto lightweight mode");
|
||||
setup_window_close_listener();
|
||||
setup_webview_focus_listener();
|
||||
}
|
||||
|
||||
pub fn disable_auto_light_weight_mode() {
|
||||
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
|
||||
logging!(info, Type::Lightweight, true, "Disable auto lightweight mode");
|
||||
let _ = cancel_light_weight_timer();
|
||||
cancel_window_close_listener();
|
||||
}
|
||||
|
||||
pub fn entry_lightweight_mode() {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
let result = WindowManager::hide_main_window();
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"轻量模式隐藏窗口结果: {:?}",
|
||||
"Lightweight mode window hide result: {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
@@ -150,7 +150,7 @@ pub fn exit_lightweight_mode() {
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"轻量模式退出操作已在进行中,跳过重复调用"
|
||||
"Lightweight mode exit already in progress; skipping duplicate call"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ pub fn exit_lightweight_mode() {
|
||||
|
||||
// 确保当前确实处于轻量模式才执行退出操作
|
||||
if !is_in_lightweight_mode() {
|
||||
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
||||
logging!(info, Type::Lightweight, true, "Not in lightweight mode; skip exit");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ fn setup_window_close_listener() -> u32 {
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"监听到关闭请求,开始轻量模式计时"
|
||||
"Close requested; starting lightweight-mode timer"
|
||||
);
|
||||
});
|
||||
return handler;
|
||||
@@ -207,7 +207,7 @@ fn setup_webview_focus_listener() -> u32 {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
"监听到窗口获得焦点,取消轻量模式计时"
|
||||
"Window focused; cancel lightweight-mode timer"
|
||||
);
|
||||
});
|
||||
return handler;
|
||||
@@ -218,7 +218,7 @@ fn setup_webview_focus_listener() -> u32 {
|
||||
fn cancel_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
window.unlisten(setup_window_close_listener());
|
||||
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
|
||||
logging!(info, Type::Lightweight, true, "Removed window close listener");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ fn setup_light_weight_timer() -> Result<()> {
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_once_by_minutes(once_by_minutes)
|
||||
.spawn_async_routine(move || async move {
|
||||
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
|
||||
logging!(info, Type::Timer, true, "Timer expired; entering lightweight mode");
|
||||
entry_lightweight_mode();
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
@@ -271,7 +271,7 @@ fn setup_light_weight_timer() -> Result<()> {
|
||||
info,
|
||||
Type::Timer,
|
||||
true,
|
||||
"计时器已设置,{} 分钟后将自动进入轻量模式",
|
||||
"Timer set; will auto-enter lightweight mode after {} minute(s)",
|
||||
once_by_minutes
|
||||
);
|
||||
|
||||
@@ -286,7 +286,7 @@ fn cancel_light_weight_timer() -> Result<()> {
|
||||
delay_timer
|
||||
.remove_task(task.task_id)
|
||||
.context("failed to remove timer task")?;
|
||||
logging!(info, Type::Timer, true, "计时器已取消");
|
||||
logging!(info, Type::Timer, true, "Timer canceled");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -28,9 +28,9 @@ impl LightWeightState {
|
||||
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
|
||||
self.is_lightweight = value;
|
||||
if value {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
logging!(info, Type::Lightweight, true, "Lightweight mode enabled");
|
||||
} else {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
||||
logging!(info, Type::Lightweight, true, "Lightweight mode disabled");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf};
|
||||
/// Windows 下的开机启动文件夹路径
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_startup_dir() -> Result<PathBuf> {
|
||||
let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("无法获取 APPDATA 环境变量"))?;
|
||||
let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("Unable to obtain APPDATA environment variable"))?;
|
||||
|
||||
let startup_dir = Path::new(&appdata)
|
||||
.join("Microsoft")
|
||||
@@ -19,7 +19,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
|
||||
.join("Startup");
|
||||
|
||||
if !startup_dir.exists() {
|
||||
return Err(anyhow!("Startup 目录不存在: {:?}", startup_dir));
|
||||
return Err(anyhow!("Startup directory does not exist: {:?}", startup_dir));
|
||||
}
|
||||
|
||||
Ok(startup_dir)
|
||||
@@ -29,7 +29,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_exe_path() -> Result<PathBuf> {
|
||||
let exe_path =
|
||||
std::env::current_exe().map_err(|e| anyhow!("无法获取当前可执行文件路径: {}", e))?;
|
||||
std::env::current_exe().map_err(|e| anyhow!("Unable to obtain the path of the current executable file: {}", e))?;
|
||||
|
||||
Ok(exe_path)
|
||||
}
|
||||
@@ -39,11 +39,11 @@ pub fn get_exe_path() -> Result<PathBuf> {
|
||||
pub fn create_shortcut() -> Result<()> {
|
||||
let exe_path = get_exe_path()?;
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
|
||||
|
||||
// 如果快捷方式已存在,直接返回成功
|
||||
// If the shortcut already exists, return success directly
|
||||
if shortcut_path.exists() {
|
||||
info!(target: "app", "启动快捷方式已存在");
|
||||
info!(target: "app", "Startup shortcut already exists");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -59,36 +59,36 @@ pub fn create_shortcut() -> Result<()> {
|
||||
|
||||
let output = std::process::Command::new("powershell")
|
||||
.args(["-Command", &powershell_command])
|
||||
// 隐藏 PowerShell 窗口
|
||||
// Hide the PowerShell window
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||
.output()
|
||||
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to execute PowerShell command: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error_msg = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!("创建快捷方式失败: {}", error_msg));
|
||||
return Err(anyhow!("Failed to create shortcut: {}", error_msg));
|
||||
}
|
||||
|
||||
info!(target: "app", "成功创建启动快捷方式");
|
||||
info!(target: "app", "Successfully created startup shortcut");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除快捷方式
|
||||
/// Remove the shortcut
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn remove_shortcut() -> Result<()> {
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
|
||||
|
||||
// 如果快捷方式不存在,直接返回成功
|
||||
// If the shortcut does not exist, return success directly
|
||||
if !shortcut_path.exists() {
|
||||
info!(target: "app", "启动快捷方式不存在,无需删除");
|
||||
info!(target: "app", "Startup shortcut does not exist, nothing to remove");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 删除快捷方式
|
||||
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?;
|
||||
// Delete the shortcut
|
||||
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("Failed to delete shortcut: {}", e))?;
|
||||
|
||||
info!(target: "app", "成功删除启动快捷方式");
|
||||
info!(target: "app", "Successfully removed startup shortcut");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ pub fn remove_shortcut() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_shortcut_enabled() -> Result<bool> {
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
|
||||
|
||||
Ok(shortcut_path.exists())
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ use std::{fs, path::PathBuf};
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
|
||||
pub static APP_ID: &str = "io.github.koala-clash";
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
pub static BACKUP_DIR: &str = "clash-verge-rev-backup";
|
||||
pub static BACKUP_DIR: &str = "io.github.koala-clash-backup";
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
|
||||
pub static APP_ID: &str = "io.github.koala-clash.dev";
|
||||
#[cfg(feature = "verge-dev")]
|
||||
pub static BACKUP_DIR: &str = "clash-verge-rev-backup-dev";
|
||||
pub static BACKUP_DIR: &str = "io.github.koala-clash-backup-dev";
|
||||
|
||||
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
|
||||
|
||||
@@ -188,13 +188,13 @@ pub fn profiles_path() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn service_path() -> Result<PathBuf> {
|
||||
let res_dir = app_resources_dir()?;
|
||||
Ok(res_dir.join("clash-verge-service"))
|
||||
Ok(res_dir.join("koala-clash-service"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn service_path() -> Result<PathBuf> {
|
||||
let res_dir = app_resources_dir()?;
|
||||
Ok(res_dir.join("clash-verge-service.exe"))
|
||||
Ok(res_dir.join("koala-clash-service.exe"))
|
||||
}
|
||||
|
||||
pub fn service_log_file() -> Result<PathBuf> {
|
||||
|
||||
@@ -178,9 +178,8 @@ fn init_dns_config() -> Result<()> {
|
||||
"default-nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("system".into()),
|
||||
Value::String("223.6.6.6".into()),
|
||||
Value::String("8.8.8.8".into()),
|
||||
Value::String("2400:3200::1".into()),
|
||||
Value::String("1.1.1.1".into()),
|
||||
Value::String("2001:4860:4860::8888".into()),
|
||||
]),
|
||||
),
|
||||
@@ -189,7 +188,8 @@ fn init_dns_config() -> Result<()> {
|
||||
Value::Sequence(vec![
|
||||
Value::String("8.8.8.8".into()),
|
||||
Value::String("https://doh.pub/dns-query".into()),
|
||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||
Value::String("https://dns.google/dns-query".into()),
|
||||
Value::String("https://cloudflare-dns.com/dns-query".into()),
|
||||
]),
|
||||
),
|
||||
("fallback".into(), Value::Sequence(vec![])),
|
||||
@@ -201,8 +201,9 @@ fn init_dns_config() -> Result<()> {
|
||||
"proxy-server-nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("https://doh.pub/dns-query".into()),
|
||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||
Value::String("tls://223.5.5.5".into()),
|
||||
Value::String("https://dns.google/dns-query".into()),
|
||||
Value::String("https://cloudflare-dns.com/dns-query".into()),
|
||||
Value::String("tls://1.1.1.1".into()),
|
||||
]),
|
||||
),
|
||||
("direct-nameserver".into(), Value::Sequence(vec![])),
|
||||
@@ -246,7 +247,7 @@ fn init_dns_config() -> Result<()> {
|
||||
help::save_yaml(
|
||||
&dns_path,
|
||||
&default_dns_config,
|
||||
Some("# Clash Verge DNS Config"),
|
||||
Some("# Koala Clash DNS Config"),
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -274,14 +275,14 @@ pub fn init_config() -> Result<()> {
|
||||
|
||||
crate::log_err!(dirs::clash_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Vergeasu"))?;
|
||||
help::save_yaml(&path, &IClashTemp::template().0, Some("# Koala Clash"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::verge_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
|
||||
help::save_yaml(&path, &IVerge::template(), Some("# Koala Clash"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
@@ -291,7 +292,7 @@ pub fn init_config() -> Result<()> {
|
||||
|
||||
crate::log_err!(dirs::profiles_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
|
||||
help::save_yaml(&path, &IProfiles::template(), Some("# Koala Clash"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
@@ -371,8 +372,8 @@ pub fn init_scheme() -> Result<()> {
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
|
||||
clash.set_value("", &"Clash Verge")?;
|
||||
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
|
||||
clash.set_value("", &"Koala Clash")?;
|
||||
clash.set_value("URL Protocol", &"Koala Clash URL Scheme Protocol")?;
|
||||
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
|
||||
default_icon.set_value("", &app_exe)?;
|
||||
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
|
||||
@@ -384,7 +385,7 @@ pub fn init_scheme() -> Result<()> {
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
let output = std::process::Command::new("xdg-mime")
|
||||
.arg("default")
|
||||
.arg("clash-verge.desktop")
|
||||
.arg("koala-clash.desktop")
|
||||
.arg("x-scheme-handler/clash")
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
@@ -397,6 +398,33 @@ pub fn init_scheme() -> Result<()> {
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
use std::process::Command;
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
// Try to re-register the app bundle with LaunchServices to ensure URL schemes are active
|
||||
if let Ok(exe) = current_exe() {
|
||||
if let (Some(_parent1), Some(_parent2), Some(app_bundle)) =
|
||||
(exe.parent(), exe.parent().and_then(|p| p.parent()), exe.parent().and_then(|p| p.parent()).and_then(|p| p.parent()))
|
||||
{
|
||||
let app_bundle_path = app_bundle.to_string_lossy().into_owned();
|
||||
let lsregister = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
||||
let output = Command::new(lsregister)
|
||||
.args(["-f", "-R", &app_bundle_path])
|
||||
.output();
|
||||
match output {
|
||||
Ok(out) => {
|
||||
if !out.status.success() {
|
||||
log::warn!(target: "app", "lsregister returned non-zero: {:?}", out.status);
|
||||
} else {
|
||||
log::info!(target: "app", "Re-registered URL schemes with LaunchServices");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "Failed to run lsregister: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@ pub mod network;
|
||||
pub mod notification;
|
||||
pub mod resolve;
|
||||
pub mod server;
|
||||
pub mod sys_info;
|
||||
pub mod tmpl;
|
||||
pub mod window_manager;
|
||||
pub mod sys_info;
|
||||
|
||||
@@ -40,7 +40,7 @@ impl NetworkManager {
|
||||
// 创建专用的异步运行时,线程数限制为4个
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.worker_threads(4)
|
||||
.thread_name("clash-verge-network")
|
||||
.thread_name("koala-clash-network")
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
@@ -65,7 +65,7 @@ impl NetworkManager {
|
||||
pub fn init(&self) {
|
||||
self.init.call_once(|| {
|
||||
self.runtime.spawn(async {
|
||||
logging!(info, Type::Network, true, "初始化网络管理器");
|
||||
logging!(info, Type::Network, true, "Initializing network manager");
|
||||
|
||||
// 创建无代理客户端
|
||||
let no_proxy_client = ClientBuilder::new()
|
||||
@@ -81,7 +81,7 @@ impl NetworkManager {
|
||||
let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock().unwrap();
|
||||
*no_proxy_guard = Some(no_proxy_client);
|
||||
|
||||
logging!(info, Type::Network, true, "网络管理器初始化完成");
|
||||
logging!(info, Type::Network, true, "Network manager initialization completed");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -112,7 +112,7 @@ impl NetworkManager {
|
||||
}
|
||||
|
||||
pub fn reset_clients(&self) {
|
||||
logging!(info, Type::Network, true, "正在重置所有HTTP客户端");
|
||||
logging!(info, Type::Network, true, "Resetting all HTTP clients");
|
||||
{
|
||||
let mut client = self.self_proxy_client.lock().unwrap();
|
||||
*client = None;
|
||||
@@ -323,8 +323,8 @@ impl NetworkManager {
|
||||
use crate::utils::resolve::VERSION;
|
||||
|
||||
let version = match VERSION.get() {
|
||||
Some(v) => format!("clash-verge/v{v}"),
|
||||
None => "clash-verge/unknown".to_string(),
|
||||
Some(v) => format!("koala-clash/v{v}"),
|
||||
None => "koala-clash/unknown".to_string(),
|
||||
};
|
||||
|
||||
builder = builder.user_agent(version);
|
||||
@@ -409,7 +409,7 @@ impl NetworkManager {
|
||||
let watchdog = tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(timeout_duration)).await;
|
||||
let _ = cancel_tx.send(());
|
||||
logging!(warn, Type::Network, true, "请求超时取消: {}", url_clone);
|
||||
logging!(warn, Type::Network, true, "Request canceled due to timeout: {}", url_clone);
|
||||
});
|
||||
|
||||
let result = tokio::select! {
|
||||
|
||||
@@ -3,10 +3,11 @@ use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::{Config, IVerge, PrfItem},
|
||||
core::*,
|
||||
core::handle::Handle,
|
||||
logging, logging_error,
|
||||
module::lightweight::{self, auto_lightweight_mode_init},
|
||||
process::AsyncHandler,
|
||||
utils::{init, logging::Type, server},
|
||||
utils::{init, logging::Type, server, window_manager::WindowManager},
|
||||
wrap_err,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
@@ -23,7 +24,6 @@ use tauri::{AppHandle, Manager};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use tauri::Url;
|
||||
use crate::config::PrfOption;
|
||||
//#[cfg(not(target_os = "linux"))]
|
||||
// use window_shadows::set_shadow;
|
||||
|
||||
@@ -66,6 +66,35 @@ impl Default for UiReadyState {
|
||||
// 获取UI就绪状态细节
|
||||
static UI_READY_STATE: OnceCell<Arc<UiReadyState>> = OnceCell::new();
|
||||
|
||||
// Early deep link capture on cold start
|
||||
static EARLY_DEEP_LINK: OnceCell<Mutex<Option<String>>> = OnceCell::new();
|
||||
// Deduplication for deep links to avoid processing same URL twice in short time
|
||||
static LAST_DEEP_LINK: OnceCell<Mutex<Option<(String, Instant)>>> = OnceCell::new();
|
||||
|
||||
fn get_early_deep_link() -> &'static Mutex<Option<String>> {
|
||||
EARLY_DEEP_LINK.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
/// Capture deep link from process arguments as early as possible (cold start on macOS)
|
||||
pub fn capture_early_deep_link_from_args() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if let Some(url) = args.iter().find(|a| a.starts_with("clash://") || a.starts_with("koala-clash://")).cloned() {
|
||||
println!("[DeepLink][argv] {}", url);
|
||||
logging!(info, Type::Setup, true, "argv captured deep link: {}", url);
|
||||
*get_early_deep_link().lock() = Some(url);
|
||||
} else {
|
||||
println!("[DeepLink][argv] none: {:?}", args);
|
||||
logging!(info, Type::Setup, true, "no deep link found in argv at startup: {:?}", args);
|
||||
}
|
||||
}
|
||||
|
||||
/// If an early deep link was captured before setup, schedule it now
|
||||
pub fn replay_early_deep_link() {
|
||||
if let Some(url) = get_early_deep_link().lock().take() {
|
||||
schedule_handle_deep_link(url);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
|
||||
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
|
||||
}
|
||||
@@ -74,6 +103,11 @@ fn get_ui_ready() -> &'static Arc<RwLock<bool>> {
|
||||
UI_READY.get_or_init(|| Arc::new(RwLock::new(false)))
|
||||
}
|
||||
|
||||
/// Check whether the UI has finished initialization on the frontend side
|
||||
pub fn is_ui_ready() -> bool {
|
||||
*get_ui_ready().read()
|
||||
}
|
||||
|
||||
fn get_ui_ready_state() -> &'static Arc<UiReadyState> {
|
||||
UI_READY_STATE.get_or_init(|| Arc::new(UiReadyState::default()))
|
||||
}
|
||||
@@ -94,7 +128,10 @@ pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
||||
pub fn mark_ui_ready() {
|
||||
let mut ready = get_ui_ready().write();
|
||||
*ready = true;
|
||||
logging!(info, Type::Window, true, "UI已标记为完全就绪");
|
||||
logging!(info, Type::Window, true, "UI marked as fully ready");
|
||||
|
||||
// If any deep links were queued while UI was not ready, handle them now
|
||||
// No queued deep links list anymore; early and runtime deep links are deduped
|
||||
}
|
||||
|
||||
// 重置UI就绪状态
|
||||
@@ -108,7 +145,83 @@ pub fn reset_ui_ready() {
|
||||
let mut stage = state.stage.write();
|
||||
*stage = UiReadyStage::NotStarted;
|
||||
}
|
||||
logging!(info, Type::Window, true, "UI就绪状态已重置");
|
||||
logging!(info, Type::Window, true, "UI readiness state has been reset");
|
||||
}
|
||||
|
||||
/// Schedule robust deep-link handling to avoid races with lightweight mode and window creation
|
||||
pub fn schedule_handle_deep_link(url: String) {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
// Normalize dedup key to the actual subscription URL inside the deep link
|
||||
let dedup_key = (|| {
|
||||
if let Ok(parsed) = Url::parse(&url) {
|
||||
for (k, v) in parsed.query_pairs() {
|
||||
if k == "url" {
|
||||
return percent_decode_str(&v).decode_utf8_lossy().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
url.clone()
|
||||
})();
|
||||
|
||||
// Deduplicate: if the same deep/subscription link was handled very recently, skip
|
||||
{
|
||||
let now = Instant::now();
|
||||
let mut last = LAST_DEEP_LINK.get_or_init(|| Mutex::new(None)).lock();
|
||||
if let Some((prev_url, prev_time)) = last.as_ref() {
|
||||
if *prev_url == dedup_key && now.duration_since(*prev_time) < Duration::from_secs(5) {
|
||||
log::warn!(target: "app", "Skip duplicate deep link within 5s: {}", dedup_key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
*last = Some((dedup_key.clone(), now));
|
||||
}
|
||||
// Wait until app handle exists
|
||||
for i in 0..100u8 {
|
||||
if Handle::global().app_handle().is_some() {
|
||||
break;
|
||||
}
|
||||
if i % 10 == 0 { logging!(info, Type::Setup, true, "waiting for app handle... ({}ms)", i as u64 * 20); }
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
|
||||
// Ensure we are not in lightweight mode (webview destroyed)
|
||||
lightweight::exit_lightweight_mode();
|
||||
for _ in 0..150u16 {
|
||||
if !lightweight::is_in_lightweight_mode() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
|
||||
// Ensure a window exists ASAP so UI can mount
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
// If lightweight mode was active, give it a bit of time to unwind before recreating window
|
||||
if lightweight::is_in_lightweight_mode() {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
let _ = WindowManager::show_main_window();
|
||||
|
||||
// Ensure profiles directory exists on cold start
|
||||
if let Ok(dir) = crate::utils::dirs::app_profiles_dir() {
|
||||
if !dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
// Process deep link (add profile regardless of UI state)
|
||||
logging!(info, Type::Setup, true, "processing deep link: {}", dedup_key);
|
||||
if let Err(e) = resolve_scheme(url.clone()).await {
|
||||
log::error!(target: "app", "Deep link handling failed: {e}");
|
||||
}
|
||||
|
||||
// If UI is ready, small delay to let listeners settle before finishing
|
||||
if is_ui_ready() {
|
||||
tokio::time::sleep(Duration::from_millis(120)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn find_unused_port() -> Result<u16> {
|
||||
@@ -131,12 +244,12 @@ pub async fn find_unused_port() -> Result<u16> {
|
||||
/// 异步方式处理启动后的额外任务
|
||||
pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
let start_time = std::time::Instant::now();
|
||||
logging!(info, Type::Setup, true, "开始执行异步设置任务...");
|
||||
logging!(info, Type::Setup, true, "Starting asynchronous setup tasks...");
|
||||
|
||||
if VERSION.get().is_none() {
|
||||
let version = app_handle.package_info().version.to_string();
|
||||
VERSION.get_or_init(|| {
|
||||
logging!(info, Type::Setup, true, "初始化版本信息: {}", version);
|
||||
logging!(info, Type::Setup, true, "Initializing version information: {}", version);
|
||||
version.clone()
|
||||
});
|
||||
}
|
||||
@@ -155,40 +268,40 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
);
|
||||
}
|
||||
|
||||
logging!(trace, Type::Config, true, "初始化配置...");
|
||||
logging!(trace, Type::Config, true, "Initializing configuration...");
|
||||
logging_error!(Type::Config, true, Config::init_config().await);
|
||||
|
||||
// 启动时清理冗余的 Profile 文件
|
||||
logging!(info, Type::Setup, true, "清理冗余的Profile文件...");
|
||||
logging!(info, Type::Setup, true, "Cleaning redundant profile files...");
|
||||
let profiles = Config::profiles();
|
||||
if let Err(e) = profiles.latest().auto_cleanup() {
|
||||
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
|
||||
logging!(warn, Type::Setup, true, "Failed to clean profile files at startup: {}", e);
|
||||
} else {
|
||||
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
|
||||
logging!(info, Type::Setup, true, "Startup profile files cleanup completed");
|
||||
}
|
||||
|
||||
logging!(trace, Type::Core, true, "启动核心管理器...");
|
||||
logging!(trace, Type::Core, true, "Starting core manager...");
|
||||
logging_error!(Type::Core, true, CoreManager::global().init().await);
|
||||
|
||||
log::trace!(target: "app", "启动内嵌服务器...");
|
||||
log::trace!(target: "app", "Starting embedded server...");
|
||||
server::embed_server();
|
||||
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().init());
|
||||
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
logging!(info, Type::Tray, true, "创建系统托盘...");
|
||||
logging!(info, Type::Tray, true, "Creating system tray...");
|
||||
let result = tray::Tray::global().create_tray_from_handle(&app_handle);
|
||||
if result.is_ok() {
|
||||
logging!(info, Type::Tray, true, "系统托盘创建成功");
|
||||
logging!(info, Type::Tray, true, "System tray created successfully");
|
||||
} else if let Err(e) = result {
|
||||
logging!(error, Type::Tray, true, "系统托盘创建失败: {}", e);
|
||||
logging!(error, Type::Tray, true, "Failed to create system tray: {}", e);
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
true,
|
||||
"无法创建系统托盘: app_handle不存在"
|
||||
"Unable to create system tray: app_handle missing"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,7 +337,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
|
||||
|
||||
logging!(trace, Type::System, true, "初始化热键...");
|
||||
logging!(trace, Type::System, true, "Initializing hotkeys...");
|
||||
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
@@ -232,7 +345,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"异步设置任务完成,耗时: {:?}",
|
||||
"Asynchronous task completed, time taken: {:?}",
|
||||
elapsed
|
||||
);
|
||||
|
||||
@@ -242,7 +355,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"异步设置任务耗时较长({:?})",
|
||||
"Asynchronous task setup takes a long time ({:?})",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
@@ -274,12 +387,12 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"开始创建/显示主窗口, is_show={}",
|
||||
"Creating/showing main window, is_show={}",
|
||||
is_show
|
||||
);
|
||||
|
||||
if !is_show {
|
||||
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
|
||||
logging!(info, Type::Window, true, "Silent start: do not create window");
|
||||
lightweight::set_lightweight_mode(true);
|
||||
handle::Handle::notify_startup_completed();
|
||||
return false;
|
||||
@@ -287,21 +400,34 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
|
||||
logging!(info, Type::Window, true, "Main window already exists; will try to show it");
|
||||
if is_show {
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
|
||||
logging!(info, Type::Window, true, "Window is minimized; unminimizing");
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
let show_result = window.show();
|
||||
let focus_result = window.set_focus();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
// If showing or focusing fails (possibly destroyed webview after lightweight), fallback to recreate
|
||||
if show_result.is_err() || focus_result.is_err() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"Failed to show existing window; will destroy and recreate"
|
||||
);
|
||||
let _ = window.destroy();
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +442,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
|
||||
"Window creation request ignored because recently created ({:?}ms)",
|
||||
elapsed.as_millis()
|
||||
);
|
||||
return false;
|
||||
@@ -327,7 +453,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
// ScopeGuard 确保创建状态重置,防止 webview 卡死
|
||||
let _guard = scopeguard::guard(creating, |mut creating_guard| {
|
||||
*creating_guard = (false, Instant::now());
|
||||
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
|
||||
logging!(debug, Type::Window, true, "[ScopeGuard] Window creation state reset");
|
||||
});
|
||||
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
@@ -335,25 +461,25 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
"main", /* the unique window label */
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge Rev Lite")
|
||||
.title("Koala Clash")
|
||||
.center()
|
||||
.decorations(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
|
||||
.min_inner_size(520.0, 520.0)
|
||||
.min_inner_size(1000.0, 800.0)
|
||||
.visible(true) // 立即显示窗口,避免用户等待
|
||||
.initialization_script(
|
||||
r#"
|
||||
console.log('[Tauri] 窗口初始化脚本开始执行');
|
||||
console.log('[Tauri] Window init script started');
|
||||
|
||||
function createLoadingOverlay() {
|
||||
|
||||
if (document.getElementById('initial-loading-overlay')) {
|
||||
console.log('[Tauri] 加载指示器已存在');
|
||||
console.log('[Tauri] Loading indicator already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Tauri] 创建加载指示器');
|
||||
console.log('[Tauri] Creating loading indicator');
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = 'initial-loading-overlay';
|
||||
loadingDiv.innerHTML = `
|
||||
@@ -372,7 +498,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
animation: spin 1s linear infinite;
|
||||
"></div>
|
||||
</div>
|
||||
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
|
||||
<div style="font-size: 14px; opacity: 0.7;">Loading Koala Clash...</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
@@ -404,13 +530,13 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
createLoadingOverlay();
|
||||
}
|
||||
|
||||
console.log('[Tauri] 窗口初始化脚本执行完成');
|
||||
console.log('[Tauri] Window init script finished');
|
||||
"#,
|
||||
)
|
||||
.build()
|
||||
{
|
||||
Ok(newly_created_window) => {
|
||||
logging!(debug, Type::Window, true, "主窗口实例创建成功");
|
||||
logging!(debug, Type::Window, true, "Main window instance created successfully");
|
||||
|
||||
update_ui_ready_stage(UiReadyStage::NotStarted);
|
||||
|
||||
@@ -420,7 +546,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"异步窗口任务开始 (启动已标记完成)"
|
||||
"Async window task started (startup marked completed)"
|
||||
);
|
||||
|
||||
// 先运行轻量模式检测
|
||||
@@ -431,7 +557,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"发送 verge://startup-completed 事件"
|
||||
"Sending verge://startup-completed event"
|
||||
);
|
||||
handle::Handle::notify_startup_completed();
|
||||
|
||||
@@ -441,7 +567,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
// 立即显示窗口
|
||||
let _ = window_clone.show();
|
||||
let _ = window_clone.set_focus();
|
||||
logging!(info, Type::Window, true, "窗口已立即显示");
|
||||
logging!(info, Type::Window, true, "Window shown immediately");
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
@@ -457,7 +583,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"开始监控UI加载状态 (最多{}秒)...",
|
||||
"Start monitoring UI load status (up to {} seconds)...",
|
||||
timeout_seconds
|
||||
);
|
||||
|
||||
@@ -476,7 +602,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI加载状态检查... ({}秒)",
|
||||
"UI loading status check... ({}s)",
|
||||
check_count / 10
|
||||
);
|
||||
}
|
||||
@@ -486,7 +612,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
|
||||
match wait_result {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Window, true, "UI已完全加载就绪");
|
||||
logging!(info, Type::Window, true, "UI fully loaded and ready");
|
||||
// 移除初始加载指示器
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let _ = window.eval(r#"
|
||||
@@ -503,7 +629,7 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI加载监控超时({}秒),但窗口已正常显示",
|
||||
"UI load monitoring timed out ({}s), but window is already visible",
|
||||
timeout_seconds
|
||||
);
|
||||
*get_ui_ready().write() = true;
|
||||
@@ -511,20 +637,20 @@ pub fn create_window(is_show: bool) -> bool {
|
||||
}
|
||||
});
|
||||
|
||||
logging!(info, Type::Window, true, "窗口显示流程完成");
|
||||
logging!(info, Type::Window, true, "Window display flow completed");
|
||||
} else {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"is_show为false,窗口保持隐藏状态"
|
||||
"is_show is false; keeping window hidden"
|
||||
);
|
||||
}
|
||||
});
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
|
||||
logging!(error, Type::Window, true, "Failed to build main window: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -549,39 +675,30 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "koala-clash" {
|
||||
let mut name: Option<String> = None;
|
||||
let mut url_param: Option<String> = None;
|
||||
let mut use_hwid = true;
|
||||
|
||||
for (key, value) in link_parsed.query_pairs() {
|
||||
match key.as_ref() {
|
||||
"name" => name = Some(value.into_owned()),
|
||||
"url" => url_param = Some(percent_decode_str(&value).decode_utf8_lossy().to_string()),
|
||||
"hwid" => use_hwid = value == "1" || value == "true",
|
||||
"url" => {
|
||||
url_param = Some(percent_decode_str(&value).decode_utf8_lossy().to_string())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let option = if use_hwid {
|
||||
log::info!(target:"app", "HWID usage requested via deep link");
|
||||
Some(PrfOption {
|
||||
use_hwid: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match url_param {
|
||||
Some(url) => {
|
||||
log::info!(target:"app", "decoded subscription url: {url}");
|
||||
|
||||
create_window(false);
|
||||
match PrfItem::from_url(url.as_ref(), name, None, option).await {
|
||||
// Deep link inside resolver is now executed via schedule_handle_deep_link
|
||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||
Ok(item) => {
|
||||
let uid = item.uid.clone().unwrap();
|
||||
let _ = wrap_err!(Config::profiles().data().append_item(item));
|
||||
// If UI not ready yet, message will be queued and flushed on ready
|
||||
handle::Handle::notice_message("import_sub_url::ok", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -7,8 +7,7 @@ use crate::{
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use port_scanner::local_port_available;
|
||||
use anyhow::Result;
|
||||
use std::convert::Infallible;
|
||||
use warp::Filter;
|
||||
|
||||
@@ -17,32 +16,6 @@ struct QueryParam {
|
||||
param: String,
|
||||
}
|
||||
|
||||
/// check whether there is already exists
|
||||
pub async fn check_singleton() -> Result<()> {
|
||||
let port = IVerge::get_singleton_port();
|
||||
if !local_port_available(port) {
|
||||
let argvs: Vec<String> = std::env::args().collect();
|
||||
if argvs.len() > 1 {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let param = argvs[1].as_str();
|
||||
if param.starts_with("clash:") {
|
||||
let _ = reqwest::get(format!(
|
||||
"http://127.0.0.1:{port}/commands/scheme?param={param}"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = reqwest::get(format!("http://127.0.0.1:{port}/commands/visible")).await;
|
||||
}
|
||||
log::error!("failed to setup singleton listen server");
|
||||
bail!("app exists");
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The embed server only be used to implement singleton process
|
||||
/// maybe it can be used as pac server later
|
||||
pub fn embed_server() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Some config file template
|
||||
|
||||
/// template for new a profile item
|
||||
pub const ITEM_LOCAL: &str = "# Profile Template for Clash Verge
|
||||
pub const ITEM_LOCAL: &str = "# Profile Template for Koala Clash
|
||||
|
||||
proxies: []
|
||||
|
||||
@@ -11,13 +11,13 @@ rules: []
|
||||
";
|
||||
|
||||
/// enhanced profile
|
||||
pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Clash Verge
|
||||
pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Koala Clash
|
||||
|
||||
profile:
|
||||
store-selected: true
|
||||
";
|
||||
|
||||
pub const ITEM_MERGE_EMPTY: &str = "# Profile Enhancement Merge Template for Clash Verge
|
||||
pub const ITEM_MERGE_EMPTY: &str = "# Profile Enhancement Merge Template for Koala Clash
|
||||
|
||||
";
|
||||
|
||||
@@ -30,7 +30,7 @@ function main(config, profileName) {
|
||||
";
|
||||
|
||||
/// enhanced profile
|
||||
pub const ITEM_RULES: &str = "# Profile Enhancement Rules Template for Clash Verge
|
||||
pub const ITEM_RULES: &str = "# Profile Enhancement Rules Template for Koala Clash
|
||||
|
||||
prepend: []
|
||||
|
||||
@@ -40,7 +40,7 @@ delete: []
|
||||
";
|
||||
|
||||
/// enhanced profile
|
||||
pub const ITEM_PROXIES: &str = "# Profile Enhancement Proxies Template for Clash Verge
|
||||
pub const ITEM_PROXIES: &str = "# Profile Enhancement Proxies Template for Koala Clash
|
||||
|
||||
prepend: []
|
||||
|
||||
@@ -50,7 +50,7 @@ delete: []
|
||||
";
|
||||
|
||||
/// enhanced profile
|
||||
pub const ITEM_GROUPS: &str = "# Profile Enhancement Groups Template for Clash Verge
|
||||
pub const ITEM_GROUPS: &str = "# Profile Enhancement Groups Template for Koala Clash
|
||||
|
||||
prepend: []
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ fn get_window_operation_debounce() -> &'static Mutex<Instant> {
|
||||
|
||||
fn should_handle_window_operation() -> bool {
|
||||
if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) {
|
||||
log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用");
|
||||
log::warn!(target: "app", "[debounce] Window operation already in progress, skipping duplicate call");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -62,16 +62,16 @@ fn should_handle_window_operation() -> bool {
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(*last_operation);
|
||||
|
||||
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
|
||||
log::debug!(target: "app", "[debounce] Checking window operation interval: {}ms (need >={}ms)",
|
||||
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||
|
||||
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
|
||||
*last_operation = now;
|
||||
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
|
||||
log::info!(target: "app", "[防抖] 窗口操作被允许执行");
|
||||
log::info!(target: "app", "[debounce] Window operation allowed to execute");
|
||||
true
|
||||
} else {
|
||||
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
|
||||
log::warn!(target: "app", "[debounce] Window operation ignored by debounce: {}ms since last < {}ms",
|
||||
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
|
||||
false
|
||||
}
|
||||
@@ -127,7 +127,7 @@ impl WindowManager {
|
||||
finish_window_operation();
|
||||
});
|
||||
|
||||
logging!(info, Type::Window, true, "开始智能显示主窗口");
|
||||
logging!(info, Type::Window, true, "Starting smart show for main window");
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
@@ -140,18 +140,18 @@ impl WindowManager {
|
||||
|
||||
match current_state {
|
||||
WindowState::NotExist => {
|
||||
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
|
||||
logging!(info, Type::Window, true, "Main window not found; creating new window");
|
||||
if Self::create_new_window() {
|
||||
logging!(info, Type::Window, true, "窗口创建成功");
|
||||
logging!(info, Type::Window, true, "Window created successfully");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
WindowOperationResult::Created
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "窗口创建失败");
|
||||
logging!(warn, Type::Window, true, "Window creation failed");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
WindowState::VisibleFocused => {
|
||||
logging!(info, Type::Window, true, "窗口已经可见且有焦点,无需操作");
|
||||
logging!(info, Type::Window, true, "Window already visible and focused; no action needed");
|
||||
WindowOperationResult::NoAction
|
||||
}
|
||||
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
|
||||
@@ -184,14 +184,14 @@ impl WindowManager {
|
||||
finish_window_operation();
|
||||
});
|
||||
|
||||
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
|
||||
logging!(info, Type::Window, true, "Toggling main window visibility");
|
||||
|
||||
let current_state = Self::get_main_window_state();
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"当前窗口状态: {:?} | 详细状态: {}",
|
||||
"Current window state: {:?} | Details: {}",
|
||||
current_state,
|
||||
Self::get_window_status_info()
|
||||
);
|
||||
@@ -199,7 +199,7 @@ impl WindowManager {
|
||||
match current_state {
|
||||
WindowState::NotExist => {
|
||||
// 窗口不存在,创建新窗口
|
||||
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
|
||||
logging!(info, Type::Window, true, "Main window not found; will create new window");
|
||||
// 由于已经有防抖保护,直接调用内部方法
|
||||
if Self::create_new_window() {
|
||||
WindowOperationResult::Created
|
||||
@@ -212,26 +212,26 @@ impl WindowManager {
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"窗口可见(焦点状态: {}),将隐藏窗口",
|
||||
"Window visible (focused: {}), hiding window",
|
||||
if current_state == WindowState::VisibleFocused {
|
||||
"有焦点"
|
||||
"focused"
|
||||
} else {
|
||||
"无焦点"
|
||||
"unfocused"
|
||||
}
|
||||
);
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
match window.hide() {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Window, true, "窗口已成功隐藏");
|
||||
logging!(info, Type::Window, true, "Window hidden successfully");
|
||||
WindowOperationResult::Hidden
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
|
||||
logging!(warn, Type::Window, true, "Failed to hide window: {}", e);
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "无法获取窗口实例");
|
||||
logging!(warn, Type::Window, true, "Unable to get window instance");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
@@ -240,12 +240,12 @@ impl WindowManager {
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"窗口存在但被隐藏或最小化,将激活窗口"
|
||||
"Window exists but is hidden or minimized; activating"
|
||||
);
|
||||
if let Some(window) = Self::get_main_window() {
|
||||
Self::activate_window(&window)
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "无法获取窗口实例");
|
||||
logging!(warn, Type::Window, true, "Unable to get window instance");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
@@ -254,35 +254,35 @@ impl WindowManager {
|
||||
|
||||
/// 激活窗口(取消最小化、显示、设置焦点)
|
||||
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
|
||||
logging!(info, Type::Window, true, "开始激活窗口");
|
||||
logging!(info, Type::Window, true, "Starting to activate window");
|
||||
|
||||
let mut operations_successful = true;
|
||||
|
||||
// 1. 如果窗口最小化,先取消最小化
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
|
||||
logging!(info, Type::Window, true, "Window minimized; unminimizing");
|
||||
if let Err(e) = window.unminimize() {
|
||||
logging!(warn, Type::Window, true, "取消最小化失败: {}", e);
|
||||
logging!(warn, Type::Window, true, "Failed to unminimize window: {}", e);
|
||||
operations_successful = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 显示窗口
|
||||
if let Err(e) = window.show() {
|
||||
logging!(warn, Type::Window, true, "显示窗口失败: {}", e);
|
||||
logging!(warn, Type::Window, true, "Failed to show window: {}", e);
|
||||
operations_successful = false;
|
||||
}
|
||||
|
||||
// 3. 设置焦点
|
||||
if let Err(e) = window.set_focus() {
|
||||
logging!(warn, Type::Window, true, "设置窗口焦点失败: {}", e);
|
||||
logging!(warn, Type::Window, true, "Failed to set window focus: {}", e);
|
||||
operations_successful = false;
|
||||
}
|
||||
|
||||
// 4. 平台特定的激活策略
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
|
||||
logging!(info, Type::Window, true, "Applying macOS-specific activation policy");
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ impl WindowManager {
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"设置置顶失败(非关键错误): {}",
|
||||
"Failed to set always-on-top (non-critical): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
@@ -304,38 +304,38 @@ impl WindowManager {
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"取消置顶失败(非关键错误): {}",
|
||||
"Failed to unset always-on-top (non-critical): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if operations_successful {
|
||||
logging!(info, Type::Window, true, "窗口激活成功");
|
||||
logging!(info, Type::Window, true, "Window activation successful");
|
||||
WindowOperationResult::Shown
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "窗口激活部分失败");
|
||||
logging!(warn, Type::Window, true, "Window activation partially failed");
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
}
|
||||
|
||||
/// 隐藏主窗口
|
||||
pub fn hide_main_window() -> WindowOperationResult {
|
||||
logging!(info, Type::Window, true, "开始隐藏主窗口");
|
||||
logging!(info, Type::Window, true, "Starting to hide main window");
|
||||
|
||||
match Self::get_main_window() {
|
||||
Some(window) => match window.hide() {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Window, true, "窗口已隐藏");
|
||||
logging!(info, Type::Window, true, "Window hidden");
|
||||
WindowOperationResult::Hidden
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
|
||||
logging!(warn, Type::Window, true, "Failed to hide window: {}", e);
|
||||
WindowOperationResult::Failed
|
||||
}
|
||||
},
|
||||
None => {
|
||||
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
|
||||
logging!(info, Type::Window, true, "Window does not exist; nothing to hide");
|
||||
WindowOperationResult::NoAction
|
||||
}
|
||||
}
|
||||
@@ -376,7 +376,7 @@ impl WindowManager {
|
||||
let is_minimized = Self::is_main_window_minimized();
|
||||
|
||||
format!(
|
||||
"窗口状态: {state:?} | 可见: {is_visible} | 有焦点: {is_focused} | 最小化: {is_minimized}"
|
||||
"WindowState: {state:?} | visible: {is_visible} | focused: {is_focused} | minimized: {is_minimized}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.7",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"longDescription": "Clash Verge Rev Lite",
|
||||
"longDescription": "Koala Clash",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
@@ -11,12 +11,18 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["resources", "resources/locales/*"],
|
||||
"publisher": "Clash Verge Rev Lite",
|
||||
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
||||
"resources": [
|
||||
"resources",
|
||||
"resources/locales/*"
|
||||
],
|
||||
"publisher": "Koala Clash",
|
||||
"externalBin": [
|
||||
"sidecar/koala-mihomo",
|
||||
"sidecar/koala-mihomo-alpha"
|
||||
],
|
||||
"copyright": "GNU General Public License v3.0",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Clash Verge Rev Lite",
|
||||
"shortDescription": "Koala Clash",
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"build": {
|
||||
@@ -25,8 +31,8 @@
|
||||
"beforeDevCommand": "pnpm run web:dev",
|
||||
"devUrl": "http://localhost:3000/"
|
||||
},
|
||||
"productName": "Clash Verge Rev Lite",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"productName": "Koala Clash",
|
||||
"identifier": "io.github.koala-clash",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K",
|
||||
@@ -40,18 +46,28 @@
|
||||
},
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["clash", "clash-verge"]
|
||||
"schemes": [
|
||||
"clash",
|
||||
"koala-clash"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"capabilities": ["desktop-capability", "migrated"],
|
||||
"capabilities": [
|
||||
"desktop-capability",
|
||||
"migrated"
|
||||
],
|
||||
"assetProtocol": {
|
||||
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
|
||||
"scope": [
|
||||
"$APPDATA/**",
|
||||
"$RESOURCE/../**",
|
||||
"**"
|
||||
],
|
||||
"enable": true
|
||||
},
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"identifier": "io.github.koala-clash",
|
||||
"bundle": {
|
||||
"targets": ["deb", "rpm"],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["openssl"],
|
||||
"desktopTemplate": "./packages/linux/clash-verge.desktop",
|
||||
"provides": ["clash-verge"],
|
||||
"conflicts": ["clash-verge"],
|
||||
"replaces": ["clash-verge"],
|
||||
"desktopTemplate": "./packages/linux/koala-clash.desktop",
|
||||
"provides": ["koala-clash"],
|
||||
"conflicts": ["koala-clash"],
|
||||
"replaces": ["koala-clash"],
|
||||
"postInstallScript": "./packages/linux/post-install.sh",
|
||||
"preRemoveScript": "./packages/linux/pre-remove.sh"
|
||||
},
|
||||
"rpm": {
|
||||
"depends": ["openssl"],
|
||||
"desktopTemplate": "./packages/linux/clash-verge.desktop",
|
||||
"provides": ["clash-verge"],
|
||||
"conflicts": ["clash-verge"],
|
||||
"obsoletes": ["clash-verge"],
|
||||
"desktopTemplate": "./packages/linux/koala-clash.desktop",
|
||||
"provides": ["koala-clash"],
|
||||
"conflicts": ["koala-clash"],
|
||||
"obsoletes": ["koala-clash"],
|
||||
"postInstallScript": "./packages/linux/post-install.sh",
|
||||
"preRemoveScript": "./packages/linux/pre-remove.sh"
|
||||
}
|
||||
},
|
||||
"externalBin": [
|
||||
"./resources/clash-verge-service",
|
||||
"./resources/koala-clash-service",
|
||||
"./resources/install-service",
|
||||
"./resources/uninstall-service",
|
||||
"./sidecar/verge-mihomo",
|
||||
"./sidecar/verge-mihomo-alpha"
|
||||
"./sidecar/koala-mihomo",
|
||||
"./sidecar/koala-mihomo-alpha"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"productName": "Clash Verge Rev Lite",
|
||||
"identifier": "io.github.koala-clash",
|
||||
"productName": "Koala Clash",
|
||||
"bundle": {
|
||||
"targets": ["app", "dmg"],
|
||||
"macOS": {
|
||||
@@ -14,11 +14,11 @@
|
||||
"background": "images/background.png",
|
||||
"appPosition": {
|
||||
"x": 180,
|
||||
"y": 170
|
||||
"y": 200
|
||||
},
|
||||
"applicationFolderPosition": {
|
||||
"x": 480,
|
||||
"y": 170
|
||||
"y": 200
|
||||
},
|
||||
"windowSize": {
|
||||
"height": 400,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"identifier": "io.github.koala-clash",
|
||||
"bundle": {
|
||||
"targets": ["nsis"],
|
||||
"windows": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"identifier": "io.github.koala-clash",
|
||||
"bundle": {
|
||||
"targets": ["nsis"],
|
||||
"windows": {
|
||||
@@ -9,7 +9,7 @@
|
||||
"timestampUrl": "",
|
||||
"webviewInstallMode": {
|
||||
"type": "fixedRuntime",
|
||||
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.arm64/"
|
||||
"path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.arm64/"
|
||||
},
|
||||
"nsis": {
|
||||
"displayLanguageSelector": true,
|
||||
@@ -31,4 +31,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"identifier": "io.github.koala-clash",
|
||||
"bundle": {
|
||||
"targets": ["nsis"],
|
||||
"windows": {
|
||||
@@ -9,7 +9,7 @@
|
||||
"timestampUrl": "",
|
||||
"webviewInstallMode": {
|
||||
"type": "fixedRuntime",
|
||||
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.x64/"
|
||||
"path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.x64/"
|
||||
},
|
||||
"nsis": {
|
||||
"displayLanguageSelector": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"identifier": "io.github.koala-clash",
|
||||
"bundle": {
|
||||
"targets": ["nsis"],
|
||||
"windows": {
|
||||
@@ -9,7 +9,7 @@
|
||||
"timestampUrl": "",
|
||||
"webviewInstallMode": {
|
||||
"type": "fixedRuntime",
|
||||
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.x86/"
|
||||
"path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.x86/"
|
||||
},
|
||||
"nsis": {
|
||||
"displayLanguageSelector": true,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { AppDataProvider } from "./providers/app-data-provider";
|
||||
import { ThemeProvider } from "@/components/layout/theme-provider";
|
||||
import Layout from "./pages/_layout";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AppDataProvider>
|
||||
<Layout />
|
||||
</AppDataProvider>
|
||||
</ThemeProvider>
|
||||
<AppDataProvider>
|
||||
<Layout />
|
||||
</AppDataProvider>
|
||||
);
|
||||
}
|
||||
export default App;
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/image/logo.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@@ -1,50 +1,108 @@
|
||||
<svg version="1.1" id="layout1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 117 27" style="enable-background:new 0 0 117 27;" xml:space="preserve">
|
||||
<g>
|
||||
<defs>
|
||||
<rect id="SVGID_1_" x="-39.9" width="157" height="27"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_00000023248255305809236420000007367745325967865768_">
|
||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
||||
</clipPath>
|
||||
<g style="clip-path:url(#SVGID_00000023248255305809236420000007367745325967865768_);">
|
||||
<path class="st1" d="M115.9,21.4c-0.5,0.3-1.1,0.5-1.8,0.7c-0.7,0.1-1.3,0.2-1.9,0.2c-2.1,0-3.8-0.5-4.9-1.5
|
||||
c-1.1-1-1.6-2.4-1.6-4.3c0-1.8,0.5-3.2,1.5-4.2c1-1,2.3-1.5,4-1.5c1.7,0,3,0.5,4,1.5c1,1,1.5,2.3,1.5,4.2c0,0.2,0,0.5,0,0.9h-7.8
|
||||
c0.3,1.7,1.4,2.6,3.4,2.6c1.4,0,2.6-0.4,3.7-1.2V21.4z M113.6,15.2c-0.2-0.7-0.5-1.2-0.9-1.5c-0.4-0.3-0.9-0.5-1.5-0.5
|
||||
c-0.6,0-1,0.2-1.4,0.5c-0.4,0.3-0.7,0.8-0.8,1.5H113.6z"/>
|
||||
<path class="st1" d="M98.5,26.6c-0.8,0-1.6-0.1-2.5-0.2c-0.8-0.1-1.5-0.3-2.2-0.5v-2.6c1.4,0.3,2.9,0.5,4.3,0.5
|
||||
c0.9,0,1.6-0.2,2.1-0.6c0.5-0.4,0.7-1,0.7-1.7c-0.7,0.5-1.6,0.7-2.6,0.7c-1,0-1.9-0.2-2.6-0.7c-0.7-0.5-1.3-1.1-1.7-2
|
||||
c-0.4-0.9-0.6-1.8-0.6-2.9c0-1.1,0.2-2.1,0.6-2.9c0.4-0.9,1-1.5,1.7-2c0.7-0.5,1.6-0.7,2.6-0.7c0.9,0,1.8,0.3,2.6,0.9v-0.7h3.1V22
|
||||
C104,25,102.2,26.6,98.5,26.6z M96.4,16.6c0,0.6,0.1,1.2,0.4,1.7c0.3,0.5,0.6,0.9,1,1.2c0.4,0.3,0.8,0.4,1.3,0.4
|
||||
c0.3,0,0.7-0.1,1.1-0.2c0.4-0.2,0.8-0.5,1.1-1l0.1-0.4v-3.7c-0.3-0.6-0.6-0.9-1.1-1.1c-0.4-0.2-0.8-0.3-1.2-0.3
|
||||
c-0.5,0-0.9,0.1-1.3,0.4c-0.4,0.3-0.7,0.7-1,1.2C96.6,15.4,96.4,16,96.4,16.6z"/>
|
||||
<path class="st1" d="M89.2,11.2v1.2c0.3-0.4,0.8-0.7,1.2-0.9c0.5-0.2,1-0.3,1.5-0.3c0.3,0,0.6,0,0.9,0.1v2.5
|
||||
c-0.4-0.1-0.7-0.1-1.1-0.1c-0.5,0-1,0.1-1.4,0.3c-0.5,0.2-0.8,0.4-1.1,0.8V22H86V11.2H89.2z"/>
|
||||
<path class="st1" d="M83.7,21.4c-0.5,0.3-1.1,0.5-1.8,0.7c-0.7,0.1-1.3,0.2-1.9,0.2c-2.1,0-3.8-0.5-4.9-1.5
|
||||
c-1.1-1-1.6-2.4-1.6-4.3c0-1.8,0.5-3.2,1.5-4.2c1-1,2.3-1.5,4-1.5c1.7,0,3,0.5,4,1.5c1,1,1.5,2.3,1.5,4.2c0,0.2,0,0.5,0,0.9h-7.8
|
||||
C76.9,19.1,78,20,80,20c1.4,0,2.6-0.4,3.7-1.2V21.4z M81.4,15.2c-0.2-0.7-0.5-1.2-0.9-1.5c-0.4-0.3-0.9-0.5-1.5-0.5
|
||||
c-0.6,0-1,0.2-1.4,0.5c-0.4,0.3-0.7,0.8-0.8,1.5H81.4z"/>
|
||||
<path class="st1" d="M59.5,8h3.6l3.4,11.8h0.1L69.9,8h3.6l-4.3,14h-5.3L59.5,8z"/>
|
||||
<path class="st1" d="M46.4,6.6v5.7c0.5-0.4,1-0.7,1.6-0.9c0.6-0.2,1.2-0.3,1.8-0.3c1,0,1.8,0.3,2.4,0.9c0.6,0.6,0.9,1.4,0.9,2.3
|
||||
V22h-3.2v-7.1c0-0.4-0.2-0.7-0.5-0.9c-0.3-0.3-0.7-0.4-1.1-0.4c-0.3,0-0.6,0.1-0.9,0.2c-0.4,0.2-0.7,0.4-1,0.6V22h-3.2V6.6H46.4z"
|
||||
/>
|
||||
<path class="st1" d="M37.9,22.2c-0.8,0-1.6,0-2.5-0.2c-0.8-0.2-1.5-0.4-2.2-0.8v-2.9c0.5,0.4,1.2,0.7,2,1c0.8,0.3,1.5,0.4,2,0.3
|
||||
c0.4,0,0.7-0.1,0.9-0.3c0.2-0.2,0.3-0.3,0.3-0.5c0.1-0.4,0-0.7-0.3-0.9c-0.3-0.2-0.8-0.4-1.5-0.6c-0.8-0.2-1.5-0.5-1.9-0.8
|
||||
c-0.5-0.3-0.8-0.6-1.1-1c-0.2-0.4-0.4-0.9-0.4-1.5c0-0.6,0.2-1.2,0.5-1.6c0.3-0.5,0.8-0.9,1.5-1.2c0.7-0.3,1.4-0.4,2.2-0.4
|
||||
c0.6,0,1.2,0.1,1.8,0.2c0.6,0.1,1.1,0.3,1.5,0.4v2.6c-0.4-0.2-0.9-0.4-1.5-0.6c-0.6-0.2-1.1-0.3-1.5-0.3c-0.9,0-1.4,0.2-1.5,0.7
|
||||
c0,0.3,0.1,0.5,0.4,0.7c0.3,0.2,0.7,0.4,1.3,0.6c0.8,0.3,1.5,0.5,2,0.8c0.5,0.3,0.9,0.6,1.2,1c0.3,0.4,0.4,1,0.4,1.6
|
||||
c0,1-0.4,1.8-1.1,2.4C40,21.9,39,22.2,37.9,22.2z"/>
|
||||
<path class="st1" d="M25.8,22.3c-1,0-1.9-0.2-2.7-0.7c-0.7-0.5-1.3-1.1-1.7-2c-0.4-0.8-0.6-1.8-0.6-2.9c0-1.1,0.2-2.1,0.6-2.9
|
||||
c0.4-0.9,1-1.5,1.7-2c0.7-0.5,1.6-0.7,2.6-0.7c0.5,0,0.9,0.1,1.4,0.3c0.5,0.2,0.9,0.4,1.3,0.7v-0.7h3.2v8.3c0,1.1,0.1,1.9,0.4,2.5
|
||||
h-3c-0.1-0.2-0.2-0.4-0.2-0.7C27.9,21.9,26.9,22.3,25.8,22.3z M23.9,16.6c0,0.6,0.1,1.2,0.4,1.7c0.3,0.5,0.6,0.9,1,1.2
|
||||
c0.4,0.3,0.8,0.4,1.3,0.4c0.3,0,0.7-0.1,1.1-0.2c0.4-0.1,0.7-0.5,1-0.9v-4.5c-0.3-0.5-0.6-0.8-1-0.9c-0.4-0.1-0.7-0.2-1.1-0.2
|
||||
c-0.5,0-0.9,0.1-1.3,0.4c-0.4,0.3-0.7,0.7-1,1.2C24,15.4,23.9,16,23.9,16.6z"/>
|
||||
<path class="st1" d="M18.5,22.2c-1.2,0-2.1-0.3-2.7-1c-0.6-0.7-0.9-1.7-0.9-3V6.6H18v10.8c0,0.5,0,0.9,0.1,1.2
|
||||
c0.1,0.3,0.2,0.5,0.4,0.6c0.1,0.1,0.3,0.2,0.5,0.2c0.2,0,0.5,0,1,0v2.6H18.5z"/>
|
||||
<path class="st1" d="M8.8,22.3c-1.5,0-2.9-0.3-4.1-0.8C3.6,20.9,2.7,20,2,19c-0.7-1.1-1-2.3-1-3.8c0-1.5,0.3-2.9,1-4
|
||||
c0.7-1.1,1.6-2,2.7-2.6c1.2-0.6,2.5-0.9,4-0.9c0.7,0,1.5,0.1,2.3,0.2s1.4,0.3,1.9,0.6V11c-1.3-0.5-2.6-0.7-3.8-0.7
|
||||
c-1.4,0-2.5,0.4-3.4,1.2c-0.9,0.8-1.3,2-1.3,3.7c0,0.9,0.2,1.7,0.6,2.3c0.4,0.7,1,1.2,1.7,1.6c0.7,0.4,1.4,0.6,2.2,0.6l0.4,0
|
||||
c0.6,0,1.2-0.1,1.8-0.3c0.6-0.2,1.1-0.4,1.6-0.7v2.8c-0.6,0.3-1.2,0.5-1.8,0.6C10.4,22.2,9.6,22.3,8.8,22.3z"/>
|
||||
</g>
|
||||
<svg width="1024" height="963" viewBox="0 0 1024 963" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_f_40_29)">
|
||||
<ellipse cx="512" cy="516" rx="254" ry="216" fill="url(#paint0_radial_40_29)" fill-opacity="0.3"/>
|
||||
</g>
|
||||
<g style="mix-blend-mode:hard-light" filter="url(#filter1_f_40_29)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint1_linear_40_29)" stroke-width="6" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g style="mix-blend-mode:hard-light" filter="url(#filter2_f_40_29)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint2_linear_40_29)" stroke-opacity="0.7" stroke-width="8" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g style="mix-blend-mode:hard-light" filter="url(#filter3_f_40_29)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint3_linear_40_29)" stroke-opacity="0.4" stroke-width="7" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g style="mix-blend-mode:hard-light" filter="url(#filter4_ddif_40_29)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint4_linear_40_29)" stroke-opacity="0.01" stroke-width="5" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_f_40_29)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint5_linear_40_29)" stroke-width="9.5" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_f_40_29)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="white" stroke-width="2.9" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_40_29" x="-42" y="0" width="1108" height="1032" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="150" result="effect1_foregroundBlur_40_29"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_40_29" x="118.94" y="227.932" width="786.12" height="527.726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="10.53" result="effect1_foregroundBlur_40_29"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_40_29" x="89" y="197.99" width="846" height="587.608" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_40_29"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_40_29" x="132.48" y="241.471" width="759.04" height="500.647" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="3.51" result="effect1_foregroundBlur_40_29"/>
|
||||
</filter>
|
||||
<filter id="filter4_ddif_40_29" x="110.5" y="219.494" width="803" height="544.604" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="3.9"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.109804 0 0 0 0 0.886275 0 0 0 0 0.968627 0 0 0 1 0"/>
|
||||
<feBlend mode="multiply" in2="BackgroundImageFix" result="effect1_dropShadow_40_29"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="7.02"/>
|
||||
<feGaussianBlur stdDeviation="4.563"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.819608 0 0 0 0 0.054902 0 0 0 0 0.996078 0 0 0 1 0"/>
|
||||
<feBlend mode="color-dodge" in2="effect1_dropShadow_40_29" result="effect2_dropShadow_40_29"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_40_29" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.39" dy="0.78"/>
|
||||
<feGaussianBlur stdDeviation="0.195"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect3_innerShadow_40_29"/>
|
||||
<feGaussianBlur stdDeviation="15" result="effect4_foregroundBlur_40_29"/>
|
||||
</filter>
|
||||
<filter id="filter5_f_40_29" x="137.15" y="246.138" width="749.7" height="491.31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.55" result="effect1_foregroundBlur_40_29"/>
|
||||
</filter>
|
||||
<filter id="filter6_f_40_29" x="137.15" y="246.146" width="749.7" height="491.302" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="2.2" result="effect1_foregroundBlur_40_29"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_40_29" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(512 516) rotate(20.8768) scale(331.625 512.736)">
|
||||
<stop stop-color="#0CF6F7"/>
|
||||
<stop offset="1" stop-color="#D10EFE"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0CF6F7"/>
|
||||
<stop offset="0.461563" stop-color="#6B86FA"/>
|
||||
<stop offset="1" stop-color="#D10EFE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0CF6F7"/>
|
||||
<stop offset="0.225986" stop-color="#6B86FA"/>
|
||||
<stop offset="0.673077" stop-color="#6B86FA"/>
|
||||
<stop offset="1" stop-color="#D10EFE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0CF6F7"/>
|
||||
<stop offset="0.225986" stop-color="#6B86FA"/>
|
||||
<stop offset="0.673077" stop-color="#6B86FA"/>
|
||||
<stop offset="1" stop-color="#D10EFE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0CF6F7"/>
|
||||
<stop offset="0.225986" stop-color="#6B86FA"/>
|
||||
<stop offset="0.673077" stop-color="#6B86FA"/>
|
||||
<stop offset="1" stop-color="#D10EFE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0CF6F7"/>
|
||||
<stop offset="0.461563" stop-color="#6B86FA"/>
|
||||
<stop offset="1" stop-color="#D10EFE"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 21 KiB |
422
src/assets/image/map.svg
Normal file
|
After Width: | Height: | Size: 453 KiB |
@@ -1,30 +0,0 @@
|
||||
"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 />;
|
||||
};
|
||||
@@ -5,4 +5,3 @@ export { BaseLoading } from "./base-loading";
|
||||
export { BaseErrorBoundary } from "./base-error-boundary";
|
||||
export { Switch } from "./base-switch";
|
||||
export { BaseLoadingOverlay } from "./base-loading-overlay";
|
||||
export { NoticeManager } from "./NoticeManager";
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import React, { useMemo, useState, useEffect, RefObject } from "react";
|
||||
import React, { useMemo, useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
Row,
|
||||
Header,
|
||||
ColumnSizingState,
|
||||
} from "@tanstack/react-table";
|
||||
import { TableVirtuoso, TableComponents } from "react-virtuoso";
|
||||
|
||||
import {
|
||||
Table,
|
||||
@@ -27,7 +26,30 @@ import { cn } from "@root/lib/utils";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
// Интерфейс для строки данных, которую использует react-table
|
||||
interface IConnectionsItem {
|
||||
id: string;
|
||||
metadata: {
|
||||
host: string;
|
||||
destinationIP: string;
|
||||
destinationPort: string;
|
||||
remoteDestination: string;
|
||||
process?: string;
|
||||
processPath?: string;
|
||||
sourceIP: string;
|
||||
sourcePort: string;
|
||||
type: string;
|
||||
network: string;
|
||||
};
|
||||
rule: string;
|
||||
rulePayload?: string;
|
||||
chains: string[];
|
||||
download: number;
|
||||
upload: number;
|
||||
curDownload?: number;
|
||||
curUpload?: number;
|
||||
start: string;
|
||||
}
|
||||
|
||||
interface ConnectionRow {
|
||||
id: string;
|
||||
host: string;
|
||||
@@ -45,22 +67,78 @@ interface ConnectionRow {
|
||||
connectionData: IConnectionsItem;
|
||||
}
|
||||
|
||||
// Интерфейс для пропсов, которые компонент получает от родителя
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
onShowDetail: (data: IConnectionsItem) => void;
|
||||
scrollerRef: (element: HTMLElement | Window | null) => void;
|
||||
}
|
||||
|
||||
const ColumnResizer = ({
|
||||
header,
|
||||
}: {
|
||||
header: Header<ConnectionRow, unknown>;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={cn(
|
||||
"absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none",
|
||||
"bg-transparent hover:bg-primary/50 active:bg-primary",
|
||||
"transition-colors duration-150",
|
||||
header.column.getIsResizing() && "bg-primary",
|
||||
)}
|
||||
style={{
|
||||
transform: header.column.getIsResizing() ? `translateX(0px)` : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectionTable = (props: Props) => {
|
||||
const { connections, onShowDetail, scrollerRef } = props;
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableContainerRef.current && scrollerRef) {
|
||||
scrollerRef(tableContainerRef.current);
|
||||
}
|
||||
}, [scrollerRef]);
|
||||
|
||||
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem("connection-table-widths");
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
return saved
|
||||
? JSON.parse(saved)
|
||||
: {
|
||||
host: 220,
|
||||
download: 88,
|
||||
upload: 88,
|
||||
dlSpeed: 88,
|
||||
ulSpeed: 88,
|
||||
chains: 340,
|
||||
rule: 280,
|
||||
process: 220,
|
||||
time: 120,
|
||||
source: 200,
|
||||
remoteDestination: 200,
|
||||
type: 160,
|
||||
};
|
||||
} 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,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,13 +185,16 @@ export const ConnectionTable = (props: Props) => {
|
||||
header: () => t("Host"),
|
||||
size: columnSizing?.host || 220,
|
||||
minSize: 180,
|
||||
maxSize: 400,
|
||||
},
|
||||
{
|
||||
accessorKey: "download",
|
||||
header: () => t("Downloaded"),
|
||||
size: columnSizing?.download || 88,
|
||||
minSize: 80,
|
||||
maxSize: 150,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="text-right">
|
||||
<div className="text-right font-mono text-sm">
|
||||
{parseTraffic(getValue<number>()).join(" ")}
|
||||
</div>
|
||||
),
|
||||
@@ -122,8 +203,10 @@ export const ConnectionTable = (props: Props) => {
|
||||
accessorKey: "upload",
|
||||
header: () => t("Uploaded"),
|
||||
size: columnSizing?.upload || 88,
|
||||
minSize: 80,
|
||||
maxSize: 150,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="text-right">
|
||||
<div className="text-right font-mono text-sm">
|
||||
{parseTraffic(getValue<number>()).join(" ")}
|
||||
</div>
|
||||
),
|
||||
@@ -132,8 +215,10 @@ export const ConnectionTable = (props: Props) => {
|
||||
accessorKey: "dlSpeed",
|
||||
header: () => t("DL Speed"),
|
||||
size: columnSizing?.dlSpeed || 88,
|
||||
minSize: 80,
|
||||
maxSize: 150,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="text-right">
|
||||
<div className="text-right font-mono text-sm">
|
||||
{parseTraffic(getValue<number>()).join(" ")}/s
|
||||
</div>
|
||||
),
|
||||
@@ -142,8 +227,10 @@ export const ConnectionTable = (props: Props) => {
|
||||
accessorKey: "ulSpeed",
|
||||
header: () => t("UL Speed"),
|
||||
size: columnSizing?.ulSpeed || 88,
|
||||
minSize: 80,
|
||||
maxSize: 150,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="text-right">
|
||||
<div className="text-right font-mono text-sm">
|
||||
{parseTraffic(getValue<number>()).join(" ")}/s
|
||||
</div>
|
||||
),
|
||||
@@ -153,26 +240,30 @@ export const ConnectionTable = (props: Props) => {
|
||||
header: () => t("Chains"),
|
||||
size: columnSizing?.chains || 340,
|
||||
minSize: 180,
|
||||
maxSize: 500,
|
||||
},
|
||||
{
|
||||
accessorKey: "rule",
|
||||
header: () => t("Rule"),
|
||||
size: columnSizing?.rule || 280,
|
||||
minSize: 180,
|
||||
maxSize: 400,
|
||||
},
|
||||
{
|
||||
accessorKey: "process",
|
||||
header: () => t("Process"),
|
||||
size: columnSizing?.process || 220,
|
||||
minSize: 180,
|
||||
maxSize: 350,
|
||||
},
|
||||
{
|
||||
accessorKey: "time",
|
||||
header: () => t("Time"),
|
||||
size: columnSizing?.time || 120,
|
||||
minSize: 100,
|
||||
maxSize: 180,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="text-right">
|
||||
<div className="text-right font-mono text-sm">
|
||||
{dayjs(getValue<string>()).fromNow()}
|
||||
</div>
|
||||
),
|
||||
@@ -182,18 +273,21 @@ export const ConnectionTable = (props: Props) => {
|
||||
header: () => t("Source"),
|
||||
size: columnSizing?.source || 200,
|
||||
minSize: 130,
|
||||
maxSize: 300,
|
||||
},
|
||||
{
|
||||
accessorKey: "remoteDestination",
|
||||
header: () => t("Destination"),
|
||||
size: columnSizing?.remoteDestination || 200,
|
||||
minSize: 130,
|
||||
maxSize: 300,
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: () => t("Type"),
|
||||
size: columnSizing?.type || 160,
|
||||
minSize: 100,
|
||||
maxSize: 220,
|
||||
},
|
||||
],
|
||||
[columnSizing],
|
||||
@@ -206,92 +300,91 @@ export const ConnectionTable = (props: Props) => {
|
||||
onColumnSizingChange: setColumnSizing,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
columnResizeMode: "onChange",
|
||||
enableColumnResizing: true,
|
||||
});
|
||||
|
||||
const VirtuosoTableComponents = useMemo<TableComponents<Row<ConnectionRow>>>(
|
||||
() => ({
|
||||
// Явно типизируем `ref` для каждого компонента
|
||||
Scroller: React.forwardRef<HTMLDivElement>((props, ref) => (
|
||||
<div className="h-full" {...props} ref={ref} />
|
||||
)),
|
||||
Table: (props) => <Table {...props} className="w-full border-collapse" />,
|
||||
TableHead: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||
<TableHeader {...props} ref={ref} />
|
||||
)),
|
||||
// Явно типизируем пропсы и `ref` для TableRow
|
||||
TableRow: React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
{ item: Row<ConnectionRow> } & React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ item: row, ...props }, ref) => {
|
||||
// `Virtuoso` передает нам готовую строку `row` в пропсе `item`.
|
||||
// Больше не нужно искать ее по индексу!
|
||||
return (
|
||||
<TableRow
|
||||
{...props}
|
||||
ref={ref}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => onShowDetail(row.original.connectionData)}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||
<TableBody {...props} ref={ref} />
|
||||
)),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const totalTableWidth = useMemo(() => {
|
||||
return table.getCenterTotalSize();
|
||||
}, [table.getState().columnSizing]);
|
||||
|
||||
if (connRows.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-muted-foreground">{t("No connections")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full rounded-md border overflow-hidden">
|
||||
{connRows.length > 0 ? (
|
||||
<TableVirtuoso
|
||||
scrollerRef={scrollerRef}
|
||||
data={table.getRowModel().rows}
|
||||
components={VirtuosoTableComponents}
|
||||
fixedHeaderContent={() =>
|
||||
table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
className="hover:bg-transparent bg-background/95 backdrop-blur"
|
||||
>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{ width: header.getSize() }}
|
||||
className="p-2"
|
||||
>
|
||||
<div className="rounded-md border relative bg-background">
|
||||
<Table
|
||||
className="w-full border-collapse table-fixed"
|
||||
style={{
|
||||
width: totalTableWidth,
|
||||
minWidth: "100%",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
className="hover:bg-transparent border-b-0 h-10"
|
||||
>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={cn(
|
||||
"sticky top-0 z-10",
|
||||
"p-2 text-xs font-semibold select-none border-r last:border-r-0 bg-background h-10",
|
||||
)}
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
minWidth: header.column.columnDef.minSize,
|
||||
maxWidth: header.column.columnDef.maxSize,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between h-full">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
itemContent={(index, row) => (
|
||||
<>
|
||||
</div>
|
||||
{header.column.getCanResize() && (
|
||||
<ColumnResizer header={header} />
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => onShowDetail(row.original.connectionData)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
className="p-2 whitespace-nowrap"
|
||||
onClick={() => onShowDetail(row.original.connectionData)}
|
||||
className="p-2 whitespace-nowrap overflow-hidden text-ellipsis text-sm border-r last:border-r-0"
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
minWidth: cell.column.columnDef.minSize,
|
||||
maxWidth: cell.column.columnDef.maxSize,
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p>No results.</p>
|
||||
</div>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
123
src/components/home/power-button.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@root/lib/utils";
|
||||
|
||||
// Компоненты и иконки
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -18,15 +17,13 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ChevronsUpDown, Timer, WholeWord } from "lucide-react";
|
||||
import {AlertTriangle, ChevronsUpDown, Timer, WholeWord} from "lucide-react";
|
||||
|
||||
// Логика
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import delayManager from "@/services/delay";
|
||||
import { updateProxy, deleteConnection } from "@/services/api";
|
||||
|
||||
// --- Типы и константы ---
|
||||
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
|
||||
const STORAGE_KEY_SORT_TYPE = "clash-verge-proxy-sort-type";
|
||||
const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"];
|
||||
@@ -37,18 +34,19 @@ interface IProxyGroup {
|
||||
now: string;
|
||||
hidden: boolean;
|
||||
all: (string | { name: string })[];
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
// --- Вспомогательная функция для цвета задержки ---
|
||||
function getDelayBadgeVariant(
|
||||
delayValue: number,
|
||||
): "default" | "secondary" | "destructive" | "outline" {
|
||||
if (delayValue < 0) return "secondary";
|
||||
if (delayValue >= 150) return "destructive";
|
||||
return "default";
|
||||
function getDelayColorClasses(delayValue: number): string {
|
||||
if (delayValue < 0) {
|
||||
return "text-muted-foreground border-border";
|
||||
}
|
||||
if (delayValue >= 150) {
|
||||
return "text-destructive border-destructive/40";
|
||||
}
|
||||
return "text-green-600 border-green-500/40 dark:text-green-400 dark:border-green-400/30";
|
||||
}
|
||||
|
||||
// --- Дочерний компонент для элемента списка с "живым" обновлением пинга ---
|
||||
const ProxySelectItem = ({
|
||||
proxyName,
|
||||
groupName,
|
||||
@@ -79,20 +77,21 @@ const ProxySelectItem = ({
|
||||
}, [proxyName, groupName]);
|
||||
|
||||
return (
|
||||
<SelectItem key={proxyName} value={proxyName}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="truncate">{proxyName}</span>
|
||||
<Badge
|
||||
variant={getDelayBadgeVariant(delay)}
|
||||
className={cn(
|
||||
"ml-4 flex-shrink-0 px-2 h-5 justify-center transition-colors duration-300",
|
||||
isJustUpdated && "bg-primary/20 border-primary/50",
|
||||
)}
|
||||
>
|
||||
{delay < 0 || delay > 10000 ? "---" : delay}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem key={proxyName} value={proxyName}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"mr-2 flex-shrink-0 px-2 h-5 w-8 justify-center transition-colors duration-300",
|
||||
getDelayColorClasses(delay),
|
||||
isJustUpdated && "bg-primary/10 border-primary/50",
|
||||
)}
|
||||
>
|
||||
{delay < 0 || delay > 10000 ? "---" : delay}
|
||||
</Badge>
|
||||
<span className="truncate">{proxyName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -112,6 +111,7 @@ export const ProxySelectors: React.FC = () => {
|
||||
(localStorage.getItem(STORAGE_KEY_SORT_TYPE) as ProxySortType) ||
|
||||
"default",
|
||||
);
|
||||
const enable_group_icon = verge?.enable_group_icon ?? true;
|
||||
|
||||
useEffect(() => {
|
||||
if (!proxies?.groups) return;
|
||||
@@ -257,11 +257,20 @@ export const ProxySelectors: React.FC = () => {
|
||||
?.all;
|
||||
|
||||
if (sourceList) {
|
||||
options = sourceList
|
||||
.map((proxy: any) => ({
|
||||
name: typeof proxy === "string" ? proxy : proxy.name,
|
||||
}))
|
||||
.filter((p: { name: string }) => p.name);
|
||||
const rawOptions = sourceList
|
||||
.map((proxy: any) => ({
|
||||
name: typeof proxy === "string" ? proxy : proxy.name,
|
||||
}))
|
||||
.filter((p: { name: string }) => p.name);
|
||||
|
||||
const uniqueNames = new Set<string>();
|
||||
options = rawOptions.filter((proxy: any) => {
|
||||
if (!uniqueNames.has(proxy.name)) {
|
||||
uniqueNames.add(proxy.name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (sortType === "name")
|
||||
@@ -291,21 +300,40 @@ export const ProxySelectors: React.FC = () => {
|
||||
disabled={isGlobalMode || isDirectMode}
|
||||
>
|
||||
<SelectTrigger className="w-100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="truncate">
|
||||
<SelectValue placeholder={t("Select a group...")} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{selectedGroup}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{isGlobalMode ? (
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
||||
<span className="font-medium text-sm">
|
||||
{t("Global Mode Active")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<span className="truncate">
|
||||
<SelectValue placeholder={t("Select a group...")} />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectorGroups.map((group: IProxyGroup) => (
|
||||
<SelectItem key={group.name} value={group.name}>
|
||||
{group.name}
|
||||
<div className="flex items-center gap-2">
|
||||
{enable_group_icon && group.icon && (
|
||||
<img
|
||||
src={
|
||||
group.icon.startsWith("data")
|
||||
? group.icon
|
||||
: group.icon.startsWith("<svg")
|
||||
? `data:image/svg+xml;base64,${btoa(group.icon)}`
|
||||
: group.icon
|
||||
}
|
||||
className="w-4 h-4 rounded-sm"
|
||||
alt={group.name}
|
||||
/>
|
||||
)}
|
||||
<span>{group.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -333,44 +361,37 @@ export const ProxySelectors: React.FC = () => {
|
||||
{sortType === "name" && <WholeWord className="h-4 w-4" />}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{sortType === "default" && <p>{t("Sort by default")}</p>}
|
||||
{sortType === "delay" && <p>{t("Sort by delay")}</p>}
|
||||
{sortType === "name" && <p>{t("Sort by name")}</p>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedProxy}
|
||||
onValueChange={handleProxyChange}
|
||||
disabled={isDirectMode}
|
||||
onOpenChange={handleProxyListOpen}
|
||||
>
|
||||
<SelectTrigger className="w-100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="truncate">
|
||||
<SelectValue placeholder={t("Select a proxy...")} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{selectedProxy}</p>
|
||||
{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>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{proxyOptions.map((proxy) => (
|
||||
<ProxySelectItem
|
||||
key={proxy.name}
|
||||
proxyName={proxy.name}
|
||||
groupName={selectedGroup}
|
||||
/>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedProxy}
|
||||
onValueChange={handleProxyChange}
|
||||
disabled={isDirectMode}
|
||||
onOpenChange={handleProxyListOpen}
|
||||
>
|
||||
<SelectTrigger className="w-100">
|
||||
<span className="truncate">
|
||||
<SelectValue placeholder={t("Select a proxy...")} />
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{proxyOptions.map((proxy) => (
|
||||
<ProxySelectItem
|
||||
key={proxy.name}
|
||||
proxyName={proxy.name}
|
||||
groupName={selectedGroup}
|
||||
/>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
107
src/components/layout/sidebar.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { t } from "i18next";
|
||||
import { cn } from "@root/lib/utils";
|
||||
|
||||
import {
|
||||
Home,
|
||||
Users,
|
||||
Server,
|
||||
Cable,
|
||||
ListChecks,
|
||||
FileText,
|
||||
Settings,
|
||||
EarthLock,
|
||||
} from "lucide-react";
|
||||
import { UpdateButton } from "@/components/layout/update-button";
|
||||
import React from "react";
|
||||
import { SheetClose } from "@/components/ui/sheet";
|
||||
import logo from "@/assets/image/logo.png";
|
||||
|
||||
const menuItems = [
|
||||
{ title: "Home", url: "/home", icon: Home },
|
||||
{ title: "Profiles", url: "/profile", icon: Users },
|
||||
{ title: "Proxies", url: "/proxies", icon: Server },
|
||||
{ title: "Connections", url: "/connections", icon: Cable },
|
||||
{ title: "Rules", url: "/rules", icon: ListChecks },
|
||||
{ title: "Logs", url: "/logs", icon: FileText },
|
||||
{ title: "Settings", url: "/settings", icon: Settings },
|
||||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
const { isMobile } = useSidebar();
|
||||
return (
|
||||
<Sidebar variant="floating" collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className={cn(
|
||||
"flex h-12 items-center transition-all duration-200",
|
||||
"group-data-[state=expanded]:w-full group-data-[state=expanded]:gap-2 group-data-[state=expanded]:px-3",
|
||||
"group-data-[state=collapsed]:w-full group-data-[state=collapsed]:justify-center",
|
||||
)}
|
||||
>
|
||||
<img src={logo} alt="logo" className="h-6 w-6 flex-shrink-0" />
|
||||
<span className="font-semibold whitespace-nowrap group-data-[state=collapsed]:hidden">
|
||||
Koala Clash
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="gap-3">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = location.pathname === item.url;
|
||||
const linkElement = (
|
||||
<Link
|
||||
key={item.title}
|
||||
to={item.url}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary",
|
||||
"data-[active=true]:font-semibold data-[active=true]:border",
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4 drop-shadow-md" />
|
||||
{t(item.title)}
|
||||
</Link>
|
||||
);
|
||||
return (
|
||||
<SidebarMenuItem key={item.title} className="my-1">
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isActive}
|
||||
tooltip={t(item.title)}
|
||||
>
|
||||
{isMobile ? (
|
||||
<SheetClose asChild>{linkElement}</SheetClose>
|
||||
) : (
|
||||
linkElement
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<div className="w-full flex justify-center">
|
||||
<UpdateButton className="bg-green-700 hover:bg-green-500 hover:text-white text-white text-shadow-md" />
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import { UpdateViewer } from "../setting/mods/update-viewer";
|
||||
import { DialogRef } from "../base";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { t } from "i18next";
|
||||
import { Download, RefreshCw } from "lucide-react";
|
||||
import { useSidebar } from "../ui/sidebar";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -14,6 +17,7 @@ export const UpdateButton = (props: Props) => {
|
||||
const { className } = props;
|
||||
const { verge } = useVerge();
|
||||
const { auto_check_update } = verge || {};
|
||||
const { state: sidebarState } = useSidebar();
|
||||
|
||||
const viewerRef = useRef<DialogRef>(null);
|
||||
|
||||
@@ -32,15 +36,26 @@ export const UpdateButton = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<UpdateViewer ref={viewerRef} />
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className={className}
|
||||
onClick={() => viewerRef.current?.open()}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
{sidebarState === "collapsed" ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={className}
|
||||
onClick={() => viewerRef.current?.open()}
|
||||
>
|
||||
<Download />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className={className}
|
||||
onClick={() => viewerRef.current?.open()}
|
||||
>
|
||||
<Download />
|
||||
{t("New update")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSetThemeMode, useThemeMode } from "@/services/states";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import {
|
||||
getCurrentWebviewWindow,
|
||||
WebviewWindow,
|
||||
} from "@tauri-apps/api/webviewWindow";
|
||||
import { Theme } from "@tauri-apps/api/window";
|
||||
|
||||
export const useCustomTheme = () => {
|
||||
const appWindow = useMemo(() => getCurrentWebviewWindow(), []);
|
||||
const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []);
|
||||
const { verge } = useVerge();
|
||||
const { theme_mode } = verge ?? {};
|
||||
|
||||
const mode = useThemeMode();
|
||||
const setMode = useSetThemeMode();
|
||||
|
||||
const [systemTheme, setSystemTheme] = useState(() =>
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setMode(
|
||||
theme_mode === "light" || theme_mode === "dark" ? theme_mode : "system",
|
||||
@@ -19,29 +28,29 @@ export const useCustomTheme = () => {
|
||||
}, [theme_mode, setMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (mode !== "system") return;
|
||||
|
||||
const activeTheme =
|
||||
mode === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: mode;
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
setSystemTheme(e.matches ? "dark" : "light");
|
||||
};
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(activeTheme);
|
||||
appWindow.setTheme(activeTheme as Theme).catch(console.error);
|
||||
}, [mode, appWindow]);
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, [mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme_mode !== "system") return;
|
||||
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
|
||||
setMode(payload);
|
||||
});
|
||||
return () => {
|
||||
unlistenPromise.then((f) => f());
|
||||
};
|
||||
}, [theme_mode, appWindow, setMode]);
|
||||
const root = document.documentElement;
|
||||
const activeTheme = mode === "system" ? systemTheme : mode;
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(activeTheme);
|
||||
|
||||
if (theme_mode === "system") {
|
||||
appWindow.setTheme(null).catch(console.error);
|
||||
} else {
|
||||
appWindow.setTheme(activeTheme as Theme).catch(console.error);
|
||||
}
|
||||
}, [mode, systemTheme, appWindow, theme_mode]);
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -69,7 +69,7 @@ const LogItem = ({ value, searchState }: Props) => {
|
||||
{renderHighlightText(value.type)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-800 dark:text-gray-200 break-all whitespace-pre-wrap">
|
||||
<div className="text-foreground break-all whitespace-pre-wrap">
|
||||
{renderHighlightText(value.payload)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Новые импорты из shadcn/ui
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -18,7 +17,7 @@ interface Props {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
onOpenChange: (open: boolean) => void; // shadcn использует этот коллбэк
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
@@ -30,7 +29,7 @@ export const ConfirmViewer = (props: Props) => {
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogTitle className="truncate">{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -32,7 +32,6 @@ import { BaseSearchBox } from "../base/base-search-box";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { cn } from "@root/lib/utils";
|
||||
|
||||
// --- Компоненты shadcn/ui и иконки ---
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -85,7 +84,6 @@ import {
|
||||
ArrowUpToLine,
|
||||
} from "lucide-react";
|
||||
|
||||
// --- Вспомогательные функции, константы и валидаторы ---
|
||||
const portValidator = (value: string): boolean =>
|
||||
/^(?:[1-9]\d{0,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/.test(
|
||||
value,
|
||||
@@ -109,7 +107,6 @@ interface Props {
|
||||
onSave?: (prev?: string, curr?: string) => void;
|
||||
}
|
||||
|
||||
// --- Новый компонент Combobox (одиночный выбор) ---
|
||||
const Combobox = ({
|
||||
options,
|
||||
value,
|
||||
@@ -123,7 +120,7 @@ const Combobox = ({
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -168,7 +165,6 @@ const Combobox = ({
|
||||
);
|
||||
};
|
||||
|
||||
// --- Новый компонент MultiSelectCombobox (множественный выбор) ---
|
||||
const MultiSelectCombobox = ({
|
||||
options,
|
||||
value,
|
||||
@@ -194,7 +190,7 @@ const MultiSelectCombobox = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -246,7 +242,6 @@ const MultiSelectCombobox = ({
|
||||
);
|
||||
};
|
||||
|
||||
// --- Новый компонент для элемента списка групп ---
|
||||
const EditorGroupItem = ({
|
||||
type,
|
||||
group,
|
||||
@@ -407,22 +402,6 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currData === "" || !visualization) return;
|
||||
try {
|
||||
let obj = yaml.load(currData) as {
|
||||
prepend: [];
|
||||
append: [];
|
||||
delete: [];
|
||||
} | null;
|
||||
setPrependSeq(obj?.prepend || []);
|
||||
setAppendSeq(obj?.append || []);
|
||||
setDeleteSeq(obj?.delete || []);
|
||||
} catch (e) {
|
||||
/* Ignore parsing errors while typing */
|
||||
}
|
||||
}, [visualization, currData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prependSeq && appendSeq && deleteSeq && visualization) {
|
||||
const serialize = () => {
|
||||
@@ -562,7 +541,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="lg:min-w-5xl h-[90vh] flex flex-col">
|
||||
<DialogContent className="min-w-5xl h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<div className="flex justify-between items-center pr-8">
|
||||
<DialogTitle>{t("Edit Groups")}</DialogTitle>
|
||||
@@ -580,9 +559,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
{visualization ? (
|
||||
<Form {...form}>
|
||||
<form className="h-full flex gap-4">
|
||||
{/* Левая панель: Конструктор групп */}
|
||||
<div className="w-1/2 flex flex-col border rounded-md p-4">
|
||||
<h3 className="text-lg font-medium mb-4">Constructor</h3>
|
||||
<h3 className="text-lg font-medium mb-4">{t("Constructor")}</h3>
|
||||
<Separator className="mb-4" />
|
||||
<div className="space-y-3 overflow-y-auto p-1 -mr-3 ">
|
||||
<FormField
|
||||
|
||||
53
src/components/profile/hwid-error-dialog.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
export const HwidErrorDialog = () => {
|
||||
const { t } = useTranslation();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShowHwidError = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<string>;
|
||||
setErrorMessage(customEvent.detail);
|
||||
};
|
||||
|
||||
window.addEventListener("show-hwid-error", handleShowHwidError);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("show-hwid-error", handleShowHwidError);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!errorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={!!errorMessage} onOpenChange={() => setErrorMessage(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||
{t("Device Limit Reached")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="pt-4 text-left">
|
||||
{errorMessage}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setErrorMessage(null)}>{t("OK")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||