24 Commits

Author SHA1 Message Date
coolcoala
53de1bc8b0 v0.2.4 2025-07-30 09:12:05 +03:00
coolcoala
77eacd3ab3 fixed icons 2025-07-30 09:12:05 +03:00
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
49 changed files with 1078 additions and 495 deletions

View File

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

View File

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

2
src-tauri/Cargo.lock generated
View File

@@ -1061,7 +1061,7 @@ dependencies = [
[[package]] [[package]]
name = "clash-verge" name = "clash-verge"
version = "0.2.3" version = "0.2.4"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"aes-gcm", "aes-gcm",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "0.2.3" version = "0.2.4"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"] authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
license = "GPL-3.0-only" license = "GPL-3.0-only"

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -9,6 +9,11 @@ use crate::{
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration; use std::time::Duration;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use std::collections::BTreeMap;
use url::Url;
use serde_yaml::Value;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use percent_encoding::percent_decode_str;
// 全局互斥锁防止并发配置更新 // 全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(()); static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
@@ -708,3 +713,467 @@ pub async fn update_profiles_on_startup() -> CmdResult {
Ok(()) 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

@@ -417,6 +417,13 @@ impl PrfItem {
None => None, None => None,
}; };
if let Some(announce_msg) = &announce {
let lower_msg = announce_msg.to_lowercase();
if lower_msg.contains("device") || lower_msg.contains("устройств") {
bail!(announce_msg.clone());
}
}
let announce_url = match header.get("announce-url") { let announce_url = match header.get("announce-url") {
Some(value) => { Some(value) => {
let str_value = value.to_str().unwrap_or(""); let str_value = value.to_str().unwrap_or("");

View File

@@ -304,6 +304,7 @@ pub fn run() {
cmd::save_profile_file, cmd::save_profile_file,
cmd::get_next_update_time, cmd::get_next_update_time,
cmd::update_profiles_on_startup, cmd::update_profiles_on_startup,
cmd::create_profile_from_share_link,
// script validation // script validation
cmd::script_validate_notice, cmd::script_validate_notice,
cmd::validate_script_file, cmd::validate_script_file,
@@ -352,7 +353,7 @@ pub fn run() {
.get_webview_window("main") .get_webview_window("main")
{ {
logging!(info, Type::Window, true, "设置macOS窗口标题"); logging!(info, Type::Window, true, "设置macOS窗口标题");
let _ = window.set_title("Clash Verge Rev Lite"); let _ = window.set_title("Koala Clash");
} }
} }
} }

View File

@@ -335,12 +335,12 @@ pub fn create_window(is_show: bool) -> bool {
"main", /* the unique window label */ "main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()), tauri::WebviewUrl::App("index.html".into()),
) )
.title("Clash Verge Rev Lite") .title("Koala Clash")
.center() .center()
.decorations(true) .decorations(true)
.fullscreen(false) .fullscreen(false)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64) .inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
.min_inner_size(520.0, 520.0) .min_inner_size(1000.0, 800.0)
.visible(true) // 立即显示窗口,避免用户等待 .visible(true) // 立即显示窗口,避免用户等待
.initialization_script( .initialization_script(
r#" r#"

View File

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

View File

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

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: 576 KiB

View File

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

View File

@@ -40,10 +40,20 @@ export function AppSidebar() {
return ( return (
<Sidebar variant="floating" collapsible="icon"> <Sidebar variant="floating" collapsible="icon">
<SidebarHeader> <SidebarHeader>
<SidebarMenuButton> <SidebarMenuButton size="lg"
<EarthLock/> className={cn(
<span className="font-semibold group-data-[state=collapsed]:hidden"> "flex h-12 items-center transition-all duration-200",
Clash Koala "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="./assets/image/logo.png"
alt="logo"
className="h-6 w-6 flex-shrink-0"
/>
<span className="font-semibold whitespace-nowrap group-data-[state=collapsed]:hidden">
Koala Clash
</span> </span>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarHeader> </SidebarHeader>

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertTriangle } from "lucide-react";
export const HwidErrorDialog = () => {
const { t } = useTranslation();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
useEffect(() => {
const handleShowHwidError = (event: Event) => {
const customEvent = event as CustomEvent<string>;
setErrorMessage(customEvent.detail);
};
window.addEventListener('show-hwid-error', handleShowHwidError);
return () => {
window.removeEventListener('show-hwid-error', handleShowHwidError);
};
}, []);
if (!errorMessage) {
return null;
}
return (
<Dialog open={!!errorMessage} onOpenChange={() => setErrorMessage(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" />
{t("Device Limit Reached")}
</DialogTitle>
<DialogDescription className="pt-4 text-left">
{errorMessage}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setErrorMessage(null)}>{t("OK")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,19 +143,6 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
</DialogHeader> </DialogHeader>
<div className="py-4 space-y-1"> <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>
<SettingRow label={t("Memory Usage")}> <SettingRow label={t("Memory Usage")}>
<GuardState <GuardState
value={verge?.enable_memory_usage ?? true} value={verge?.enable_memory_usage ?? true}
@@ -197,134 +184,6 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
<Switch /> <Switch />
</GuardState> </GuardState>
</SettingRow> </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>
{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")}
</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> </div>
<DialogFooter> <DialogFooter>

View File

@@ -127,7 +127,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
size="sm" size="sm"
onClick={() => onClick={() =>
openUrl( openUrl(
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`, `https://github.com/coolcoala/clash-verge-rev-lite/releases/tag/v${updateInfo?.version}`,
) )
} }
> >

View File

@@ -396,7 +396,7 @@ const SettingSystem = ({ onError }: Props) => {
label={<LabelWithIcon icon={Fingerprint} text={t("Send HWID")} />} label={<LabelWithIcon icon={Fingerprint} text={t("Send HWID")} />}
> >
<GuardState <GuardState
value={verge?.enable_send_hwid ?? true} // По умолчанию включено value={verge?.enable_send_hwid ?? true}
valueProps="checked" valueProps="checked"
onChangeProps="onCheckedChange" onChangeProps="onCheckedChange"
onFormat={onSwitchFormat} onFormat={onSwitchFormat}
@@ -404,7 +404,7 @@ const SettingSystem = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ enable_send_hwid: e })} onGuard={(e) => patchVerge({ enable_send_hwid: e })}
onCatch={onError} onCatch={onError}
> >
<Switch /> <Switch disabled={true} />
</GuardState> </GuardState>
</SettingRow> </SettingRow>
</div> </div>

View File

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

View File

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

View File

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

@@ -125,3 +125,7 @@
/* h-full уже применен выше к body */ /* h-full уже применен выше к body */
} }
} }
svg {
stroke-width: var(--icon-stroke-width, 2);
}

View File

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

View File

@@ -511,7 +511,7 @@
"Validate Merge File": "Validate Merge File", "Validate Merge File": "Validate Merge File",
"Validation Success": "Validation Success", "Validation Success": "Validation Success",
"Validation Failed": "Validation Failed", "Validation Failed": "Validation Failed",
"Service Administrator Prompt": "Clash Verge requires administrator privileges to reinstall the system service", "Service Administrator Prompt": "Koala Clash requires administrator privileges to reinstall the system service",
"DNS Settings": "DNS Settings", "DNS Settings": "DNS Settings",
"DNS settings saved": "DNS settings saved", "DNS settings saved": "DNS settings saved",
"DNS Overwrite": "DNS Overwrite", "DNS Overwrite": "DNS Overwrite",
@@ -645,6 +645,10 @@
"Attention Required": "Attention Required", "Attention Required": "Attention Required",
"Menu": "Menu", "Menu": "Menu",
"Add Profile": "Add Profile", "Add Profile": "Add Profile",
"Proxy enabled": "Proxy enabled",
"Proxy disabled": "Proxy disabled",
"Connecting...": "Connecting...",
"Disconnecting...": "Disconnecting...",
"Delete Profile": "Delete Profile {{name}}?", "Delete Profile": "Delete Profile {{name}}?",
"This action cannot be undone.": "This action cannot be undone.", "This action cannot be undone.": "This action cannot be undone.",
"Check Group Latency": "Check Group Latency", "Check Group Latency": "Check Group Latency",
@@ -665,5 +669,11 @@
"Send HWID": "Send HWID", "Send HWID": "Send HWID",
"New Version is available": "New Version is available", "New Version is available": "New Version is available",
"New Version": "New Version", "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"
} }

View File

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

View File

@@ -24,6 +24,9 @@ import { showNotice } from "@/services/noticeService";
import { NoticeManager } from "@/components/base/NoticeManager"; import { NoticeManager } from "@/components/base/NoticeManager";
import { SidebarProvider, useSidebar } from "@/components/ui/sidebar"; import { SidebarProvider, useSidebar } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/layout/sidebar"; import { AppSidebar } from "@/components/layout/sidebar";
import { useZoomControls } from "@/hooks/useZoomControls";
import { HwidErrorDialog } from "@/components/profile/hwid-error-dialog";
const appWindow = getCurrentWebviewWindow(); const appWindow = getCurrentWebviewWindow();
export let portableFlag = false; export let portableFlag = false;
@@ -49,8 +52,13 @@ const handleNoticeMessage = (
sessionStorage.setItem('activateProfile', msg); sessionStorage.setItem('activateProfile', msg);
break; break;
case "import_sub_url::error": case "import_sub_url::error":
showNotice("error", msg); console.log(msg);
break; 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": case "set_config::error":
showNotice("error", msg); showNotice("error", msg);
break; break;
@@ -143,6 +151,7 @@ const handleNoticeMessage = (
const Layout = () => { const Layout = () => {
const mode = useThemeMode(); const mode = useThemeMode();
useZoomControls();
const isDark = mode === "light" ? false : true; const isDark = mode === "light" ? false : true;
const { t } = useTranslation(); const { t } = useTranslation();
useCustomTheme(); useCustomTheme();
@@ -454,6 +463,7 @@ const Layout = () => {
{routersEles && React.cloneElement(routersEles, { key: location.pathname })} {routersEles && React.cloneElement(routersEles, { key: location.pathname })}
</div> </div>
</main> </main>
<HwidErrorDialog />
</> </>
); );
}; };

View File

@@ -25,7 +25,7 @@ import {
AlertTriangle, AlertTriangle,
Loader2, Loader2,
Globe, Globe,
Send, ExternalLink, RefreshCw, Send, ExternalLink, RefreshCw, ArrowDown, ArrowUp,
} from "lucide-react"; } from "lucide-react";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useSystemState } from "@/hooks/use-system-state"; import { useSystemState } from "@/hooks/use-system-state";
@@ -37,6 +37,8 @@ import { closeAllConnections } from "@/services/api";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { updateProfile } from "@/services/cmds"; import { updateProfile } from "@/services/cmds";
import { SidebarTrigger } from "@/components/ui/sidebar"; import { SidebarTrigger } from "@/components/ui/sidebar";
import parseTraffic from "@/utils/parse-traffic";
import { useAppData } from "@/providers/app-data-provider";
const MinimalHomePage: React.FC = () => { const MinimalHomePage: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -46,6 +48,7 @@ const MinimalHomePage: React.FC = () => {
useProfiles(); useProfiles();
const viewerRef = useRef<ProfileViewerRef>(null); const viewerRef = useRef<ProfileViewerRef>(null);
const [uidToActivate, setUidToActivate] = useState<string | null>(null); const [uidToActivate, setUidToActivate] = useState<string | null>(null);
const { connections } = useAppData();
const profileItems = useMemo(() => { const profileItems = useMemo(() => {
const items = const items =
@@ -57,7 +60,6 @@ const MinimalHomePage: React.FC = () => {
const currentProfile = useMemo(() => { const currentProfile = useMemo(() => {
return profileItems.find(p => p.uid === profiles?.current); return profileItems.find(p => p.uid === profiles?.current);
}, [profileItems, profiles?.current]); }, [profileItems, profiles?.current]);
console.log("Current profile", currentProfile);
const currentProfileName = currentProfile?.name || profiles?.current; const currentProfileName = currentProfile?.name || profiles?.current;
const activateProfile = useCallback( const activateProfile = useCallback(
@@ -240,13 +242,26 @@ const MinimalHomePage: React.FC = () => {
)} )}
</div> </div>
)} )}
<div className="text-center"> <div className="relative text-center">
<h1 <h1
className="text-4xl mb-2 font-semibold" className="text-4xl mb-2 font-semibold"
style={{ color: isProxyEnabled ? "#22c55e" : "#ef4444" }} style={{ color: isProxyEnabled ? "#22c55e" : "#ef4444" }}
> >
{isProxyEnabled ? t("Connected") : t("Disconnected")} {isProxyEnabled ? t("Connected") : t("Disconnected")}
</h1> </h1>
{isProxyEnabled && (
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-48 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>
)}
<p className="h-6 text-sm text-muted-foreground transition-opacity duration-300"> <p className="h-6 text-sm text-muted-foreground transition-opacity duration-300">
{isToggling && {isToggling &&
(isProxyEnabled ? t("Disconnecting...") : t("Connecting..."))} (isProxyEnabled ? t("Disconnecting...") : t("Connecting..."))}
@@ -262,6 +277,7 @@ const MinimalHomePage: React.FC = () => {
/> />
</div> </div>
{showTunAlert && ( {showTunAlert && (
<div className="w-full max-w-sm"> <div className="w-full max-w-sm">
<Alert <Alert

View File

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

View File

@@ -400,3 +400,7 @@ export const isAdmin = async () => {
export async function getNextUpdateTime(uid: string) { export async function getNextUpdateTime(uid: string) {
return invoke<number | null>("get_next_update_time", { uid }); 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 });
}