53 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
94 changed files with 2361 additions and 1260 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
echo "Tag and package.json version are consistent."
create_release_notes:
name: Create Release Notes
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
- run: |
if [ -z "$UPDATE_LOGS" ]; then
echo "No update logs found, using default message"
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
fi
cat > release.txt << EOF
$UPDATE_LOGS
## Which version should I download?
### macOS
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_aarch64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Apple%20Silicon"></a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Intel"></a><br>
> :warning: **Warning**
If you get a notification that the application is corrupted when you run it on macOS, run this command:<br>
<code>sudo xattr -r -c /Applications/Koala\ Clash.app</code>
### Linux
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.x86_64.rpm"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=fedora&label=RPM"> </a>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64.deb"><img src="https://img.shields.io/badge/arm64-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.aarch64.rpm"><img src="https://img.shields.io/badge/aarch64-default?style=flat&logo=fedora&label=RPM"> </a>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_armhf.deb"><img src="https://img.shields.io/badge/armhf-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.armhfp.rpm"><img src="https://img.shields.io/badge/armhfp-default?style=flat&logo=fedora&label=RPM"> </a>
### Windows (Win7 is no longer supported)
#### Normal version (recommended)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
#### Portable version is no longer available with many problems
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{env.VERSION}}
name: "Koala Clash v${{env.VERSION}}"
body_path: release.txt
token: ${{ secrets.GITHUB_TOKEN }}
release:
name: Release Build
needs: check_tag_version
needs: [check_tag_version, create_release_notes]
strategy:
fail-fast: false
matrix:
@@ -96,19 +178,78 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Create .p8 file
run: |
mkdir -p ~/.appstoreconnect/private_keys
echo "${{ secrets.APPLE_API_KEY_CONTENT }}" > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8
- name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: "~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8"
with:
tagName: v__VERSION__
releaseName: "Clash Verge Rev Lite v__VERSION__"
tauriScript: pnpm
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:
name: Release Build for Linux ARM
strategy:
@@ -220,11 +361,34 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
- name: Rename
shell: bash
run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then
exit 1
fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
dir_path=$(dirname "$old_path")
old_filename=$(basename "$old_path")
new_filename=$(echo "$old_filename" \
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
)
new_path="${dir_path}/${new_filename}"
if [ "$old_path" != "$new_path" ]; then
echo " - '$old_filename' -> '$new_filename'"
mv "$old_path" "$new_path"
fi
done
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev Lite v${{env.VERSION}}"
name: "Koala Clash v${{env.VERSION}}"
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
@@ -294,19 +458,19 @@ jobs:
run: |
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
Rename-Item $file.FullName $newName
}
@@ -314,7 +478,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: v${{steps.build.outputs.appVersion}}
name: "Clash Verge Rev Lite v${{steps.build.outputs.appVersion}}"
name: "Koala Clash v${{steps.build.outputs.appVersion}}"
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
@@ -374,9 +538,9 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
create_release_notes:
name: Create Release Notes
push-notify-to-telegram:
runs-on: ubuntu-latest
needs: [release-update, release-update-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -412,46 +576,31 @@ jobs:
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
UPDATE_LOGS=$(echo "$UPDATE_LOGS" | sed 's/^## \(v.*\)/\*\1\*/')
fi
cat > release.txt << EOF
Вышло обновление!
$UPDATE_LOGS
## Which version should I download?
### macOS
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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 }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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>
`sudo xattr -r -c /Applications/Clash\ Verge\ Rev\ Lite.app`
### Linux
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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 }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.x86_64.rpm"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=fedora&label=RPM"> </a>
[Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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 }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.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 }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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 }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.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 }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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 }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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 }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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 }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_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
- name: notify to channel
uses: appleboy/telegram-action@master
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev Lite v${{env.VERSION}}"
body_path: release.txt
token: ${{ secrets.GITHUB_TOKEN }}
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,33 @@
## 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"

View File

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

View File

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

View File

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

View File

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

192
src-tauri/Cargo.lock generated
View File

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

View File

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

View File

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

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
chmod +x /usr/bin/install-service
chmod +x /usr/bin/uninstall-service
chmod +x /usr/bin/clash-verge-service
chmod +x /usr/bin/koala-clash-service

View File

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

View File

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

View File

