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
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:
name: Release Build
needs: check_tag_version
needs: [check_tag_version, create_release_notes]
strategy:
fail-fast: false
matrix:
@@ -97,6 +179,7 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
@@ -104,11 +187,56 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: "Clash Verge Rev Lite v__VERSION__"
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Rename Artifact (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$version = ${{steps.build.outputs.appVersion}}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "_${version}_", "_"
Rename-Item $file.FullName $newName
}
- name: Rename Artifact (Linux/macOS)
if: runner.os == 'Linux' || runner.os == 'macOS'
shell: bash
run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then
exit 1
fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
dir_path=$(dirname "$old_path")
old_filename=$(basename "$old_path")
new_filename=$(echo "$old_filename" \
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
)
new_path="${dir_path}/${new_filename}"
if [ "$old_path" != "$new_path" ]; then
echo " - '$old_filename' -> '$new_filename'"
mv "$old_path" "$new_path"
fi
done
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{steps.build.outputs.appVersion}}
name: "Koala Clash v${{steps.build.outputs.appVersion}}"
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
release-for-linux-arm:
name: Release Build for Linux ARM
strategy:
@@ -220,11 +348,34 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
- name: Rename
shell: bash
run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then
exit 1
fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
dir_path=$(dirname "$old_path")
old_filename=$(basename "$old_path")
new_filename=$(echo "$old_filename" \
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
)
new_path="${dir_path}/${new_filename}"
if [ "$old_path" != "$new_path" ]; then
echo " - '$old_filename' -> '$new_filename'"
mv "$old_path" "$new_path"
fi
done
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev Lite v${{env.VERSION}}"
name: "Koala Clash v${{env.VERSION}}"
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
@@ -294,19 +445,19 @@ jobs:
run: |
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
Rename-Item $file.FullName $newName
}
@@ -314,7 +465,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: v${{steps.build.outputs.appVersion}}
name: "Clash Verge Rev Lite v${{steps.build.outputs.appVersion}}"
name: "Koala Clash v${{steps.build.outputs.appVersion}}"
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
@@ -374,9 +525,9 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
create_release_notes:
name: Create Release Notes
push-notify-to-telegram:
runs-on: ubuntu-latest
needs: [release-update, release-update-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -413,45 +564,28 @@ jobs:
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 }}/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
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.x86_64.rpm"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=fedora&label=RPM"> </a>
[Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_arm64.deb"><img src="https://img.shields.io/badge/arm64-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.aarch64.rpm"><img src="https://img.shields.io/badge/aarch64-default?style=flat&logo=fedora&label=RPM"> </a>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_armhf.deb"><img src="https://img.shields.io/badge/armhf-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite-${{ env.VERSION }}-1.armhfp.rpm"><img src="https://img.shields.io/badge/armhfp-default?style=flat&logo=fedora&label=RPM"> </a>
### Windows (Win7 is no longer supported)
#### Normal version (recommended)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_x64-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_arm64-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
#### Portable version is no longer available with many problems
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_x64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
- name: notify to channel
uses: appleboy/telegram-action@master
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev Lite v${{env.VERSION}}"
body_path: release.txt
token: ${{ secrets.GITHUB_TOKEN }}
to: ${{ secrets.TELEGRAM_TO_CHANNEL }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message_file: release.txt
- 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
- fixed problem with profile inactivation after adding via deeplink on windows

View File

@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "0.2.3",
"version": "0.2.4",
"license": "GPL-3.0-only",
"scripts": {
"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]]
name = "clash-verge"
version = "0.2.3"
version = "0.2.4"
dependencies = [
"ab_glyph",
"aes-gcm",

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 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::time::Duration;
use tokio::sync::{Mutex, RwLock};
use std::collections::BTreeMap;
use url::Url;
use serde_yaml::Value;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use percent_encoding::percent_decode_str;
// 全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
@@ -708,3 +713,467 @@ pub async fn update_profiles_on_startup() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn create_profile_from_share_link(link: String, template_name: String) -> CmdResult {
const DEFAULT_TEMPLATE: &str = r#"
mixed-port: 2080
allow-lan: true
tcp-concurrent: true
enable-process: true
find-process-mode: always
global-client-fingerprint: chrome
mode: rule
log-level: debug
ipv6: false
keep-alive-interval: 30
unified-delay: false
profile:
store-selected: true
store-fake-ip: true
sniffer:
enable: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
tun:
enable: true
stack: mixed
dns-hijack: ['any:53']
auto-route: true
auto-detect-interface: true
strict-route: true
dns:
enable: true
listen: :1053
prefer-h3: false
ipv6: false
enhanced-mode: fake-ip
fake-ip-filter: ['+.lan', '+.local']
nameserver: ['https://doh.dns.sb/dns-query']
proxies:
- name: myproxy
type: vless
server: YOURDOMAIN
port: 443
uuid: YOURUUID
network: tcp
flow: xtls-rprx-vision
udp: true
tls: true
reality-opts:
public-key: YOURPUBLIC
short-id: YOURSHORTID
servername: YOURREALITYDEST
client-fingerprint: chrome
proxy-groups:
- name: PROXY
type: select
proxies:
- myproxy
rule-providers:
ru-bundle:
type: http
behavior: domain
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/ru-bundle/rule.mrs
path: ./ru-bundle/rule.mrs
interval: 86400
refilter_domains:
type: http
behavior: domain
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/re-filter/domain-rule.mrs
path: ./re-filter/domain-rule.mrs
interval: 86400
refilter_ipsum:
type: http
behavior: ipcidr
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/re-filter/ip-rule.mrs
path: ./re-filter/ip-rule.mrs
interval: 86400
oisd_big:
type: http
behavior: domain
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/oisd/big.mrs
path: ./oisd/big.mrs
interval: 86400
rules:
- OR,((DOMAIN,ipwhois.app),(DOMAIN,ipwho.is),(DOMAIN,api.ip.sb),(DOMAIN,ipapi.co),(DOMAIN,ipinfo.io)),PROXY
- RULE-SET,oisd_big,REJECT
- PROCESS-NAME,Discord.exe,PROXY
- RULE-SET,ru-bundle,PROXY
- RULE-SET,refilter_domains,PROXY
- RULE-SET,refilter_ipsum,PROXY
- MATCH,DIRECT
"#;
const WITHOUT_RU_TEMPLATE: &str = r#"
mixed-port: 7890
allow-lan: true
tcp-concurrent: true
enable-process: true
find-process-mode: always
mode: rule
log-level: debug
ipv6: false
keep-alive-interval: 30
unified-delay: false
profile:
store-selected: true
store-fake-ip: true
sniffer:
enable: true
force-dns-mapping: true
parse-pure-ip: true
sniff:
HTTP:
ports:
- 80
- 8080-8880
override-destination: true
TLS:
ports:
- 443
- 8443
tun:
enable: true
stack: gvisor
auto-route: true
auto-detect-interface: false
dns-hijack:
- any:53
strict-route: true
mtu: 1500
dns:
enable: true
prefer-h3: true
use-hosts: true
use-system-hosts: true
listen: 127.0.0.1:6868
ipv6: false
enhanced-mode: redir-host
default-nameserver:
- tls://1.1.1.1
- tls://1.0.0.1
proxy-server-nameserver:
- tls://1.1.1.1
- tls://1.0.0.1
direct-nameserver:
- tls://77.88.8.8
nameserver:
- https://cloudflare-dns.com/dns-query
proxies:
- name: myproxy
type: vless
server: YOURDOMAIN
port: 443
uuid: YOURUUID
network: tcp
flow: xtls-rprx-vision
udp: true
tls: true
reality-opts:
public-key: YOURPUBLIC
short-id: YOURSHORTID
servername: YOURREALITYDEST
client-fingerprint: chrome
proxy-groups:
- name: PROXY
icon: https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Hijacking.png
type: select
proxies:
- ⚡️ Fastest
- 📶 First Available
- myproxy
- name: ⚡️ Fastest
icon: https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Auto.png
type: url-test
tolerance: 150
url: https://cp.cloudflare.com/generate_204
interval: 300
proxies:
- myproxy
- name: 📶 First Available
icon: https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Download.png
type: fallback
url: https://cp.cloudflare.com/generate_204
interval: 300
proxies:
- myproxy
rule-providers:
torrent-trackers:
type: http
behavior: domain
format: mrs
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/other/torrent-trackers.mrs
path: ./rule-sets/torrent-trackers.mrs
interval: 86400
torrent-clients:
type: http
behavior: classical
format: yaml
url: https://github.com/legiz-ru/mihomo-rule-sets/raw/main/other/torrent-clients.yaml
path: ./rule-sets/torrent-clients.yaml
interval: 86400
geosite-ru:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/category-ru.mrs
path: ./geosite-ru.mrs
interval: 86400
xiaomi:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/xiaomi.mrs
path: ./rule-sets/xiaomi.mrs
interval: 86400
blender:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/blender.mrs
path: ./rule-sets/blender.mrs
interval: 86400
drweb:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/drweb.mrs
path: ./rule-sets/drweb.mrs
interval: 86400
debian:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/debian.mrs
path: ./rule-sets/debian.mrs
interval: 86400
canonical:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/canonical.mrs
path: ./rule-sets/canonical.mrs
interval: 86400
python:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/python.mrs
path: ./rule-sets/python.mrs
interval: 86400
geoip-ru:
type: http
behavior: ipcidr
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geoip/ru.mrs
path: ./geoip-ru.mrs
interval: 86400
geosite-private:
type: http
behavior: domain
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/private.mrs
path: ./geosite-private.mrs
interval: 86400
geoip-private:
type: http
behavior: ipcidr
format: mrs
url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geoip/private.mrs
path: ./geoip-private.mrs
interval: 86400
rules:
- DOMAIN-SUFFIX,habr.com,PROXY
- DOMAIN-SUFFIX,kemono.su,PROXY
- DOMAIN-SUFFIX,jut.su,PROXY
- DOMAIN-SUFFIX,kara.su,PROXY
- DOMAIN-SUFFIX,theins.ru,PROXY
- DOMAIN-SUFFIX,tvrain.ru,PROXY
- DOMAIN-SUFFIX,echo.msk.ru,PROXY
- DOMAIN-SUFFIX,the-village.ru,PROXY
- DOMAIN-SUFFIX,snob.ru,PROXY
- DOMAIN-SUFFIX,novayagazeta.ru,PROXY
- DOMAIN-SUFFIX,moscowtimes.ru,PROXY
- DOMAIN-KEYWORD,animego,PROXY
- DOMAIN-KEYWORD,yummyanime,PROXY
- DOMAIN-KEYWORD,yummy-anime,PROXY
- DOMAIN-KEYWORD,animeportal,PROXY
- DOMAIN-KEYWORD,anime-portal,PROXY
- DOMAIN-KEYWORD,animedub,PROXY
- DOMAIN-KEYWORD,anidub,PROXY
- DOMAIN-KEYWORD,animelib,PROXY
- DOMAIN-KEYWORD,ikianime,PROXY
- DOMAIN-KEYWORD,anilibria,PROXY
- PROCESS-NAME,Discord.exe,PROXY
- PROCESS-NAME,discord,PROXY
- RULE-SET,geosite-private,DIRECT,no-resolve
- RULE-SET,geoip-private,DIRECT
- RULE-SET,torrent-clients,DIRECT
- RULE-SET,torrent-trackers,DIRECT
- DOMAIN-SUFFIX,.ru,DIRECT
- DOMAIN-SUFFIX,.su,DIRECT
- DOMAIN-SUFFIX,.ru.com,DIRECT
- DOMAIN-SUFFIX,.ru.net,DIRECT
- DOMAIN-SUFFIX,wikipedia.org,DIRECT
- DOMAIN-SUFFIX,kudago.com,DIRECT
- DOMAIN-SUFFIX,kinescope.io,DIRECT
- DOMAIN-SUFFIX,redheadsound.studio,DIRECT
- DOMAIN-SUFFIX,plplayer.online,DIRECT
- DOMAIN-SUFFIX,lomont.site,DIRECT
- DOMAIN-SUFFIX,remanga.org,DIRECT
- DOMAIN-SUFFIX,shopstory.live,DIRECT
- DOMAIN-KEYWORD,miradres,DIRECT
- DOMAIN-KEYWORD,premier,DIRECT
- DOMAIN-KEYWORD,shutterstock,DIRECT
- DOMAIN-KEYWORD,2gis,DIRECT
- DOMAIN-KEYWORD,diginetica,DIRECT
- DOMAIN-KEYWORD,kinescopecdn,DIRECT
- DOMAIN-KEYWORD,researchgate,DIRECT
- DOMAIN-KEYWORD,springer,DIRECT
- DOMAIN-KEYWORD,nextcloud,DIRECT
- DOMAIN-KEYWORD,wiki,DIRECT
- DOMAIN-KEYWORD,kaspersky,DIRECT
- DOMAIN-KEYWORD,stepik,DIRECT
- DOMAIN-KEYWORD,likee,DIRECT
- DOMAIN-KEYWORD,snapchat,DIRECT
- DOMAIN-KEYWORD,yappy,DIRECT
- DOMAIN-KEYWORD,pikabu,DIRECT
- DOMAIN-KEYWORD,okko,DIRECT
- DOMAIN-KEYWORD,wink,DIRECT
- DOMAIN-KEYWORD,kion,DIRECT
- DOMAIN-KEYWORD,roblox,DIRECT
- DOMAIN-KEYWORD,ozon,DIRECT
- DOMAIN-KEYWORD,wildberries,DIRECT
- DOMAIN-KEYWORD,aliexpress,DIRECT
- RULE-SET,geosite-ru,DIRECT
- RULE-SET,xiaomi,DIRECT
- RULE-SET,blender,DIRECT
- RULE-SET,drweb,DIRECT
- RULE-SET,debian,DIRECT
- RULE-SET,canonical,DIRECT
- RULE-SET,python,DIRECT
- RULE-SET,geoip-ru,DIRECT
- MATCH,PROXY
"#;
let template_yaml = match template_name.as_str() {
"without_ru" => WITHOUT_RU_TEMPLATE,
_ => DEFAULT_TEMPLATE,
};
let parsed_url = Url::parse(&link).map_err(|e| e.to_string())?;
let scheme = parsed_url.scheme();
let proxy_name = parsed_url.fragment()
.map(|f| percent_decode_str(f).decode_utf8_lossy().to_string())
.unwrap_or_else(|| "Proxy from Link".to_string());
let mut proxy_map: BTreeMap<String, Value> = BTreeMap::new();
proxy_map.insert("name".into(), proxy_name.clone().into());
proxy_map.insert("type".into(), scheme.into());
proxy_map.insert("server".into(), parsed_url.host_str().unwrap_or_default().into());
proxy_map.insert("port".into(), parsed_url.port().unwrap_or(443).into());
proxy_map.insert("udp".into(), true.into());
match scheme {
"vless" | "trojan" => {
proxy_map.insert("uuid".into(), parsed_url.username().into());
let mut reality_opts: BTreeMap<String, Value> = BTreeMap::new();
for (key, value) in parsed_url.query_pairs() {
match key.as_ref() {
"security" if value == "reality" => {
proxy_map.insert("tls".into(), true.into());
}
"security" if value == "tls" => {
proxy_map.insert("tls".into(), true.into());
}
"flow" => { proxy_map.insert("flow".into(), value.to_string().into()); }
"sni" => { proxy_map.insert("servername".into(), value.to_string().into()); }
"fp" => { proxy_map.insert("client-fingerprint".into(), value.to_string().into()); }
"pbk" => { reality_opts.insert("public-key".into(), value.to_string().into()); }
"sid" => { reality_opts.insert("short-id".into(), value.to_string().into()); }
_ => {}
}
}
if !reality_opts.is_empty() {
proxy_map.insert("reality-opts".into(), serde_yaml::to_value(reality_opts).map_err(|e| e.to_string())?);
}
}
"ss" => {
if let Ok(decoded_user) = STANDARD.decode(parsed_url.username()) {
if let Ok(user_str) = String::from_utf8(decoded_user) {
if let Some((cipher, password)) = user_str.split_once(':') {
proxy_map.insert("cipher".into(), cipher.into());
proxy_map.insert("password".into(), password.into());
}
}
}
}
"vmess" => {
if let Ok(decoded_bytes) = STANDARD.decode(parsed_url.host_str().unwrap_or_default()) {
if let Ok(json_str) = String::from_utf8(decoded_bytes) {
if let Ok(vmess_params) = serde_json::from_str::<BTreeMap<String, Value>>(&json_str) {
if let Some(add) = vmess_params.get("add") { proxy_map.insert("server".into(), add.clone()); }
if let Some(port) = vmess_params.get("port") { proxy_map.insert("port".into(), port.clone()); }
if let Some(id) = vmess_params.get("id") { proxy_map.insert("uuid".into(), id.clone()); }
if let Some(aid) = vmess_params.get("aid") { proxy_map.insert("alterId".into(), aid.clone()); }
if let Some(net) = vmess_params.get("net") { proxy_map.insert("network".into(), net.clone()); }
if let Some(ps) = vmess_params.get("ps") { proxy_map.insert("name".into(), ps.clone()); }
}
}
}
}
_ => {
}
}
let mut config: Value = serde_yaml::from_str(template_yaml).map_err(|e| e.to_string())?;
if let Some(proxies) = config.get_mut("proxies").and_then(|v| v.as_sequence_mut()) {
proxies.clear();
proxies.push(serde_yaml::to_value(proxy_map).map_err(|e| e.to_string())?);
}
if let Some(groups) = config.get_mut("proxy-groups").and_then(|v| v.as_sequence_mut()) {
for group in groups.iter_mut() {
if let Some(mapping) = group.as_mapping_mut() {
if let Some(proxies_list) = mapping.get_mut("proxies").and_then(|p| p.as_sequence_mut()) {
let new_proxies_list: Vec<Value> = proxies_list
.iter()
.map(|p| {
if p.as_str() == Some("myproxy") {
proxy_name.clone().into()
} else {
p.clone()
}
})
.collect();
*proxies_list = new_proxies_list;
}
}
}
}
let new_yaml_content = serde_yaml::to_string(&config).map_err(|e| e.to_string())?;
let item = PrfItem::from_local(proxy_name, "Created from share link".into(), Some(new_yaml_content), None)
.map_err(|e| e.to_string())?;
wrap_err!(Config::profiles().data().append_item(item))
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -40,10 +40,20 @@ export function AppSidebar() {
return (
<Sidebar variant="floating" collapsible="icon">
<SidebarHeader>
<SidebarMenuButton>
<EarthLock/>
<span className="font-semibold group-data-[state=collapsed]:hidden">
Clash Koala
<SidebarMenuButton size="lg"
className={cn(
"flex h-12 items-center transition-all duration-200",
"group-data-[state=expanded]:w-full group-data-[state=expanded]:gap-2 group-data-[state=expanded]:px-3",
"group-data-[state=collapsed]:w-full group-data-[state=collapsed]:justify-center"
)}
>
<img
src="./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>
</SidebarMenuButton>
</SidebarHeader>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,19 +143,6 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
</DialogHeader>
<div className="py-4 space-y-1">
<SettingRow label={t("Traffic Graph")}>
<GuardState
value={verge?.traffic_graph ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ traffic_graph: e })}
onGuard={(e) => patchVerge({ traffic_graph: e })}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={t("Memory Usage")}>
<GuardState
value={verge?.enable_memory_usage ?? true}
@@ -197,134 +184,6 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
<Switch />
</GuardState>
</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>
<DialogFooter>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -511,7 +511,7 @@
"Validate Merge File": "Validate Merge File",
"Validation Success": "Validation Success",
"Validation Failed": "Validation Failed",
"Service Administrator Prompt": "Clash Verge requires administrator privileges to reinstall the system service",
"Service Administrator Prompt": "Koala Clash requires administrator privileges to reinstall the system service",
"DNS Settings": "DNS Settings",
"DNS settings saved": "DNS settings saved",
"DNS Overwrite": "DNS Overwrite",
@@ -645,6 +645,10 @@
"Attention Required": "Attention Required",
"Menu": "Menu",
"Add Profile": "Add Profile",
"Proxy enabled": "Proxy enabled",
"Proxy disabled": "Proxy disabled",
"Connecting...": "Connecting...",
"Disconnecting...": "Disconnecting...",
"Delete Profile": "Delete Profile {{name}}?",
"This action cannot be undone.": "This action cannot be undone.",
"Check Group Latency": "Check Group Latency",
@@ -665,5 +669,11 @@
"Send HWID": "Send HWID",
"New Version is available": "New Version is available",
"New Version": "New Version",
"New update": "New update"
"New update": "New update",
"Device Limit Reached": "Device Limit Reached",
"Update Profile": "Update Profile",
"Template": "Template",
"Select a template...": "Select a template...",
"Default Template": "Ru-bundle template",
"Template without RU Rules": "Without-ru template"
}

View File

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

View File

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

View File

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

View File

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

View File

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