Compare commits
6 Commits
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -271,9 +271,23 @@ jobs:
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
|
||||
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
|
||||
$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"
|
||||
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"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip\.sig$", "_fixed_webview2-setup.nsis.zip.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
||||
19
UPDATELOG.md
19
UPDATELOG.md
@@ -1,7 +1,20 @@
|
||||
## v2.1.0 发行代号:臻
|
||||
## v2.1.1
|
||||
|
||||
**发行代号:臻**
|
||||
|
||||
代号释义: 千锤百炼臻至善,集性能跃升、功能拓展、交互焕新于一体,彰显持续打磨、全方位优化的迭代精神。
|
||||
|
||||
感谢 Tychristine 对社区群组管理做出的重大贡献!
|
||||
|
||||
2.1.1相对2.1.0(已下架不在提供)更新了:
|
||||
|
||||
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
|
||||
- 文件类型判断不准导致脚本检测报错的问题
|
||||
- 打开Win下的阴影(Win10因底层兼容性问题,可能圆角和边框显示不太完美)
|
||||
- 边框去白边
|
||||
- 修复Linux下编译问题
|
||||
- 修复热键无法关闭面板的问题
|
||||
|
||||
### 功能新增
|
||||
|
||||
- 新增窗口状态实时监控与自动保存功能
|
||||
@@ -22,12 +35,12 @@
|
||||
- 重构代理列表渲染逻辑,提升布局计算效率
|
||||
- 优化代理数据更新机制,采用乐观UI策略
|
||||
- 改进虚拟列表渲染性能(Virtuoso)
|
||||
- 提升主窗口Clash模式切换速度 (感谢Tunglies)
|
||||
- 提升主窗口Clash模式切换速度(感谢Tunglies)
|
||||
- 加速内核关闭流程并优化管理逻辑
|
||||
- 优化节点延迟刷新速率
|
||||
- 改进托盘网速显示更新逻辑
|
||||
- 提升配置验证错误信息的可读性
|
||||
- 重构服务架构,优化代码组织结构 (感谢Tunglies)
|
||||
- 重构服务架构,优化代码组织结构(感谢Tunglies)
|
||||
- 优化内核启动时的配置验证流程
|
||||
|
||||
### 问题修复
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
|
||||
@@ -37,7 +37,7 @@
|
||||
"@tauri-apps/plugin-notification": "^2.2.1",
|
||||
"@tauri-apps/plugin-process": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.4.0",
|
||||
"@tauri-apps/plugin-updater": "2.3.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.9",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -62,8 +62,8 @@ importers:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
"@tauri-apps/plugin-updater":
|
||||
specifier: 2.4.0
|
||||
version: 2.4.0
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
"@types/json-schema":
|
||||
specifier: ^7.0.15
|
||||
version: 7.0.15
|
||||
@@ -2291,10 +2291,10 @@ packages:
|
||||
integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==,
|
||||
}
|
||||
|
||||
"@tauri-apps/plugin-updater@2.4.0":
|
||||
"@tauri-apps/plugin-updater@2.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-BkeKN2WObAjobf2G77HyW/DxAfI0In+VSqWGnw/0cVPlM+VmA7fw9dKUnSunryZOG7ys9y07tj7FQa1ABMXGZQ==,
|
||||
integrity: sha512-qdzyZEUN69FZQ/nRx51fBub10tT6wffJl3DLVo9q922Gvw8Wk++rZhoD9eethPlZYbog/7RGgT8JkrfLh5BKAg==,
|
||||
}
|
||||
|
||||
"@types/babel__core@7.20.5":
|
||||
@@ -6279,7 +6279,7 @@ snapshots:
|
||||
dependencies:
|
||||
"@tauri-apps/api": 2.2.0
|
||||
|
||||
"@tauri-apps/plugin-updater@2.4.0":
|
||||
"@tauri-apps/plugin-updater@2.3.0":
|
||||
dependencies:
|
||||
"@tauri-apps/api": 2.2.0
|
||||
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -999,7 +999,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -81,7 +81,7 @@ users = "0.11.0"
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.2.0"
|
||||
tauri-plugin-global-shortcut = "2.2.0"
|
||||
tauri-plugin-updater = "2.4.0"
|
||||
tauri-plugin-updater = "2.3.0"
|
||||
tauri-plugin-window-state = "2.2.1"
|
||||
#openssl
|
||||
|
||||
|
||||
Binary file not shown.
BIN
src-tauri/assets/fonts/SF-Pro.ttf
Executable file
BIN
src-tauri/assets/fonts/SF-Pro.ttf
Executable file
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
use crate::config::*;
|
||||
use crate::core::{clash_api, handle, service};
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::core::tray::Tray;
|
||||
use crate::log_err;
|
||||
use crate::utils::{dirs, help};
|
||||
@@ -273,6 +274,14 @@ impl CoreManager {
|
||||
|
||||
/// 检查文件是否为脚本文件
|
||||
fn is_script_file(&self, path: &str) -> Result<bool> {
|
||||
// 1. 先通过扩展名快速判断
|
||||
if path.ends_with(".yaml") || path.ends_with(".yml") {
|
||||
return Ok(false); // YAML文件不是脚本文件
|
||||
} else if path.ends_with(".js") {
|
||||
return Ok(true); // JS文件是脚本文件
|
||||
}
|
||||
|
||||
// 2. 读取文件内容
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
@@ -281,15 +290,52 @@ impl CoreManager {
|
||||
}
|
||||
};
|
||||
|
||||
// 检查文件前几行是否包含JavaScript特征
|
||||
let first_lines = content.lines().take(5).collect::<String>();
|
||||
Ok(first_lines.contains("function") ||
|
||||
first_lines.contains("//") ||
|
||||
first_lines.contains("/*") ||
|
||||
first_lines.contains("import") ||
|
||||
first_lines.contains("export") ||
|
||||
first_lines.contains("const ") ||
|
||||
first_lines.contains("let "))
|
||||
// 3. 检查是否存在明显的YAML特征
|
||||
let has_yaml_features = content.contains(": ") ||
|
||||
content.contains("#") ||
|
||||
content.contains("---") ||
|
||||
content.lines().any(|line| line.trim().starts_with("- "));
|
||||
|
||||
// 4. 检查是否存在明显的JS特征
|
||||
let has_js_features = content.contains("function ") ||
|
||||
content.contains("const ") ||
|
||||
content.contains("let ") ||
|
||||
content.contains("var ") ||
|
||||
content.contains("//") ||
|
||||
content.contains("/*") ||
|
||||
content.contains("*/") ||
|
||||
content.contains("export ") ||
|
||||
content.contains("import ");
|
||||
|
||||
// 5. 决策逻辑
|
||||
if has_yaml_features && !has_js_features {
|
||||
// 只有YAML特征,没有JS特征
|
||||
return Ok(false);
|
||||
} else if has_js_features && !has_yaml_features {
|
||||
// 只有JS特征,没有YAML特征
|
||||
return Ok(true);
|
||||
} else if has_yaml_features && has_js_features {
|
||||
// 两种特征都有,需要更精细判断
|
||||
// 优先检查是否有明确的JS结构特征
|
||||
if content.contains("function main") ||
|
||||
content.contains("module.exports") ||
|
||||
content.contains("export default") {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// 检查冒号后是否有空格(YAML的典型特征)
|
||||
let yaml_pattern_count = content.lines()
|
||||
.filter(|line| line.contains(": "))
|
||||
.count();
|
||||
|
||||
if yaml_pattern_count > 2 {
|
||||
return Ok(false); // 多个键值对格式,更可能是YAML
|
||||
}
|
||||
}
|
||||
|
||||
// 默认情况:无法确定时,假设为非脚本文件(更安全)
|
||||
log::debug!(target: "app", "无法确定文件类型,默认当作YAML处理: {}", path);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// 验证脚本文件语法
|
||||
@@ -394,85 +440,4 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
async fn create_test_script() -> Result<String> {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_path = temp_dir.join("test_script.js");
|
||||
let script_content = r#"
|
||||
// This is a test script
|
||||
function main(config) {
|
||||
console.log("Testing script");
|
||||
return config;
|
||||
}
|
||||
"#;
|
||||
|
||||
fs::write(&script_path, script_content)?;
|
||||
Ok(script_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn create_invalid_script() -> Result<String> {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_path = temp_dir.join("invalid_script.js");
|
||||
let script_content = r#"
|
||||
// This is an invalid script
|
||||
function main(config { // Missing closing parenthesis
|
||||
console.log("Testing script");
|
||||
return config;
|
||||
}
|
||||
"#;
|
||||
|
||||
fs::write(&script_path, script_content)?;
|
||||
Ok(script_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn create_no_main_script() -> Result<String> {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_path = temp_dir.join("no_main_script.js");
|
||||
let script_content = r#"
|
||||
// This script has no main function
|
||||
function helper(config) {
|
||||
console.log("Testing script");
|
||||
return config;
|
||||
}
|
||||
"#;
|
||||
|
||||
fs::write(&script_path, script_content)?;
|
||||
Ok(script_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_validate_script_file() -> Result<()> {
|
||||
let core_manager = CoreManager::global();
|
||||
|
||||
// 测试有效脚本
|
||||
let script_path = create_test_script().await?;
|
||||
let result = core_manager.validate_config_file(&script_path).await?;
|
||||
assert!(result.0, "有效脚本应该通过验证");
|
||||
|
||||
// 测试无效脚本
|
||||
let invalid_script_path = create_invalid_script().await?;
|
||||
let result = core_manager.validate_config_file(&invalid_script_path).await?;
|
||||
assert!(!result.0, "无效脚本不应该通过验证");
|
||||
assert!(result.1.contains("脚本语法错误"), "无效脚本应该返回语法错误");
|
||||
|
||||
// 测试缺少main函数的脚本
|
||||
let no_main_script_path = create_no_main_script().await?;
|
||||
let result = core_manager.validate_config_file(&no_main_script_path).await?;
|
||||
assert!(!result.0, "缺少main函数的脚本不应该通过验证");
|
||||
assert!(result.1.contains("缺少main函数"), "应该提示缺少main函数");
|
||||
|
||||
// 清理测试文件
|
||||
let _ = fs::remove_file(script_path);
|
||||
let _ = fs::remove_file(invalid_script_path);
|
||||
let _ = fs::remove_file(no_main_script_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,9 +104,32 @@ impl Hotkey {
|
||||
|
||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||
async_runtime::spawn_blocking(|| {
|
||||
println!("Creating window in spawn_blocking");
|
||||
log::info!(target: "app", "Creating window in spawn_blocking");
|
||||
resolve::create_window();
|
||||
println!("Toggle dashboard window visibility");
|
||||
log::info!(target: "app", "Toggle dashboard window visibility");
|
||||
|
||||
// 检查窗口是否存在
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
// 如果窗口可见,则隐藏它
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
println!("Window is visible, hiding it");
|
||||
log::info!(target: "app", "Window is visible, hiding it");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
// 如果窗口不可见,则显示它
|
||||
println!("Window is hidden, showing it");
|
||||
log::info!(target: "app", "Window is hidden, showing it");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
} else {
|
||||
// 如果窗口不存在,创建一个新窗口
|
||||
println!("Window does not exist, creating a new one");
|
||||
log::info!(target: "app", "Window does not exist, creating a new one");
|
||||
resolve::create_window();
|
||||
}
|
||||
});
|
||||
|
||||
println!("=== Hotkey Dashboard Window Operation End ===");
|
||||
@@ -146,7 +169,23 @@ impl Hotkey {
|
||||
// 直接执行函数,不做任何状态检查
|
||||
println!("Executing function directly");
|
||||
log::info!(target: "app", "Executing function directly");
|
||||
f();
|
||||
|
||||
// 获取轻量模式状态和全局热键状态
|
||||
let is_lite_mode = Config::verge().latest().enable_lite_mode.unwrap_or(false);
|
||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
// 在轻量模式下或配置了全局热键时,始终执行热键功能
|
||||
if is_lite_mode || is_enable_global_hotkey {
|
||||
f();
|
||||
} else if let Some(window) = app_handle.get_webview_window("main") {
|
||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||
let is_visible = window.is_visible().unwrap_or(false);
|
||||
let is_focused = window.is_focused().unwrap_or(false);
|
||||
|
||||
if is_focused && is_visible {
|
||||
f();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -225,23 +225,27 @@ impl Tray {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
|
||||
let is_template =
|
||||
crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
||||
|
||||
let icon_bytes = if enable_tray_speed {
|
||||
let is_colorful = tray_icon == "colorful";
|
||||
|
||||
// 处理图标和速率
|
||||
let final_icon_bytes = if enable_tray_speed {
|
||||
let rate = rate.or_else(|| {
|
||||
self.speed_rate
|
||||
.lock()
|
||||
.as_ref()
|
||||
.and_then(|speed_rate| speed_rate.get_curent_rate())
|
||||
});
|
||||
|
||||
// 使用新的方法渲染图标和速率
|
||||
SpeedRate::add_speed_text(icon_bytes, rate)?
|
||||
} else {
|
||||
icon_bytes
|
||||
};
|
||||
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(is_template);
|
||||
// 设置系统托盘图标
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
|
||||
// 只对单色图标使用 template 模式
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::core::clash_api::{get_traffic_ws_url, Rate};
|
||||
use crate::utils::help::format_bytes_speed;
|
||||
use anyhow::Result;
|
||||
use futures::Stream;
|
||||
use image::{ImageBuffer, Rgba};
|
||||
use image::{Rgba, GenericImageView, RgbaImage};
|
||||
use imageproc::drawing::draw_text_mut;
|
||||
use parking_lot::Mutex;
|
||||
use rusttype::{Font, Scale};
|
||||
@@ -14,7 +14,7 @@ use tokio_tungstenite::tungstenite::Message;
|
||||
pub struct SpeedRate {
|
||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||
last_update: Arc<Mutex<std::time::Instant>>,
|
||||
base_image: Arc<Mutex<Option<(ImageBuffer<Rgba<u8>, Vec<u8>>, u32, u32)>>>, // 存储基础图像和尺寸
|
||||
// 移除 base_image,不再缓存原始图像
|
||||
}
|
||||
|
||||
impl SpeedRate {
|
||||
@@ -22,7 +22,6 @@ impl SpeedRate {
|
||||
Self {
|
||||
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
|
||||
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
|
||||
base_image: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,125 +66,114 @@ impl SpeedRate {
|
||||
Some(current.clone())
|
||||
}
|
||||
|
||||
pub fn add_speed_text(icon: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
||||
// 分离图标加载和速率渲染
|
||||
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
||||
|
||||
// 获取或创建基础图像
|
||||
let base_image = {
|
||||
let tray = Self::global();
|
||||
let mut base = tray.base_image.lock();
|
||||
if base.is_none() {
|
||||
let img = image::load_from_memory(&icon)?;
|
||||
let (width, height) = (img.width(), img.height());
|
||||
let icon_text_gap = 10;
|
||||
let max_text_width = 510.0;
|
||||
let total_width = width as f32 + icon_text_gap as f32 + max_text_width;
|
||||
|
||||
let mut image = ImageBuffer::new(total_width.ceil() as u32, height);
|
||||
image::imageops::replace(&mut image, &img, 0_i64, 0_i64);
|
||||
*base = Some((image, width, height));
|
||||
}
|
||||
base.clone().unwrap()
|
||||
};
|
||||
|
||||
let (mut image, width, height) = base_image;
|
||||
// 加载原始图标
|
||||
let icon_image = image::load_from_memory(&icon_bytes)?;
|
||||
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
|
||||
|
||||
let font =
|
||||
Font::try_from_bytes(include_bytes!("../../../assets/fonts/FiraCode-Medium.ttf")).unwrap();
|
||||
|
||||
// 修改颜色和阴影参数
|
||||
let text_color = Rgba([255u8, 255u8, 255u8, 255u8]); // 纯白色
|
||||
let shadow_color = Rgba([0u8, 0u8, 0u8, 120u8]); // 降低阴影不透明度
|
||||
let base_size = height as f32 * 0.6; // 保持字体大小
|
||||
let scale = Scale::uniform(base_size);
|
||||
|
||||
let up_text = format_bytes_speed(rate.up);
|
||||
let down_text = format_bytes_speed(rate.down);
|
||||
|
||||
// 计算文本宽度
|
||||
let up_width = font
|
||||
.layout(&up_text, scale, rusttype::Point { x: 0.0, y: 0.0 })
|
||||
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
|
||||
.last()
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let down_width = font
|
||||
.layout(&down_text, scale, rusttype::Point { x: 0.0, y: 0.0 })
|
||||
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
|
||||
.last()
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let icon_text_gap = 10;
|
||||
let max_text_width: f32 = 510.0;
|
||||
let text_area_start = width as i32 + icon_text_gap;
|
||||
// 判断是否为彩色图标
|
||||
let is_colorful = !crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
||||
|
||||
// 用透明色清除文字区域
|
||||
for x in text_area_start..image.width() as i32 {
|
||||
for y in 0..image.height() as i32 {
|
||||
image.put_pixel(x as u32, y as u32, Rgba([0, 0, 0, 0]));
|
||||
// 增加文本宽度和间距
|
||||
let text_width = 580; // 文本区域宽度
|
||||
let total_width = icon_width + text_width;
|
||||
|
||||
// 创建新的透明画布
|
||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||
|
||||
// 将原始图标绘制到新画布的左侧
|
||||
for y in 0..icon_height {
|
||||
for x in 0..icon_width {
|
||||
let pixel = icon_image.get_pixel(x, y);
|
||||
combined_image.put_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算文字的起始x坐标,使文字右对齐
|
||||
let text_start_x_up = (width as f32 + icon_text_gap as f32 + max_text_width - up_width).max(width as f32 + icon_text_gap as f32) as i32;
|
||||
let text_start_x_down = (width as f32 + icon_text_gap as f32 + max_text_width - down_width).max(width as f32 + icon_text_gap as f32) as i32;
|
||||
|
||||
// 计算垂直位置
|
||||
let up_y = 0; // 上行速率紧贴顶部
|
||||
let down_y = height as i32 - base_size as i32; // 下行速率紧贴底部
|
||||
|
||||
|
||||
// 选择文本颜色
|
||||
let (text_color, shadow_color) = if is_colorful {
|
||||
// 彩色图标使用黑色文本和轻微白色阴影
|
||||
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 160u8]))
|
||||
} else {
|
||||
// 单色图标使用白色文本和轻微黑色阴影
|
||||
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 120u8]))
|
||||
};
|
||||
|
||||
// 减小字体大小以适应文本区域
|
||||
let font = Font::try_from_bytes(include_bytes!("../../../assets/fonts/SF-Pro.ttf")).unwrap();
|
||||
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
|
||||
let scale = Scale::uniform(font_size);
|
||||
|
||||
// 使用更简洁的速率格式
|
||||
let up_text = format_bytes_speed(rate.up);
|
||||
let down_text = format_bytes_speed(rate.down);
|
||||
|
||||
// 计算文本位置,确保垂直间距合适
|
||||
// 修改文本位置为居右显示
|
||||
let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
|
||||
let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
|
||||
|
||||
// 计算右对齐的文本位置
|
||||
let up_text_x = total_width - up_text_width;
|
||||
let down_text_x = total_width - down_text_width;
|
||||
|
||||
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
|
||||
let text_height = font_size as i32;
|
||||
let total_text_height = text_height * 2;
|
||||
let up_y = (icon_height as i32 - total_text_height) / 2;
|
||||
let down_y = up_y + text_height;
|
||||
|
||||
// 绘制速率文本(先阴影后文字)
|
||||
let shadow_offset = 1;
|
||||
|
||||
// 绘制上行速率(先画阴影,再画文字)
|
||||
|
||||
// 绘制上行速率
|
||||
draw_text_mut(
|
||||
&mut image,
|
||||
&mut combined_image,
|
||||
shadow_color,
|
||||
text_start_x_up + shadow_offset,
|
||||
up_text_x as i32 + shadow_offset,
|
||||
up_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
draw_text_mut(
|
||||
&mut image,
|
||||
&mut combined_image,
|
||||
text_color,
|
||||
text_start_x_up,
|
||||
up_text_x as i32,
|
||||
up_y,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
|
||||
// 绘制下行速率(先画阴影,再画文字)
|
||||
|
||||
// 绘制下行速率
|
||||
draw_text_mut(
|
||||
&mut image,
|
||||
&mut combined_image,
|
||||
shadow_color,
|
||||
text_start_x_down + shadow_offset,
|
||||
down_text_x as i32 + shadow_offset,
|
||||
down_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
);
|
||||
draw_text_mut(
|
||||
&mut image,
|
||||
&mut combined_image,
|
||||
text_color,
|
||||
text_start_x_down,
|
||||
down_text_x as i32,
|
||||
down_y,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
);
|
||||
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
let mut cursor = Cursor::new(&mut bytes);
|
||||
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
||||
|
||||
// 将结果转换为 PNG 数据
|
||||
let mut bytes = Vec::new();
|
||||
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn global() -> &'static SpeedRate {
|
||||
static INSTANCE: once_cell::sync::OnceCell<SpeedRate> = once_cell::sync::OnceCell::new();
|
||||
INSTANCE.get_or_init(SpeedRate::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -161,7 +161,7 @@ pub fn create_window() {
|
||||
.maximizable(true)
|
||||
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
|
||||
.transparent(true)
|
||||
.shadow(false)
|
||||
.shadow(true)
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"copyright": "GNU General Public License v3.0",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Clash Verge Rev",
|
||||
"createUpdaterArtifacts": true
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
},
|
||||
"build": {
|
||||
"beforeBuildCommand": "pnpm run web:build",
|
||||
@@ -25,7 +25,7 @@
|
||||
"devUrl": "http://localhost:3000/"
|
||||
},
|
||||
"productName": "Clash Verge",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 2px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,5 +445,11 @@
|
||||
"Config Validation Failed": "Subscription configuration validation failed. Please check the subscription configuration file; modifications have been rolled back.",
|
||||
"Boot Config Validation Failed": "Boot subscription configuration validation failed. Started with the default configuration; please check the subscription configuration file.",
|
||||
"Core Change Config Validation Failed": "Configuration validation failed when switching the kernel. Started with the default configuration; please check the subscription configuration file.",
|
||||
"Config Validation Process Terminated": "The validation process has been terminated."
|
||||
"Config Validation Process Terminated": "The validation process has been terminated.",
|
||||
"Script Syntax Error": "Script syntax error, changes reverted",
|
||||
"Script Missing Main": "Script error, changes reverted",
|
||||
"File Not Found": "File missing, changes reverted",
|
||||
"Script File Error": "Script file error, changes reverted",
|
||||
"Core Changed Successfully": "Core changed successfully",
|
||||
"Failed to Change Core": "Failed to change core"
|
||||
}
|
||||
|
||||
@@ -204,9 +204,9 @@ const Layout = () => {
|
||||
({ palette }) => ({ bgcolor: palette.background.paper }),
|
||||
{
|
||||
borderRadius: "8px",
|
||||
border: "2px solid var(--divider-color)",
|
||||
width: "calc(100vw - 4px)",
|
||||
height: "calc(100vh - 4px)",
|
||||
border: "0px solid var(--divider-color)",
|
||||
width: "calc(100vw - 1px)",
|
||||
height: "calc(100vh - 1px)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user