61 Commits

Author SHA1 Message Date
coolcoala
df595f4835 v0.2.5 2025-08-05 19:14:31 +03:00
coolcoala
63e4d2f686 updated UPDATELOG.md 2025-08-05 19:12:40 +03:00
coolcoala
971580def8 fixed map in background 2025-08-05 19:06:28 +03:00
coolcoala
ffd32426b5 fixed an issue with enabling tun and system proxy if profiles are missing 2025-08-05 19:05:44 +03:00
coolcoala
d2d26cc822 fixed icon for windows 2025-08-05 19:03:38 +03:00
coolcoala
a373b0b6eb fixed locales 2025-08-05 18:52:02 +03:00
coolcoala
f515fa1443 unnecessary menu items removed 2025-08-05 17:51:44 +03:00
coolcoala
e32e83d45e renamed to koala clash 2025-08-05 17:51:26 +03:00
coolcoala
7be3cdeb65 fixed command for macOS 2025-08-05 09:29:25 +03:00
coolcoala
b234b9166d code signing for macOS 2025-08-03 15:41:18 +03:00
coolcoala
2c485b5efb fixed an issue with opening a window via a shortcut when the application is already active 2025-08-03 12:56:46 +03:00
coolcoala
b7d7e1a1af fixed issue with clicking on shortcut 2025-08-03 11:21:02 +03:00
coolcoala
01be6ae70a fixing the tray customization issue 2025-08-03 11:12:25 +03:00
coolcoala
445eaadac3 minor fix in russian localization 2025-08-03 11:11:36 +03:00
coolcoala
d5b1dfddee new homepage 2025-08-03 11:10:00 +03:00
coolcoala
c68ea04f06 fixed icon in sidebar 2025-08-03 11:09:36 +03:00
coolcoala
9abc30b60c fix for dark mode in pop-up notifications, system theme detection 2025-08-03 11:08:32 +03:00
coolcoala
1f7561298c minor fix 2025-08-02 08:51:44 +03:00
coolcoala
611c5757e0 new icons 2025-08-02 04:21:34 +03:00
coolcoala
ab56e82173 fix logo in sidebar 2025-08-02 03:31:12 +03:00
coolcoala
34350fadb6 v0.2.4 2025-08-01 19:41:28 +03:00
coolcoala
77786da53f fix icons 2025-08-01 19:41:21 +03:00
coolcoala
f794ca5426 added links for donate 2025-08-01 19:41:15 +03:00
coolcoala
a2010e6d1d fixed renaming files 2025-07-30 09:11:57 +03:00
coolcoala
4ce6e9bfd7 updated UPDATELOG.md 2025-07-30 06:59:48 +03:00
coolcoala
9a3794073b fixed flag display when adding a link via vless:// 2025-07-30 06:53:02 +03:00
coolcoala
d6197d6d21 added traffic information display to the main page 2025-07-30 06:53:02 +03:00
coolcoala
1f321cf6bc fixed translations 2025-07-30 06:52:55 +03:00
coolcoala
5c6d3f4078 unused settings removed 2025-07-30 06:31:49 +03:00
coolcoala
6b8b95e4ca traffic information has been reworked 2025-07-30 06:31:49 +03:00
coolcoala
ae08d48641 added application icon to sidebar 2025-07-30 06:31:45 +03:00
coolcoala
d1ce5566cf added new background for dmg installer 2025-07-28 08:52:39 +03:00
coolcoala
5f027ebc79 started the process of renaming to Koala Clash 2025-07-28 08:43:36 +03:00
coolcoala
8cf83f8338 minor fix 2025-07-28 08:43:36 +03:00
coolcoala
b96e2c1fe0 notification of exceeding the number of devices in the subscription, support for vless:// links with templates by @legiz-ru 2025-07-28 08:43:30 +03:00
coolcoala
4ad1379773 new icons 2025-07-28 06:53:48 +03:00
coolcoala
ef0883f732 notifications in Telegram, and changes have been made so that the link to the release does not change over time 2025-07-26 09:28:24 +03:00
coolcoala
a2076b4e2d minor fix 2025-07-26 06:54:34 +03:00
coolcoala
0a3998530e the alphabetical index has been removed, and additional information about proxies is now hidden by default 2025-07-26 06:54:25 +03:00
coolcoala
ed2ec56a44 the size of modal windows has been adjusted due to an increase in the minimum window size 2025-07-26 06:53:35 +03:00
coolcoala
87473bdf92 fixed log color when dark theme is enabled 2025-07-26 06:53:00 +03:00
coolcoala
8186a6841a added icons for proxy groups 2025-07-26 06:52:36 +03:00
coolcoala
0a0b5b6612 direct was removed, and the translation for rules and global was replaced 2025-07-26 06:52:07 +03:00
coolcoala
72704f9dc9 the minimum window size has been changed 2025-07-26 06:50:42 +03:00
coolcoala
06ad23d904 added auto-scaling and scaling via key combination 2025-07-26 06:50:18 +03:00
coolcoala
fbd1c55f44 v0.2.3 2025-07-22 02:11:13 +03:00
coolcoala
9668a04a1a updated UPDATELOG.md 2025-07-21 03:41:07 +03:00
coolcoala
24af375a8e started work on translating console logs from Chinese to English 2025-07-21 03:40:47 +03:00
coolcoala
a32c973ab8 fixed problem with profile inactivation after adding via deeplink on windows 2025-07-21 03:06:37 +03:00
coolcoala
50beb913de fixed command mapping for macos installation 2025-07-21 03:06:22 +03:00
coolcoala
05f1ec7b34 added that it is not possible to enable proxy if no profile is available 2025-07-21 01:57:37 +03:00
coolcoala
9271b107b6 fixed a layout issue in the proxy menu, now all cards are the same size 2025-07-21 01:56:24 +03:00
coolcoala
e7208dd7d2 fixed problem with menu reopening when opening a page in a compressed window 2025-07-21 01:55:33 +03:00
coolcoala
e5dfb34082 v0.2.2 2025-07-19 03:57:29 +03:00
coolcoala
2ba5c4e706 new menu added, layout corrected in some places 2025-07-19 03:57:07 +03:00
coolcoala
27bcc5f4f8 v0.2.1 2025-07-18 04:51:35 +03:00
coolcoala
d884bd539b fixed release body and configs for webview 2025-07-18 04:30:43 +03:00
coolcoala
580a56727c new localization lines added 2025-07-18 04:18:19 +03:00
coolcoala
ac3163d061 added button to turn off hwid sending 2025-07-18 04:17:58 +03:00
coolcoala
8bc7a6c3e1 added saving the location on the main page when adding a profile via deeplink, profile update button on the main page, corrected url for telegram icon for support, and announcement-url header support. 2025-07-18 04:17:18 +03:00
coolcoala
31d368979e added update profiles at startup, “announce-url” header, and also when adding check if the profile already exists and if it does, just update it 2025-07-18 04:12:55 +03:00
104 changed files with 3587 additions and 1628 deletions

2
.github/FUNDING.yml vendored
View File

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

View File

@@ -40,9 +40,91 @@ jobs:
fi fi
echo "Tag and package.json version are consistent." 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: release:
name: Release Build name: Release Build
needs: check_tag_version needs: [check_tag_version, create_release_notes]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -96,20 +178,78 @@ jobs:
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Create .p8 file
run: |
mkdir -p ~/.appstoreconnect/private_keys
echo "${{ secrets.APPLE_API_KEY_CONTENT }}" > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8
- name: Tauri build - name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
env: env:
NODE_OPTIONS: "--max_old_space_size=4096" NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: "~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8"
with: with:
tagName: v__VERSION__
releaseName: "Clash Verge Rev Lite v__VERSION__"
releaseBody: "More new features are now supported."
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
- name: Rename Artifact (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$version = ${{steps.build.outputs.appVersion}}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "_${version}_", "_"
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: release-for-linux-arm:
name: Release Build for Linux ARM name: Release Build for Linux ARM
strategy: strategy:
@@ -219,14 +359,36 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install jq sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV 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 - name: Upload Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{env.VERSION}} tag_name: v${{env.VERSION}}
name: "Clash Verge Rev Lite v${{env.VERSION}}" name: "Koala Clash v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: | files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
@@ -275,8 +437,8 @@ jobs:
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
run: | 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 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.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
@@ -296,19 +458,19 @@ jobs:
run: | run: |
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe" $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) { foreach ($file in $files) {
$newName = $file.Name -replace "-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 Rename-Item $file.FullName $newName
} }
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip" $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) { 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 Rename-Item $file.FullName $newName
} }
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig" $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
foreach ($file in $files) { 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 Rename-Item $file.FullName $newName
} }
@@ -316,8 +478,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{steps.build.outputs.appVersion}} tag_name: v${{steps.build.outputs.appVersion}}
name: "Clash Verge Rev Lite v${{steps.build.outputs.appVersion}}" name: "Koala Clash v${{steps.build.outputs.appVersion}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
@@ -376,3 +537,70 @@ jobs:
run: pnpm updater-fixed-webview2 run: pnpm updater-fixed-webview2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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

View File

@@ -1,3 +1,40 @@
## 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 ## v0.2
- added handlers for "Announe", "Support-Url", "New-Sub-Domain", "Profile-Title" headers: - added handlers for "Announe", "Support-Url", "New-Sub-Domain", "Profile-Title" headers:

19
hooks/use-mobile.ts Normal file
View File

@@ -0,0 +1,19 @@
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
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "koala-clash",
"version": "0.2.0", "version": "0.2.5",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",

View File

@@ -42,16 +42,16 @@ async function resolvePortable() {
const zip = new AdmZip(); const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe")); zip.addLocalFile(path.join(releaseDir, "Koala Clash.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe")); zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe")); zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources"); zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder( zip.addLocalFolder(
path.join( path.join(
releaseDir, 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"); zip.addLocalFolder(configDir, ".config");

View File

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

View File

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

192
src-tauri/Cargo.lock generated
View File

@@ -1059,80 +1059,6 @@ dependencies = [
"inout", "inout",
] ]
[[package]]
name = "clash-verge"
version = "0.2.0"
dependencies = [
"ab_glyph",
"aes-gcm",
"anyhow",
"async-trait",
"base64 0.22.1",
"boa_engine",
"chrono",
"deelevate",
"delay_timer",
"dirs 6.0.0",
"dunce",
"futures",
"gethostname 1.0.2",
"getrandom 0.3.3",
"hex",
"hmac",
"image",
"imageproc",
"lazy_static",
"libc",
"log",
"log4rs",
"machine-uid",
"mihomo_api",
"nanoid",
"network-interface",
"once_cell",
"open",
"os_info",
"parking_lot",
"percent-encoding",
"port_scanner",
"regex",
"reqwest",
"reqwest_dav",
"runas",
"scopeguard",
"serde",
"serde_json",
"serde_yaml",
"sha2 0.10.9",
"sys-locale",
"sysinfo",
"sysproxy",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-clipboard-manager",
"tauri-plugin-deep-link",
"tauri-plugin-devtools",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-notification",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tempfile",
"tokio",
"tokio-tungstenite 0.27.0",
"tungstenite 0.27.0",
"url",
"users",
"warp",
"winapi",
"winreg 0.55.0",
"zip",
]
[[package]] [[package]]
name = "clipboard-win" name = "clipboard-win"
version = "5.4.0" version = "5.4.0"
@@ -3622,6 +3548,81 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "koala-clash"
version = "0.2.5"
dependencies = [
"ab_glyph",
"aes-gcm",
"anyhow",
"async-trait",
"base64 0.22.1",
"boa_engine",
"chrono",
"deelevate",
"delay_timer",
"dirs 6.0.0",
"dunce",
"futures",
"gethostname 1.0.2",
"getrandom 0.3.3",
"hex",
"hmac",
"image",
"imageproc",
"lazy_static",
"libc",
"log",
"log4rs",
"machine-uid",
"mihomo_api",
"nanoid",
"network-interface",
"once_cell",
"open",
"os_info",
"parking_lot",
"percent-encoding",
"port_scanner",
"regex",
"reqwest",
"reqwest_dav",
"runas",
"scopeguard",
"serde",
"serde_json",
"serde_yaml",
"sha2 0.10.9",
"sys-locale",
"sysinfo",
"sysproxy",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-clipboard-manager",
"tauri-plugin-deep-link",
"tauri-plugin-devtools",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-notification",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tempfile",
"tokio",
"tokio-tungstenite 0.27.0",
"tungstenite 0.27.0",
"url",
"users",
"warp",
"winapi",
"winreg 0.55.0",
"zip",
]
[[package]] [[package]]
name = "kuchikiki" name = "kuchikiki"
version = "0.8.8-speedreader" version = "0.8.8-speedreader"
@@ -3887,11 +3888,12 @@ dependencies = [
[[package]] [[package]]
name = "machine-uid" name = "machine-uid"
version = "0.2.0" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212" checksum = "0c4506fa0abb0a2ea93f5862f55973da0a662d2ad0e98f337a1c5aac657f0892"
dependencies = [ dependencies = [
"winreg 0.6.2", "libc",
"winreg 0.52.0",
] ]
[[package]] [[package]]
@@ -6846,9 +6848,9 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.35.2" version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e" checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d"
dependencies = [ dependencies = [
"libc", "libc",
"memchr", "memchr",
@@ -7294,6 +7296,21 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a0e5a4ce43cb3a733c3aef85e8478bc769dac743c615e26639cbf5d953faf7"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.12",
"tracing",
"windows-sys 0.60.2",
"zbus",
]
[[package]] [[package]]
name = "tauri-plugin-updater" name = "tauri-plugin-updater"
version = "2.9.0" version = "2.9.0"
@@ -9323,15 +9340,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winreg"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.10.1" version = "0.10.1"
@@ -9568,9 +9576,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "5.7.1" version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-executor", "async-executor",
@@ -9602,9 +9610,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "5.7.1" version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659"
dependencies = [ dependencies = [
"proc-macro-crate 3.3.0", "proc-macro-crate 3.3.0",
"proc-macro2", "proc-macro2",

View File

@@ -1,16 +1,16 @@
[package] [package]
name = "clash-verge" name = "koala-clash"
version = "0.2.0" version = "0.2.5"
description = "clash verge" description = "koala clash"
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"] authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/coolcoala/clash-verge-rev-lite.git" repository = "https://github.com/coolcoala/clash-verge-rev-lite.git"
default-run = "clash-verge" default-run = "koala-clash"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
[package.metadata.bundle] [package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev" identifier = "io.github.koala-clash"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.3.0", features = [] } tauri-build = { version = "2.3.0", features = [] }
@@ -18,7 +18,7 @@ tauri-build = { version = "2.3.0", features = [] }
[dependencies] [dependencies]
url = "2.5.4" url = "2.5.4"
os_info = "3.0" os_info = "3.0"
machine-uid = "0.2" machine-uid = "0.5.3"
warp = "0.3.7" warp = "0.3.7"
anyhow = "1.0.98" anyhow = "1.0.98"
dirs = "6.0" dirs = "6.0"
@@ -28,7 +28,7 @@ dunce = "1.0.5"
log4rs = "1.3.0" log4rs = "1.3.0"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4.41" chrono = "0.4.41"
sysinfo = "0.35.2" sysinfo = "0.36.1"
boa_engine = "0.20.0" boa_engine = "0.20.0"
serde_json = "1.0.140" serde_json = "1.0.140"
serde_yaml = "0.9.34-deprecated" serde_yaml = "0.9.34-deprecated"
@@ -110,6 +110,7 @@ users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.0" tauri-plugin-autostart = "2.5.0"
tauri-plugin-global-shortcut = "2.3.0" tauri-plugin-global-shortcut = "2.3.0"
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2.9.0" tauri-plugin-updater = "2.9.0"
[features] [features]

View File

@@ -18,6 +18,7 @@
"autostart:allow-disable", "autostart:allow-disable",
"autostart:allow-is-enabled", "autostart:allow-is-enabled",
"core:window:allow-set-theme", "core:window:allow-set-theme",
"notification:default" "notification:default",
"core:webview:allow-set-webview-zoom"
] ]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,11 @@ use crate::{
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration; use std::time::Duration;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use std::collections::BTreeMap;
use url::Url;
use serde_yaml::Value;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use percent_encoding::percent_decode_str;
// 全局互斥锁防止并发配置更新 // 全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(()); static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
@@ -129,8 +134,24 @@ pub async fn enhance_profiles() -> CmdResult {
/// 导入配置文件 /// 导入配置文件
#[tauri::command] #[tauri::command]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult { pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; let existing_uid = {
wrap_err!(Config::profiles().data().append_item(item)) let profiles = Config::profiles();
let profiles = profiles.latest();
profiles.items.as_ref()
.and_then(|items| items.iter().find(|item| item.url.as_deref() == Some(&url)))
.and_then(|item| item.uid.clone())
};
if let Some(uid) = existing_uid {
logging!(info, Type::Cmd, true, "The profile with URL {} already exists (UID: {}). Running the update...", url, uid);
update_profile(uid, option).await
} else {
logging!(info, Type::Cmd, true, "Profile with URL {} not found. Create a new one...", url);
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
wrap_err!(Config::profiles().data().append_item(item))
}
} }
/// 重新排序配置文件 /// 重新排序配置文件
@@ -155,7 +176,34 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件 /// 删除配置文件
#[tauri::command] #[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult { pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?; let should_update;
{
let profiles_config = Config::profiles();
let mut profiles_data = profiles_config.data();
should_update = profiles_data.delete_item(index.clone()).map_err(|e| e.to_string())?;
let was_last_profile = profiles_data.items.as_ref().map_or(true, |items| {
!items.iter().any(|item|
item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string())
)
});
if was_last_profile {
logging!(info, Type::Cmd, true, "The last profile has been deleted. Disabling proxy modes...");
let verge_config = Config::verge();
let mut verge_data = verge_config.data();
if verge_data.enable_tun_mode == Some(true) || verge_data.enable_system_proxy == Some(true) {
verge_data.enable_tun_mode = Some(false);
verge_data.enable_system_proxy = Some(false);
verge_data.save_file().map_err(|e| e.to_string())?;
handle::Handle::refresh_verge();
handle::Handle::notice_message("info", "All profiles deleted, proxy disabled.");
}
}
}
// 删除后自动清理冗余文件 // 删除后自动清理冗余文件
let _ = Config::profiles().latest().auto_cleanup(); let _ = Config::profiles().latest().auto_cleanup();
@@ -647,3 +695,512 @@ pub fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
let next_time = timer.get_next_update_time(&uid); let next_time = timer.get_next_update_time(&uid);
Ok(next_time) Ok(next_time)
} }
#[tauri::command]
pub async fn update_profiles_on_startup() -> CmdResult {
logging!(info, Type::Cmd, true, "Checking profiles for updates at startup...");
let profiles_to_update = {
let profiles = Config::profiles();
let profiles = profiles.latest();
profiles.items.as_ref()
.map_or_else(
Vec::new,
|items| items.iter()
.filter(|item| item.option.as_ref().is_some_and(|opt| opt.update_always == Some(true)))
.filter_map(|item| item.uid.clone())
.collect()
)
};
if profiles_to_update.is_empty() {
logging!(info, Type::Cmd, true, "No profiles to update immediately.");
return Ok(());
}
logging!(info, Type::Cmd, true, "Found profiles to update: {:?}", profiles_to_update);
let mut update_futures = Vec::new();
for uid in profiles_to_update {
update_futures.push(update_profile(uid, None));
}
let results = futures::future::join_all(update_futures).await;
if results.iter().any(|res| res.is_ok()) {
logging!(info, Type::Cmd, true, "The startup update is complete, restart the kernel...");
CoreManager::global().update_config().await.map_err(|e| e.to_string())?;
handle::Handle::refresh_clash();
} else {
logging!(warn, Type::Cmd, true, "All updates completed with errors on startup.");
}
Ok(())
}
#[tauri::command]
pub async fn create_profile_from_share_link(link: String, template_name: String) -> CmdResult {
const DEFAULT_TEMPLATE: &str = r#"
mixed-port: 2080
allow-lan: true
tcp-concurrent: true
enable-process: true
find-process-mode: always
global-client-fingerprint: chrome
mode: rule
log-level: debug
ipv6: false
keep-alive-interval: 30
unified-delay: false
profile:
store-selected: true
store-fake-ip: true
sniffer:
enable: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
tun:
enable: true
stack: mixed
dns-hijack: ['any:53']
auto-route: true
auto-detect-interface: true
strict-route: true
dns:
enable: true
listen: :1053
prefer-h3: false
ipv6: false
enhanced-mode: fake-ip
fake-ip-filter: ['+.lan', '+.local']
nameserver: ['https://doh.dns.sb/dns-query']
proxies:
- name: myproxy
type: vless
server: YOURDOMAIN
port: 443
uuid: YOURUUID
network: tcp
flow: xtls-rprx-vision
udp: true
tls: true
reality-opts:
public-key: YOURPUBLIC
short-id: YOURSHORTID
servername: YOURREALITYDEST
client-fingerprint: chrome
proxy-groups:
- name: PROXY
type: select
proxies:
- myproxy
rule-providers:
ru-bundle:
type: http
behavior: domain
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/ru-bundle/rule.mrs
path: ./ru-bundle/rule.mrs
interval: 86400
refilter_domains:
type: http
behavior: domain
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/re-filter/domain-rule.mrs
path: ./re-filter/domain-rule.mrs
interval: 86400
refilter_ipsum:
type: http
behavior: ipcidr
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/re-filter/ip-rule.mrs
path: ./re-filter/ip-rule.mrs
interval: 86400
oisd_big:
type: http
behavior: domain
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/oisd/big.mrs
path: ./oisd/big.mrs
interval: 86400
rules:
- OR,((DOMAIN,ipwhois.app),(DOMAIN,ipwho.is),(DOMAIN,api.ip.sb),(DOMAIN,ipapi.co),(DOMAIN,ipinfo.io)),PROXY
- RULE-SET,oisd_big,REJECT
- PROCESS-NAME,Discord.exe,PROXY
- RULE-SET,ru-bundle,PROXY
- RULE-SET,refilter_domains,PROXY
- RULE-SET,refilter_ipsum,PROXY
- MATCH,DIRECT
"#;
const WITHOUT_RU_TEMPLATE: &str = r#"
mixed-port: 7890
allow-lan: true
tcp-concurrent: true
enable-process: true
find-process-mode: always
mode: rule
log-level: debug
ipv6: false
keep-alive-interval: 30
unified-delay: false
profile:
store-selected: true
store-fake-ip: true
sniffer:
enable: true
force-dns-mapping: true
parse-pure-ip: true
sniff:
HTTP:
ports:
- 80
- 8080-8880
override-destination: true
TLS:
ports:
- 443
- 8443
tun:
enable: true
stack: gvisor
auto-route: true
auto-detect-interface: false
dns-hijack:
- any:53
strict-route: true
mtu: 1500
dns:
enable: true
prefer-h3: true
use-hosts: true
use-system-hosts: true
listen: 127.0.0.1:6868
ipv6: false
enhanced-mode: redir-host
default-nameserver:
- tls://1.1.1.1
- tls://1.0.0.1
proxy-server-nameserver:
- tls://1.1.1.1
- tls://1.0.0.1
direct-nameserver:
- tls://77.88.8.8
nameserver:
- https://cloudflare-dns.com/dns-query
proxies:
- name: myproxy
type: vless
server: YOURDOMAIN
port: 443
uuid: YOURUUID
network: tcp
flow: xtls-rprx-vision
udp: true
tls: true
reality-opts:
public-key: YOURPUBLIC
short-id: YOURSHORTID
servername: YOURREALITYDEST
client-fingerprint: chrome
proxy-groups:
- name: PROXY
icon: https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Hijacking.png
type: select
proxies:
- ⚡️ Fastest
- 📶 First Available
- myproxy
- name: ⚡️ Fastest
icon: https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Auto.png
type: url-test
tolerance: 150
url: https://cp.cloudflare.com/generate_204
interval: 300
proxies:
- myproxy
- name: 📶 First Available
icon: https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Download.png
type: fallback
url: https://cp.cloudflare.com/generate_204
interval: 300
proxies:
- myproxy
rule-providers:
torrent-trackers:
type: http
behavior: domain
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/other/torrent-trackers.mrs
path: ./rule-sets/torrent-trackers.mrs
interval: 86400
torrent-clients:
type: http
behavior: classical
format: yaml
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/other/torrent-clients.yaml
path: ./rule-sets/torrent-clients.yaml
interval: 86400
geosite-ru:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/category-ru.mrs
path: ./geosite-ru.mrs
interval: 86400
xiaomi:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/xiaomi.mrs
path: ./rule-sets/xiaomi.mrs
interval: 86400
blender:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/blender.mrs
path: ./rule-sets/blender.mrs
interval: 86400
drweb:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/drweb.mrs
path: ./rule-sets/drweb.mrs
interval: 86400
debian:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/debian.mrs
path: ./rule-sets/debian.mrs
interval: 86400
canonical:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/canonical.mrs
path: ./rule-sets/canonical.mrs
interval: 86400
python:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/python.mrs
path: ./rule-sets/python.mrs
interval: 86400
geoip-ru:
type: http
behavior: ipcidr
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geoip/ru.mrs
path: ./geoip-ru.mrs
interval: 86400
geosite-private:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/private.mrs
path: ./geosite-private.mrs
interval: 86400
geoip-private:
type: http
behavior: ipcidr
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geoip/private.mrs
path: ./geoip-private.mrs
interval: 86400
rules:
- DOMAIN-SUFFIX,habr.com,PROXY
- DOMAIN-SUFFIX,kemono.su,PROXY
- DOMAIN-SUFFIX,jut.su,PROXY
- DOMAIN-SUFFIX,kara.su,PROXY
- DOMAIN-SUFFIX,theins.ru,PROXY
- DOMAIN-SUFFIX,tvrain.ru,PROXY
- DOMAIN-SUFFIX,echo.msk.ru,PROXY
- DOMAIN-SUFFIX,the-village.ru,PROXY
- DOMAIN-SUFFIX,snob.ru,PROXY
- DOMAIN-SUFFIX,novayagazeta.ru,PROXY
- DOMAIN-SUFFIX,moscowtimes.ru,PROXY
- DOMAIN-KEYWORD,animego,PROXY
- DOMAIN-KEYWORD,yummyanime,PROXY
- DOMAIN-KEYWORD,yummy-anime,PROXY
- DOMAIN-KEYWORD,animeportal,PROXY
- DOMAIN-KEYWORD,anime-portal,PROXY
- DOMAIN-KEYWORD,animedub,PROXY
- DOMAIN-KEYWORD,anidub,PROXY
- DOMAIN-KEYWORD,animelib,PROXY
- DOMAIN-KEYWORD,ikianime,PROXY
- DOMAIN-KEYWORD,anilibria,PROXY
- PROCESS-NAME,Discord.exe,PROXY
- PROCESS-NAME,discord,PROXY
- RULE-SET,geosite-private,DIRECT,no-resolve
- RULE-SET,geoip-private,DIRECT
- RULE-SET,torrent-clients,DIRECT
- RULE-SET,torrent-trackers,DIRECT
- DOMAIN-SUFFIX,.ru,DIRECT
- DOMAIN-SUFFIX,.su,DIRECT
- DOMAIN-SUFFIX,.ru.com,DIRECT
- DOMAIN-SUFFIX,.ru.net,DIRECT
- DOMAIN-SUFFIX,wikipedia.org,DIRECT
- DOMAIN-SUFFIX,kudago.com,DIRECT
- DOMAIN-SUFFIX,kinescope.io,DIRECT
- DOMAIN-SUFFIX,redheadsound.studio,DIRECT
- DOMAIN-SUFFIX,plplayer.online,DIRECT
- DOMAIN-SUFFIX,lomont.site,DIRECT
- DOMAIN-SUFFIX,remanga.org,DIRECT
- DOMAIN-SUFFIX,shopstory.live,DIRECT
- DOMAIN-KEYWORD,miradres,DIRECT
- DOMAIN-KEYWORD,premier,DIRECT
- DOMAIN-KEYWORD,shutterstock,DIRECT
- DOMAIN-KEYWORD,2gis,DIRECT
- DOMAIN-KEYWORD,diginetica,DIRECT
- DOMAIN-KEYWORD,kinescopecdn,DIRECT
- DOMAIN-KEYWORD,researchgate,DIRECT
- DOMAIN-KEYWORD,springer,DIRECT
- DOMAIN-KEYWORD,nextcloud,DIRECT
- DOMAIN-KEYWORD,wiki,DIRECT
- DOMAIN-KEYWORD,kaspersky,DIRECT
- DOMAIN-KEYWORD,stepik,DIRECT
- DOMAIN-KEYWORD,likee,DIRECT
- DOMAIN-KEYWORD,snapchat,DIRECT
- DOMAIN-KEYWORD,yappy,DIRECT
- DOMAIN-KEYWORD,pikabu,DIRECT
- DOMAIN-KEYWORD,okko,DIRECT
- DOMAIN-KEYWORD,wink,DIRECT
- DOMAIN-KEYWORD,kion,DIRECT
- DOMAIN-KEYWORD,roblox,DIRECT
- DOMAIN-KEYWORD,ozon,DIRECT
- DOMAIN-KEYWORD,wildberries,DIRECT
- DOMAIN-KEYWORD,aliexpress,DIRECT
- RULE-SET,geosite-ru,DIRECT
- RULE-SET,xiaomi,DIRECT
- RULE-SET,blender,DIRECT
- RULE-SET,drweb,DIRECT
- RULE-SET,debian,DIRECT
- RULE-SET,canonical,DIRECT
- RULE-SET,python,DIRECT
- RULE-SET,geoip-ru,DIRECT
- MATCH,PROXY
"#;
let template_yaml = match template_name.as_str() {
"without_ru" => WITHOUT_RU_TEMPLATE,
_ => DEFAULT_TEMPLATE,
};
let parsed_url = Url::parse(&link).map_err(|e| e.to_string())?;
let scheme = parsed_url.scheme();
let proxy_name = parsed_url.fragment()
.map(|f| percent_decode_str(f).decode_utf8_lossy().to_string())
.unwrap_or_else(|| "Proxy from Link".to_string());
let mut proxy_map: BTreeMap<String, Value> = BTreeMap::new();
proxy_map.insert("name".into(), proxy_name.clone().into());
proxy_map.insert("type".into(), scheme.into());
proxy_map.insert("server".into(), parsed_url.host_str().unwrap_or_default().into());
proxy_map.insert("port".into(), parsed_url.port().unwrap_or(443).into());
proxy_map.insert("udp".into(), true.into());
match scheme {
"vless" | "trojan" => {
proxy_map.insert("uuid".into(), parsed_url.username().into());
let mut reality_opts: BTreeMap<String, Value> = BTreeMap::new();
for (key, value) in parsed_url.query_pairs() {
match key.as_ref() {
"security" if value == "reality" => {
proxy_map.insert("tls".into(), true.into());
}
"security" if value == "tls" => {
proxy_map.insert("tls".into(), true.into());
}
"flow" => { proxy_map.insert("flow".into(), value.to_string().into()); }
"sni" => { proxy_map.insert("servername".into(), value.to_string().into()); }
"fp" => { proxy_map.insert("client-fingerprint".into(), value.to_string().into()); }
"pbk" => { reality_opts.insert("public-key".into(), value.to_string().into()); }
"sid" => { reality_opts.insert("short-id".into(), value.to_string().into()); }
_ => {}
}
}
if !reality_opts.is_empty() {
proxy_map.insert("reality-opts".into(), serde_yaml::to_value(reality_opts).map_err(|e| e.to_string())?);
}
}
"ss" => {
if let Ok(decoded_user) = STANDARD.decode(parsed_url.username()) {
if let Ok(user_str) = String::from_utf8(decoded_user) {
if let Some((cipher, password)) = user_str.split_once(':') {
proxy_map.insert("cipher".into(), cipher.into());
proxy_map.insert("password".into(), password.into());
}
}
}
}
"vmess" => {
if let Ok(decoded_bytes) = STANDARD.decode(parsed_url.host_str().unwrap_or_default()) {
if let Ok(json_str) = String::from_utf8(decoded_bytes) {
if let Ok(vmess_params) = serde_json::from_str::<BTreeMap<String, Value>>(&json_str) {
if let Some(add) = vmess_params.get("add") { proxy_map.insert("server".into(), add.clone()); }
if let Some(port) = vmess_params.get("port") { proxy_map.insert("port".into(), port.clone()); }
if let Some(id) = vmess_params.get("id") { proxy_map.insert("uuid".into(), id.clone()); }
if let Some(aid) = vmess_params.get("aid") { proxy_map.insert("alterId".into(), aid.clone()); }
if let Some(net) = vmess_params.get("net") { proxy_map.insert("network".into(), net.clone()); }
if let Some(ps) = vmess_params.get("ps") { proxy_map.insert("name".into(), ps.clone()); }
}
}
}
}
_ => {
}
}
let mut config: Value = serde_yaml::from_str(template_yaml).map_err(|e| e.to_string())?;
if let Some(proxies) = config.get_mut("proxies").and_then(|v| v.as_sequence_mut()) {
proxies.clear();
proxies.push(serde_yaml::to_value(proxy_map).map_err(|e| e.to_string())?);
}
if let Some(groups) = config.get_mut("proxy-groups").and_then(|v| v.as_sequence_mut()) {
for group in groups.iter_mut() {
if let Some(mapping) = group.as_mapping_mut() {
if let Some(proxies_list) = mapping.get_mut("proxies").and_then(|p| p.as_sequence_mut()) {
let new_proxies_list: Vec<Value> = proxies_list
.iter()
.map(|p| {
if p.as_str() == Some("myproxy") {
proxy_name.clone().into()
} else {
p.clone()
}
})
.collect();
*proxies_list = new_proxies_list;
}
}
}
}
let new_yaml_content = serde_yaml::to_string(&config).map_err(|e| e.to_string())?;
let item = PrfItem::from_local(proxy_name, "Created from share link".into(), Some(new_yaml_content), None)
.map_err(|e| e.to_string())?;
wrap_err!(Config::profiles().data().append_item(item))
}

