feat(tauri): macOS 中文菜单栏 + 单实例运行 + 圆角图标
- 自定义 macOS 菜单栏(美家卡智影 / 编辑 / 窗口) - 添加 tauri-plugin-single-instance,防止多开 - 重新生成 macOS Big Sur 风格圆角图标(22% 圆角半径) - 新增 icons/generate-icons.py 脚本
@@ -4235,6 +4235,7 @@ dependencies = [
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
@@ -4416,6 +4417,21 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
|
||||
@@ -44,4 +44,5 @@ thiserror = "1"
|
||||
fs2 = "0.4"
|
||||
# 异步运行时定时器(FFmpeg 超时保护)
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
tauri-plugin-single-instance = "2"
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 macOS 风格圆角图标(Big Sur 22% 圆角半径)"""
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# 配置
|
||||
LOGO_PATH = "../../public/assets/logo.png"
|
||||
OUTPUT_DIR = "."
|
||||
BG_COLOR = (255, 255, 255, 255)
|
||||
# macOS Big Sur 风格圆角:约 22% 半径
|
||||
CORNER_RADIUS_RATIO = 0.22
|
||||
# Logo 缩放比例
|
||||
LOGO_SCALE = 0.65
|
||||
|
||||
|
||||
def create_rounded_rect(size, radius, fill):
|
||||
"""创建圆角矩形遮罩"""
|
||||
img = Image.new("RGBA", size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rounded_rectangle((0, 0, size[0] - 1, size[1] - 1), radius=radius, fill=fill)
|
||||
return img
|
||||
|
||||
|
||||
def generate_icon(size):
|
||||
"""生成指定尺寸的图标"""
|
||||
radius = int(size[0] * CORNER_RADIUS_RATIO)
|
||||
|
||||
# 白色圆角背景
|
||||
bg = create_rounded_rect(size, radius, BG_COLOR)
|
||||
|
||||
# 加载并缩放 logo
|
||||
logo = Image.open(LOGO_PATH).convert("RGBA")
|
||||
logo_size = int(min(size) * LOGO_SCALE)
|
||||
logo.thumbnail((logo_size, logo_size), Image.LANCZOS)
|
||||
|
||||
# 居中绘制 logo
|
||||
x = (size[0] - logo.width) // 2
|
||||
y = (size[1] - logo.height) // 2
|
||||
bg.paste(logo, (x, y), logo)
|
||||
|
||||
return bg
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# 生成各尺寸 PNG
|
||||
sizes = {
|
||||
"icon_16x16.png": (16, 16),
|
||||
"icon_32x32.png": (32, 32),
|
||||
"icon_128x128.png": (128, 128),
|
||||
"icon_256x256.png": (256, 256),
|
||||
"icon_512x512.png": (512, 512),
|
||||
"icon_512x512@2x.png": (1024, 1024),
|
||||
}
|
||||
|
||||
# 用于 ICNS 的临时目录
|
||||
icns_dir = "/tmp/icon.iconset"
|
||||
os.makedirs(icns_dir, exist_ok=True)
|
||||
|
||||
for filename, size in sizes.items():
|
||||
img = generate_icon(size)
|
||||
img.save(f"{icns_dir}/{filename}", "PNG")
|
||||
print(f"Generated {filename} ({size[0]}x{size[1]})")
|
||||
|
||||
# 复制到输出目录
|
||||
for src, dst in [
|
||||
("icon_32x32.png", "32x32.png"),
|
||||
("icon_128x128.png", "128x128.png"),
|
||||
("icon_256x256.png", "128x128@2x.png"),
|
||||
("icon_512x512.png", "icon.png"),
|
||||
]:
|
||||
subprocess.run(["cp", f"{icns_dir}/{src}", f"{OUTPUT_DIR}/{dst}"])
|
||||
print(f"Copied to {dst}")
|
||||
|
||||
# 生成 ICNS
|
||||
subprocess.run(
|
||||
["iconutil", "-c", "icns", "-o", f"{OUTPUT_DIR}/icon.icns", icns_dir],
|
||||
check=True,
|
||||
)
|
||||
print("Generated icon.icns")
|
||||
|
||||
# 生成 ICO(Windows 需要多分辨率)
|
||||
ico_sizes = [16, 32, 48, 64, 128, 256]
|
||||
ico_images = []
|
||||
for s in ico_sizes:
|
||||
img = generate_icon((s, s))
|
||||
# ICO 需要 RGB 模式
|
||||
if img.mode == "RGBA":
|
||||
rgb = Image.new("RGB", img.size, (255, 255, 255))
|
||||
rgb.paste(img, mask=img.split()[3])
|
||||
ico_images.append(rgb)
|
||||
else:
|
||||
ico_images.append(img)
|
||||
|
||||
ico_images[0].save(
|
||||
f"{OUTPUT_DIR}/icon.ico",
|
||||
format="ICO",
|
||||
sizes=[(s, s) for s in ico_sizes],
|
||||
)
|
||||
print("Generated icon.ico")
|
||||
|
||||
# 清理临时目录
|
||||
subprocess.run(["rm", "-rf", icns_dir])
|
||||
|
||||
print("\n✅ 所有图标已生成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 379 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 23 KiB |
@@ -55,6 +55,51 @@ pub fn run() {
|
||||
if let Ok(app_data_dir) = app.path().app_local_data_dir() {
|
||||
crate::storage::init_app_data_dir(app_data_dir);
|
||||
}
|
||||
|
||||
// macOS 自定义菜单栏(中文本地化)
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::menu::{AboutMetadata, MenuBuilder, SubmenuBuilder};
|
||||
|
||||
let app_menu = SubmenuBuilder::new(app, "美家卡智影")
|
||||
.about_with_text(
|
||||
"关于 美家卡智影",
|
||||
Some(AboutMetadata {
|
||||
name: Some("美家卡智影".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.quit_with_text("退出")
|
||||
.build()?;
|
||||
|
||||
let edit_menu = SubmenuBuilder::new(app, "编辑")
|
||||
.undo_with_text("撤销")
|
||||
.redo_with_text("重做")
|
||||
.separator()
|
||||
.cut_with_text("剪切")
|
||||
.copy_with_text("复制")
|
||||
.paste_with_text("粘贴")
|
||||
.separator()
|
||||
.select_all_with_text("全选")
|
||||
.build()?;
|
||||
|
||||
let window_menu = SubmenuBuilder::new(app, "窗口")
|
||||
.minimize_with_text("最小化")
|
||||
.maximize_with_text("最大化")
|
||||
.separator()
|
||||
.close_window_with_text("关闭窗口")
|
||||
.build()?;
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&app_menu)
|
||||
.item(&edit_menu)
|
||||
.item(&window_menu)
|
||||
.build()?;
|
||||
|
||||
app.set_menu(menu)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
@@ -63,6 +108,13 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
// 单实例:第二个实例启动时,将已有窗口带到前台
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
storage::config::load_app_config,
|
||||
storage::config::save_app_config,
|
||||
|
||||