@@ -9,6 +9,11 @@ use crate::{
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
use std::collections::BTreeMap;
use url::Url;
use serde_yaml::Value;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use percent_encoding::percent_decode_str;
// 全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
@@ -171,7 +176,34 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件
#[tauri::command]
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();
@@ -708,3 +740,467 @@ pub async fn update_profiles_on_startup() -> CmdResult {
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(
&dirs::clash_path()?,
&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 tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
pub const RUNTIME_CONFIG: &str = "koala-clash.yaml";
pub const CHECK_CONFIG: &str = "koala-clash-check.yaml";
pub struct Config {
clash_config: Draft<Box<IClashTemp>>,
@@ -141,7 +141,7 @@ impl Config {
.as_ref()
.ok_or(anyhow!("failed to get runtime config"))?;
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
help::save_yaml(&path, &config, Some("# Generated by Koala Clash"))?;
Ok(path)
}

View File

@@ -4,11 +4,11 @@ use crate::utils::{
tmpl,
};
use anyhow::{bail, Context, Result};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::{fs, time::Duration};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use url::Url;
use super::Config;
@@ -407,7 +407,8 @@ impl PrfItem {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
if let Some(b64_data) = str_value.strip_prefix("base64:") {
STANDARD.decode(b64_data)
STANDARD
.decode(b64_data)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
} else {
@@ -417,6 +418,13 @@ impl PrfItem {
None => None,
};
if let Some(announce_msg) = &announce {
let lower_msg = announce_msg.to_lowercase();
if lower_msg.contains("device") || lower_msg.contains("устройств") {
bail!(announce_msg.clone());
}
}
let announce_url = match header.get("announce-url") {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
@@ -429,7 +437,8 @@ impl PrfItem {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
if let Some(b64_data) = str_value.strip_prefix("base64:") {
STANDARD.decode(b64_data)
STANDARD
.decode(b64_data)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
} else {
@@ -441,7 +450,9 @@ impl PrfItem {
let uid = help::get_uid("R");
let file = format!("{uid}.yaml");
let name = name.or(profile_title).unwrap_or(filename.unwrap_or("Remote File".into()));
let name = name
.or(profile_title)
.unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?;
// process the charset "UTF-8 with BOM"

View File

@@ -66,7 +66,7 @@ impl IProfiles {
help::save_yaml(
&dirs::profiles_path()?,
self,
Some("# Profiles Config for Clash Verge"),
Some("# Profiles Config for Koala Clash"),
)
}

View File

@@ -238,7 +238,7 @@ pub struct IVergeTheme {
impl IVerge {
/// 有效的clash核心名称
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
pub const VALID_CLASH_CORES: &'static [&'static str] = &["koala-mihomo", "koala-mihomo-alpha"];
/// 验证并修正配置文件中的clash_core值
pub fn validate_and_fix_config() -> Result<()> {
@@ -257,10 +257,10 @@ impl IVerge {
warn,
Type::Config,
true,
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'koala-mihomo'",
core
);
config.clash_core = Some("verge-mihomo".to_string());
config.clash_core = Some("koala-mihomo".to_string());
needs_fix = true;
}
} else {
@@ -268,16 +268,16 @@ impl IVerge {
info,
Type::Config,
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;
}
// 修正后保存配置
if needs_fix {
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!(
info,
Type::Config,
@@ -321,7 +321,7 @@ impl IVerge {
pub fn get_valid_clash_core(&self) -> String {
self.clash_core
.clone()
.unwrap_or_else(|| "verge-mihomo".to_string())
.unwrap_or_else(|| "koala-mihomo".to_string())
}
fn get_system_language() -> String {
@@ -340,18 +340,15 @@ impl IVerge {
}
pub fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config,
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
}
}
dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)).unwrap_or_else(|err| {
log::error!(target: "app", "{err}");
Self::template()
})
}
pub fn template() -> Self {
Self {
clash_core: Some("verge-mihomo".into()),
clash_core: Some("koala-mihomo".into()),
language: Some(Self::get_system_language()),
theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))]
@@ -415,7 +412,7 @@ impl IVerge {
/// Save IVerge App Config
pub fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
help::save_yaml(&dirs::verge_path()?, &self, Some("# Koala Clash Config"))
}
/// patch verge config

View File

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

View File

@@ -146,7 +146,7 @@ impl CoreManager {
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
Some("# Clash Verge Runtime"),
Some("# Koala Clash Runtime"),
)?;
handle::Handle::notice_message(msg_type, msg_content);
Ok(())
@@ -443,7 +443,7 @@ impl CoreManager {
child_guard.as_ref().map(|child| child.pid())
};
let target_processes = ["verge-mihomo", "verge-mihomo-alpha"];
let target_processes = ["koala-mihomo", "koala-mihomo-alpha"];
// 并行查找所有目标进程
let mut process_futures = Vec::new();

View File

@@ -578,7 +578,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
}
Err(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) => {
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) => {
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 response = send_ipc_request(IpcCommand::StopClash, payload)
.await
.context("无法连接到Clash Verge Service")?;
.context("无法连接到Koala Clash Service")?;
if !response.success {
bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string()));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -335,12 +335,12 @@ pub fn create_window(is_show: bool) -> bool {
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge Rev Lite")
.title("Koala Clash")
.center()
.decorations(true)
.fullscreen(false)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
.min_inner_size(520.0, 520.0)
.min_inner_size(1000.0, 800.0)
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(
r#"
@@ -549,7 +549,7 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
}
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "koala-clash" {
let mut name: Option<String> = None;
let mut url_param: Option<String> = None;
@@ -565,7 +565,7 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
create_window(false);
create_window(true);
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = item.uid.clone().unwrap();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 21 KiB

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

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

View File

@@ -7,7 +7,7 @@ import {
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuItem, useSidebar,
} from "@/components/ui/sidebar"
import { t } from 'i18next';
import { cn } from '@root/lib/utils';
@@ -23,6 +23,8 @@ import {
} 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 },
@@ -35,13 +37,24 @@ const menuItems = [
];
export function AppSidebar() {
const { isMobile } = useSidebar();
return (
<Sidebar variant="floating" collapsible="icon">
<SidebarHeader>
<SidebarMenuButton>
<EarthLock/>
<span className="font-semibold group-data-[state=collapsed]:hidden">
Clash Koala
<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>
@@ -51,13 +64,8 @@ export function AppSidebar() {
<SidebarMenu className="gap-3">
{menuItems.map((item) => {
const isActive = location.pathname === item.url;
return (
<SidebarMenuItem key={item.title} className="my-1">
<SidebarMenuButton
asChild
isActive={isActive}
tooltip={t(item.title)}>
<Link
const linkElement = (
<Link
key={item.title}
to={item.url}
className={cn(
@@ -68,6 +76,20 @@ export function AppSidebar() {
<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>
)

View File

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

View File

@@ -69,7 +69,7 @@ const LogItem = ({ value, searchState }: Props) => {
{renderHighlightText(value.type)}
</span>
</div>
<div className="text-gray-800 dark:text-gray-200 break-all whitespace-pre-wrap">
<div className="text-foreground break-all whitespace-pre-wrap">
{renderHighlightText(value.payload)}
</div>
</div>

View File

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

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

View File

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

View File

@@ -302,7 +302,7 @@ export const ProxiesEditorViewer = (props: Props) => {
return (
<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>
<div className="flex justify-between items-center pr-8">
<DialogTitle>{t("Edit Proxies")}</DialogTitle>

View File

@@ -513,7 +513,7 @@ export const RulesEditorViewer = (props: Props) => {
return (
<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>
<div className="flex justify-between items-center pr-8">
<DialogTitle>{t("Edit Rules")}</DialogTitle>
@@ -529,8 +529,8 @@ export const RulesEditorViewer = (props: Props) => {
<div className="flex-1 min-h-0 mt-4">
{visualization ? (
<div className="h-full flex flex-col lg:flex-row gap-4">
<div className="w-full lg:w-1/3 flex flex-col gap-4 p-1">
<div className="h-full flex flex-row gap-4">
<div className="w-1/3 flex flex-col gap-4 p-1">
<div className="space-y-2">
<Label>{t("Rule Type")}</Label>
<Combobox
@@ -617,8 +617,8 @@ export const RulesEditorViewer = (props: Props) => {
</Button>
</div>
</div>
<Separator orientation="vertical" className="hidden lg:flex" />
<div className="w-full lg:w-2/3 flex flex-col min-w-0 flex-1">
<Separator orientation="vertical" className="flex" />
<div className="w-2/3 flex flex-col min-w-0 flex-1">
<BaseSearchBox
onSearch={(matcher) => setMatch(() => matcher)}
/>

View File

@@ -23,14 +23,8 @@ import { ProxyRender } from "./proxy-render";
import delayManager from "@/services/delay";
import { useTranslation } from "react-i18next";
import { ScrollTopButton } from "../layout/scroll-top-button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
// Вспомогательная функция для плавного скролла (взята из вашего оригинального файла)
function throttle<T extends (...args: any[]) => any>(
func: T,
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 {
mode: string;
@@ -108,33 +72,6 @@ export const ProxyGroups = memo((props: Props) => {
const scrollerRef = useRef<Element | null>(null);
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(
@@ -161,20 +98,6 @@ export const ProxyGroups = memo((props: Props) => {
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(
async (group: IProxyGroupItem, proxy: IProxyItem) => {
@@ -288,18 +211,6 @@ export const ProxyGroups = memo((props: Props) => {
)}
/>
<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>
);
});

View File

@@ -66,7 +66,7 @@ export const ProxyItemMini = (props: Props) => {
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"
>
<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>
{showType && (

View File

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

View File

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

View File

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

View File

@@ -127,7 +127,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
size="sm"
onClick={() =>
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 { useLockFn } from "ahooks";
import { mutate } from "swr";
@@ -56,6 +56,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {useProfiles} from "@/hooks/use-profiles";
const isWIN = getSystem() === "windows";
interface Props {
@@ -106,6 +107,12 @@ const SettingSystem = ({ onError }: Props) => {
const { verge, patchVerge, mutateVerge } = useVerge();
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 {
actualState: systemProxyActualState,
indicator: systemProxyIndicator,
@@ -261,7 +268,7 @@ const SettingSystem = ({ onError }: Props) => {
}}
onCatch={onError}
>
<Switch disabled={!isTunAvailable} />
<Switch disabled={!isTunAvailable || !hasProfiles} />
</GuardState>
</SettingRow>
@@ -297,7 +304,7 @@ const SettingSystem = ({ onError }: Props) => {
}}
onCatch={onError}
>
<Switch />
<Switch disabled={!hasProfiles} />
</GuardState>
</SettingRow>
@@ -396,7 +403,7 @@ const SettingSystem = ({ onError }: Props) => {
label={<LabelWithIcon icon={Fingerprint} text={t("Send HWID")} />}
>
<GuardState
value={verge?.enable_send_hwid ?? true} // По умолчанию включено
value={verge?.enable_send_hwid ?? true}
valueProps="checked"
onChangeProps="onCheckedChange"
onFormat={onSwitchFormat}
@@ -404,7 +411,7 @@ const SettingSystem = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ enable_send_hwid: e })}
onCatch={onError}
>
<Switch />
<Switch disabled={true} />
</GuardState>
</SettingRow>
</div>

View File

@@ -180,32 +180,12 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
extra={<TooltipIcon tooltip={t("LightWeight Mode Info")} />}
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
label={<LabelWithIcon icon={Info} text={t("Verge Version")} />}
>
<p className="text-sm font-medium pr-2 font-mono">v{version}</p>
</SettingRow>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 2 --- */}
</div>
</div>
);

View File

@@ -188,24 +188,22 @@ const SettingVergeBasic = ({ onError }: Props) => {
{OS !== "linux" && (
<SettingRow
label={
<LabelWithIcon
icon={MousePointerClick}
text={t("Tray Click Event")}
/>
}
label={
<LabelWithIcon
icon={MousePointerClick}
text={t("Tray Click Event")}
/>
}
>
<GuardState
value={tray_event ?? "main_window"}
onCatch={onError}
onFormat={(v) => v}
onChange={(e) => onChangeData({ tray_event: e })}
onGuard={(e) => patchVerge({ tray_event: e })}
value={tray_event ?? "main_window"}
onCatch={onError}
onFormat={(v) => v}
onChange={(e) => onChangeData({ tray_event: e })}
onGuard={(e) => patchVerge({ tray_event: e })}
onChangeProps="onValueChange"
>
<Select
onValueChange={(value) => onChangeData({ tray_event: value })}
value={tray_event}
>
<Select>
<SelectTrigger className="w-40 h-8">
<SelectValue />
</SelectTrigger>
@@ -227,41 +225,6 @@ const SettingVergeBasic = ({ onError }: Props) => {
</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
label={<LabelWithIcon icon={Home} text={t("Start Page")} />}
>
@@ -290,59 +253,10 @@ const SettingVergeBasic = ({ onError }: Props) => {
</GuardState>
</SettingRow>
<SettingRow
label={
<LabelWithIcon icon={FileTerminal} text={t("Startup Script")} />
}
>
<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*/}
{/* onClick={() => themeRef.current?.open()}*/}
{/* label={<LabelWithIcon icon={SwatchBook} text={t("Theme Setting")} />}*/}
{/*/>*/}
<SettingRow
onClick={() => layoutRef.current?.open()}
label={

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 "tw-animate-css";
@variant dark .dark &;
@theme {
--tailwind-darkMode: 'class';
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
@@ -125,3 +127,25 @@
/* 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"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clash Verge Rev Lite</title>
<title>Koala Clash</title>
</head>
<body>
<div id="root"></div>

View File

@@ -511,7 +511,7 @@
"Validate Merge File": "Validate Merge File",
"Validation Success": "Validation Success",
"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 saved": "DNS settings saved",
"DNS Overwrite": "DNS Overwrite",
@@ -645,6 +645,10 @@
"Attention Required": "Attention Required",
"Menu": "Menu",
"Add Profile": "Add Profile",
"Proxy enabled": "Proxy enabled",
"Proxy disabled": "Proxy disabled",
"Connecting...": "Connecting...",
"Disconnecting...": "Disconnecting...",
"Delete Profile": "Delete Profile {{name}}?",
"This action cannot be undone.": "This action cannot be undone.",
"Check Group Latency": "Check Group Latency",
@@ -665,5 +669,14 @@
"Send HWID": "Send HWID",
"New Version is available": "New Version is available",
"New Version": "New Version",
"New update": "New update"
"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": "Прокси",
"Proxy Groups": "Группы прокси",
"Proxy Provider": "Провайдер прокси",
"Proxy Count": "Число прокси",
"Update All": "Обновить все",
"Update At": "Обновлено в",
"rule": "правила",
"global": "глобальный",
"rule": "По правилам",
"global": "Глобально",
"direct": "прямой",
"script": "скриптовый",
"locate": "Местоположение",
@@ -156,6 +157,7 @@
"Edit File": "Изменить файл",
"Open File": "Открыть файл",
"Update": "Обновить",
"Update via proxy": "Обновить через прокси",
"Update(Proxy)": "Обновить (прокси)",
"Confirm deletion": "Подтвердите удаление",
"This operation is not reversible": "Эта операция необратима",
@@ -200,15 +202,19 @@
"Settings": "Настройки",
"System Setting": "Настройки системы",
"Tun Mode": "Режим TUN",
"TUN requires Service Mode": "Режим TUN требует установленную службу Clash Verge",
"TUN requires Service Mode": "Режим TUN требует установленную службу Koala Clash",
"Install Service": "Установить службу",
"Install Service failed": "Установка сервиса не удалась",
"Uninstall Service": "Удалить сервис",
"Restart Core failed": "Перезапуск ядра не удалась",
"Reset to Default": "Сбросить настройки",
"Tun Mode Info": "Режим Tun: захватывает весь системный трафик, при включении нет необходимости включать системный прокси-сервер.",
"TUN requires Service Mode or Admin Mode": "TUN режим требует Режима Службы или прав Администратора",
"System Proxy Enabled": "Системный прокси включен, ваши приложения будут получать доступ к сети через него",
"System Proxy Disabled": "Системный прокси отключен, большинству пользователей рекомендуется включить эту опцию",
"TUN Mode Enabled": "Режим TUN включен, приложения будут получать доступ к сети через виртуальную сетевую карту",
"TUN Mode Disabled": "Режим TUN отключен",
"TUN Mode Service Required": "Режим TUN требует установленную службу Clash Verge",
"TUN Mode Service Required": "Режим TUN требует установленную службу Koala Clash",
"TUN Mode Intercept Info": "Режим TUN может перехватить трафик всех приложений, подходит для приложений, которые не работают в режиме системного прокси.",
"Rule Mode Description": "Направляет трафик в соответствии с предустановленными правилами",
"Global Mode Description": "Направляет весь трафик через прокси-серверы",
@@ -255,8 +261,11 @@
"PAC Script Content": "Содержание сценария PAC",
"PAC URL": "Адрес PAC: ",
"Auto Launch": "Автозапуск",
"Administrator mode may not support auto launch": "Режим администратора может не поддерживать автоматический запуск",
"Silent Start": "Тихий запуск",
"Silent Start Info": "Запускать программу в фоновом режиме без отображения панели",
"Hover Jump Navigator": "Hover Jump Navigator",
"Hover Jump Navigator Info": "Автоматически переходить к соответствующей группе прокси при наведении курсора на буквы алфавита",
"TG Channel": "Telegram-канал",
"Manual": "Документация",
"Github Repo": "GitHub репозиторий",
@@ -321,7 +330,7 @@
"Success Color": "Цвет успеха",
"Font Family": "Семейство шрифтов",
"CSS Injection": "Внедрение CSS",
"Layout Setting": "Настройки раскладки",
"Layout Setting": "Настройки макета",
"Traffic Graph": "График трафика",
"Memory Usage": "Использование памяти",
"Memory Cleanup": "Нажмите, чтобы очистить память",
@@ -372,7 +381,7 @@
"Export Diagnostic Info": "Экспорт диагностической информации",
"Export Diagnostic Info For Issue Reporting": "Экспорт диагностической информации для отчета об ошибке",
"Exit": "Выход",
"Verge Version": "Версия Clash Verge Rev",
"Verge Version": "Версия Koala Clash",
"ReadOnly": "Только для чтения",
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
"Filter": "Фильтр",
@@ -383,6 +392,7 @@
"Profile Imported Successfully": "Профиль успешно импортирован",
"Profile Switched": "Профиль изменен",
"Profile Reactivated": "Профиль перезапущен",
"Profile switch interrupted by new selection": "Переключение профилей прервано новым выбором",
"Only YAML Files Supported": "Поддерживаются только файлы YAML",
"Settings Applied": "Настройки применены",
"Installing Service...": "Установка службы...",
@@ -390,6 +400,17 @@
"Service Uninstalled Successfully": "Служба успешно удалена",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Продолжительность работы прокси-демона не может быть меньше 1 секунды",
"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 изменен",
"Port Conflict": "Конфликт портов",
"Restart Application to Apply Modifications": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
@@ -399,6 +420,7 @@
"Clash Core Restarted": "Ядро перезапущено",
"GeoData Updated": "Файлы GeoData обновлены",
"Currently on the Latest Version": "Обновление не требуется",
"Already Using Latest Core": "Уже используется последняя версия ядра",
"Import Subscription Successful": "Подписка успешно импортирована",
"WebDAV Server URL": "URL-адрес сервера WebDAV http(s)://",
"Username": "Имя пользователя",
@@ -489,8 +511,9 @@
"Validate Merge File": "Проверить Merge File",
"Validation Success": "Файл успешно проверен",
"Validation Failed": "Проверка не удалась",
"Service Administrator Prompt": "Clash Verge требует прав администратора для переустановки системной службы",
"Service Administrator Prompt": "Koala Clash требует прав администратора для переустановки системной службы",
"DNS Settings": "Настройки DNS",
"DNS settings saved": "Настройки DNS сохранены",
"DNS Overwrite": "Переопределение настроек DNS",
"DNS Settings Warning": "Если вы не знакомы с этими настройками, пожалуйста, не изменяйте и не отключайте их",
"Enable DNS": "Включить DNS",
@@ -498,6 +521,7 @@
"Enhanced Mode": "Enhanced Mode",
"Fake IP Range": "Диапазон FakeIP",
"Fake IP Filter Mode": "FakeIP Filter Mode",
"Enable IPv6 DNS resolution": "Включить разрешение DNS по IPv6",
"Prefer H3": "Предпочитать H3",
"DNS DOH使用HTTP/3": "DNS DOH использует http/3",
"Respect Rules": "Приоритизировать правила",
@@ -530,6 +554,9 @@
"IP CIDRs not using fallback servers": "Диапазоны IP-адресов, не использующие резервные серверы, разделенные запятой",
"Fallback Domain": "Fallback домены",
"Domains using fallback servers": "Домены, использующие резервные серверы, разделенные запятой",
"Hosts Settings": "Настройки хостов",
"Hosts": "Хосты",
"Custom domain to IP or domain mapping": "Настраиваемое сопоставление домена с IP-адресом или доменом",
"Enable Alpha Channel": "Включить альфа-канал",
"Alpha versions may contain experimental features and bugs": "Альфа-версии могут содержать экспериментальные функции и ошибки",
"Home Settings": "Настройки главной страницы",
@@ -553,9 +580,24 @@
"OS Info": "Версия ОС",
"Running Mode": "Режим работы",
"Sidecar Mode": "Пользовательский режим",
"Administrator Mode": "Режим администратора",
"Administrator + Service Mode": "Административный + сервисный режим",
"Last Check Update": "Последняя проверка обновлений",
"Click to import subscription": "Нажмите, чтобы импортировать подписку",
"Last Update failed": "Последнее обновление не удалось",
"Next Up": "Далее",
"No schedule": "Нет расписания",
"Unknown": "Неизвестно",
"Auto update disabled": "Автоматическое обновление отключено",
"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": "Текущий сервер",
"No active proxy node": "Нет активного прокси-узла",
"Network Settings": "Настройки сети",
@@ -582,27 +624,37 @@
"No (IP Banned By Disney+)": "Нет (IP забанен Disney+)",
"Unsupported Country/Region": "Страна/регион не поддерживается",
"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": "Подключено",
"Disconnected": "Отключено",
"Attention Required": "Требуется внимание",
"TUN requires Service Mode or Admin Mode": "TUN режим требует Режима Службы или прав Администратора",
"Menu": "Меню",
"Add Profile": "Добавить профиль",
"Proxy enabled": "Прокси включено",
"Proxy disabled": "Прокси выключено",
"Connecting...": "Подключение...",
"Disconnecting...": "Отключение...",
"Add Profile": "Добавить профиль",
"Delete Profile": "Удалить профиль {{name}}?",
"This action cannot be undone.": "Это действие не может быть отменено",
"Update via proxy": "Обновить через прокси",
"Check Group Latency": "Проверка задержки в группе",
"Locate Current Proxy": "Найти текущий прокси",
"Show Basic 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-адрес тестирования задержки",
"Filter by Name": "Фильтр по имени",
"Expires in": "Истекает через {{duration}}",
@@ -617,5 +669,14 @@
"Send HWID": "Отправлять HWID",
"New Version is available": "Доступна новая версия",
"New Version": "Новая версия",
"New update": "Доступно обновление"
"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

@@ -21,9 +21,12 @@ import { useClashInfo } from "@/hooks/use-clash";
import { initGlobalLogService } from "@/services/global-log-service";
import { invoke } from "@tauri-apps/api/core";
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();
export let portableFlag = false;
@@ -32,25 +35,30 @@ dayjs.extend(relativeTime);
const OS = getSystem();
// 通知处理函数
// Notification Handler
const handleNoticeMessage = (
status: string,
msg: string,
t: (key: string) => string,
navigate: (path: string, options?: any) => void,
) => {
console.log("[通知监听 V2] 收到消息:", status, msg);
console.log("[Notification Listener V2] Receiving a message:", status, msg);
switch (status) {
case "import_sub_url::ok":
mutate("getProfiles");
navigate("/", { state: { activateProfile: msg } });
navigate("/");
showNotice("success", t("Import Subscription Successful"));
window.dispatchEvent(new CustomEvent('activate-profile', { detail: msg }));
sessionStorage.setItem('activateProfile', msg);
break;
case "import_sub_url::error":
showNotice("error", msg);
break;
console.log(msg);
if (msg.toLowerCase().includes('device') || msg.toLowerCase().includes('устройств')) {
window.dispatchEvent(new CustomEvent('show-hwid-error', { detail: msg }));
} else {
showNotice("error", msg);
}
break;
case "set_config::error":
showNotice("error", msg);
break;
@@ -136,13 +144,14 @@ const handleNoticeMessage = (
showNotice("error", `${t("Failed to Change Core")}: ${msg}`);
break;
default: // Optional: Log unhandled statuses
console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
console.warn(`[Notification Listener V2] Unprocessed state: ${status}`);
break;
}
};
const Layout = () => {
const mode = useThemeMode();
useZoomControls();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
useCustomTheme();
@@ -163,14 +172,14 @@ const Layout = () => {
try {
handleNoticeMessage(status, msg, t, navigate);
} catch (error) {
console.error("[Layout] 处理通知消息失败:", error);
console.error("[Layout] Failure to process a notification message:", error);
}
}, 0);
},
[t, navigate],
);
// 初始化全局日志服务
// Initialize the global logging service
useEffect(() => {
if (clashInfo) {
const { server = "", secret = "" } = clashInfo;
@@ -178,7 +187,7 @@ const Layout = () => {
}
}, [clashInfo, enableLog]);
// 设置监听器
// Setting up a listener
useEffect(() => {
const listeners = [
addListener("verge://refresh-clash-config", async () => {
@@ -224,11 +233,11 @@ const Layout = () => {
try {
unlisten();
} catch (error) {
console.error("[Layout] 清理事件监听器失败:", error);
console.error("[Layout] Failed to clear event listener:", error);
}
})
.catch((error) => {
console.error("[Layout] 获取unlisten函数失败:", error);
console.error("[Layout] Failed to get unlisten function:", error);
});
}
});
@@ -238,11 +247,11 @@ const Layout = () => {
try {
cleanup();
} catch (error) {
console.error("[Layout] 清理窗口监听器失败:", error);
console.error("[Layout] Failed to clear window listener:", error);
}
})
.catch((error) => {
console.error("[Layout] 获取cleanup函数失败:", error);
console.error("[Layout] Failed to get cleanup function:", error);
});
}, 0);
};
@@ -250,10 +259,10 @@ const Layout = () => {
useEffect(() => {
if (initRef.current) {
console.log("[Layout] 初始化代码已执行过,跳过");
console.log("[Layout] Initialization code has already been executed, skip");
return;
}
console.log("[Layout] 开始执行初始化代码");
console.log("[Layout] Begin executing initialization code");
initRef.current = true;
let isInitialized = false;
@@ -263,27 +272,27 @@ const Layout = () => {
const notifyBackend = async (action: string, stage?: string) => {
try {
if (stage) {
console.log(`[Layout] 通知后端 ${action}: ${stage}`);
console.log(`[Layout] Notification Backend ${action}: ${stage}`);
await invoke("update_ui_stage", { stage });
} else {
console.log(`[Layout] 通知后端 ${action}`);
console.log(`[Layout] Notification Backend ${action}`);
await invoke("notify_ui_ready");
}
} catch (err) {
console.error(`[Layout] 通知失败 ${action}:`, err);
console.error(`[Layout] Notification failure ${action}:`, err);
}
};
const removeLoadingOverlay = () => {
const initialOverlay = document.getElementById("initial-loading-overlay");
if (initialOverlay) {
console.log("[Layout] 移除加载指示器");
console.log("[Layout] Remove loading indicator");
initialOverlay.style.opacity = "0";
setTimeout(() => {
try {
initialOverlay.remove();
} catch (e) {
console.log("[Layout] 加载指示器已被移除");
console.log("[Layout] Load indicator has been removed");
}
}, 300);
}
@@ -291,23 +300,23 @@ const Layout = () => {
const performInitialization = async () => {
if (isInitialized) {
console.log("[Layout] 已经初始化过,跳过");
console.log("[Layout] Already initialized, skip");
return;
}
initializationAttempts++;
console.log(`[Layout] 开始第 ${initializationAttempts} 次初始化尝试`);
console.log(`[Layout] Start ${initializationAttempts} for the first time`);
try {
removeLoadingOverlay();
await notifyBackend("加载阶段", "Loading");
await notifyBackend("Loading phase", "Loading");
await new Promise<void>((resolve) => {
const checkReactMount = () => {
const rootElement = document.getElementById("root");
if (rootElement && rootElement.children.length > 0) {
console.log("[Layout] React组件已挂载");
console.log("[Layout] React components are mounted");
resolve();
} else {
setTimeout(checkReactMount, 50);
@@ -317,43 +326,43 @@ const Layout = () => {
checkReactMount();
setTimeout(() => {
console.log("[Layout] React组件挂载检查超时,继续执行");
console.log("[Layout] React components mount check timeout, continue execution");
resolve();
}, 2000);
});
await notifyBackend("DOM就绪", "DomReady");
await notifyBackend("DOM ready", "DomReady");
await new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve());
});
await notifyBackend("资源加载完成", "ResourcesLoaded");
await notifyBackend("Resource loading completed", "ResourcesLoaded");
await notifyBackend("UI就绪");
await notifyBackend("UI ready");
isInitialized = true;
console.log(`[Layout] ${initializationAttempts} 次初始化完成`);
console.log(`[Layout] The ${initializationAttempts} initialization is complete`);
} catch (error) {
console.error(
`[Layout] ${initializationAttempts} 次初始化失败:`,
`[Layout] Initialization failure at ${initializationAttempts}:`,
error,
);
if (initializationAttempts < maxAttempts) {
console.log(
`[Layout] 将在500ms后进行第 ${initializationAttempts + 1} 次重试`,
`[Layout] The first ${initializationAttempts + 1} retry will be made after 500ms`,
);
setTimeout(performInitialization, 500);
} else {
console.error("[Layout] 所有初始化尝试都失败,执行紧急初始化");
console.error("[Layout] All initialization attempts fail, perform emergency initialization");
removeLoadingOverlay();
try {
await notifyBackend("UI就绪");
await notifyBackend("UI ready");
isInitialized = true;
} catch (e) {
console.error("[Layout] 紧急初始化也失败:", e);
console.error("[Layout] Emergency initialization also failed:", e);
}
}
}
@@ -363,39 +372,39 @@ const Layout = () => {
const setupEventListener = async () => {
try {
console.log("[Layout] 开始监听启动完成事件");
console.log("[Layout] Start listening for startup completion events");
const unlisten = await listen("verge://startup-completed", () => {
if (!hasEventTriggered) {
console.log("[Layout] 收到启动完成事件,开始初始化");
console.log("[Layout] Receive startup completion event, start initialization");
hasEventTriggered = true;
performInitialization();
}
});
return unlisten;
} catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err);
console.error("[Layout] Failed to listen for startup completion event:", err);
return () => {};
}
};
const checkImmediateInitialization = async () => {
try {
console.log("[Layout] 检查后端是否已就绪");
console.log("[Layout] Check if the backend is ready");
await invoke("update_ui_stage", { stage: "Loading" });
if (!hasEventTriggered && !isInitialized) {
console.log("[Layout] 后端已就绪,立即开始初始化");
console.log("[Layout] Backend is ready, start initialization immediately");
hasEventTriggered = true;
performInitialization();
}
} catch (err) {
console.log("[Layout] 后端尚未就绪,等待启动完成事件");
console.log("[Layout] Backend not yet ready, waiting for startup completion event");
}
};
const backupInitialization = setTimeout(() => {
if (!hasEventTriggered && !isInitialized) {
console.warn("[Layout] 备用初始化触发1.5秒内未开始初始化");
console.warn("[Layout] Standby initialization trigger: initialization not started within 1.5 seconds");
hasEventTriggered = true;
performInitialization();
}
@@ -403,9 +412,9 @@ const Layout = () => {
const emergencyInitialization = setTimeout(() => {
if (!isInitialized) {
console.error("[Layout] 紧急初始化触发5秒内未完成初始化");
console.error("[Layout] Emergency initialization trigger: initialization not completed within 5 seconds");
removeLoadingOverlay();
notifyBackend("UI就绪").catch(() => {});
notifyBackend("UI ready").catch(() => {});
isInitialized = true;
}
}, 5000);
@@ -421,10 +430,10 @@ const Layout = () => {
};
}, []);
// 语言和起始页设置
// Language and start page settings
useEffect(() => {
if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language);
dayjs.locale(language === "ru" ? "ru-ru" : language);
i18next.changeLanguage(language);
}
}, [language]);
@@ -454,6 +463,7 @@ const Layout = () => {
{routersEles && React.cloneElement(routersEles, { key: location.pathname })}
</div>
</main>
<HwidErrorDialog />
</>
);
};
@@ -462,9 +472,9 @@ const Layout = () => {
return (
<SWRConfig value={{ errorRetryCount: 3 }}>
<NoticeManager />
<SidebarProvider defaultOpen={false}>
<AppLayout />
<Toaster />
</SidebarProvider>
</SWRConfig>
);

View File

@@ -25,7 +25,7 @@ import {
AlertTriangle,
Loader2,
Globe,
Send, ExternalLink, RefreshCw,
Send, ExternalLink, RefreshCw, ArrowDown, ArrowUp,
} from "lucide-react";
import { useVerge } from "@/hooks/use-verge";
import { useSystemState } from "@/hooks/use-system-state";
@@ -37,6 +37,11 @@ import { closeAllConnections } from "@/services/api";
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 { t } = useTranslation();
@@ -46,6 +51,7 @@ const MinimalHomePage: React.FC = () => {
useProfiles();
const viewerRef = useRef<ProfileViewerRef>(null);
const [uidToActivate, setUidToActivate] = useState<string | null>(null);
const { connections } = useAppData();
const profileItems = useMemo(() => {
const items =
@@ -57,7 +63,6 @@ const MinimalHomePage: React.FC = () => {
const currentProfile = useMemo(() => {
return profileItems.find(p => p.uid === profiles?.current);
}, [profileItems, profiles?.current]);
console.log("Current profile", currentProfile);
const currentProfileName = currentProfile?.name || profiles?.current;
const activateProfile = useCallback(
@@ -78,26 +83,13 @@ const MinimalHomePage: React.FC = () => {
);
useEffect(() => {
const handleActivationEvent = (event: Event) => {
const customEvent = event as CustomEvent<string>;
const profileId = customEvent.detail;
if (profileId) {
setUidToActivate(profileId);
}
};
window.addEventListener('activate-profile', handleActivationEvent);
return () => {
window.removeEventListener('activate-profile', handleActivationEvent);
};
}, []);
useEffect(() => {
const uidToActivate = sessionStorage.getItem('activateProfile');
if (uidToActivate && profileItems.some(p => p.uid === uidToActivate)) {
activateProfile(uidToActivate, false);
setUidToActivate(null);
sessionStorage.removeItem('activateProfile');
}
}, [uidToActivate, profileItems, activateProfile]);
}, [profileItems, activateProfile]);
const handleProfileChange = useLockFn(async (uid: string) => {
if (profiles?.current === uid) return;
@@ -157,7 +149,7 @@ const MinimalHomePage: React.FC = () => {
try {
await updateProfile(currentProfile.uid);
toast.success(t("Profile Updated Successfully"));
mutateProfiles(); // Обновляем данные в UI
mutateProfiles();
} catch (err: any) {
toast.error(t("Failed to update profile"), { description: err.message });
} finally {
@@ -165,8 +157,48 @@ const MinimalHomePage: React.FC = () => {
}
});
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 (
<div className="h-full w-full flex flex-col">
<div className="absolute inset-0 opacity-20 pointer-events-none z-0 [transform:translateZ(0)]">
<img
src={map}
alt="World map"
className="w-full h-full object-cover"
/>
</div>
{isProxyEnabled && (
<div
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"
style={{
background: 'radial-gradient(circle, rgba(34,197,94,0.3) 0%, transparent 70%)',
filter: 'blur(100px)',
}}
/>
)}
<header className="flex-shrink-0 p-5 grid grid-cols-3 items-center z-10">
<div className="flex justify-start">
<SidebarTrigger />
@@ -240,38 +272,50 @@ const MinimalHomePage: React.FC = () => {
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"
title={currentProfile.announce_url}
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}</span>
<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">
<p className="text-base font-semibold text-foreground whitespace-pre-wrap">
{currentProfile.announce}
</p>
)}
</div>
)}
<div className="text-center">
<h1
className="text-4xl mb-2 font-semibold"
style={{ color: isProxyEnabled ? "#22c55e" : "#ef4444" }}
>
{isProxyEnabled ? t("Connected") : t("Disconnected")}
</h1>
<p className="h-6 text-sm text-muted-foreground transition-opacity duration-300">
{isToggling &&
(isProxyEnabled ? t("Disconnecting...") : t("Connecting..."))}
</p>
<div className="relative text-center">
<h1
className={cn(
"text-4xl mb-2 font-semibold transition-colors duration-300",
statusInfo.isAnimating && "animate-pulse"
)}
style={{ color: statusInfo.color }}
>
{statusInfo.text}
</h1>
{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 className="scale-[7] my-16">
<Switch
disabled={showTunAlert || isToggling}
checked={!!isProxyEnabled}
onCheckedChange={handleToggleProxy}
aria-label={t("Toggle Proxy")}
<div className="relative -translate-y-6">
<PowerButton
loading={isToggling}
checked={!!isProxyEnabled}
onClick={handleToggleProxy}
disabled={showTunAlert || isToggling || profileItems.length === 0}
aria-label={t("Toggle Proxy")}
/>
</div>
@@ -300,7 +344,7 @@ const MinimalHomePage: React.FC = () => {
</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 ? (
<ProxySelectors />
) : (

View File

@@ -23,7 +23,7 @@ const ProxyPage = () => {
);
const { verge } = useVerge();
const modeList = ["rule", "global", "direct"];
const modeList = ["rule", "global"];
const curMode = clashConfig?.mode?.toLowerCase();
const onChangeMode = useLockFn(async (mode: string) => {
@@ -58,7 +58,7 @@ const ProxyPage = () => {
variant={mode === curMode ? "default" : "ghost"}
size="sm"
onClick={() => onChangeMode(mode)}
className="capitalize px-3 py-1 h-auto"
className="px-3 py-1 h-auto"
>
{t(mode)}
</Button>

View File

@@ -400,3 +400,7 @@ export const isAdmin = async () => {
export async function getNextUpdateTime(uid: string) {
return invoke<number | null>("get_next_update_time", { uid });
}
export async function createProfileFromShareLink(link: string, templateName: string) {
return invoke<void>("create_profile_from_share_link", { link, templateName });
}

View File

@@ -1,81 +1,25 @@
import { ReactNode } from "react";
import { toast } from "sonner";
export interface NoticeItem {
id: number;
type: "success" | "error" | "info";
message: ReactNode;
duration: number;
timerId?: ReturnType<typeof setTimeout>;
}
type NoticeType = 'success' | 'error' | 'info' | 'warning';
type Listener = (notices: NoticeItem[]) => void;
export const showNotice = (type: NoticeType, message: string, duration?: number) => {
const options = duration ? { duration } : {};
let nextId = 0;
let notices: NoticeItem[] = [];
const listeners: Set<Listener> = new Set();
function notifyListeners() {
listeners.forEach((listener) => listener([...notices])); // Pass a copy
}
// Shows a notification.
export function showNotice(
type: "success" | "error" | "info",
message: ReactNode,
duration?: number,
): number {
const id = nextId++;
const effectiveDuration =
duration ?? (type === "error" ? 8000 : type === "info" ? 5000 : 3000); // Longer defaults
const newNotice: NoticeItem = {
id,
type,
message,
duration: effectiveDuration,
};
// Auto-hide timer (only if duration is not null/0)
if (effectiveDuration > 0) {
newNotice.timerId = setTimeout(() => {
hideNotice(id);
}, effectiveDuration);
switch (type) {
case 'success':
toast.success(message, options);
break;
case 'error':
toast.error(message, options);
break;
case 'info':
toast.info(message, options);
break;
case 'warning':
toast.warning(message, options);
break;
default:
toast(message, options);
break;
}
notices = [...notices, newNotice];
notifyListeners();
return id;
}
// Hides a specific notification by its ID.
export function hideNotice(id: number) {
const notice = notices.find((n) => n.id === id);
if (notice?.timerId) {
clearTimeout(notice.timerId); // Clear timeout if manually closed
}
notices = notices.filter((n) => n.id !== id);
notifyListeners();
}
// Subscribes a listener function to notice state changes.
export function subscribeNotices(listener: () => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
export function getSnapshotNotices() {
return notices;
}
// Function to clear all notices at once
export function clearAllNotices() {
notices.forEach((n) => {
if (n.timerId) clearTimeout(n.timerId);
});
notices = [];
notifyListeners();
}
};