Add macOS-specific tray menu and dock management enhancements
This commit is contained in:
1 parent
802e8369de
commit
801685427f
5 files changed
+229
-4
No files matched your search
@@ -58,7 +58,7 @@ if (-not (Test-Path -LiteralPath $AppBin -PathType Leaf)) {
|
||||
throw "Build finished, but executable not found: $AppBin"
|
||||
}
|
||||
|
||||
& bun $PreparePortable --binary $AppBin --output $BinDir
|
||||
& bun $PreparePortable --binary $AppBin --output $BinDir --download true
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Portable preparation failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ fi
|
||||
mkdir -p "$BIN_DIR"
|
||||
bun "$ROOT_DIR/scripts/prepare-portable.mjs" \
|
||||
--binary "$APP_BIN" \
|
||||
--output "$BIN_DIR"
|
||||
--output "$BIN_DIR" \
|
||||
--download true
|
||||
chmod +x "$BIN_OUT"
|
||||
|
||||
echo "Built: $APP_BIN"
|
||||
|
||||
Generated
+24
@@ -437,6 +437,8 @@ dependencies = [
|
||||
"futures-util",
|
||||
"json5",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"reqwest 0.12.28",
|
||||
"rusqlite",
|
||||
"semver",
|
||||
@@ -2029,9 +2031,17 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-text",
|
||||
"objc2-core-video",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2051,6 +2061,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
@@ -2111,6 +2122,19 @@ dependencies = [
|
||||
"objc2-core-graphics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-video"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
|
||||
@@ -23,7 +23,7 @@ serde_norway = "0.9"
|
||||
serde_yaml = "0.9"
|
||||
sha2 = "0.10"
|
||||
tar = "0.4"
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri = { version = "~2.11", features = ["tray-icon"] }
|
||||
tokio = { version = "1", features = ["macros", "sync", "time"] }
|
||||
tokio-util = "0.7"
|
||||
toml = "0.8"
|
||||
@@ -35,6 +35,10 @@ zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSEvent", "NSRunningApplication"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = [
|
||||
"Win32_Foundation",
|
||||
|
||||
+197
-1
@@ -4,8 +4,14 @@ mod usage;
|
||||
|
||||
use flate2::read::GzDecoder;
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2::MainThreadMarker;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEvent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
env, fs,
|
||||
fs::File,
|
||||
@@ -18,6 +24,11 @@ use std::{
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tar::Archive;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItem},
|
||||
tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent},
|
||||
};
|
||||
use tauri::{Emitter, Manager};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use zip::ZipArchive;
|
||||
@@ -8605,6 +8616,170 @@ fn path_to_string(path: &Path) -> String {
|
||||
path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const MACOS_TRAY_ID: &str = "macos-tray";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Default)]
|
||||
struct MacosTrayClickState {
|
||||
last_click: Option<Instant>,
|
||||
sequence: u64,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_macos_dock_visible(visible: bool) -> bool {
|
||||
let Some(mtm) = MainThreadMarker::new() else {
|
||||
eprintln!("更新 Dock 图标状态必须在主线程执行");
|
||||
return false;
|
||||
};
|
||||
let policy = if visible {
|
||||
NSApplicationActivationPolicy::Regular
|
||||
} else {
|
||||
NSApplicationActivationPolicy::Accessory
|
||||
};
|
||||
if !NSApplication::sharedApplication(mtm).setActivationPolicy(policy) {
|
||||
eprintln!("更新 Dock 图标状态失败");
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn show_main_window_on_main_thread(app_handle: &tauri::AppHandle) {
|
||||
let Some(window) = app_handle.get_webview_window("main") else {
|
||||
return;
|
||||
};
|
||||
if !set_macos_dock_visible(true) {
|
||||
return;
|
||||
}
|
||||
if let Err(error) = window.show() {
|
||||
eprintln!("显示主窗口失败: {error}");
|
||||
set_macos_dock_visible(false);
|
||||
return;
|
||||
}
|
||||
if let Err(error) = window.set_focus() {
|
||||
eprintln!("聚焦主窗口失败: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn show_main_window(app_handle: &tauri::AppHandle) {
|
||||
if MainThreadMarker::new().is_some() {
|
||||
show_main_window_on_main_thread(app_handle);
|
||||
return;
|
||||
}
|
||||
|
||||
let app_handle = app_handle.clone();
|
||||
if let Err(error) = app_handle.clone().run_on_main_thread(move || {
|
||||
show_main_window_on_main_thread(&app_handle);
|
||||
}) {
|
||||
eprintln!("调度主窗口显示失败: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn show_macos_tray_menu<R: tauri::Runtime>(tray: &TrayIcon<R>) {
|
||||
let result = tray.with_inner_tray_icon(|tray_icon| {
|
||||
let Some(status_item) = tray_icon.ns_status_item() else {
|
||||
return;
|
||||
};
|
||||
let mtm = MainThreadMarker::new().expect("tray menu must be shown on the main thread");
|
||||
if let Some(menu) = status_item.menu(mtm) {
|
||||
#[allow(deprecated)]
|
||||
status_item.popUpStatusItemMenu(&menu);
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(error) = result {
|
||||
eprintln!("显示托盘菜单失败: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn setup_macos_tray(app: &mut tauri::App<tauri::Wry>) -> tauri::Result<()> {
|
||||
let open_main_window =
|
||||
MenuItem::with_id(app, "open-main-window", "打开主界面", true, None::<&str>)?;
|
||||
let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&open_main_window, &quit])?;
|
||||
let click_state = Arc::new(Mutex::new(MacosTrayClickState::default()));
|
||||
let double_click_interval = Duration::from_secs_f64(NSEvent::doubleClickInterval());
|
||||
|
||||
TrayIconBuilder::with_id(MACOS_TRAY_ID)
|
||||
.icon(
|
||||
app.default_window_icon()
|
||||
.cloned()
|
||||
.expect("application icon is required for the tray"),
|
||||
)
|
||||
.menu(&menu)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_menu_event(move |app_handle, event| match event.id().as_ref() {
|
||||
"open-main-window" => show_main_window(app_handle),
|
||||
"quit" => app_handle.exit(0),
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(move |tray, event| {
|
||||
if !matches!(
|
||||
event,
|
||||
TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
}
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let Ok(mut state) = click_state.lock() else {
|
||||
eprintln!("读取托盘点击状态失败");
|
||||
return;
|
||||
};
|
||||
state.sequence += 1;
|
||||
let sequence = state.sequence;
|
||||
|
||||
if state
|
||||
.last_click
|
||||
.is_some_and(|last_click| now.duration_since(last_click) <= double_click_interval)
|
||||
{
|
||||
state.last_click = None;
|
||||
drop(state);
|
||||
show_main_window(tray.app_handle());
|
||||
return;
|
||||
}
|
||||
|
||||
state.last_click = Some(now);
|
||||
drop(state);
|
||||
|
||||
let app_handle = tray.app_handle().clone();
|
||||
let click_state = Arc::clone(&click_state);
|
||||
let tray_id = tray.id().clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(double_click_interval);
|
||||
let should_show_menu = match click_state.lock() {
|
||||
Ok(mut state) if state.sequence == sequence => {
|
||||
state.last_click = None;
|
||||
true
|
||||
}
|
||||
Ok(_) => false,
|
||||
Err(_) => {
|
||||
eprintln!("读取托盘点击状态失败");
|
||||
false
|
||||
}
|
||||
};
|
||||
if !should_show_menu {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(tray) = app_handle.tray_by_id(&tray_id) {
|
||||
show_macos_tray_menu(&tray);
|
||||
}
|
||||
});
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let gui_config = match load_or_create_gui_config() {
|
||||
Ok(config) => config,
|
||||
@@ -8622,8 +8797,29 @@ fn main() {
|
||||
.manage(CoreDownloadState::default())
|
||||
.manage(CoreProcessState::default())
|
||||
.manage(usage::UsageCollectorState::default())
|
||||
.manage(GuiConfigState::new(gui_config))
|
||||
.manage(GuiConfigState::new(gui_config));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let app = app.on_window_event(|window, event| {
|
||||
if window.label() == "main" {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
if !set_macos_dock_visible(false) {
|
||||
return;
|
||||
}
|
||||
if let Err(error) = window.hide() {
|
||||
eprintln!("隐藏主窗口失败: {error}");
|
||||
set_macos_dock_visible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let app = app
|
||||
.setup(|app| {
|
||||
#[cfg(target_os = "macos")]
|
||||
setup_macos_tray(app)?;
|
||||
|
||||
let usage_app = app.handle().clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
if let Err(error) = usage::initialize_usage_storage() {
|
||||
|
||||
Reference in new issue
Block a user