View File

@@ -129,7 +129,7 @@ impl IClashTemp {
help::save_yaml( help::save_yaml(
&dirs::clash_path()?, &dirs::clash_path()?,
&self.0, &self.0,
Some("# Generated by Clash Verge"), Some("# Generated by Koala Clash"),
) )
} }

View File

@@ -11,8 +11,8 @@ use once_cell::sync::OnceCell;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; pub const RUNTIME_CONFIG: &str = "koala-clash.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; pub const CHECK_CONFIG: &str = "koala-clash-check.yaml";
pub struct Config { pub struct Config {
clash_config: Draft<Box<IClashTemp>>, clash_config: Draft<Box<IClashTemp>>,
@@ -141,7 +141,7 @@ impl Config {
.as_ref() .as_ref()
.ok_or(anyhow!("failed to get runtime config"))?; .ok_or(anyhow!("failed to get runtime config"))?;
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?; help::save_yaml(&path, &config, Some("# Generated by Koala Clash"))?;
Ok(path) Ok(path)
} }

View File

@@ -4,11 +4,11 @@ use crate::utils::{
tmpl, tmpl,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use reqwest::StatusCode; use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{fs, time::Duration}; use std::{fs, time::Duration};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use url::Url; use url::Url;
use super::Config; use super::Config;
@@ -63,6 +63,10 @@ pub struct PrfItem {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub announce: Option<String>, pub announce: Option<String>,
/// profile announce url
#[serde(skip_serializing_if = "Option::is_none")]
pub announce_url: Option<String>,
/// the file data /// the file data
#[serde(skip)] #[serde(skip)]
pub file_data: Option<String>, pub file_data: Option<String>,
@@ -126,6 +130,9 @@ pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub use_hwid: Option<bool>, pub use_hwid: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_always: Option<bool>,
} }
impl PrfOption { impl PrfOption {
@@ -146,6 +153,7 @@ impl PrfOption {
a.groups = b.groups.or(a.groups); a.groups = b.groups.or(a.groups);
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds); a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
a.use_hwid = b.use_hwid.or(a.use_hwid); a.use_hwid = b.use_hwid.or(a.use_hwid);
a.update_always = b.update_always.or(a.update_always);
Some(a) Some(a)
} }
t => t.0.or(t.1), t => t.0.or(t.1),
@@ -246,6 +254,7 @@ impl PrfItem {
home: None, home: None,
support_url: None, support_url: None,
announce: None, announce: None,
announce_url: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())), 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 user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval); 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 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 merge = opt_ref.and_then(|o| o.merge.clone());
let mut script = opt_ref.and_then(|o| o.script.clone()); let mut script = opt_ref.and_then(|o| o.script.clone());
let mut rules = opt_ref.and_then(|o| o.rules.clone()); let mut rules = opt_ref.and_then(|o| o.rules.clone());
@@ -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") { let home = match header.get("profile-web-page-url") {
Some(value) => { Some(value) => {
let str_value = value.to_str().unwrap_or(""); let str_value = value.to_str().unwrap_or("");
@@ -393,7 +407,8 @@ impl PrfItem {
Some(value) => { Some(value) => {
let str_value = value.to_str().unwrap_or(""); let str_value = value.to_str().unwrap_or("");
if let Some(b64_data) = str_value.strip_prefix("base64:") { if let Some(b64_data) = str_value.strip_prefix("base64:") {
STANDARD.decode(b64_data) STANDARD
.decode(b64_data)
.ok() .ok()
.and_then(|bytes| String::from_utf8(bytes).ok()) .and_then(|bytes| String::from_utf8(bytes).ok())
} else { } else {
@@ -403,11 +418,27 @@ impl PrfItem {
None => None, 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") { let profile_title = match header.get("profile-title") {
Some(value) => { Some(value) => {
let str_value = value.to_str().unwrap_or(""); let str_value = value.to_str().unwrap_or("");
if let Some(b64_data) = str_value.strip_prefix("base64:") { if let Some(b64_data) = str_value.strip_prefix("base64:") {
STANDARD.decode(b64_data) STANDARD
.decode(b64_data)
.ok() .ok()
.and_then(|bytes| String::from_utf8(bytes).ok()) .and_then(|bytes| String::from_utf8(bytes).ok())
} else { } else {
@@ -419,7 +450,9 @@ impl PrfItem {
let uid = help::get_uid("R"); let uid = help::get_uid("R");
let file = format!("{uid}.yaml"); let file = format!("{uid}.yaml");
let name = name.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?; let data = resp.text_with_charset("utf-8").await?;
// process the charset "UTF-8 with BOM" // process the charset "UTF-8 with BOM"
@@ -472,6 +505,7 @@ impl PrfItem {
extra, extra,
option: Some(PrfOption { option: Some(PrfOption {
update_interval, update_interval,
update_always,
merge, merge,
script, script,
rules, rules,
@@ -482,6 +516,7 @@ impl PrfItem {
home, home,
support_url, support_url,
announce, announce,
announce_url,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(data.into()), file_data: Some(data.into()),
}) })
@@ -511,6 +546,7 @@ impl PrfItem {
home: None, home: None,
support_url: None, support_url: None,
announce: None, announce: None,
announce_url: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(template), file_data: Some(template),
}) })
@@ -535,6 +571,7 @@ impl PrfItem {
home: None, home: None,
support_url: None, support_url: None,
announce: None, announce: None,
announce_url: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
@@ -558,6 +595,7 @@ impl PrfItem {
home: None, home: None,
support_url: None, support_url: None,
announce: None, announce: None,
announce_url: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
@@ -581,6 +619,7 @@ impl PrfItem {
home: None, home: None,
support_url: None, support_url: None,
announce: None, announce: None,
announce_url: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
@@ -604,6 +643,7 @@ impl PrfItem {
home: None, home: None,
support_url: None, support_url: None,
announce: None, announce: None,
announce_url: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,

View File

@@ -66,7 +66,7 @@ impl IProfiles {
help::save_yaml( help::save_yaml(
&dirs::profiles_path()?, &dirs::profiles_path()?,
self, self,
Some("# Profiles Config for Clash Verge"), Some("# Profiles Config for Koala Clash"),
) )
} }
@@ -221,6 +221,7 @@ impl IProfiles {
each.updated = item.updated; each.updated = item.updated;
each.home = item.home; each.home = item.home;
each.announce = item.announce; each.announce = item.announce;
each.announce_url = item.announce_url;
each.support_url = item.support_url; each.support_url = item.support_url;
each.name = item.name; each.name = item.name;
each.url = item.url; each.url = item.url;

View File

@@ -74,6 +74,8 @@ pub struct IVerge {
/// enable dns settings - this controls whether dns_config.yaml is applied /// enable dns settings - this controls whether dns_config.yaml is applied
pub enable_dns_settings: Option<bool>, pub enable_dns_settings: Option<bool>,
pub enable_send_hwid: Option<bool>,
pub primary_action: Option<String>, pub primary_action: Option<String>,
/// always use default bypass /// always use default bypass
@@ -236,7 +238,7 @@ pub struct IVergeTheme {
impl IVerge { impl IVerge {
/// 有效的clash核心名称 /// 有效的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值 /// 验证并修正配置文件中的clash_core值
pub fn validate_and_fix_config() -> Result<()> { pub fn validate_and_fix_config() -> Result<()> {
@@ -255,10 +257,10 @@ impl IVerge {
warn, warn,
Type::Config, Type::Config,
true, true,
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'", "启动时发现无效的clash_core配置: '{}', 将自动修正为 'koala-mihomo'",
core core
); );
config.clash_core = Some("verge-mihomo".to_string()); config.clash_core = Some("koala-mihomo".to_string());
needs_fix = true; needs_fix = true;
} }
} else { } else {
@@ -266,16 +268,16 @@ impl IVerge {
info, info,
Type::Config, Type::Config,
true, true,
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'" "启动时发现未配置clash_core, 将设置为默认值 'koala-mihomo'"
); );
config.clash_core = Some("verge-mihomo".to_string()); config.clash_core = Some("koala-mihomo".to_string());
needs_fix = true; needs_fix = true;
} }
// 修正后保存配置 // 修正后保存配置
if needs_fix { if needs_fix {
logging!(info, Type::Config, true, "正在保存修正后的配置文件..."); logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?; help::save_yaml(&config_path, &config, Some("# Koala Clash Config"))?;
logging!( logging!(
info, info,
Type::Config, Type::Config,
@@ -319,7 +321,7 @@ impl IVerge {
pub fn get_valid_clash_core(&self) -> String { pub fn get_valid_clash_core(&self) -> String {
self.clash_core self.clash_core
.clone() .clone()
.unwrap_or_else(|| "verge-mihomo".to_string()) .unwrap_or_else(|| "koala-mihomo".to_string())
} }
fn get_system_language() -> String { fn get_system_language() -> String {
@@ -338,18 +340,15 @@ impl IVerge {
} }
pub fn new() -> Self { pub fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) { dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)).unwrap_or_else(|err| {
Ok(config) => config, log::error!(target: "app", "{err}");
Err(err) => { Self::template()
log::error!(target: "app", "{err}"); })
Self::template()
}
}
} }
pub fn template() -> Self { pub fn template() -> Self {
Self { Self {
clash_core: Some("verge-mihomo".into()), clash_core: Some("koala-mihomo".into()),
language: Some(Self::get_system_language()), language: Some(Self::get_system_language()),
theme_mode: Some("system".into()), theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@@ -403,6 +402,7 @@ impl IVerge {
enable_auto_light_weight_mode: Some(false), enable_auto_light_weight_mode: Some(false),
auto_light_weight_minutes: Some(10), auto_light_weight_minutes: Some(10),
enable_dns_settings: Some(false), enable_dns_settings: Some(false),
enable_send_hwid: Some(true),
primary_action: Some("tun-mode".into()), primary_action: Some("tun-mode".into()),
home_cards: None, home_cards: None,
service_state: None, service_state: None,
@@ -412,7 +412,7 @@ impl IVerge {
/// Save IVerge App Config /// Save IVerge App Config
pub fn save_file(&self) -> Result<()> { pub fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")) help::save_yaml(&dirs::verge_path()?, &self, Some("# Koala Clash Config"))
} }
/// patch verge config /// patch verge config
@@ -492,6 +492,7 @@ impl IVerge {
patch!(enable_auto_light_weight_mode); patch!(enable_auto_light_weight_mode);
patch!(auto_light_weight_minutes); patch!(auto_light_weight_minutes);
patch!(enable_dns_settings); patch!(enable_dns_settings);
patch!(enable_send_hwid);
patch!(primary_action); patch!(primary_action);
patch!(home_cards); patch!(home_cards);
patch!(service_state); patch!(service_state);
@@ -588,6 +589,7 @@ pub struct IVergeResponse {
pub enable_auto_light_weight_mode: Option<bool>, pub enable_auto_light_weight_mode: Option<bool>,
pub auto_light_weight_minutes: Option<u64>, pub auto_light_weight_minutes: Option<u64>,
pub enable_dns_settings: Option<bool>, pub enable_dns_settings: Option<bool>,
pub enable_send_hwid: Option<bool>,
pub primary_action: Option<String>, pub primary_action: Option<String>,
pub home_cards: Option<serde_json::Value>, pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>, pub enable_hover_jump_navigator: Option<bool>,
@@ -661,6 +663,7 @@ impl From<IVerge> for IVergeResponse {
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode, enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
auto_light_weight_minutes: verge.auto_light_weight_minutes, auto_light_weight_minutes: verge.auto_light_weight_minutes,
enable_dns_settings: verge.enable_dns_settings, enable_dns_settings: verge.enable_dns_settings,
enable_send_hwid: verge.enable_send_hwid,
primary_action: verge.primary_action, primary_action: verge.primary_action,
home_cards: verge.home_cards, home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator, enable_hover_jump_navigator: verge.enable_hover_jump_navigator,

View File

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

View File

@@ -146,7 +146,7 @@ impl CoreManager {
help::save_yaml( help::save_yaml(
&runtime_path, &runtime_path,
&Config::clash().latest().0, &Config::clash().latest().0,
Some("# Clash Verge Runtime"), Some("# Koala Clash Runtime"),
)?; )?;
handle::Handle::notice_message(msg_type, msg_content); handle::Handle::notice_message(msg_type, msg_content);
Ok(()) Ok(())
@@ -443,7 +443,7 @@ impl CoreManager {
child_guard.as_ref().map(|child| child.pid()) 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(); let mut process_futures = Vec::new();

View File

@@ -578,7 +578,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "IPC通信失败: {}", e); logging!(error, Type::Service, true, "IPC通信失败: {}", e);
bail!("无法连接到Clash Verge Service: {}", e) bail!("无法连接到Koala Clash Service: {}", e)
} }
} }
} }
@@ -667,7 +667,7 @@ pub async fn check_service_version() -> Result<String> {
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "IPC通信失败: {}", e); logging!(error, Type::Service, true, "IPC通信失败: {}", e);
bail!("无法连接到Clash Verge Service: {}", e) bail!("无法连接到Koala Clash Service: {}", e)
} }
} }
} }
@@ -814,7 +814,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
} }
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "启动核心IPC通信失败: {}", e); logging!(error, Type::Service, true, "启动核心IPC通信失败: {}", e);
bail!("无法连接到Clash Verge Service: {}", e) bail!("无法连接到Koala Clash Service: {}", e)
} }
} }
} }
@@ -910,7 +910,7 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
let payload = serde_json::json!({}); let payload = serde_json::json!({});
let response = send_ipc_request(IpcCommand::StopClash, payload) let response = send_ipc_request(IpcCommand::StopClash, payload)
.await .await
.context("无法连接到Clash Verge Service")?; .context("无法连接到Koala Clash Service")?;
if !response.success { if !response.success {
bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string())); bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string()));

View File

@@ -6,9 +6,9 @@ use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
const IPC_SOCKET_NAME: &str = if cfg!(windows) { const IPC_SOCKET_NAME: &str = if cfg!(windows) {
r"\\.\pipe\clash-verge-service" r"\\.\pipe\koala-clash-service"
} else { } else {
"/tmp/clash-verge-service.sock" "/tmp/koala-clash-service.sock"
}; };
// 定义命令类型 // 定义命令类型
@@ -43,7 +43,7 @@ pub struct IpcResponse {
fn derive_secret_key() -> Vec<u8> { fn derive_secret_key() -> Vec<u8> {
// to do // to do
// 从系统安全存储中获取或从程序安装时生成的密钥文件中读取 // 从系统安全存储中获取或从程序安装时生成的密钥文件中读取
let unique_app_id = "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(); let mut hasher = Sha256::new();
hasher.update(unique_app_id.as_bytes()); hasher.update(unique_app_id.as_bytes());
hasher.finalize().to_vec() hasher.finalize().to_vec()

View File

@@ -414,7 +414,7 @@ impl Tray {
if let Some(tray) = app_handle.tray_by_id("main") { if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&format!( let _ = tray.set_tooltip(Some(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}", "Koala Clash {version}\n{}: {}\n{}: {}\n{}: {}",
t("SysProxy"), t("SysProxy"),
switch_map[system_proxy], switch_map[system_proxy],
t("TUN"), t("TUN"),
@@ -601,16 +601,6 @@ fn create_tray_menu(
) )
.unwrap(); .unwrap();
let direct_mode = &CheckMenuItem::with_id(
app_handle,
"direct_mode",
t("Direct Mode"),
true,
mode == "direct",
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
)
.unwrap();
let profiles = &Submenu::with_id_and_items( let profiles = &Submenu::with_id_and_items(
app_handle, app_handle,
"profiles", "profiles",
@@ -650,45 +640,6 @@ fn create_tray_menu(
) )
.unwrap(); .unwrap();
let copy_env =
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
let open_app_dir = &MenuItem::with_id(
app_handle,
"open_app_dir",
t("Conf Dir"),
true,
None::<&str>,
)
.unwrap();
let open_core_dir = &MenuItem::with_id(
app_handle,
"open_core_dir",
t("Core Dir"),
true,
None::<&str>,
)
.unwrap();
let open_logs_dir = &MenuItem::with_id(
app_handle,
"open_logs_dir",
t("Logs Dir"),
true,
None::<&str>,
)
.unwrap();
let open_dir = &Submenu::with_id_and_items(
app_handle,
"open_dir",
t("Open Dir"),
true,
&[open_app_dir, open_core_dir, open_logs_dir],
)
.unwrap();
let restart_clash = &MenuItem::with_id( let restart_clash = &MenuItem::with_id(
app_handle, app_handle,
"restart_clash", "restart_clash",
@@ -736,7 +687,6 @@ fn create_tray_menu(
separator, separator,
rule_mode, rule_mode,
global_mode, global_mode,
direct_mode,
separator, separator,
profiles, profiles,
separator, separator,
@@ -744,8 +694,6 @@ fn create_tray_menu(
tun_mode, tun_mode,
separator, separator,
lighteweight_mode, lighteweight_mode,
copy_env,
open_dir,
more, more,
separator, separator,
quit, quit,
@@ -789,16 +737,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
"tun_mode" => { "tun_mode" => {
feat::toggle_tun_mode(None); feat::toggle_tun_mode(None);
} }
"copy_env" => feat::copy_clash_env(),
"open_app_dir" => {
let _ = cmd::open_app_dir();
}
"open_core_dir" => {
let _ = cmd::open_core_dir();
}
"open_logs_dir" => {
let _ = cmd::open_logs_dir();
}
"restart_clash" => feat::restart_clash_core(), "restart_clash" => feat::restart_clash_core(),
"restart_app" => feat::restart_app(), "restart_app" => feat::restart_app(),
"entry_lightweight_mode" => { "entry_lightweight_mode" => {

View File

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

View File

@@ -10,7 +10,7 @@ mod utils;
use crate::{ use crate::{
core::hotkey, core::hotkey,
process::AsyncHandler, process::AsyncHandler,
utils::{resolve, resolve::resolve_scheme, server}, utils::{resolve, resolve::resolve_scheme},
}; };
use config::Config; use config::Config;
use std::sync::{Mutex, Once}; use std::sync::{Mutex, Once};
@@ -90,33 +90,6 @@ pub fn run() {
let _ = utils::dirs::init_portable_flag(); let _ = utils::dirs::init_portable_flag();
// 异步单例检测
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
}
}
Err(_) => {
logging!(
warn,
Type::Setup,
true,
"单例检查超时,假定没有其他实例运行"
);
}
}
});
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
@@ -125,6 +98,13 @@ pub fn run() {
#[allow(unused_mut)] #[allow(unused_mut)]
let mut builder = tauri::Builder::default() let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}))
.plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
@@ -216,6 +196,14 @@ pub fn run() {
app.manage(Mutex::new(state::proxy::CmdProxyState::default())); app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
app.manage(Mutex::new(state::lightweight::LightWeightState::default())); app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
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, "初始化完成,继续执行"); logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(()) Ok(())
}) })
@@ -295,6 +283,8 @@ pub fn run() {
cmd::read_profile_file, cmd::read_profile_file,
cmd::save_profile_file, cmd::save_profile_file,
cmd::get_next_update_time, cmd::get_next_update_time,
cmd::update_profiles_on_startup,
cmd::create_profile_from_share_link,
// script validation // script validation
cmd::script_validate_notice, cmd::script_validate_notice,
cmd::validate_script_file, cmd::validate_script_file,
@@ -343,7 +333,7 @@ pub fn run() {
.get_webview_window("main") .get_webview_window("main")
{ {
logging!(info, Type::Window, true, "设置macOS窗口标题"); logging!(info, Type::Window, true, "设置macOS窗口标题");
let _ = window.set_title("Clash Verge Rev Lite"); let _ = window.set_title("Koala Clash");
} }
} }
} }
@@ -354,6 +344,10 @@ pub fn run() {
} => { } => {
if !has_visible_windows { if !has_visible_windows {
AppHandleManager::global().set_activation_policy_regular(); AppHandleManager::global().set_activation_policy_regular();
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
} }
AppHandleManager::global().init(app_handle.clone()); AppHandleManager::global().init(app_handle.clone());
} }
@@ -374,7 +368,6 @@ pub fn run() {
match event { match event {
tauri::WindowEvent::CloseRequested { api, .. } => { tauri::WindowEvent::CloseRequested { api, .. } => {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() { if core::handle::Handle::global().is_exiting() {
return; return;
} }

View File

@@ -39,7 +39,7 @@ pub fn get_exe_path() -> Result<PathBuf> {
pub fn create_shortcut() -> Result<()> { pub fn create_shortcut() -> Result<()> {
let exe_path = get_exe_path()?; let exe_path = get_exe_path()?;
let startup_dir = get_startup_dir()?; let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk"); let shortcut_path = startup_dir.join("Koala-Clash.lnk");
// 如果快捷方式已存在,直接返回成功 // 如果快捷方式已存在,直接返回成功
if shortcut_path.exists() { if shortcut_path.exists() {
@@ -77,7 +77,7 @@ pub fn create_shortcut() -> Result<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn remove_shortcut() -> Result<()> { pub fn remove_shortcut() -> Result<()> {
let startup_dir = get_startup_dir()?; let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk"); let shortcut_path = startup_dir.join("Koala-Clash.lnk");
// 如果快捷方式不存在,直接返回成功 // 如果快捷方式不存在,直接返回成功
if !shortcut_path.exists() { if !shortcut_path.exists() {
@@ -96,7 +96,7 @@ pub fn remove_shortcut() -> Result<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn is_shortcut_enabled() -> Result<bool> { pub fn is_shortcut_enabled() -> Result<bool> {
let startup_dir = get_startup_dir()?; let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Clash-Verge.lnk"); let shortcut_path = startup_dir.join("Koala-Clash.lnk");
Ok(shortcut_path.exists()) Ok(shortcut_path.exists())
} }

View File

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

View File

@@ -246,7 +246,7 @@ fn init_dns_config() -> Result<()> {
help::save_yaml( help::save_yaml(
&dns_path, &dns_path,
&default_dns_config, &default_dns_config,
Some("# Clash Verge DNS Config"), Some("# Koala Clash DNS Config"),
)?; )?;
} }
@@ -274,14 +274,14 @@ pub fn init_config() -> Result<()> {
crate::log_err!(dirs::clash_path().map(|path| { crate::log_err!(dirs::clash_path().map(|path| {
if !path.exists() { if !path.exists() {
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Vergeasu"))?; help::save_yaml(&path, &IClashTemp::template().0, Some("# Koala Clash"))?;
} }
<Result<()>>::Ok(()) <Result<()>>::Ok(())
})); }));
crate::log_err!(dirs::verge_path().map(|path| { crate::log_err!(dirs::verge_path().map(|path| {
if !path.exists() { if !path.exists() {
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?; help::save_yaml(&path, &IVerge::template(), Some("# Koala Clash"))?;
} }
<Result<()>>::Ok(()) <Result<()>>::Ok(())
})); }));
@@ -291,7 +291,7 @@ pub fn init_config() -> Result<()> {
crate::log_err!(dirs::profiles_path().map(|path| { crate::log_err!(dirs::profiles_path().map(|path| {
if !path.exists() { if !path.exists() {
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?; help::save_yaml(&path, &IProfiles::template(), Some("# Koala Clash"))?;
} }
<Result<()>>::Ok(()) <Result<()>>::Ok(())
})); }));
@@ -371,8 +371,8 @@ pub fn init_scheme() -> Result<()> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER); let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?; let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
clash.set_value("", &"Clash Verge")?; clash.set_value("", &"Koala Clash")?;
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?; clash.set_value("URL Protocol", &"Koala Clash URL Scheme Protocol")?;
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?; let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
default_icon.set_value("", &app_exe)?; default_icon.set_value("", &app_exe)?;
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?; let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
@@ -384,7 +384,7 @@ pub fn init_scheme() -> Result<()> {
pub fn init_scheme() -> Result<()> { pub fn init_scheme() -> Result<()> {
let output = std::process::Command::new("xdg-mime") let output = std::process::Command::new("xdg-mime")
.arg("default") .arg("default")
.arg("clash-verge.desktop") .arg("koala-clash.desktop")
.arg("x-scheme-handler/clash") .arg("x-scheme-handler/clash")
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {

View File

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

View File

@@ -40,7 +40,7 @@ impl NetworkManager {
// 创建专用的异步运行时线程数限制为4个 // 创建专用的异步运行时线程数限制为4个
let runtime = Builder::new_multi_thread() let runtime = Builder::new_multi_thread()
.worker_threads(4) .worker_threads(4)
.thread_name("clash-verge-network") .thread_name("koala-clash-network")
.enable_io() .enable_io()
.enable_time() .enable_time()
.build() .build()
@@ -323,8 +323,8 @@ impl NetworkManager {
use crate::utils::resolve::VERSION; use crate::utils::resolve::VERSION;
let version = match VERSION.get() { let version = match VERSION.get() {
Some(v) => format!("clash-verge/v{v}"), Some(v) => format!("koala-clash/v{v}"),
None => "clash-verge/unknown".to_string(), None => "koala-clash/unknown".to_string(),
}; };
builder = builder.user_agent(version); builder = builder.user_agent(version);

View File

@@ -335,12 +335,12 @@ pub fn create_window(is_show: bool) -> bool {
"main", /* the unique window label */ "main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()), tauri::WebviewUrl::App("index.html".into()),
) )
.title("Clash Verge Rev Lite") .title("Koala Clash")
.center() .center()
.decorations(true) .decorations(true)
.fullscreen(false) .fullscreen(false)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) .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) // 立即显示窗口,避免用户等待 .visible(true) // 立即显示窗口,避免用户等待
.initialization_script( .initialization_script(
r#" r#"
@@ -549,36 +549,24 @@ 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 name: Option<String> = None;
let mut url_param: Option<String> = None; let mut url_param: Option<String> = None;
let mut use_hwid = true;
for (key, value) in link_parsed.query_pairs() { for (key, value) in link_parsed.query_pairs() {
match key.as_ref() { match key.as_ref() {
"name" => name = Some(value.into_owned()), "name" => name = Some(value.into_owned()),
"url" => url_param = Some(percent_decode_str(&value).decode_utf8_lossy().to_string()), "url" => url_param = Some(percent_decode_str(&value).decode_utf8_lossy().to_string()),
"hwid" => use_hwid = value == "1" || value == "true",
_ => {} _ => {}
} }
} }
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 { match url_param {
Some(url) => { Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}"); log::info!(target:"app", "decoded subscription url: {url}");
create_window(false); create_window(true);
match PrfItem::from_url(url.as_ref(), name, None, option).await { match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => { Ok(item) => {
let uid = item.uid.clone().unwrap(); let uid = item.uid.clone().unwrap();
let _ = wrap_err!(Config::profiles().data().append_item(item)); let _ = wrap_err!(Config::profiles().data().append_item(item));

View File

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

View File

@@ -1,7 +1,7 @@
//! Some config file template //! Some config file template
/// template for new a profile item /// template for new a profile item
pub const ITEM_LOCAL: &str = "# Profile Template for Clash Verge pub const ITEM_LOCAL: &str = "# Profile Template for Koala Clash
proxies: [] proxies: []
@@ -11,13 +11,13 @@ rules: []
"; ";
/// enhanced profile /// 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: profile:
store-selected: true 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 /// 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: [] prepend: []
@@ -40,7 +40,7 @@ delete: []
"; ";
/// enhanced profile /// 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: [] prepend: []
@@ -50,7 +50,7 @@ delete: []
"; ";
/// enhanced profile /// 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: [] prepend: []

View File

@@ -1,9 +1,9 @@
{ {
"version": "0.2.0", "version": "0.2.5",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": { "bundle": {
"active": true, "active": true,
"longDescription": "Clash Verge Rev Lite", "longDescription": "Koala Clash",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
@@ -12,11 +12,11 @@
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": ["resources", "resources/locales/*"], "resources": ["resources", "resources/locales/*"],
"publisher": "Clash Verge Rev Lite", "publisher": "Koala Clash",
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"], "externalBin": ["sidecar/koala-mihomo", "sidecar/koala-mihomo-alpha"],
"copyright": "GNU General Public License v3.0", "copyright": "GNU General Public License v3.0",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "Clash Verge Rev Lite", "shortDescription": "Koala Clash",
"createUpdaterArtifacts": true "createUpdaterArtifacts": true
}, },
"build": { "build": {
@@ -25,8 +25,8 @@
"beforeDevCommand": "pnpm run web:dev", "beforeDevCommand": "pnpm run web:dev",
"devUrl": "http://localhost:3000/" "devUrl": "http://localhost:3000/"
}, },
"productName": "Clash Verge Rev Lite", "productName": "Koala Clash",
"identifier": "io.github.clash-verge-rev.clash-verge-rev", "identifier": "io.github.koala-clash",
"plugins": { "plugins": {
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K",
@@ -40,7 +40,7 @@
}, },
"deep-link": { "deep-link": {
"desktop": { "desktop": {
"schemes": ["clash", "clash-verge"] "schemes": ["clash", "koala-clash"]
} }
} }
}, },

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.clash-verge-rev.clash-verge-rev", "identifier": "io.github.koala-clash",
"productName": "Clash Verge Rev Lite", "productName": "Koala Clash",
"bundle": { "bundle": {
"targets": ["app", "dmg"], "targets": ["app", "dmg"],
"macOS": { "macOS": {
@@ -14,11 +14,11 @@
"background": "images/background.png", "background": "images/background.png",
"appPosition": { "appPosition": {
"x": 180, "x": 180,
"y": 170 "y": 200
}, },
"applicationFolderPosition": { "applicationFolderPosition": {
"x": 480, "x": 480,
"y": 170 "y": 200
}, },
"windowSize": { "windowSize": {
"height": 400, "height": 400,

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.clash-verge-rev.clash-verge-rev", "identifier": "io.github.koala-clash",
"bundle": { "bundle": {
"targets": ["nsis"], "targets": ["nsis"],
"windows": { "windows": {
@@ -9,7 +9,7 @@
"timestampUrl": "", "timestampUrl": "",
"webviewInstallMode": { "webviewInstallMode": {
"type": "fixedRuntime", "type": "fixedRuntime",
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.arm64/" "path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.arm64/"
}, },
"nsis": { "nsis": {
"displayLanguageSelector": true, "displayLanguageSelector": true,

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.clash-verge-rev.clash-verge-rev", "identifier": "io.github.koala-clash",
"bundle": { "bundle": {
"targets": ["nsis"], "targets": ["nsis"],
"windows": { "windows": {
@@ -9,7 +9,7 @@
"timestampUrl": "", "timestampUrl": "",
"webviewInstallMode": { "webviewInstallMode": {
"type": "fixedRuntime", "type": "fixedRuntime",
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.x64/" "path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.x64/"
}, },
"nsis": { "nsis": {
"displayLanguageSelector": true, "displayLanguageSelector": true,

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.clash-verge-rev.clash-verge-rev", "identifier": "io.github.koala-clash",
"bundle": { "bundle": {
"targets": ["nsis"], "targets": ["nsis"],
"windows": { "windows": {
@@ -9,7 +9,7 @@
"timestampUrl": "", "timestampUrl": "",
"webviewInstallMode": { "webviewInstallMode": {
"type": "fixedRuntime", "type": "fixedRuntime",
"path": "./Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.x86/" "path": "./Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.x86/"
}, },
"nsis": { "nsis": {
"displayLanguageSelector": true, "displayLanguageSelector": true,

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/assets/image/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

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

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 21 KiB

422
src/assets/image/map.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 453 KiB

View File

@@ -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 />;
};

View File

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

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { cn } from '@root/lib/utils';
import { Power } from 'lucide-react';
export interface PowerButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
checked?: boolean;
loading?: boolean;
}
export const PowerButton = React.forwardRef<HTMLButtonElement, PowerButtonProps>(
({ className, checked = false, loading = false, ...props }, ref) => {
const state = checked ? 'on' : 'off';
return (
<div className="relative flex items-center justify-center h-44 w-44">
<div
className={cn(
'absolute h-28 w-28 rounded-full blur-3xl transition-all duration-500',
state === 'on' ? 'bg-green-400/60' : 'bg-red-500/40',
props.disabled && 'opacity-0'
)}
/>
<button
ref={ref}
type="button"
disabled={loading || props.disabled}
data-state={state}
className={cn(
'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',
'text-red-500 shadow-[0_0_30px_rgba(239,68,68,0.6)]',
'data-[state=on]:text-green-500 dark:data-[state=on]:text-white',
'data-[state=on]:shadow-[0_0_50px_rgba(34,197,94,1)]',
'transition-all duration-300 hover:scale-105 active:scale-95 focus:outline-none',
'disabled:cursor-not-allowed disabled:scale-100',
// Стили ТОЛЬКО для отключенного состояния (но не для загрузки)
(props.disabled && !loading) && 'grayscale opacity-50 shadow-none bg-slate-100/70 border-slate-300/80',
className
)}
{...props}
>
<Power className={cn(
"h-20 w-20",
!props.disabled && "active:scale-90 transition-transform duration-300"
)} />
</button>
{loading && (
<div className="absolute inset-0 flex items-center justify-center z-20">
<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'
)} />
</div>
)}
</div>
);
}
);

View File

@@ -37,6 +37,7 @@ interface IProxyGroup {
now: string; now: string;
hidden: boolean; hidden: boolean;
all: (string | { name: string })[]; all: (string | { name: string })[];
icon?: string;
} }
// --- Вспомогательная функция для цвета задержки --- // --- Вспомогательная функция для цвета задержки ---
@@ -112,6 +113,7 @@ export const ProxySelectors: React.FC = () => {
(localStorage.getItem(STORAGE_KEY_SORT_TYPE) as ProxySortType) || (localStorage.getItem(STORAGE_KEY_SORT_TYPE) as ProxySortType) ||
"default", "default",
); );
const enable_group_icon = verge?.enable_group_icon ?? true;
useEffect(() => { useEffect(() => {
if (!proxies?.groups) return; if (!proxies?.groups) return;
@@ -291,21 +293,31 @@ export const ProxySelectors: React.FC = () => {
disabled={isGlobalMode || isDirectMode} disabled={isGlobalMode || isDirectMode}
> >
<SelectTrigger className="w-100"> <SelectTrigger className="w-100">
<Tooltip> <div className="flex items-center gap-2 truncate">
<TooltipTrigger asChild> <span className="truncate">
<span className="truncate"> <SelectValue placeholder={t("Select a group...")} />
<SelectValue placeholder={t("Select a group...")} /> </span>
</span> </div>
</TooltipTrigger>
<TooltipContent>
<p>{selectedGroup}</p>
</TooltipContent>
</Tooltip>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{selectorGroups.map((group: IProxyGroup) => ( {selectorGroups.map((group: IProxyGroup) => (
<SelectItem key={group.name} value={group.name}> <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> </SelectItem>
))} ))}
</SelectContent> </SelectContent>

View File

@@ -0,0 +1,108 @@
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>
)
}

View File

@@ -5,6 +5,9 @@ import { UpdateViewer } from "../setting/mods/update-viewer";
import { DialogRef } from "../base"; import { DialogRef } from "../base";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { t } from "i18next";
import {Download, RefreshCw} from "lucide-react";
import { useSidebar } from "../ui/sidebar";
interface Props { interface Props {
className?: string; className?: string;
@@ -14,6 +17,7 @@ export const UpdateButton = (props: Props) => {
const { className } = props; const { className } = props;
const { verge } = useVerge(); const { verge } = useVerge();
const { auto_check_update } = verge || {}; const { auto_check_update } = verge || {};
const { state: sidebarState } = useSidebar();
const viewerRef = useRef<DialogRef>(null); const viewerRef = useRef<DialogRef>(null);
@@ -32,15 +36,26 @@ export const UpdateButton = (props: Props) => {
return ( return (
<> <>
<UpdateViewer ref={viewerRef} /> <UpdateViewer ref={viewerRef} />
{sidebarState === 'collapsed' ? (
<Button <Button
variant="destructive" variant="outline"
size="sm" size="icon"
className={className} className={className}
onClick={() => viewerRef.current?.open()} onClick={() => viewerRef.current?.open()}
> >
New <Download />
</Button> </Button>
) : (
<Button
variant="outline"
size="lg"
className={className}
onClick={() => viewerRef.current?.open()}
>
<Download />
{t("New update")}
</Button>
)}
</> </>
); );
}; };

View File

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

View File

@@ -69,7 +69,7 @@ const LogItem = ({ value, searchState }: Props) => {
{renderHighlightText(value.type)} {renderHighlightText(value.type)}
</span> </span>
</div> </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)} {renderHighlightText(value.payload)}
</div> </div>
</div> </div>

View File

@@ -562,7 +562,7 @@ export const GroupsEditorViewer = (props: Props) => {
return ( return (
<Dialog open={open} onOpenChange={onClose}> <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> <DialogHeader>
<div className="flex justify-between items-center pr-8"> <div className="flex justify-between items-center pr-8">
<DialogTitle>{t("Edit Groups")}</DialogTitle> <DialogTitle>{t("Edit Groups")}</DialogTitle>

View 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>
);
};

View File

@@ -56,6 +56,7 @@ import {
Loader2, Loader2,
Info, Info,
DownloadCloud, DownloadCloud,
Download,
Trash2, Trash2,
Edit3, Edit3,
FileText as FileTextIcon, FileText as FileTextIcon,
@@ -66,7 +67,7 @@ import {
ListTree, ListTree,
CheckCircle, CheckCircle,
Infinity, Infinity,
RefreshCw, RefreshCw, Network,
} from "lucide-react"; } from "lucide-react";
import { t } from "i18next"; import { t } from "i18next";
@@ -343,8 +344,8 @@ export const ProfileItem = (props: Props) => {
</div> </div>
<div className="flex items-center flex-shrink-0"> <div className="flex items-center flex-shrink-0">
<Badge <Badge
variant={type === "local" ? "secondary" : "outline"} variant="outline"
className="text-xs" className="text-xs shadow-sm"
> >
{type} {type}
</Badge> </Badge>
@@ -384,20 +385,21 @@ export const ProfileItem = (props: Props) => {
)} )}
</div> </div>
</div> </div>
</div> <div className="flex items-center justify-between">
</div> <div className="flex items-center">
<Download className="h-3 w-3 inline mr-1.5" />
<span className="pr-5">
{parseTraffic(download)}
</span>
<Network className="h-3 w-3 inline mr-1.5" />
{total > 0 ? (
<span>{parseTraffic(total)}</span>
) : <Infinity className="h-3 w-3 inline mr-1.5" />}
{hasExtra && total > 0 && ( </div>
<div className="relative h-5">
<Progress value={progress} className="h-full rounded-none" />
<div className="absolute inset-0 flex items-center justify-between px-2 text-xs text-white/90 font-medium">
<span>
{parseTraffic(download)} / {parseTraffic(upload)}
</span>
<span>{parseTraffic(total)}</span>
</div> </div>
</div> </div>
)} </div>
</Card> </Card>
</ContextMenuTrigger> </ContextMenuTrigger>
@@ -405,7 +407,6 @@ export const ProfileItem = (props: Props) => {
className="w-56" className="w-56"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Объединяем все части меню */}
{[...homeMenuItem, ...mainMenuItems].map((item) => ( {[...homeMenuItem, ...mainMenuItems].map((item) => (
<ContextMenuItem <ContextMenuItem
key={item.label} key={item.label}
@@ -420,7 +421,7 @@ export const ProfileItem = (props: Props) => {
<ContextMenuSub> <ContextMenuSub>
<ContextMenuSubTrigger disabled={!hasUrl || isLoading}> <ContextMenuSubTrigger disabled={!hasUrl || isLoading}>
<DownloadCloud className="mr-2 h-4 w-4" /> <DownloadCloud className="mr-2 h-4 w-4" />
<span>{t("Update")}</span> <span className="px-2">{t("Update")}</span>
</ContextMenuSubTrigger> </ContextMenuSubTrigger>
<ContextMenuPortal> <ContextMenuPortal>
<ContextMenuSubContent> <ContextMenuSubContent>

View File

@@ -12,13 +12,14 @@ import {
createProfile, createProfile,
patchProfile, patchProfile,
importProfile, importProfile,
enhanceProfiles, enhanceProfiles, createProfileFromShareLink,
} from "@/services/cmds"; } from "@/services/cmds";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import { version } from "@root/package.json"; import { version } from "@root/package.json";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -72,6 +73,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
const [isCheckingUrl, setIsCheckingUrl] = useState(false); const [isCheckingUrl, setIsCheckingUrl] = useState(false);
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState("default");
const form = useForm<IProfileItem>({ const form = useForm<IProfileItem>({
defaultValues: { defaultValues: {
@@ -136,14 +138,9 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
setIsCheckingUrl(true); setIsCheckingUrl(true);
const handler = setTimeout(() => { const handler = setTimeout(() => {
try { const isValid = /^(https?|vmess|vless|ss|socks|trojan):\/\//.test(importUrl);
new URL(importUrl); setIsUrlValid(isValid);
setIsUrlValid(true); setIsCheckingUrl(false);
} catch (error) {
setIsUrlValid(false);
} finally {
setIsCheckingUrl(false);
}
}, 500); }, 500);
return () => { return () => {
clearTimeout(handler); clearTimeout(handler);
@@ -151,30 +148,40 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
}, [importUrl]); }, [importUrl]);
const handleImport = useLockFn(async () => { const handleImport = useLockFn(async () => {
if (!importUrl) return; if (!importUrl || !isUrlValid) return;
setIsImporting(true); setIsImporting(true);
const isShareLink = /^(vmess|vless|ss|socks|trojan):\/\//.test(importUrl);
try { try {
await importProfile(importUrl); if (isShareLink) {
showNotice("success", t("Profile Imported Successfully")); await createProfileFromShareLink(importUrl, selectedTemplate);
showNotice("success", t("Profile created from link successfully"));
} else {
await importProfile(importUrl);
showNotice("success", t("Profile Imported Successfully"));
}
props.onChange(); props.onChange();
await enhanceProfiles(); await enhanceProfiles();
setOpen(false); setOpen(false);
} catch (err) { } catch (err: any) {
showNotice("info", t("Import failed, retrying with Clash proxy...")); const errorMessage = typeof err === 'string' ? err : (err.message || String(err));
try { const lowerErrorMessage = errorMessage.toLowerCase();
await importProfile(importUrl, { if (lowerErrorMessage.includes('device') || lowerErrorMessage.includes('устройств')) {
with_proxy: false, window.dispatchEvent(new CustomEvent('show-hwid-error', { detail: errorMessage }));
self_proxy: true, } else if (!isShareLink && errorMessage.includes("failed to fetch")) {
}); showNotice("info", t("Import failed, retrying with Clash proxy..."));
showNotice("success", t("Profile Imported with Clash proxy")); try {
props.onChange(); await importProfile(importUrl, { with_proxy: false, self_proxy: true });
await enhanceProfiles(); showNotice("success", t("Profile Imported with Clash proxy"));
setOpen(false); props.onChange();
} catch (retryErr: any) { await enhanceProfiles();
showNotice( setOpen(false);
"error", } catch (retryErr: any) {
`${t("Import failed even with Clash proxy")}: ${retryErr?.message || retryErr.toString()}`, showNotice("error", `${t("Import failed even with Clash proxy")}: ${retryErr?.message || retryErr.toString()}`);
); }
} else {
showNotice("error", errorMessage);
} }
} finally { } finally {
setIsImporting(false); setIsImporting(false);
@@ -289,11 +296,26 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
</Button> </Button>
{!isUrlValid && importUrl && ( {!isUrlValid && importUrl && (
<p className="text-sm text-destructive px-1"> <p className="text-sm text-destructive px-1">
{t("Please enter a valid URL")} {t("Invalid Profile URL")}
</p> </p>
)} )}
</div> </div>
{/^(vmess|vless|ss|socks|trojan):\/\//.test(importUrl) && (
<div className="space-y-2">
<Label>{t("Template")}</Label>
<Select value={selectedTemplate} onValueChange={setSelectedTemplate}>
<SelectTrigger>
<SelectValue placeholder="Select a template..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">{t("Default Template")}</SelectItem>
<SelectItem value="without_ru">{t("Template without RU Rules")}</SelectItem>
</SelectContent>
</Select>
</div>
)}
<Button <Button
variant="outline" variant="outline"
onClick={() => setShowAdvanced(!showAdvanced)} onClick={() => setShowAdvanced(!showAdvanced)}
@@ -440,13 +462,28 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
<FormLabel>User Agent</FormLabel> <FormLabel>User Agent</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder={`clash-verge/v${version}`} placeholder={`koala-clash/v${version}`}
{...field} {...field}
/> />
</FormControl> </FormControl>
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={control}
name="option.update_always"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<FormLabel>{t("Update on Startup")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField <FormField
control={control} control={control}
name="option.with_proxy" name="option.with_proxy"

View File

@@ -302,7 +302,7 @@ export const ProxiesEditorViewer = (props: Props) => {
return ( return (
<Dialog open={open} onOpenChange={onClose}> <Dialog open={open} onOpenChange={onClose}>
<DialogContent className="lg:max-w-4xl h-[80vh] flex flex-col"> <DialogContent className="min-w-4xl h-[90vh] flex flex-col">
<DialogHeader> <DialogHeader>
<div className="flex justify-between items-center pr-8"> <div className="flex justify-between items-center pr-8">
<DialogTitle>{t("Edit Proxies")}</DialogTitle> <DialogTitle>{t("Edit Proxies")}</DialogTitle>

View File

@@ -513,7 +513,7 @@ export const RulesEditorViewer = (props: Props) => {
return ( return (
<Dialog open={open} onOpenChange={onClose}> <Dialog open={open} onOpenChange={onClose}>
<DialogContent className="w-[95vw] lg:min-w-5xl h-[80vh] flex flex-col"> <DialogContent className="min-w-4xl h-[90vh] flex flex-col">
<DialogHeader> <DialogHeader>
<div className="flex justify-between items-center pr-8"> <div className="flex justify-between items-center pr-8">
<DialogTitle>{t("Edit Rules")}</DialogTitle> <DialogTitle>{t("Edit Rules")}</DialogTitle>
@@ -529,8 +529,8 @@ export const RulesEditorViewer = (props: Props) => {
<div className="flex-1 min-h-0 mt-4"> <div className="flex-1 min-h-0 mt-4">
{visualization ? ( {visualization ? (
<div className="h-full flex flex-col lg:flex-row gap-4"> <div className="h-full flex flex-row gap-4">
<div className="w-full lg:w-1/3 flex flex-col gap-4 p-1"> <div className="w-1/3 flex flex-col gap-4 p-1">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("Rule Type")}</Label> <Label>{t("Rule Type")}</Label>
<Combobox <Combobox
@@ -617,8 +617,8 @@ export const RulesEditorViewer = (props: Props) => {
</Button> </Button>
</div> </div>
</div> </div>
<Separator orientation="vertical" className="hidden lg:flex" /> <Separator orientation="vertical" className="flex" />
<div className="w-full lg:w-2/3 flex flex-col min-w-0 flex-1"> <div className="w-2/3 flex flex-col min-w-0 flex-1">
<BaseSearchBox <BaseSearchBox
onSearch={(matcher) => setMatch(() => matcher)} onSearch={(matcher) => setMatch(() => matcher)}
/> />

View File

@@ -23,14 +23,8 @@ import { ProxyRender } from "./proxy-render";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScrollTopButton } from "../layout/scroll-top-button"; import { ScrollTopButton } from "../layout/scroll-top-button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
// Вспомогательная функция для плавного скролла (взята из вашего оригинального файла)
function throttle<T extends (...args: any[]) => any>( function throttle<T extends (...args: any[]) => any>(
func: T, func: T,
wait: number, wait: number,
@@ -59,36 +53,6 @@ function throttle<T extends (...args: any[]) => any>(
}; };
} }
// Компонент для одной буквы в навигаторе, переписанный на Tailwind и shadcn/ui
const LetterItem = memo(
({
name,
onClick,
getFirstChar,
}: {
name: string;
onClick: (name: string) => void;
getFirstChar: (str: string) => string;
}) => {
return (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<div
className="flex items-center justify-center w-6 h-6 text-xs rounded-md border shadow-sm cursor-pointer text-muted-foreground transition-transform hover:bg-accent hover:text-accent-foreground hover:scale-125"
onClick={() => onClick(name)}
>
{getFirstChar(name)}
</div>
</TooltipTrigger>
<TooltipContent side="left">
<p>{name}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
);
interface Props { interface Props {
mode: string; mode: string;
@@ -108,33 +72,6 @@ export const ProxyGroups = memo((props: Props) => {
const scrollerRef = useRef<Element | null>(null); const scrollerRef = useRef<Element | null>(null);
const [showScrollTop, setShowScrollTop] = useState(false); const [showScrollTop, setShowScrollTop] = useState(false);
// Мемоизация вычисления букв и индексов для навигатора
const { groupFirstLetters, letterIndexMap } = useMemo(() => {
const letters = new Set<string>();
const indexMap: Record<string, number> = {};
renderList.forEach((item, index) => {
if (item.type === 0) {
// type 0 - это заголовок группы
const fullName = item.group.name;
letters.add(fullName);
if (!(fullName in indexMap)) {
indexMap[fullName] = index;
}
}
});
return {
groupFirstLetters: Array.from(letters),
letterIndexMap: indexMap,
};
}, [renderList]);
// Мемоизация функции для получения первой буквы (поддерживает эмодзи)
const getFirstChar = useCallback((str: string) => {
const match = str.match(
/\p{Regional_Indicator}{2}|\p{Extended_Pictographic}|\p{L}|\p{N}|./u,
);
return match ? match[0] : str.charAt(0);
}, []);
// Обработчик скролла для показа/скрытия кнопки "Наверх" // Обработчик скролла для показа/скрытия кнопки "Наверх"
const handleScroll = useCallback( const handleScroll = useCallback(
@@ -161,20 +98,6 @@ export const ProxyGroups = memo((props: Props) => {
virtuosoRef.current?.scrollTo({ top: 0, behavior: "smooth" }); virtuosoRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}, []); }, []);
const handleLetterClick = useCallback(
(name: string) => {
const index = letterIndexMap[name];
if (index !== undefined) {
virtuosoRef.current?.scrollToIndex({
index,
align: "start",
behavior: "smooth",
});
}
},
[letterIndexMap],
);
// Вся бизнес-логика из оригинального файла // Вся бизнес-логика из оригинального файла
const handleChangeProxy = useLockFn( const handleChangeProxy = useLockFn(
async (group: IProxyGroupItem, proxy: IProxyItem) => { async (group: IProxyGroupItem, proxy: IProxyItem) => {
@@ -288,18 +211,6 @@ export const ProxyGroups = memo((props: Props) => {
)} )}
/> />
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} /> <ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
{/* Алфавитный указатель */}
<div className="fixed top-1/2 right-4 z-50 flex -translate-y-1/2 flex-col gap-1 rounded-md bg-background/50 p-1 backdrop-blur-sm">
{groupFirstLetters.map((name) => (
<LetterItem
key={name}
name={name}
onClick={handleLetterClick}
getFirstChar={getFirstChar}
/>
))}
</div>
</div> </div>
); );
}); });

View File

@@ -66,7 +66,7 @@ export const ProxyItemMini = (props: Props) => {
title={`${proxy.name}\n${proxy.now ?? ""}`} title={`${proxy.name}\n${proxy.now ?? ""}`}
className="group relative flex h-16 cursor-pointer items-center justify-between rounded-lg border bg-card p-3 shadow-sm transition-colors duration-200 hover:bg-accent data-[selected=true]:ring-2 data-[selected=true]:ring-primary" className="group relative flex h-16 cursor-pointer items-center justify-between rounded-lg border bg-card p-3 shadow-sm transition-colors duration-200 hover:bg-accent data-[selected=true]:ring-2 data-[selected=true]:ring-primary"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0 w-0">
<p className="truncate text-sm font-medium">{proxy.name}</p> <p className="truncate text-sm font-medium">{proxy.name}</p>
{showType && ( {showType && (

View File

@@ -16,7 +16,7 @@ type HeadStateStorage = Record<string, Record<string, HeadState>>;
const HEAD_STATE_KEY = "proxy-head-state"; const HEAD_STATE_KEY = "proxy-head-state";
export const DEFAULT_STATE: HeadState = { export const DEFAULT_STATE: HeadState = {
open: false, open: false,
showType: true, showType: false,
sortType: 0, sortType: 0,
filterText: "", filterText: "",
textState: null, textState: null,

View File

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

View File

@@ -36,6 +36,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { Loader2 } from "lucide-react";
const OS = getSystem(); const OS = getSystem();
@@ -69,6 +70,9 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge(); const { verge, patchVerge, mutateVerge } = useVerge();
const [localConfig, setLocalConfig] = useState<Partial<IVergeConfig>>({});
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [commonIcon, setCommonIcon] = useState(""); const [commonIcon, setCommonIcon] = useState("");
const [sysproxyIcon, setSysproxyIcon] = useState(""); const [sysproxyIcon, setSysproxyIcon] = useState("");
@@ -96,28 +100,26 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (open) initIconPath(); if (open) {
}, [open, initIconPath]); setLocalConfig(verge ?? {});
initIconPath();
}
}, [open, verge, initIconPath]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => setOpen(true), open: () => setOpen(true),
close: () => setOpen(false), close: () => setOpen(false),
})); }));
const onSwitchFormat = (_e: any, value: boolean) => value; const handleConfigChange = (patch: Partial<IVergeConfig>) => {
const onError = (err: any) => { setLocalConfig(prev => ({ ...prev, ...patch }));
showNotice("error", err.message || err.toString());
};
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
}; };
const handleIconChange = useLockFn( const handleIconChange = useLockFn(
async (type: "common" | "sysproxy" | "tun") => { async (type: "common" | "sysproxy" | "tun") => {
const key = `${type}_tray_icon` as keyof IVergeConfig; const key = `${type}_tray_icon` as keyof IVergeConfig;
if (verge?.[key]) { if (localConfig[key]) {
onChangeData({ [key]: false }); handleConfigChange({ [key]: false });
await patchVerge({ [key]: false });
} else { } else {
const selected = await openDialog({ const selected = await openDialog({
directory: false, directory: false,
@@ -128,213 +130,94 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const path = Array.isArray(selected) ? selected[0] : selected; const path = Array.isArray(selected) ? selected[0] : selected;
await copyIconFile(path, type); await copyIconFile(path, type);
await initIconPath(); await initIconPath();
onChangeData({ [key]: true }); handleConfigChange({ [key]: true });
await patchVerge({ [key]: true });
} }
} }
}, });
);
const handleSave = useLockFn(async () => {
setLoading(true);
try {
await patchVerge(localConfig);
showNotice("success", t("Settings saved successfully"));
setOpen(false);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} finally {
setLoading(false);
}
});
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("Layout Setting")}</DialogTitle> <DialogTitle>{t("Layout Setting")}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-4 space-y-1"> <div className="py-4 space-y-1">
<SettingRow label={t("Traffic Graph")}> {OS === "macos" && (
<GuardState <>
value={verge?.traffic_graph ?? true} <SettingRow label={t("Tray Icon")}>
valueProps="checked" <Select
onCatch={onError} onValueChange={(value) => handleConfigChange({ tray_icon: value as any })}
onFormat={onSwitchFormat} value={localConfig.tray_icon ?? "monochrome"}
onChange={(e) => onChangeData({ traffic_graph: e })} >
onGuard={(e) => patchVerge({ traffic_graph: e })} <SelectTrigger className="w-40 h-8"><SelectValue /></SelectTrigger>
> <SelectContent>
<Switch /> <SelectItem value="monochrome">{t("Monochrome")}</SelectItem>
</GuardState> <SelectItem value="colorful">{t("Colorful")}</SelectItem>
</SettingRow> </SelectContent>
</Select>
</SettingRow>
<SettingRow label={t("Memory Usage")}> <SettingRow label={t("Enable Tray Icon")}>
<GuardState <Switch
value={verge?.enable_memory_usage ?? true} checked={localConfig.enable_tray_icon ?? true}
valueProps="checked" onCheckedChange={(checked) => handleConfigChange({ enable_tray_icon: checked })}
onCatch={onError} />
onFormat={onSwitchFormat} </SettingRow>
onChange={(e) => onChangeData({ enable_memory_usage: e })} </>
onGuard={(e) => patchVerge({ enable_memory_usage: e })} )}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={t("Proxy Group Icon")}> <SettingRow label={t("Common Tray Icon")}>
<GuardState <Button variant="outline" size="sm" className="h-8" onClick={() => handleIconChange("common")}>
value={verge?.enable_group_icon ?? true} {localConfig.common_tray_icon && commonIcon && (
valueProps="checked" <img src={convertFileSrc(commonIcon)} className="h-5 mr-2" alt="common tray icon" />
onCatch={onError} )}
onFormat={onSwitchFormat} {localConfig.common_tray_icon ? t("Clear") : t("Browse")}
onChange={(e) => onChangeData({ enable_group_icon: e })} </Button>
onGuard={(e) => patchVerge({ enable_group_icon: e })} </SettingRow>
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow <SettingRow label={t("System Proxy Tray Icon")}>
label={t("Hover Jump Navigator")} <Button variant="outline" size="sm" className="h-8" onClick={() => handleIconChange("sysproxy")}>
extra={<TooltipIcon tooltip={t("Hover Jump Navigator Info")} />} {localConfig.sysproxy_tray_icon && sysproxyIcon && (
> <img src={convertFileSrc(sysproxyIcon)} className="h-5 mr-2" alt="system proxy tray icon" />
<GuardState )}
value={verge?.enable_hover_jump_navigator ?? true} {localConfig.sysproxy_tray_icon ? t("Clear") : t("Browse")}
valueProps="checked" </Button>
onCatch={onError} </SettingRow>
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_hover_jump_navigator: e })}
onGuard={(e) => patchVerge({ enable_hover_jump_navigator: e })}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={t("Nav Icon")}> <SettingRow label={t("Tun Tray Icon")}>
<GuardState <Button variant="outline" size="sm" className="h-8" onClick={() => handleIconChange("tun")}>
value={verge?.menu_icon ?? "monochrome"} {localConfig.tun_tray_icon && tunIcon && (
onCatch={onError} <img src={convertFileSrc(tunIcon)} className="h-5 mr-2" alt="tun mode tray icon" />
onFormat={(v) => v} )}
onChange={(e) => onChangeData({ menu_icon: e })} {localConfig.tun_tray_icon ? t("Clear") : t("Browse")}
onGuard={(e) => patchVerge({ menu_icon: e })} </Button>
> </SettingRow>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ 1 --- */} </div>
<Select
onValueChange={(value) =>
onChangeData({ menu_icon: value as any })
}
value={verge?.menu_icon}
>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 1 --- */}
<SelectTrigger className="w-40 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="monochrome">{t("Monochrome")}</SelectItem>
<SelectItem value="colorful">{t("Colorful")}</SelectItem>
<SelectItem value="disable">{t("Disable")}</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
{OS === "macos" && ( <DialogFooter>
<> <DialogClose asChild>
<SettingRow label={t("Tray Icon")}> <Button type="button" variant="outline">{t("Cancel")}</Button>
<GuardState </DialogClose>
value={verge?.tray_icon ?? "monochrome"} <Button type="button" onClick={handleSave} disabled={loading}>
onCatch={onError} {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
onFormat={(v) => v} {t("Save")}
onChange={(e) => onChangeData({ tray_icon: e })}
onGuard={(e) => patchVerge({ tray_icon: e })}
>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ 2 --- */}
<Select
onValueChange={(value) =>
onChangeData({ tray_icon: value as any })
}
value={verge?.tray_icon}
>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 2 --- */}
<SelectTrigger className="w-40 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="monochrome">
{t("Monochrome")}
</SelectItem>
<SelectItem value="colorful">{t("Colorful")}</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
<SettingRow label={t("Enable Tray Icon")}>
<GuardState
value={verge?.enable_tray_icon ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_tray_icon: e })}
onGuard={(e) => patchVerge({ enable_tray_icon: e })}
>
<Switch />
</GuardState>
</SettingRow>
</>
)}
<SettingRow label={t("Common Tray Icon")}>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={() => handleIconChange("common")}
>
{verge?.common_tray_icon && commonIcon && (
<img
src={convertFileSrc(commonIcon)}
className="h-5 mr-2"
alt="common tray icon"
/>
)}
{verge?.common_tray_icon ? t("Clear") : t("Browse")}
</Button> </Button>
</SettingRow> </DialogFooter>
</DialogContent>
<SettingRow label={t("System Proxy Tray Icon")}> </Dialog>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={() => handleIconChange("sysproxy")}
>
{verge?.sysproxy_tray_icon && sysproxyIcon && (
<img
src={convertFileSrc(sysproxyIcon)}
className="h-5 mr-2"
alt="system proxy tray icon"
/>
)}
{verge?.sysproxy_tray_icon ? t("Clear") : t("Browse")}
</Button>
</SettingRow>
<SettingRow label={t("Tun Tray Icon")}>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={() => handleIconChange("tun")}
>
{verge?.tun_tray_icon && tunIcon && (
<img
src={convertFileSrc(tunIcon)}
className="h-5 mr-2"
alt="tun mode tray icon"
/>
)}
{verge?.tun_tray_icon ? t("Clear") : t("Browse")}
</Button>
</SettingRow>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Close")}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
); );
}); });

View File

@@ -118,7 +118,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-2xl"> <DialogContent className="sm:max-w-2xl">
<DialogHeader> <DialogHeader>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center pr-5">
<DialogTitle> <DialogTitle>
{t("New Version")} v{updateInfo?.version} {t("New Version")} v{updateInfo?.version}
</DialogTitle> </DialogTitle>
@@ -127,7 +127,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
size="sm" size="sm"
onClick={() => onClick={() =>
openUrl( openUrl(
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`, `https://github.com/coolcoala/clash-verge-rev-lite/releases/tag/v${updateInfo?.version}`,
) )
} }
> >

View File

@@ -1,4 +1,4 @@
import { useRef, useState } from "react"; import {useMemo, useRef, useState} from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { mutate } from "swr"; import { mutate } from "swr";
@@ -43,6 +43,7 @@ import {
Power, Power,
BellOff, BellOff,
Repeat, Repeat,
Fingerprint
} from "lucide-react"; } from "lucide-react";
// Модальные окна // Модальные окна
@@ -55,6 +56,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {useProfiles} from "@/hooks/use-profiles";
const isWIN = getSystem() === "windows"; const isWIN = getSystem() === "windows";
interface Props { interface Props {
@@ -105,6 +107,12 @@ const SettingSystem = ({ onError }: Props) => {
const { verge, patchVerge, mutateVerge } = useVerge(); const { verge, patchVerge, mutateVerge } = useVerge();
const { installServiceAndRestartCore } = useServiceInstaller(); const { installServiceAndRestartCore } = useServiceInstaller();
const { profiles } = useProfiles();
const hasProfiles = useMemo(() => {
const items = profiles?.items ?? [];
return items.some(p => p.type === 'local' || p.type === 'remote');
}, [profiles]);
const { const {
actualState: systemProxyActualState, actualState: systemProxyActualState,
indicator: systemProxyIndicator, indicator: systemProxyIndicator,
@@ -260,7 +268,7 @@ const SettingSystem = ({ onError }: Props) => {
}} }}
onCatch={onError} onCatch={onError}
> >
<Switch disabled={!isTunAvailable} /> <Switch disabled={!isTunAvailable || !hasProfiles} />
</GuardState> </GuardState>
</SettingRow> </SettingRow>
@@ -296,7 +304,7 @@ const SettingSystem = ({ onError }: Props) => {
}} }}
onCatch={onError} onCatch={onError}
> >
<Switch /> <Switch disabled={!hasProfiles} />
</GuardState> </GuardState>
</SettingRow> </SettingRow>
@@ -390,6 +398,22 @@ const SettingSystem = ({ onError }: Props) => {
</Select> </Select>
</GuardState> </GuardState>
</SettingRow> </SettingRow>
<SettingRow
label={<LabelWithIcon icon={Fingerprint} text={t("Send HWID")} />}
>
<GuardState
value={verge?.enable_send_hwid ?? true}
valueProps="checked"
onChangeProps="onCheckedChange"
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_send_hwid: e })}
onGuard={(e) => patchVerge({ enable_send_hwid: e })}
onCatch={onError}
>
<Switch disabled={true} />
</GuardState>
</SettingRow>
</div> </div>
</div> </div>
); );

View File

@@ -180,32 +180,12 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
extra={<TooltipIcon tooltip={t("LightWeight Mode Info")} />} extra={<TooltipIcon tooltip={t("LightWeight Mode Info")} />}
onClick={() => liteModeRef.current?.open()} onClick={() => liteModeRef.current?.open()}
/> />
<SettingRow
onClick={exitApp}
label={<LabelWithIcon icon={LogOut} text={t("Exit")} />}
/>
<SettingRow
label={
<LabelWithIcon
icon={ClipboardList}
text={t("Export Diagnostic Info")}
/>
}
>
<TooltipIcon
tooltip={t("Copy")}
icon={<Copy className="h-4 w-4" />}
onClick={onExportDiagnosticInfo}
/>
</SettingRow>
<SettingRow <SettingRow
label={<LabelWithIcon icon={Info} text={t("Verge Version")} />} label={<LabelWithIcon icon={Info} text={t("Verge Version")} />}
> >
<p className="text-sm font-medium pr-2 font-mono">v{version}</p> <p className="text-sm font-medium pr-2 font-mono">v{version}</p>
</SettingRow> </SettingRow>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 2 --- */}
</div> </div>
</div> </div>
); );

View File

@@ -188,24 +188,22 @@ const SettingVergeBasic = ({ onError }: Props) => {
{OS !== "linux" && ( {OS !== "linux" && (
<SettingRow <SettingRow
label={ label={
<LabelWithIcon <LabelWithIcon
icon={MousePointerClick} icon={MousePointerClick}
text={t("Tray Click Event")} text={t("Tray Click Event")}
/> />
} }
> >
<GuardState <GuardState
value={tray_event ?? "main_window"} value={tray_event ?? "main_window"}
onCatch={onError} onCatch={onError}
onFormat={(v) => v} onFormat={(v) => v}
onChange={(e) => onChangeData({ tray_event: e })} onChange={(e) => onChangeData({ tray_event: e })}
onGuard={(e) => patchVerge({ tray_event: e })} onGuard={(e) => patchVerge({ tray_event: e })}
onChangeProps="onValueChange"
> >
<Select <Select>
onValueChange={(value) => onChangeData({ tray_event: value })}
value={tray_event}
>
<SelectTrigger className="w-40 h-8"> <SelectTrigger className="w-40 h-8">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
@@ -227,41 +225,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingRow> </SettingRow>
)} )}
<SettingRow
label={<LabelWithIcon icon={Copy} text={t("Copy Env Type")} />}
extra={
<TooltipIcon
tooltip={t("Copy")}
icon={<Copy className="h-4 w-4" />}
onClick={onCopyClashEnv}
/>
}
>
<GuardState
value={env_type ?? (OS === "windows" ? "powershell" : "bash")}
onCatch={onError}
onFormat={(v) => v}
onChange={(e) => onChangeData({ env_type: e })}
onGuard={(e) => patchVerge({ env_type: e })}
>
<Select
onValueChange={(value) => onChangeData({ env_type: value })}
value={env_type}
>
<SelectTrigger className="w-36 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="bash">Bash</SelectItem>
<SelectItem value="fish">Fish</SelectItem>
<SelectItem value="nushell">Nushell</SelectItem>
<SelectItem value="cmd">CMD</SelectItem>
<SelectItem value="powershell">PowerShell</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
<SettingRow <SettingRow
label={<LabelWithIcon icon={Home} text={t("Start Page")} />} label={<LabelWithIcon icon={Home} text={t("Start Page")} />}
> >
@@ -290,59 +253,10 @@ const SettingVergeBasic = ({ onError }: Props) => {
</GuardState> </GuardState>
</SettingRow> </SettingRow>
<SettingRow {/*<SettingRow*/}
label={ {/* onClick={() => themeRef.current?.open()}*/}
<LabelWithIcon icon={FileTerminal} text={t("Startup Script")} /> {/* label={<LabelWithIcon icon={SwatchBook} text={t("Theme Setting")} />}*/}
} {/*/>*/}
>
<div className="flex items-center gap-2">
<Input
readOnly
value={startup_script ?? ""}
placeholder={t("Not Set")}
className="h-8 flex-1"
/>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={async () => {
const selected = await open({
directory: false,
multiple: false,
filters: [
{ name: "Shell Script", extensions: ["sh", "bat", "ps1"] },
],
});
if (selected) {
const path = Array.isArray(selected) ? selected[0] : selected;
onChangeData({ startup_script: path });
patchVerge({ startup_script: path });
}
}}
>
{t("Browse")}
</Button>
{startup_script && (
<Button
variant="destructive"
size="sm"
className="h-8"
onClick={async () => {
onChangeData({ startup_script: "" });
patchVerge({ startup_script: "" });
}}
>
{t("Clear")}
</Button>
)}
</div>
</SettingRow>
<SettingRow
onClick={() => themeRef.current?.open()}
label={<LabelWithIcon icon={SwatchBook} text={t("Theme Setting")} />}
/>
<SettingRow <SettingRow
onClick={() => layoutRef.current?.open()} onClick={() => layoutRef.current?.open()}
label={ label={

View File

@@ -0,0 +1,724 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@root/hooks/use-mobile"
import { cn } from "@root/lib/utils"
import { Button } from "@root/src/components/ui/button"
import { Input } from "@root/src/components/ui/input"
import { Separator } from "@root/src/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@root/src/components/ui/sheet"
import { Skeleton } from "@root/src/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@root/src/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
)}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
)
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@@ -0,0 +1,13 @@
import { cn } from "@root/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,93 @@
import { useEffect, useState, useCallback } from 'react';
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
// Константы для управления масштабом
const ZOOM_STEP = 0.1;
const ZOOM_WHEEL_STEP = 0.05;
const MIN_ZOOM = 0.5; // 50%
const MAX_ZOOM = 2.0; // 200%
export const useZoomControls = () => {
const [zoomLevel, setZoomLevel] = useState(1.0);
const appWindow = WebviewWindow.getCurrent();
useEffect(() => {
const setInitialZoom = async () => {
// 1. Получаем и физический размер, и коэффициент масштабирования
const size = await appWindow.innerSize();
const scaleFactor = await appWindow.scaleFactor();
// 2. Вычисляем логическую ширину
const logicalWidth = size.width / scaleFactor;
let initialZoom = 1.0;
console.log(`Physical width: ${size.width}, Scale Factor: ${scaleFactor}, Logical width: ${logicalWidth}`);
// 3. Используем логическую ширину для принятия решения
if (logicalWidth < 1300) {
initialZoom = 1.0;
} else if (logicalWidth > 2000) {
initialZoom = 2.0;
}
await appWindow.setZoom(initialZoom);
setZoomLevel(initialZoom);
};
setInitialZoom();
}, []);
const handleZoom = useCallback((delta: number, isReset = false) => {
setZoomLevel(currentZoom => {
const newZoom = isReset ? 1.0 : currentZoom + delta;
const clampedZoom = Math.max(MIN_ZOOM, Math.min(newZoom, MAX_ZOOM));
const roundedZoom = Math.round(clampedZoom * 100) / 100;
appWindow.setZoom(roundedZoom);
const newStrokeWidth = 2 / roundedZoom;
document.documentElement.style.setProperty('--icon-stroke-width', newStrokeWidth.toString());
return roundedZoom;
});
}, [appWindow]);
useEffect(() => {
const handleWheel = (event: WheelEvent) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
const delta = event.deltaY > 0 ? -ZOOM_WHEEL_STEP : ZOOM_WHEEL_STEP;
handleZoom(delta);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey) {
switch (event.code) {
case 'Equal':
case 'NumpadAdd':
event.preventDefault();
handleZoom(ZOOM_STEP);
break;
case 'Minus':
case 'NumpadSubtract':
event.preventDefault();
handleZoom(-ZOOM_STEP);
break;
case 'Digit0':
case 'Numpad0':
event.preventDefault();
handleZoom(0, true);
break;
}
}
};
window.addEventListener('wheel', handleWheel, { passive: false });
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('wheel', handleWheel);
window.removeEventListener('keydown', handleKeyDown);
};
}, [handleZoom]);
};

View File

@@ -1,7 +1,9 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@variant dark .dark &; @theme {
--tailwind-darkMode: 'class';
}
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
@@ -125,3 +127,25 @@
/* h-full уже применен выше к body */ /* h-full уже применен выше к body */
} }
} }
svg {
stroke-width: var(--icon-stroke-width, 2);
}
@keyframes gradient-wave {
0% {
background-position: -200% center;
}
100% {
background-position: 200% center;
}
}
.animate-gradient-wave {
background-size: 200% auto;
background-clip: text;
-webkit-background-clip: text;
color: transparent;
animation: gradient-wave 2s linear infinite;
}

View File

@@ -8,7 +8,7 @@
type="image/x-icon" type="image/x-icon"
/> />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clash Verge Rev Lite</title> <title>Koala Clash</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -511,7 +511,7 @@
"Validate Merge File": "Validate Merge File", "Validate Merge File": "Validate Merge File",
"Validation Success": "Validation Success", "Validation Success": "Validation Success",
"Validation Failed": "Validation Failed", "Validation Failed": "Validation Failed",
"Service Administrator Prompt": "Clash Verge requires administrator privileges to reinstall the system service", "Service Administrator Prompt": "Koala Clash requires administrator privileges to reinstall the system service",
"DNS Settings": "DNS Settings", "DNS Settings": "DNS Settings",
"DNS settings saved": "DNS settings saved", "DNS settings saved": "DNS settings saved",
"DNS Overwrite": "DNS Overwrite", "DNS Overwrite": "DNS Overwrite",
@@ -645,6 +645,10 @@
"Attention Required": "Attention Required", "Attention Required": "Attention Required",
"Menu": "Menu", "Menu": "Menu",
"Add Profile": "Add Profile", "Add Profile": "Add Profile",
"Proxy enabled": "Proxy enabled",
"Proxy disabled": "Proxy disabled",
"Connecting...": "Connecting...",
"Disconnecting...": "Disconnecting...",
"Delete Profile": "Delete Profile {{name}}?", "Delete Profile": "Delete Profile {{name}}?",
"This action cannot be undone.": "This action cannot be undone.", "This action cannot be undone.": "This action cannot be undone.",
"Check Group Latency": "Check Group Latency", "Check Group Latency": "Check Group Latency",
@@ -660,5 +664,19 @@
"Show Advanced Settings": "Show Advanced Settings", "Show Advanced Settings": "Show Advanced Settings",
"Hide Advanced Settings": "Hide Advanced Settings", "Hide Advanced Settings": "Hide Advanced Settings",
"Main Toggle Action": "Main Toggle Action", "Main Toggle Action": "Main Toggle Action",
"Support": "Support" "Support": "Support",
"Update on Startup": "Update on Startup",
"Send HWID": "Send HWID",
"New Version is available": "New Version is available",
"New Version": "New Version",
"New update": "New update",
"Device Limit Reached": "Device Limit Reached",
"Update Profile": "Update Profile",
"Template": "Template",
"Select a template...": "Select a template...",
"Default Template": "Ru-bundle template",
"Template without RU Rules": "Without-ru template",
"Stopping Core...": "Stopping Core...",
"Uninstalling Service...": "Uninstalling Service...",
"Try running core as Sidecar...": "Try running core as Sidecar..."
} }

View File

@@ -27,10 +27,11 @@
"Proxies": "Прокси", "Proxies": "Прокси",
"Proxy Groups": "Группы прокси", "Proxy Groups": "Группы прокси",
"Proxy Provider": "Провайдер прокси", "Proxy Provider": "Провайдер прокси",
"Proxy Count": "Число прокси",
"Update All": "Обновить все", "Update All": "Обновить все",
"Update At": "Обновлено в", "Update At": "Обновлено в",
"rule": "правила", "rule": "По правилам",
"global": "глобальный", "global": "Глобально",
"direct": "прямой", "direct": "прямой",
"script": "скриптовый", "script": "скриптовый",
"locate": "Местоположение", "locate": "Местоположение",
@@ -156,6 +157,7 @@
"Edit File": "Изменить файл", "Edit File": "Изменить файл",
"Open File": "Открыть файл", "Open File": "Открыть файл",
"Update": "Обновить", "Update": "Обновить",
"Update via proxy": "Обновить через прокси",
"Update(Proxy)": "Обновить (прокси)", "Update(Proxy)": "Обновить (прокси)",
"Confirm deletion": "Подтвердите удаление", "Confirm deletion": "Подтвердите удаление",
"This operation is not reversible": "Эта операция необратима", "This operation is not reversible": "Эта операция необратима",
@@ -200,15 +202,19 @@
"Settings": "Настройки", "Settings": "Настройки",
"System Setting": "Настройки системы", "System Setting": "Настройки системы",
"Tun Mode": "Режим TUN", "Tun Mode": "Режим TUN",
"TUN requires Service Mode": "Режим TUN требует установленную службу Clash Verge", "TUN requires Service Mode": "Режим TUN требует установленную службу Koala Clash",
"Install Service": "Установить службу", "Install Service": "Установить службу",
"Install Service failed": "Установка сервиса не удалась",
"Uninstall Service": "Удалить сервис",
"Restart Core failed": "Перезапуск ядра не удалась",
"Reset to Default": "Сбросить настройки", "Reset to Default": "Сбросить настройки",
"Tun Mode Info": "Режим Tun: захватывает весь системный трафик, при включении нет необходимости включать системный прокси-сервер.", "Tun Mode Info": "Режим Tun: захватывает весь системный трафик, при включении нет необходимости включать системный прокси-сервер.",
"TUN requires Service Mode or Admin Mode": "TUN режим требует Режима Службы или прав Администратора",
"System Proxy Enabled": "Системный прокси включен, ваши приложения будут получать доступ к сети через него", "System Proxy Enabled": "Системный прокси включен, ваши приложения будут получать доступ к сети через него",
"System Proxy Disabled": "Системный прокси отключен, большинству пользователей рекомендуется включить эту опцию", "System Proxy Disabled": "Системный прокси отключен, большинству пользователей рекомендуется включить эту опцию",
"TUN Mode Enabled": "Режим TUN включен, приложения будут получать доступ к сети через виртуальную сетевую карту", "TUN Mode Enabled": "Режим TUN включен, приложения будут получать доступ к сети через виртуальную сетевую карту",
"TUN Mode Disabled": "Режим TUN отключен", "TUN Mode Disabled": "Режим TUN отключен",
"TUN Mode Service Required": "Режим TUN требует установленную службу Clash Verge", "TUN Mode Service Required": "Режим TUN требует установленную службу Koala Clash",
"TUN Mode Intercept Info": "Режим TUN может перехватить трафик всех приложений, подходит для приложений, которые не работают в режиме системного прокси.", "TUN Mode Intercept Info": "Режим TUN может перехватить трафик всех приложений, подходит для приложений, которые не работают в режиме системного прокси.",
"Rule Mode Description": "Направляет трафик в соответствии с предустановленными правилами", "Rule Mode Description": "Направляет трафик в соответствии с предустановленными правилами",
"Global Mode Description": "Направляет весь трафик через прокси-серверы", "Global Mode Description": "Направляет весь трафик через прокси-серверы",
@@ -255,8 +261,11 @@
"PAC Script Content": "Содержание сценария PAC", "PAC Script Content": "Содержание сценария PAC",
"PAC URL": "Адрес PAC: ", "PAC URL": "Адрес PAC: ",
"Auto Launch": "Автозапуск", "Auto Launch": "Автозапуск",
"Administrator mode may not support auto launch": "Режим администратора может не поддерживать автоматический запуск",
"Silent Start": "Тихий запуск", "Silent Start": "Тихий запуск",
"Silent Start Info": "Запускать программу в фоновом режиме без отображения панели", "Silent Start Info": "Запускать программу в фоновом режиме без отображения панели",
"Hover Jump Navigator": "Hover Jump Navigator",
"Hover Jump Navigator Info": "Автоматически переходить к соответствующей группе прокси при наведении курсора на буквы алфавита",
"TG Channel": "Telegram-канал", "TG Channel": "Telegram-канал",
"Manual": "Документация", "Manual": "Документация",
"Github Repo": "GitHub репозиторий", "Github Repo": "GitHub репозиторий",
@@ -321,7 +330,7 @@
"Success Color": "Цвет успеха", "Success Color": "Цвет успеха",
"Font Family": "Семейство шрифтов", "Font Family": "Семейство шрифтов",
"CSS Injection": "Внедрение CSS", "CSS Injection": "Внедрение CSS",
"Layout Setting": "Настройки раскладки", "Layout Setting": "Настройки макета",
"Traffic Graph": "График трафика", "Traffic Graph": "График трафика",
"Memory Usage": "Использование памяти", "Memory Usage": "Использование памяти",
"Memory Cleanup": "Нажмите, чтобы очистить память", "Memory Cleanup": "Нажмите, чтобы очистить память",
@@ -372,7 +381,7 @@
"Export Diagnostic Info": "Экспорт диагностической информации", "Export Diagnostic Info": "Экспорт диагностической информации",
"Export Diagnostic Info For Issue Reporting": "Экспорт диагностической информации для отчета об ошибке", "Export Diagnostic Info For Issue Reporting": "Экспорт диагностической информации для отчета об ошибке",
"Exit": "Выход", "Exit": "Выход",
"Verge Version": "Версия Clash Verge Rev", "Verge Version": "Версия Koala Clash",
"ReadOnly": "Только для чтения", "ReadOnly": "Только для чтения",
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения", "ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
"Filter": "Фильтр", "Filter": "Фильтр",
@@ -383,6 +392,7 @@
"Profile Imported Successfully": "Профиль успешно импортирован", "Profile Imported Successfully": "Профиль успешно импортирован",
"Profile Switched": "Профиль изменен", "Profile Switched": "Профиль изменен",
"Profile Reactivated": "Профиль перезапущен", "Profile Reactivated": "Профиль перезапущен",
"Profile switch interrupted by new selection": "Переключение профилей прервано новым выбором",
"Only YAML Files Supported": "Поддерживаются только файлы YAML", "Only YAML Files Supported": "Поддерживаются только файлы YAML",
"Settings Applied": "Настройки применены", "Settings Applied": "Настройки применены",
"Installing Service...": "Установка службы...", "Installing Service...": "Установка службы...",
@@ -390,6 +400,17 @@
"Service Uninstalled Successfully": "Служба успешно удалена", "Service Uninstalled Successfully": "Служба успешно удалена",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Продолжительность работы прокси-демона не может быть меньше 1 секунды", "Proxy Daemon Duration Cannot be Less than 1 Second": "Продолжительность работы прокси-демона не может быть меньше 1 секунды",
"Invalid Bypass Format": "Неверный формат обхода", "Invalid Bypass Format": "Неверный формат обхода",
"Waiting for service to be ready...": "Ожидание готовности сервиса...",
"Service not ready, retrying attempt {count}/{total}...": "Служба не готова, повторная попытка {{count}}/{{total}}...",
"Failed to check service status, retrying attempt {count}/{total}...": "Не удалось проверить состояние службы, повторная попытка {{count}}/{{total}}...",
"Service did not become ready after attempts. Proceeding with core restart.": "Служба не была готова после нескольких попыток. Продолжаем перезапуск ядра.",
"Restarting Core...": "Перезапуск ядра...",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "Служба была готова, но при перезапуске ядра могли возникнуть проблемы или служба стала недоступна. Пожалуйста, проверьте.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "При установке службы или перезапуске ядра возникли проблемы. Служба может быть недоступна. Проверьте системные журналы.",
"Attempting to restart core as a fallback...": "Попытка перезапустить ядро в резервном режиме...",
"Fallback core restart also failed: {message}": "Перезапуск резервного ядра также не удался: {{message}}",
"Service is ready and core restarted": "Служба готова, ядро перезапущено",
"Core restarted. Service is now available.": "Ядро перезапущено. Сервис теперь доступен.",
"Clash Port Modified": "Порт Clash изменен", "Clash Port Modified": "Порт Clash изменен",
"Port Conflict": "Конфликт портов", "Port Conflict": "Конфликт портов",
"Restart Application to Apply Modifications": "Чтобы изменения вступили в силу, необходимо перезапустить приложение", "Restart Application to Apply Modifications": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
@@ -399,6 +420,7 @@
"Clash Core Restarted": "Ядро перезапущено", "Clash Core Restarted": "Ядро перезапущено",
"GeoData Updated": "Файлы GeoData обновлены", "GeoData Updated": "Файлы GeoData обновлены",
"Currently on the Latest Version": "Обновление не требуется", "Currently on the Latest Version": "Обновление не требуется",
"Already Using Latest Core": "Уже используется последняя версия ядра",
"Import Subscription Successful": "Подписка успешно импортирована", "Import Subscription Successful": "Подписка успешно импортирована",
"WebDAV Server URL": "URL-адрес сервера WebDAV http(s)://", "WebDAV Server URL": "URL-адрес сервера WebDAV http(s)://",
"Username": "Имя пользователя", "Username": "Имя пользователя",
@@ -489,8 +511,9 @@
"Validate Merge File": "Проверить Merge File", "Validate Merge File": "Проверить Merge File",
"Validation Success": "Файл успешно проверен", "Validation Success": "Файл успешно проверен",
"Validation Failed": "Проверка не удалась", "Validation Failed": "Проверка не удалась",
"Service Administrator Prompt": "Clash Verge требует прав администратора для переустановки системной службы", "Service Administrator Prompt": "Koala Clash требует прав администратора для переустановки системной службы",
"DNS Settings": "Настройки DNS", "DNS Settings": "Настройки DNS",
"DNS settings saved": "Настройки DNS сохранены",
"DNS Overwrite": "Переопределение настроек DNS", "DNS Overwrite": "Переопределение настроек DNS",
"DNS Settings Warning": "Если вы не знакомы с этими настройками, пожалуйста, не изменяйте и не отключайте их", "DNS Settings Warning": "Если вы не знакомы с этими настройками, пожалуйста, не изменяйте и не отключайте их",
"Enable DNS": "Включить DNS", "Enable DNS": "Включить DNS",
@@ -498,6 +521,7 @@
"Enhanced Mode": "Enhanced Mode", "Enhanced Mode": "Enhanced Mode",
"Fake IP Range": "Диапазон FakeIP", "Fake IP Range": "Диапазон FakeIP",
"Fake IP Filter Mode": "FakeIP Filter Mode", "Fake IP Filter Mode": "FakeIP Filter Mode",
"Enable IPv6 DNS resolution": "Включить разрешение DNS по IPv6",
"Prefer H3": "Предпочитать H3", "Prefer H3": "Предпочитать H3",
"DNS DOH使用HTTP/3": "DNS DOH использует http/3", "DNS DOH使用HTTP/3": "DNS DOH использует http/3",
"Respect Rules": "Приоритизировать правила", "Respect Rules": "Приоритизировать правила",
@@ -530,6 +554,9 @@
"IP CIDRs not using fallback servers": "Диапазоны IP-адресов, не использующие резервные серверы, разделенные запятой", "IP CIDRs not using fallback servers": "Диапазоны IP-адресов, не использующие резервные серверы, разделенные запятой",
"Fallback Domain": "Fallback домены", "Fallback Domain": "Fallback домены",
"Domains using fallback servers": "Домены, использующие резервные серверы, разделенные запятой", "Domains using fallback servers": "Домены, использующие резервные серверы, разделенные запятой",
"Hosts Settings": "Настройки хостов",
"Hosts": "Хосты",
"Custom domain to IP or domain mapping": "Настраиваемое сопоставление домена с IP-адресом или доменом",
"Enable Alpha Channel": "Включить альфа-канал", "Enable Alpha Channel": "Включить альфа-канал",
"Alpha versions may contain experimental features and bugs": "Альфа-версии могут содержать экспериментальные функции и ошибки", "Alpha versions may contain experimental features and bugs": "Альфа-версии могут содержать экспериментальные функции и ошибки",
"Home Settings": "Настройки главной страницы", "Home Settings": "Настройки главной страницы",
@@ -553,9 +580,24 @@
"OS Info": "Версия ОС", "OS Info": "Версия ОС",
"Running Mode": "Режим работы", "Running Mode": "Режим работы",
"Sidecar Mode": "Пользовательский режим", "Sidecar Mode": "Пользовательский режим",
"Administrator Mode": "Режим администратора",
"Administrator + Service Mode": "Административный + сервисный режим",
"Last Check Update": "Последняя проверка обновлений", "Last Check Update": "Последняя проверка обновлений",
"Click to import subscription": "Нажмите, чтобы импортировать подписку", "Click to import subscription": "Нажмите, чтобы импортировать подписку",
"Last Update failed": "Последнее обновление не удалось",
"Next Up": "Далее",
"No schedule": "Нет расписания",
"Unknown": "Неизвестно",
"Auto update disabled": "Автоматическое обновление отключено",
"Update subscription successfully": "Подписка успешно обновлена", "Update subscription successfully": "Подписка успешно обновлена",
"Update failed, retrying with Clash proxy...": "Обновление не удалось, пробую повторно с помощью прокси Clash...",
"Update with Clash proxy successfully": "Обновление с помощью прокси Clash прошло успешно",
"Update failed even with Clash proxy": "Обновление не удалось даже с помощью прокси Clash",
"Profile creation failed, retrying with Clash proxy...": "Создание профиля не удалось, повторная попытка с прокси Clash...",
"Profile creation succeeded with Clash proxy": "Создание профиля с помощью прокси Clash прошло успешно",
"Import failed, retrying with Clash proxy...": "Импорт не удался, повторная попытка с прокси Clash...",
"Profile Imported with Clash proxy": "Профиль импортирован с помощью прокси Clash",
"Import failed even with Clash proxy": "Импорт не удался даже с прокси Clash",
"Current Node": "Текущий сервер", "Current Node": "Текущий сервер",
"No active proxy node": "Нет активного прокси-узла", "No active proxy node": "Нет активного прокси-узла",
"Network Settings": "Настройки сети", "Network Settings": "Настройки сети",
@@ -582,27 +624,37 @@
"No (IP Banned By Disney+)": "Нет (IP забанен Disney+)", "No (IP Banned By Disney+)": "Нет (IP забанен Disney+)",
"Unsupported Country/Region": "Страна/регион не поддерживается", "Unsupported Country/Region": "Страна/регион не поддерживается",
"Failed (Network Connection)": "Ошибка подключения", "Failed (Network Connection)": "Ошибка подключения",
"DashboardToggledTitle": "Панель управления переключена",
"DashboardToggledBody": "Видимость панели инструментов переключена с помощью горячей клавиши",
"ClashModeChangedTitle": "Режим Clash изменен",
"ClashModeChangedBody": "Переключено в режим {{mode}}",
"SystemProxyToggledTitle": "Системный прокси переключен",
"SystemProxyToggledBody": "Состояние системного прокси-сервера переключена с помощью горячей клавиши",
"TunModeToggledTitle": "Режим TUN переключен",
"TunModeToggledBody": "Режим TUN переключен с помощью горячей клавиши",
"LightweightModeEnteredTitle": "Легкий режим",
"LightweightModeEnteredBody": "Вход в легкий режим с помощью горячей клавиши",
"AppQuitTitle": "Выход из приложения",
"AppQuitBody": "Приложение закрыто с помощью горячей клавиши",
"AppHiddenTitle": "Приложение скрыто",
"AppHiddenBody": "Окно приложения скрыто с помощью горячей клавиши",
"Invalid Profile URL": "Неверный URL-адрес профиля. Введите URL-адрес, начинающийся с http:// или https://",
"Saved Successfully": "Успешно сохранено",
"Connected": "Подключено", "Connected": "Подключено",
"Disconnected": "Отключено", "Disconnected": "Отключено",
"Attention Required": "Требуется внимание", "Attention Required": "Требуется внимание",
"TUN requires Service Mode or Admin Mode": "TUN режим требует Режима Службы или прав Администратора",
"Menu": "Меню", "Menu": "Меню",
"Add Profile": "Добавить профиль",
"Proxy enabled": "Прокси включено", "Proxy enabled": "Прокси включено",
"Proxy disabled": "Прокси выключено", "Proxy disabled": "Прокси выключено",
"Connecting...": "Подключение...", "Connecting...": "Подключение...",
"Disconnecting...": "Отключение...", "Disconnecting...": "Отключение...",
"Add Profile": "Добавить профиль",
"Delete Profile": "Удалить профиль {{name}}?", "Delete Profile": "Удалить профиль {{name}}?",
"This action cannot be undone.": "Это действие не может быть отменено", "This action cannot be undone.": "Это действие не может быть отменено",
"Update via proxy": "Обновить через прокси",
"Check Group Latency": "Проверка задержки в группе", "Check Group Latency": "Проверка задержки в группе",
"Locate Current Proxy": "Найти текущий прокси", "Locate Current Proxy": "Найти текущий прокси",
"Show Basic Info": "Показать основную информацию", "Show Basic Info": "Показать основную информацию",
"Show Detailed Info": "Показать подробную информацию", "Show Detailed Info": "Показать подробную информацию",
"Update failed, retrying with Clash proxy...": "Обновление не удалось, пробую повторно с помощью прокси Clash...",
"Update failed even with Clash proxy": "Обновление не удалось даже с помощью прокси Clash",
"Update with Clash proxy successfully": "Обновление с помощью прокси Clash прошло успешно",
"Proxy Count": "Число прокси",
"Set Latency Test URL": "Установить URL-адрес тестирования задержки", "Set Latency Test URL": "Установить URL-адрес тестирования задержки",
"Filter by Name": "Фильтр по имени", "Filter by Name": "Фильтр по имени",
"Expires in": "Истекает через {{duration}}", "Expires in": "Истекает через {{duration}}",
@@ -612,5 +664,19 @@
"Show Advanced Settings": "Показать дополнительные настройки", "Show Advanced Settings": "Показать дополнительные настройки",
"Hide Advanced Settings": "Скрыть дополнительные настройки", "Hide Advanced Settings": "Скрыть дополнительные настройки",
"Main Toggle Action": "Действие главного переключателя", "Main Toggle Action": "Действие главного переключателя",
"Support": "Поддержка" "Support": "Поддержка",
"Update on Startup": "Обновлять при запуске",
"Send HWID": "Отправлять HWID",
"New Version is available": "Доступна новая версия",
"New Version": "Новая версия",
"New update": "Доступно обновление",
"Device Limit Reached": "Достигнут лимит устройств",
"Update Profile": "Обновить профиль",
"Template": "Шаблон",
"Select a template...": "Выберите шаблон...",
"Default Template": "Шаблон ru-bundle",
"Template without RU Rules": "Шаблон without-ru",
"Stopping Core...": "Остановка ядра...",
"Uninstalling Service...": "Удаление сервиса...",
"Try running core as Sidecar...": "Попытка запустить ядро как Sidecar..."
} }

View File

@@ -5,23 +5,15 @@ import { SWRConfig, mutate } from "swr";
import { useEffect, useCallback, useState, useRef } from "react"; import { useEffect, useCallback, useState, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useRoutes, useNavigate } from "react-router-dom"; import { useLocation, useRoutes, useNavigate } from "react-router-dom";
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { routers } from "./_routers"; import { routers } from "./_routers";
import { getAxios } from "@/services/api"; import { getAxios } from "@/services/api";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import LogoSvg from "@/assets/image/logo.svg?react";
import iconLight from "@/assets/image/icon_light.svg?react";
import iconDark from "@/assets/image/icon_dark.svg?react";
import { useThemeMode, useEnableLog } from "@/services/states"; import { useThemeMode, useEnableLog } from "@/services/states";
import { LayoutItem } from "@/components/layout/layout-item";
import { LayoutTraffic } from "@/components/layout/layout-traffic";
import { UpdateButton } from "@/components/layout/update-button";
import { useCustomTheme } from "@/components/layout/use-custom-theme"; import { useCustomTheme } from "@/components/layout/use-custom-theme";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import "dayjs/locale/ru"; import "dayjs/locale/ru";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { getPortableFlag } from "@/services/cmds";
import React from "react"; import React from "react";
import { useListen } from "@/hooks/use-listen"; import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
@@ -29,7 +21,12 @@ import { useClashInfo } from "@/hooks/use-clash";
import { initGlobalLogService } from "@/services/global-log-service"; import { initGlobalLogService } from "@/services/global-log-service";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import { NoticeManager } from "@/components/base/NoticeManager"; import { Toaster } from "@/components/ui/sonner";
import { SidebarProvider, useSidebar } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/layout/sidebar";
import { useZoomControls } from "@/hooks/useZoomControls";
import { HwidErrorDialog } from "@/components/profile/hwid-error-dialog";
const appWindow = getCurrentWebviewWindow(); const appWindow = getCurrentWebviewWindow();
export let portableFlag = false; export let portableFlag = false;
@@ -38,25 +35,30 @@ dayjs.extend(relativeTime);
const OS = getSystem(); const OS = getSystem();
// 通知处理函数 // Notification Handler
const handleNoticeMessage = ( const handleNoticeMessage = (
status: string, status: string,
msg: string, msg: string,
t: (key: string) => string, t: (key: string) => string,
navigate: (path: string, options?: any) => void, navigate: (path: string, options?: any) => void,
) => { ) => {
console.log("[通知监听 V2] 收到消息:", status, msg); console.log("[Notification Listener V2] Receiving a message:", status, msg);
switch (status) { switch (status) {
case "import_sub_url::ok": case "import_sub_url::ok":
mutate("getProfiles"); mutate("getProfiles");
navigate("/profile", { state: { current: msg } }); navigate("/");
showNotice("success", t("Import Subscription Successful")); showNotice("success", t("Import Subscription Successful"));
sessionStorage.setItem('activateProfile', msg);
break; break;
case "import_sub_url::error": case "import_sub_url::error":
navigate("/profile"); console.log(msg);
showNotice("error", msg); if (msg.toLowerCase().includes('device') || msg.toLowerCase().includes('устройств')) {
break; window.dispatchEvent(new CustomEvent('show-hwid-error', { detail: msg }));
} else {
showNotice("error", msg);
}
break;
case "set_config::error": case "set_config::error":
showNotice("error", msg); showNotice("error", msg);
break; break;
@@ -142,13 +144,14 @@ const handleNoticeMessage = (
showNotice("error", `${t("Failed to Change Core")}: ${msg}`); showNotice("error", `${t("Failed to Change Core")}: ${msg}`);
break; break;
default: // Optional: Log unhandled statuses default: // Optional: Log unhandled statuses
console.warn(`[通知监听 V2] 未处理的状态: ${status}`); console.warn(`[Notification Listener V2] Unprocessed state: ${status}`);
break; break;
} }
}; };
const Layout = () => { const Layout = () => {
const mode = useThemeMode(); const mode = useThemeMode();
useZoomControls();
const isDark = mode === "light" ? false : true; const isDark = mode === "light" ? false : true;
const { t } = useTranslation(); const { t } = useTranslation();
useCustomTheme(); useCustomTheme();
@@ -169,14 +172,14 @@ const Layout = () => {
try { try {
handleNoticeMessage(status, msg, t, navigate); handleNoticeMessage(status, msg, t, navigate);
} catch (error) { } catch (error) {
console.error("[Layout] 处理通知消息失败:", error); console.error("[Layout] Failure to process a notification message:", error);
} }
}, 0); }, 0);
}, },
[t, navigate], [t, navigate],
); );
// 初始化全局日志服务 // Initialize the global logging service
useEffect(() => { useEffect(() => {
if (clashInfo) { if (clashInfo) {
const { server = "", secret = "" } = clashInfo; const { server = "", secret = "" } = clashInfo;
@@ -184,7 +187,7 @@ const Layout = () => {
} }
}, [clashInfo, enableLog]); }, [clashInfo, enableLog]);
// 设置监听器 // Setting up a listener
useEffect(() => { useEffect(() => {
const listeners = [ const listeners = [
addListener("verge://refresh-clash-config", async () => { addListener("verge://refresh-clash-config", async () => {
@@ -230,11 +233,11 @@ const Layout = () => {
try { try {
unlisten(); unlisten();
} catch (error) { } catch (error) {
console.error("[Layout] 清理事件监听器失败:", error); console.error("[Layout] Failed to clear event listener:", error);
} }
}) })
.catch((error) => { .catch((error) => {
console.error("[Layout] 获取unlisten函数失败:", error); console.error("[Layout] Failed to get unlisten function:", error);
}); });
} }
}); });
@@ -244,11 +247,11 @@ const Layout = () => {
try { try {
cleanup(); cleanup();
} catch (error) { } catch (error) {
console.error("[Layout] 清理窗口监听器失败:", error); console.error("[Layout] Failed to clear window listener:", error);
} }
}) })
.catch((error) => { .catch((error) => {
console.error("[Layout] 获取cleanup函数失败:", error); console.error("[Layout] Failed to get cleanup function:", error);
}); });
}, 0); }, 0);
}; };
@@ -256,10 +259,10 @@ const Layout = () => {
useEffect(() => { useEffect(() => {
if (initRef.current) { if (initRef.current) {
console.log("[Layout] 初始化代码已执行过,跳过"); console.log("[Layout] Initialization code has already been executed, skip");
return; return;
} }
console.log("[Layout] 开始执行初始化代码"); console.log("[Layout] Begin executing initialization code");
initRef.current = true; initRef.current = true;
let isInitialized = false; let isInitialized = false;
@@ -269,27 +272,27 @@ const Layout = () => {
const notifyBackend = async (action: string, stage?: string) => { const notifyBackend = async (action: string, stage?: string) => {
try { try {
if (stage) { if (stage) {
console.log(`[Layout] 通知后端 ${action}: ${stage}`); console.log(`[Layout] Notification Backend ${action}: ${stage}`);
await invoke("update_ui_stage", { stage }); await invoke("update_ui_stage", { stage });
} else { } else {
console.log(`[Layout] 通知后端 ${action}`); console.log(`[Layout] Notification Backend ${action}`);
await invoke("notify_ui_ready"); await invoke("notify_ui_ready");
} }
} catch (err) { } catch (err) {
console.error(`[Layout] 通知失败 ${action}:`, err); console.error(`[Layout] Notification failure ${action}:`, err);
} }
}; };
const removeLoadingOverlay = () => { const removeLoadingOverlay = () => {
const initialOverlay = document.getElementById("initial-loading-overlay"); const initialOverlay = document.getElementById("initial-loading-overlay");
if (initialOverlay) { if (initialOverlay) {
console.log("[Layout] 移除加载指示器"); console.log("[Layout] Remove loading indicator");
initialOverlay.style.opacity = "0"; initialOverlay.style.opacity = "0";
setTimeout(() => { setTimeout(() => {
try { try {
initialOverlay.remove(); initialOverlay.remove();
} catch (e) { } catch (e) {
console.log("[Layout] 加载指示器已被移除"); console.log("[Layout] Load indicator has been removed");
} }
}, 300); }, 300);
} }
@@ -297,23 +300,23 @@ const Layout = () => {
const performInitialization = async () => { const performInitialization = async () => {
if (isInitialized) { if (isInitialized) {
console.log("[Layout] 已经初始化过,跳过"); console.log("[Layout] Already initialized, skip");
return; return;
} }
initializationAttempts++; initializationAttempts++;
console.log(`[Layout] 开始第 ${initializationAttempts} 次初始化尝试`); console.log(`[Layout] Start ${initializationAttempts} for the first time`);
try { try {
removeLoadingOverlay(); removeLoadingOverlay();
await notifyBackend("加载阶段", "Loading"); await notifyBackend("Loading phase", "Loading");
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
const checkReactMount = () => { const checkReactMount = () => {
const rootElement = document.getElementById("root"); const rootElement = document.getElementById("root");
if (rootElement && rootElement.children.length > 0) { if (rootElement && rootElement.children.length > 0) {
console.log("[Layout] React组件已挂载"); console.log("[Layout] React components are mounted");
resolve(); resolve();
} else { } else {
setTimeout(checkReactMount, 50); setTimeout(checkReactMount, 50);
@@ -323,43 +326,43 @@ const Layout = () => {
checkReactMount(); checkReactMount();
setTimeout(() => { setTimeout(() => {
console.log("[Layout] React组件挂载检查超时,继续执行"); console.log("[Layout] React components mount check timeout, continue execution");
resolve(); resolve();
}, 2000); }, 2000);
}); });
await notifyBackend("DOM就绪", "DomReady"); await notifyBackend("DOM ready", "DomReady");
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve()); requestAnimationFrame(() => resolve());
}); });
await notifyBackend("资源加载完成", "ResourcesLoaded"); await notifyBackend("Resource loading completed", "ResourcesLoaded");
await notifyBackend("UI就绪"); await notifyBackend("UI ready");
isInitialized = true; isInitialized = true;
console.log(`[Layout] ${initializationAttempts} 次初始化完成`); console.log(`[Layout] The ${initializationAttempts} initialization is complete`);
} catch (error) { } catch (error) {
console.error( console.error(
`[Layout] ${initializationAttempts} 次初始化失败:`, `[Layout] Initialization failure at ${initializationAttempts}:`,
error, error,
); );
if (initializationAttempts < maxAttempts) { if (initializationAttempts < maxAttempts) {
console.log( console.log(
`[Layout] 将在500ms后进行第 ${initializationAttempts + 1} 次重试`, `[Layout] The first ${initializationAttempts + 1} retry will be made after 500ms`,
); );
setTimeout(performInitialization, 500); setTimeout(performInitialization, 500);
} else { } else {
console.error("[Layout] 所有初始化尝试都失败,执行紧急初始化"); console.error("[Layout] All initialization attempts fail, perform emergency initialization");
removeLoadingOverlay(); removeLoadingOverlay();
try { try {
await notifyBackend("UI就绪"); await notifyBackend("UI ready");
isInitialized = true; isInitialized = true;
} catch (e) { } catch (e) {
console.error("[Layout] 紧急初始化也失败:", e); console.error("[Layout] Emergency initialization also failed:", e);
} }
} }
} }
@@ -369,39 +372,39 @@ const Layout = () => {
const setupEventListener = async () => { const setupEventListener = async () => {
try { try {
console.log("[Layout] 开始监听启动完成事件"); console.log("[Layout] Start listening for startup completion events");
const unlisten = await listen("verge://startup-completed", () => { const unlisten = await listen("verge://startup-completed", () => {
if (!hasEventTriggered) { if (!hasEventTriggered) {
console.log("[Layout] 收到启动完成事件,开始初始化"); console.log("[Layout] Receive startup completion event, start initialization");
hasEventTriggered = true; hasEventTriggered = true;
performInitialization(); performInitialization();
} }
}); });
return unlisten; return unlisten;
} catch (err) { } catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err); console.error("[Layout] Failed to listen for startup completion event:", err);
return () => {}; return () => {};
} }
}; };
const checkImmediateInitialization = async () => { const checkImmediateInitialization = async () => {
try { try {
console.log("[Layout] 检查后端是否已就绪"); console.log("[Layout] Check if the backend is ready");
await invoke("update_ui_stage", { stage: "Loading" }); await invoke("update_ui_stage", { stage: "Loading" });
if (!hasEventTriggered && !isInitialized) { if (!hasEventTriggered && !isInitialized) {
console.log("[Layout] 后端已就绪,立即开始初始化"); console.log("[Layout] Backend is ready, start initialization immediately");
hasEventTriggered = true; hasEventTriggered = true;
performInitialization(); performInitialization();
} }
} catch (err) { } catch (err) {
console.log("[Layout] 后端尚未就绪,等待启动完成事件"); console.log("[Layout] Backend not yet ready, waiting for startup completion event");
} }
}; };
const backupInitialization = setTimeout(() => { const backupInitialization = setTimeout(() => {
if (!hasEventTriggered && !isInitialized) { if (!hasEventTriggered && !isInitialized) {
console.warn("[Layout] 备用初始化触发1.5秒内未开始初始化"); console.warn("[Layout] Standby initialization trigger: initialization not started within 1.5 seconds");
hasEventTriggered = true; hasEventTriggered = true;
performInitialization(); performInitialization();
} }
@@ -409,9 +412,9 @@ const Layout = () => {
const emergencyInitialization = setTimeout(() => { const emergencyInitialization = setTimeout(() => {
if (!isInitialized) { if (!isInitialized) {
console.error("[Layout] 紧急初始化触发5秒内未完成初始化"); console.error("[Layout] Emergency initialization trigger: initialization not completed within 5 seconds");
removeLoadingOverlay(); removeLoadingOverlay();
notifyBackend("UI就绪").catch(() => {}); notifyBackend("UI ready").catch(() => {});
isInitialized = true; isInitialized = true;
} }
}, 5000); }, 5000);
@@ -427,10 +430,10 @@ const Layout = () => {
}; };
}, []); }, []);
// 语言和起始页设置 // Language and start page settings
useEffect(() => { useEffect(() => {
if (language) { if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language); dayjs.locale(language === "ru" ? "ru-ru" : language);
i18next.changeLanguage(language); i18next.changeLanguage(language);
} }
}, [language]); }, [language]);
@@ -442,17 +445,37 @@ const Layout = () => {
}, [start_page]); }, [start_page]);
if (!routersEles) { if (!routersEles) {
return <div className="h-screen w-screen bg-background" />; return <div className="h-screen w-screen bg-background" />;
} }
const AppLayout = () => {
const { state, isMobile } = useSidebar();
const location = useLocation();
const routersEles = useRoutes(routers);
return (
<>
<AppSidebar />
<main
className="h-screen w-full overflow-y-auto transition-[margin] duration-200 ease-linear"
>
<div className="h-full w-full relative">
{routersEles && React.cloneElement(routersEles, { key: location.pathname })}
</div>
</main>
<HwidErrorDialog />
</>
);
};
return ( return (
<SWRConfig value={{ errorRetryCount: 3 }}> <SWRConfig value={{ errorRetryCount: 3 }}>
<NoticeManager /> <SidebarProvider defaultOpen={false}>
<div className="h-screen w-screen bg-background text-foreground overflow-hidden"> <AppLayout />
<div className="h-full w-full relative"> <Toaster />
{React.cloneElement(routersEles, { key: location.pathname })} </SidebarProvider>
</div>
</div>
</SWRConfig> </SWRConfig>
); );
}; };

View File

@@ -8,7 +8,6 @@ import React, {
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useConnectionSetting } from "@/services/states"; import { useConnectionSetting } from "@/services/states";
import { useVisibility } from "@/hooks/use-visibility"; import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-provider"; import { useAppData } from "@/providers/app-data-provider";
@@ -16,7 +15,6 @@ import { closeAllConnections } from "@/services/api";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
import { cn } from "@root/lib/utils"; import { cn } from "@root/lib/utils";
// Компоненты
import { BaseEmpty } from "@/components/base"; import { BaseEmpty } from "@/components/base";
import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionItem } from "@/components/connection/connection-item";
import { ConnectionTable } from "@/components/connection/connection-table"; import { ConnectionTable } from "@/components/connection/connection-table";
@@ -26,7 +24,6 @@ import {
} from "@/components/connection/connection-detail"; } from "@/components/connection/connection-detail";
import { import {
BaseSearchBox, BaseSearchBox,
type SearchState,
} from "@/components/base/base-search-box"; } from "@/components/base/base-search-box";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -36,14 +33,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import {
Tooltip, Tooltip,
@@ -52,7 +42,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
// Иконки
import { import {
List, List,
Table2, Table2,
@@ -62,6 +51,7 @@ import {
ArrowUp, ArrowUp,
Menu, Menu,
} from "lucide-react"; } from "lucide-react";
import {SidebarTrigger} from "@/components/ui/sidebar";
const initConn: IConnections = { const initConn: IConnections = {
uploadTotal: 0, uploadTotal: 0,
@@ -73,7 +63,6 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
const ConnectionsPage = () => { const ConnectionsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const pageVisible = useVisibility(); const pageVisible = useVisibility();
const [match, setMatch] = useState(() => (_: string) => true); const [match, setMatch] = useState(() => (_: string) => true);
const [curOrderOpt, setOrderOpt] = useState("Default"); const [curOrderOpt, setOrderOpt] = useState("Default");
@@ -166,15 +155,6 @@ const ConnectionsPage = () => {
}); });
}, [connections]); }, [connections]);
const menuItems = [
{ label: t("Home"), path: "/home" },
{ label: t("Profiles"), path: "/profile" },
{ label: t("Settings"), path: "/settings" },
{ label: t("Logs"), path: "/logs" },
{ label: t("Proxies"), path: "/proxies" },
{ label: t("Rules"), path: "/rules" },
];
return ( return (
<div className="h-full w-full relative"> <div className="h-full w-full relative">
<div <div
@@ -184,6 +164,9 @@ const ConnectionsPage = () => {
)} )}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="w-10">
<SidebarTrigger />
</div>
<h2 className="text-2xl font-semibold tracking-tight"> <h2 className="text-2xl font-semibold tracking-tight">
{t("Connections")} {t("Connections")}
</h2> </h2>
@@ -245,26 +228,6 @@ const ConnectionsPage = () => {
<Button size="sm" variant="destructive" onClick={onCloseAll}> <Button size="sm" variant="destructive" onClick={onCloseAll}>
{t("Close All")} {t("Close All")}
</Button> </Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" title={t("Menu")}>
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("Menu")}</DropdownMenuLabel>
<DropdownMenuSeparator />
{menuItems.map((item) => (
<DropdownMenuItem
key={item.path}
onSelect={() => navigate(item.path)}
disabled={location.pathname === item.path}
>
{item.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</TooltipProvider> </TooltipProvider>
</div> </div>

View File

@@ -1,5 +1,4 @@
import React, { useRef, useMemo, useCallback, useState } from "react"; import React, {useRef, useMemo, useCallback, useState, useEffect} from "react";
import { useNavigate } from "react-router-dom";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -22,12 +21,11 @@ import {
ChevronsUpDown, ChevronsUpDown,
Check, Check,
PlusCircle, PlusCircle,
Menu,
Wrench, Wrench,
AlertTriangle, AlertTriangle,
Loader2, Loader2,
Globe, Globe,
Send, Send, ExternalLink, RefreshCw, ArrowDown, ArrowUp,
} from "lucide-react"; } from "lucide-react";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useSystemState } from "@/hooks/use-system-state"; import { useSystemState } from "@/hooks/use-system-state";
@@ -37,14 +35,23 @@ import { ProxySelectors } from "@/components/home/proxy-selectors";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { closeAllConnections } from "@/services/api"; import { closeAllConnections } from "@/services/api";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { updateProfile } from "@/services/cmds";
import { SidebarTrigger } from "@/components/ui/sidebar";
import parseTraffic from "@/utils/parse-traffic";
import { useAppData } from "@/providers/app-data-provider";
import { PowerButton } from "@/components/home/power-button";
import { cn } from "@root/lib/utils";
import map from "../assets/image/map.svg";
const MinimalHomePage: React.FC = () => { const MinimalHomePage: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [isToggling, setIsToggling] = useState(false); const [isToggling, setIsToggling] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const { profiles, patchProfiles, activateSelected, mutateProfiles } = const { profiles, patchProfiles, activateSelected, mutateProfiles } =
useProfiles(); useProfiles();
const viewerRef = useRef<ProfileViewerRef>(null); const viewerRef = useRef<ProfileViewerRef>(null);
const [uidToActivate, setUidToActivate] = useState<string | null>(null);
const { connections } = useAppData();
const profileItems = useMemo(() => { const profileItems = useMemo(() => {
const items = const items =
@@ -56,7 +63,6 @@ const MinimalHomePage: React.FC = () => {
const currentProfile = useMemo(() => { const currentProfile = useMemo(() => {
return profileItems.find(p => p.uid === profiles?.current); return profileItems.find(p => p.uid === profiles?.current);
}, [profileItems, profiles?.current]); }, [profileItems, profiles?.current]);
console.log(currentProfile);
const currentProfileName = currentProfile?.name || profiles?.current; const currentProfileName = currentProfile?.name || profiles?.current;
const activateProfile = useCallback( const activateProfile = useCallback(
@@ -76,6 +82,15 @@ const MinimalHomePage: React.FC = () => {
[patchProfiles, activateSelected, mutateProfiles, t], [patchProfiles, activateSelected, mutateProfiles, t],
); );
useEffect(() => {
const uidToActivate = sessionStorage.getItem('activateProfile');
if (uidToActivate && profileItems.some(p => p.uid === uidToActivate)) {
activateProfile(uidToActivate, false);
sessionStorage.removeItem('activateProfile');
}
}, [profileItems, activateProfile]);
const handleProfileChange = useLockFn(async (uid: string) => { const handleProfileChange = useLockFn(async (uid: string) => {
if (profiles?.current === uid) return; if (profiles?.current === uid) return;
await activateProfile(uid, true); await activateProfile(uid, true);
@@ -128,105 +143,179 @@ const MinimalHomePage: React.FC = () => {
} }
}); });
const navMenuItems = [ const handleUpdateProfile = useLockFn(async () => {
{ label: "Profiles", path: "/profile" }, if (!currentProfile?.uid || currentProfile.type !== 'remote') return;
{ label: "Settings", path: "/settings" }, setIsUpdating(true);
{ label: "Logs", path: "/logs" }, try {
{ label: "Proxies", path: "/proxies" }, await updateProfile(currentProfile.uid);
{ label: "Connections", path: "/connections" }, toast.success(t("Profile Updated Successfully"));
{ label: "Rules", path: "/rules" }, mutateProfiles();
]; } catch (err: any) {
toast.error(t("Failed to update profile"), { description: err.message });
} finally {
setIsUpdating(false);
}
});
const statusInfo = useMemo(() => {
if (isToggling) {
return {
text: isProxyEnabled ? t('Disconnecting...') : t('Connecting...'),
color: isProxyEnabled ? '#f59e0b' : '#84cc16',
isAnimating: true,
};
}
if (isProxyEnabled) {
return {
text: t('Connected'),
color: '#22c55e',
isAnimating: false,
};
}
return {
text: t('Disconnected'),
color: '#ef4444',
isAnimating: false,
};
}, [isToggling, isProxyEnabled, t]);
return ( return (
<div className="flex flex-col h-screen p-5"> <div className="h-full w-full flex flex-col">
<header className="absolute top-0 left-0 right-0 p-5 flex items-center justify-between z-20"> <div className="absolute inset-0 opacity-20 pointer-events-none z-0 [transform:translateZ(0)]">
<div className="w-10"></div> <img
src={map}
alt="World map"
className="w-full h-full object-cover"
/>
</div>
{profileItems.length > 0 && ( {isProxyEnabled && (
<div className="flex-shrink-0"> <div
<DropdownMenu> className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[500px] w-[500px] rounded-full pointer-events-none z-0 transition-opacity duration-500"
<DropdownMenuTrigger asChild> style={{
<Button background: 'radial-gradient(circle, rgba(34,197,94,0.3) 0%, transparent 70%)',
variant="outline" filter: 'blur(100px)',
className="w-full max-w-[250px] sm:max-w-xs" }}
> />
<span className="truncate">{currentProfileName}</span> )}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> <header className="flex-shrink-0 p-5 grid grid-cols-3 items-center z-10">
</DropdownMenuTrigger> <div className="flex justify-start">
<DropdownMenuContent className="w-[--radix-dropdown-menu-trigger-width]"> <SidebarTrigger />
<DropdownMenuLabel>{t("Profiles")}</DropdownMenuLabel> </div>
<DropdownMenuSeparator /> <div className="justify-self-center flex flex-col items-center gap-2">
{profileItems.map((p) => ( <div className="relative">
<DropdownMenuItem {profileItems.length > 0 && (
key={p.uid} <div className="flex-shrink-0">
onSelect={() => handleProfileChange(p.uid)} <DropdownMenu>
> <DropdownMenuTrigger asChild>
<span className="flex-1 truncate">{p.name}</span> <Button variant="outline" className="w-full max-w-[250px] sm:max-w-xs">
{profiles?.current === p.uid && ( <span className="truncate">{currentProfileName}</span>
<Check className="ml-4 h-4 w-4" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
)} </Button>
</DropdownMenuItem> </DropdownMenuTrigger>
))} <DropdownMenuContent className="w-[--radix-dropdown-menu-trigger-width]">
<DropdownMenuSeparator /> <DropdownMenuLabel>{t("Profiles")}</DropdownMenuLabel>
<DropdownMenuItem onSelect={() => viewerRef.current?.create()}> <DropdownMenuSeparator />
<PlusCircle className="mr-2 h-4 w-4" /> {profileItems.map((p) => (
<span>{t("Add Profile")}</span> <DropdownMenuItem
</DropdownMenuItem> key={p.uid}
</DropdownMenuContent> onSelect={() => handleProfileChange(p.uid)}
</DropdownMenu> >
<span className="flex-1 truncate">{p.name}</span>
{profiles?.current === p.uid && (
<Check className="ml-4 h-4 w-4" />
)}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => viewerRef.current?.create()}>
<PlusCircle className="mr-2 h-4 w-4" />
<span>{t("Add Profile")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{currentProfile?.type === 'remote' && (
<div className="absolute top-1/2 -translate-y-1/2 left-full ml-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleUpdateProfile}
disabled={isUpdating}
className="flex-shrink-0"
>
{isUpdating ? <Loader2 className="h-5 w-5 animate-spin" /> : <RefreshCw className="h-5 w-5" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Update Profile")}</p></TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div> </div>
)} </div>
<div className="flex justify-end">
<div className="w-10">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("Menu")}</DropdownMenuLabel>
<DropdownMenuSeparator />
{navMenuItems.map((item) => (
<DropdownMenuItem
key={item.path}
onSelect={() => navigate(item.path)}
>
{t(item.label)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</header> </header>
<div className="flex items-center justify-center flex-grow w-full"> <main className="flex-1 overflow-y-auto flex items-center justify-center">
<div className="flex flex-col items-center gap-8 pt-10"> <div className="relative flex flex-col items-center gap-8 py-10 w-full max-w-4xl px-4">
{currentProfile?.announce && ( {currentProfile?.announce && (
<p className="relative -translate-y-15 text-xl font-semibold text-foreground max-w-lg text-center"> <div className="absolute -top-15 w-full flex justify-center text-center max-w-lg">
{currentProfile.announce} {currentProfile.announce_url ? (
</p> <a
href={currentProfile.announce_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-base font-semibold text-foreground hover:underline hover:opacity-80 transition-all whitespace-pre-wrap"
title={currentProfile.announce_url.replace(/\\n/g, '\n')}
>
<span>{currentProfile.announce.replace(/\\n/g, '\n')}</span>
<ExternalLink className="h-4 w-4 flex-shrink-0" />
</a>
) : (
<p className="text-base font-semibold text-foreground whitespace-pre-wrap">
{currentProfile.announce}
</p>
)}
</div>
)} )}
<div className="text-center"> <div className="relative text-center">
<h1 <h1
className="text-4xl mb-2 font-semibold" className={cn(
style={{ color: isProxyEnabled ? "#22c55e" : "#ef4444" }} "text-4xl mb-2 font-semibold transition-colors duration-300",
> statusInfo.isAnimating && "animate-pulse"
{isProxyEnabled ? t("Connected") : t("Disconnected")} )}
</h1> style={{ color: statusInfo.color }}
<p className="h-6 text-sm text-muted-foreground transition-opacity duration-300"> >
{isToggling && {statusInfo.text}
(isProxyEnabled ? t("Disconnecting...") : t("Connecting..."))} </h1>
</p> {isProxyEnabled && (
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-52 flex justify-center items-center text-sm text-muted-foreground gap-6">
<div className="flex items-center gap-1">
<ArrowDown className="h-4 w-4 text-green-500" />
{parseTraffic(connections.downloadTotal)}
</div>
<div className="flex items-center gap-1">
<ArrowUp className="h-4 w-4 text-sky-500" />
{parseTraffic(connections.uploadTotal)}
</div>
</div>
)}
</div> </div>
<div className="scale-[7] my-16"> <div className="relative -translate-y-6">
<Switch <PowerButton
disabled={showTunAlert || isToggling} loading={isToggling}
checked={!!isProxyEnabled} checked={!!isProxyEnabled}
onCheckedChange={handleToggleProxy} onClick={handleToggleProxy}
aria-label={t("Toggle Proxy")} disabled={showTunAlert || isToggling || profileItems.length === 0}
aria-label={t("Toggle Proxy")}
/> />
</div> </div>
@@ -255,7 +344,7 @@ const MinimalHomePage: React.FC = () => {
</div> </div>
)} )}
<div className="w-full mt-4 flex justify-center"> <div className="w-full max-w-sm mt-4 flex justify-center">
{profileItems.length > 0 ? ( {profileItems.length > 0 ? (
<ProxySelectors /> <ProxySelectors />
) : ( ) : (
@@ -276,8 +365,8 @@ const MinimalHomePage: React.FC = () => {
</Alert> </Alert>
)} )}
</div> </div>
</div>
</div> </div>
</main>
<footer className="flex justify-center p-4 flex-shrink-0"> <footer className="flex justify-center p-4 flex-shrink-0">
{currentProfile?.support_url && ( {currentProfile?.support_url && (
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -286,7 +375,7 @@ const MinimalHomePage: React.FC = () => {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<a href={currentProfile.support_url} target="_blank" rel="noopener noreferrer" className="transition-colors hover:text-primary"> <a href={currentProfile.support_url} target="_blank" rel="noopener noreferrer" className="transition-colors hover:text-primary">
{(currentProfile.support_url.includes('t.me') || currentProfile.support_url.includes('telegram')) ? ( {(currentProfile.support_url.includes('t.me') || currentProfile.support_url.includes('telegram') || currentProfile.support_url.startsWith('tg://')) ? (
<Send className="h-5 w-5" /> <Send className="h-5 w-5" />
) : ( ) : (
<Globe className="h-5 w-5" /> <Globe className="h-5 w-5" />

View File

@@ -1,5 +1,3 @@
// LogPage.tsx
import React, { import React, {
useMemo, useMemo,
useState, useState,
@@ -10,8 +8,7 @@ import React, {
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocalStorage } from "foxact/use-local-storage"; import { useLocalStorage } from "foxact/use-local-storage";
import { useNavigate } from "react-router-dom"; import { Play, Pause, Trash2 } from "lucide-react";
import { Play, Pause, Trash2, Menu } from "lucide-react";
import { LogLevel } from "@/hooks/use-log-data"; import { LogLevel } from "@/hooks/use-log-data";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { useEnableLog } from "@/services/states"; import { useEnableLog } from "@/services/states";
@@ -34,18 +31,10 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { import {SidebarTrigger} from "@/components/ui/sidebar";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
const LogPage = () => { const LogPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [enableLog, setEnableLog] = useEnableLog(); const [enableLog, setEnableLog] = useEnableLog();
const { clashInfo } = useClashInfo(); const { clashInfo } = useClashInfo();
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>( const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
@@ -104,28 +93,16 @@ const LogPage = () => {
[], [],
); );
const menuItems = [
{ label: t("Home"), path: "/home" },
{ label: t("Profiles"), path: "/profile" },
{ label: t("Settings"), path: "/settings" },
{ label: t("Proxies"), path: "/proxies" },
{ label: t("Connections"), path: "/connections" },
{ label: t("Rules"), path: "/rules" },
];
return ( return (
<div className="h-full w-full relative"> <div className="h-full w-full relative">
{/* "Липкая" шапка */}
<div <div
className={cn( className={cn(
"absolute top-0 left-0 right-0 z-10 p-4 transition-all duration-200", "absolute top-0 left-0 right-0 z-10 p-4 transition-all duration-200",
// --- НАЧАЛО ИЗМЕНЕНИЙ ---
// Вместо блюра делаем солидный фон с тенью при прокрутке
{ "bg-background shadow-md": isScrolled }, { "bg-background shadow-md": isScrolled },
// --- КОНЕЦ ИЗМЕНЕНИЙ ---
)} )}
> >
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<SidebarTrigger />
<h2 className="text-2xl font-semibold tracking-tight">{t("Logs")}</h2> <h2 className="text-2xl font-semibold tracking-tight">{t("Logs")}</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@@ -146,26 +123,6 @@ const LogPage = () => {
{t("Clear")} {t("Clear")}
</Button> </Button>
)} )}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" title={t("Menu")}>
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("Menu")}</DropdownMenuLabel>
<DropdownMenuSeparator />
{menuItems.map((item) => (
<DropdownMenuItem
key={item.path}
onSelect={() => navigate(item.path)}
disabled={location.pathname === item.path}
>
{item.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -187,7 +144,6 @@ const LogPage = () => {
</div> </div>
</div> </div>
{/* Возвращаем Virtuoso на место */}
<div <div
ref={scrollContainerRef} ref={scrollContainerRef}
className="absolute top-0 left-0 right-0 bottom-0 pt-32 overflow-y-auto" className="absolute top-0 left-0 right-0 bottom-0 pt-32 overflow-y-auto"

View File

@@ -41,14 +41,12 @@ import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import { readTextFile } from "@tauri-apps/plugin-fs"; import { readTextFile } from "@tauri-apps/plugin-fs";
import { readText } from "@tauri-apps/plugin-clipboard-manager"; import { readText } from "@tauri-apps/plugin-clipboard-manager";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useListen } from "@/hooks/use-listen"; import { useListen } from "@/hooks/use-listen";
import { listen, TauriEvent } from "@tauri-apps/api/event"; import { listen, TauriEvent } from "@tauri-apps/api/event";
import { showNotice } from "@/services/noticeService"; import { showNotice } from "@/services/noticeService";
import { cn } from "@root/lib/utils"; import { cn } from "@root/lib/utils";
// Компоненты shadcn/ui
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Tooltip, Tooltip,
@@ -56,31 +54,19 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
// Иконки
import { import {
ClipboardPaste,
X,
PlusCircle, PlusCircle,
RefreshCw, RefreshCw,
Zap, Zap,
FileText, FileText,
Loader2, Loader2,
Menu,
} from "lucide-react"; } from "lucide-react";
import { SidebarTrigger } from "@/components/ui/sidebar";
const ProfilePage = () => { const ProfilePage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const { addListener } = useListen(); const { addListener } = useListen();
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
@@ -89,7 +75,6 @@ const ProfilePage = () => {
const [updateAllLoading, setUpdateAllLoading] = useState(false); const [updateAllLoading, setUpdateAllLoading] = useState(false);
const [enhanceLoading, setEnhanceLoading] = useState(false); const [enhanceLoading, setEnhanceLoading] = useState(false);
// Логика для "липкой" шапки
const scrollerRef = useRef<HTMLDivElement>(null); const scrollerRef = useRef<HTMLDivElement>(null);
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
@@ -335,15 +320,6 @@ const ProfilePage = () => {
}; };
}, [mutateProfiles]); }, [mutateProfiles]);
const menuItems = [
{ label: t("Home"), path: "/home" },
{ label: t("Settings"), path: "/settings" },
{ label: t("Logs"), path: "/logs" },
{ label: t("Proxies"), path: "/proxies" },
{ label: t("Connections"), path: "/connections" },
{ label: t("Rules"), path: "/rules" },
];
return ( return (
<div className="h-full w-full relative"> <div className="h-full w-full relative">
<div <div
@@ -353,6 +329,9 @@ const ProfilePage = () => {
)} )}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="w-10">
<SidebarTrigger />
</div>
<h2 className="text-2xl font-semibold tracking-tight"> <h2 className="text-2xl font-semibold tracking-tight">
{t("Profiles")} {t("Profiles")}
</h2> </h2>
@@ -424,74 +403,14 @@ const ProfilePage = () => {
<p>{t("View Runtime Config")}</p> <p>{t("View Runtime Config")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" title={t("Menu")}>
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("Menu")}</DropdownMenuLabel>
<DropdownMenuSeparator />
{menuItems.map((item) => (
<DropdownMenuItem
key={item.path}
onSelect={() => navigate(item.path)}
disabled={location.pathname === item.path}
>
{item.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</TooltipProvider> </TooltipProvider>
</div> </div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 flex-grow sm:flex-grow-0">
<Input
type="text"
placeholder={t("Profile URL")}
value={url}
onChange={(e) => setUrl(e.target.value)}
className="h-9 min-w-[200px] flex-grow sm:w-80"
/>
{url ? (
<Button
variant="ghost"
size="icon"
title={t("Clear")}
onClick={() => setUrl("")}
className="h-9 w-9 flex-shrink-0"
>
<X className="h-4 w-4" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
title={t("Paste")}
onClick={onCopyLink}
className="h-9 w-9 flex-shrink-0"
>
<ClipboardPaste className="h-4 w-4" />
</Button>
)}
</div>
<Button
onClick={onImport}
disabled={!url || disabled || importLoading}
className="h-9"
>
{importLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t("Import")}
</Button>
</div>
</div> </div>
<div <div
ref={scrollerRef} ref={scrollerRef}
className="absolute top-0 left-0 right-0 bottom-0 pt-40 overflow-y-auto" className="absolute top-0 left-0 right-0 bottom-0 pt-25 overflow-y-auto"
> >
<DndContext <DndContext
sensors={sensors} sensors={sensors}

View File

@@ -1,27 +1,17 @@
import useSWR from "swr"; import useSWR from "swr";
import { useEffect } from "react"; import React, { useEffect } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { closeAllConnections, getClashConfig } from "@/services/api"; import { closeAllConnections, getClashConfig } from "@/services/api";
import { patchClashMode } from "@/services/cmds"; import { patchClashMode } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { ProxyGroups } from "@/components/proxy/proxy-groups"; import { ProxyGroups } from "@/components/proxy/proxy-groups";
import { ProviderButton } from "@/components/proxy/provider-button"; import { ProviderButton } from "@/components/proxy/provider-button";
import { import { SidebarTrigger } from "@/components/ui/sidebar";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Menu } from "lucide-react";
const ProxyPage = () => { const ProxyPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const { data: clashConfig, mutate: mutateClash } = useSWR( const { data: clashConfig, mutate: mutateClash } = useSWR(
"getClashConfig", "getClashConfig",
@@ -33,7 +23,7 @@ const ProxyPage = () => {
); );
const { verge } = useVerge(); const { verge } = useVerge();
const modeList = ["rule", "global", "direct"]; const modeList = ["rule", "global"];
const curMode = clashConfig?.mode?.toLowerCase(); const curMode = clashConfig?.mode?.toLowerCase();
const onChangeMode = useLockFn(async (mode: string) => { const onChangeMode = useLockFn(async (mode: string) => {
@@ -50,18 +40,12 @@ const ProxyPage = () => {
} }
}, [curMode]); }, [curMode]);
const menuItems = [
{ label: t("Home"), path: "/home" },
{ label: t("Profiles"), path: "/profile" },
{ label: t("Settings"), path: "/settings" },
{ label: t("Logs"), path: "/logs" },
{ label: t("Connections"), path: "/connections" },
{ label: t("Rules"), path: "/rules" },
];
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="p-4 pb-2 flex justify-between items-center"> <div className="p-4 pb-2 flex justify-between items-center">
<div className="w-10">
<SidebarTrigger />
</div>
<h2 className="text-2xl font-semibold tracking-tight"> <h2 className="text-2xl font-semibold tracking-tight">
{t("Proxies")} {t("Proxies")}
</h2> </h2>
@@ -74,32 +58,12 @@ const ProxyPage = () => {
variant={mode === curMode ? "default" : "ghost"} variant={mode === curMode ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => onChangeMode(mode)} onClick={() => onChangeMode(mode)}
className="capitalize px-3 py-1 h-auto" className="px-3 py-1 h-auto"
> >
{t(mode)} {t(mode)}
</Button> </Button>
))} ))}
</div> </div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" title={t("Menu")}>
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("Menu")}</DropdownMenuLabel>
<DropdownMenuSeparator />
{menuItems.map((item) => (
<DropdownMenuItem
key={item.path}
onSelect={() => navigate(item.path)}
disabled={location.pathname === item.path}
>
{item.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>

View File

@@ -1,33 +1,20 @@
import { useState, useMemo, useRef, useEffect, useCallback } from "react"; import React, { useState, useMemo, useRef, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
import { useAppData } from "@/providers/app-data-provider"; import { useAppData } from "@/providers/app-data-provider";
import { useVisibility } from "@/hooks/use-visibility"; import { useVisibility } from "@/hooks/use-visibility";
import { cn } from "@root/lib/utils"; import { cn } from "@root/lib/utils";
// Компоненты
import { BaseEmpty } from "@/components/base"; import { BaseEmpty } from "@/components/base";
import RuleItem from "@/components/rule/rule-item"; import RuleItem from "@/components/rule/rule-item";
import { ProviderButton } from "@/components/rule/provider-button"; import { ProviderButton } from "@/components/rule/provider-button";
import { BaseSearchBox, SearchState } from "@/components/base/base-search-box"; import { BaseSearchBox } from "@/components/base/base-search-box";
import { ScrollTopButton } from "@/components/layout/scroll-top-button"; import { ScrollTopButton } from "@/components/layout/scroll-top-button";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
// Иконки import { SidebarTrigger } from "@/components/ui/sidebar";
import { Menu } from "lucide-react";
const RulesPage = () => { const RulesPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const { rules = [], refreshRules, refreshRuleProviders } = useAppData(); const { rules = [], refreshRules, refreshRuleProviders } = useAppData();
const [match, setMatch] = useState(() => (_: string) => true); const [match, setMatch] = useState(() => (_: string) => true);
const virtuosoRef = useRef<VirtuosoHandle>(null); const virtuosoRef = useRef<VirtuosoHandle>(null);
@@ -36,24 +23,17 @@ const RulesPage = () => {
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
const pageVisible = useVisibility(); const pageVisible = useVisibility();
// --- НАЧАЛО ИЗМЕНЕНИЙ 1 ---
// Разделяем логику на два безопасных useEffect
useEffect(() => { useEffect(() => {
// Этот эффект сработает только один раз при монтировании компонента
refreshRules(); refreshRules();
refreshRuleProviders(); refreshRuleProviders();
// eslint-disable-next-line react-hooks/exhaustive-deps }, []);
}, []); // Пустой массив зависимостей = запуск только один раз
useEffect(() => { useEffect(() => {
// Этот эффект будет срабатывать только при изменении видимости страницы
if (pageVisible) { if (pageVisible) {
refreshRules(); refreshRules();
refreshRuleProviders(); refreshRuleProviders();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageVisible]); }, [pageVisible]);
// --- КОНЕЦ ИЗМЕНЕНИЙ 1 ---
const filteredRules = useMemo(() => { const filteredRules = useMemo(() => {
return rules.filter((item) => match(item.payload)); return rules.filter((item) => match(item.payload));
@@ -75,21 +55,9 @@ const RulesPage = () => {
virtuosoRef.current?.scrollTo({ top: 0, behavior: "smooth" }); virtuosoRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}, []); }, []);
// --- НАЧАЛО ИЗМЕНЕНИЙ 2 ---
// Оборачиваем обработчик поиска в useCallback для стабильности
const handleSearch = useCallback((matcher: (content: string) => boolean) => { const handleSearch = useCallback((matcher: (content: string) => boolean) => {
setMatch(() => matcher); setMatch(() => matcher);
}, []); }, []);
// --- КОНЕЦ ИЗМЕНЕНИЙ 2 ---
const menuItems = [
{ label: t("Home"), path: "/home" },
{ label: t("Profiles"), path: "/profile" },
{ label: t("Settings"), path: "/settings" },
{ label: t("Logs"), path: "/logs" },
{ label: t("Proxies"), path: "/proxies" },
{ label: t("Connections"), path: "/connections" },
];
return ( return (
<div className="h-full w-full relative"> <div className="h-full w-full relative">
@@ -100,35 +68,17 @@ const RulesPage = () => {
)} )}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="w-10">
<SidebarTrigger />
</div>
<h2 className="text-2xl font-semibold tracking-tight"> <h2 className="text-2xl font-semibold tracking-tight">
{t("Rules")} {t("Rules")}
</h2> </h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-70"> <div className="w-70">
{/* Передаем стабильную функцию handleSearch в пропс */}
<BaseSearchBox onSearch={handleSearch} /> <BaseSearchBox onSearch={handleSearch} />
</div> </div>
<ProviderButton /> <ProviderButton />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" title={t("Menu")}>
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("Menu")}</DropdownMenuLabel>
<DropdownMenuSeparator />
{menuItems.map((item) => (
<DropdownMenuItem
key={item.path}
onSelect={() => navigate(item.path)}
disabled={location.pathname === item.path}
>
{item.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</div> </div>
@@ -150,7 +100,6 @@ const RulesPage = () => {
<BaseEmpty /> <BaseEmpty />
)} )}
</div> </div>
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} /> <ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
</div> </div>
); );

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