托盘功能: - 添加主题子菜单(浅色/深色/跟随系统) - 添加完全退出菜单项 - 托盘图标使用应用默认图标 完全退出功能: - 新增 AppState.allow_exit 状态控制 - 点击'完全退出'时允许应用真正退出 - 关闭窗口隐藏到托盘的默认行为 主题切换功能: - 后端:添加 AppTheme 枚举和 set_theme/get_theme 命令 - 前端:实现主题切换逻辑,支持浅色/深色/跟随系统 - 前端:添加主题选择器 UI 组件和样式 - 通过 CSS 变量实现深色/浅色主题切换 - 支持 Tauri 事件监听实现后端主题同步 修改文件: - src/app/state.rs: 添加 AppTheme 枚举和状态管理 - src/app/mod.rs: 完善托盘菜单和退出逻辑 - src/app/commands.rs: 添加主题相关 Tauri 命令 - web/src/App.tsx: 实现主题切换逻辑 - web/src/App.css: 添加主题 CSS 变量和选择器样式 - web/src/pages/SettingsPage.tsx: 添加主题选择器 UI
368 lines
14 KiB
Rust
368 lines
14 KiB
Rust
//! Tauri 应用模块
|
||
//!
|
||
//! 增强诊断日志版本 - 详细记录窗口创建、前端加载和托盘初始化过程
|
||
|
||
use anyhow::Result;
|
||
use tauri::{
|
||
menu::{Menu, MenuItem},
|
||
tray::TrayIconBuilder,
|
||
Manager,
|
||
};
|
||
use tracing::{info, warn};
|
||
|
||
pub mod commands;
|
||
pub mod state;
|
||
|
||
/// 应用状态
|
||
pub use state::AppState;
|
||
pub use state::AppTheme;
|
||
|
||
/// 运行 Tauri 应用
|
||
pub fn run() -> Result<()> {
|
||
eprintln!();
|
||
eprintln!(" [应用] 开始构建 Tauri 应用...");
|
||
info!("启动 Tauri 应用...");
|
||
|
||
// 检查构建上下文
|
||
eprintln!(" [检查] 验证 Tauri 上下文...");
|
||
let context = tauri::generate_context!();
|
||
eprintln!(" ✓ 上下文生成成功");
|
||
eprintln!(" - 应用标识:{}", context.config().identifier);
|
||
eprintln!(" - 应用名称:{:?}", context.config().product_name);
|
||
|
||
let app = tauri::Builder::default()
|
||
.plugin(tauri_plugin_shell::init())
|
||
.plugin(tauri_plugin_dialog::init())
|
||
.plugin(tauri_plugin_fs::init())
|
||
.manage(AppState::default())
|
||
.invoke_handler(tauri::generate_handler![
|
||
commands::start_recording,
|
||
commands::stop_recording,
|
||
commands::recognize_audio,
|
||
commands::get_config_cmd,
|
||
commands::save_config,
|
||
commands::get_history,
|
||
commands::clear_history,
|
||
commands::get_theme,
|
||
commands::set_theme,
|
||
])
|
||
.setup(|app| {
|
||
info!("[设置] 初始化应用插件...");
|
||
info!(" - tauri_plugin_shell: 已加载");
|
||
info!(" - tauri_plugin_dialog: 已加载");
|
||
info!(" - tauri_plugin_fs: 已加载");
|
||
|
||
// 获取主窗口(由 tauri.conf.json 自动创建)
|
||
info!("[设置] 获取主窗口...");
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
info!(" ✓ 主窗口已存在 (由配置文件创建)");
|
||
|
||
// 获取窗口位置和大小
|
||
if let Ok(position) = window.outer_position() {
|
||
info!(" - 窗口位置:({}, {})", position.x, position.y);
|
||
}
|
||
if let Ok(size) = window.outer_size() {
|
||
info!(" - 窗口大小:{}x{}", size.width, size.height);
|
||
}
|
||
|
||
// 确保窗口可见并获得焦点
|
||
info!(" - 显示窗口并聚焦...");
|
||
let _ = window.show();
|
||
let _ = window.set_focus();
|
||
info!(" ✓ 窗口已显示并聚焦");
|
||
|
||
// 再次检查状态
|
||
match window.is_visible() {
|
||
Ok(true) => info!(" ✓ 窗口确认可见"),
|
||
Ok(false) => info!(" ⚠ 窗口仍然隐藏"),
|
||
Err(e) => info!(" ⚠ 可见性检查失败:{}", e),
|
||
}
|
||
|
||
// 获取前端 URL
|
||
if let Ok(url) = window.url() {
|
||
info!(" - 前端 URL: {}", url);
|
||
}
|
||
} else {
|
||
info!(" ⚠ 主窗口未找到,手动创建...");
|
||
let window_result = tauri::WebviewWindowBuilder::new(
|
||
app,
|
||
"main",
|
||
tauri::WebviewUrl::App("index.html".into()),
|
||
)
|
||
.title("impress ASR Input")
|
||
.inner_size(1000.0, 700.0)
|
||
.min_inner_size(800.0, 600.0)
|
||
.center()
|
||
.visible(true)
|
||
.build();
|
||
|
||
match window_result {
|
||
Ok(_window) => {
|
||
info!(" ✓ 窗口手动创建成功");
|
||
}
|
||
Err(e) => {
|
||
info!(" ❌ 窗口创建失败:{}", e);
|
||
warn!("窗口创建失败:{}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 设置系统托盘
|
||
info!(" [设置] 配置系统托盘...");
|
||
match setup_tray(app) {
|
||
Ok(_) => info!(" ✓ 系统托盘设置完成"),
|
||
Err(e) => {
|
||
info!(" ⚠ 系统托盘设置失败:{}", e);
|
||
warn!("托盘设置失败:{}", e);
|
||
}
|
||
}
|
||
|
||
// 在 setup 结束时检查窗口状态
|
||
info!(" [设置] setup 结束前检查窗口...");
|
||
let all_windows = app.webview_windows();
|
||
info!(" - 当前窗口数量:{}", all_windows.len());
|
||
for (label, _) in all_windows.iter() {
|
||
info!(" - 窗口:label='{}'", label);
|
||
if let Some(win) = app.get_webview_window(label) {
|
||
let visible = win.is_visible().unwrap_or(false);
|
||
let pos = win.outer_position().ok();
|
||
let size = win.outer_size().ok();
|
||
let url = win.url().ok();
|
||
info!(" - 可见性:{}", if visible { "可见" } else { "隐藏" });
|
||
if let Some(p) = pos {
|
||
info!(" - 位置:({}, {})", p.x, p.y);
|
||
}
|
||
if let Some(s) = size {
|
||
info!(" - 大小:{}x{}", s.width, s.height);
|
||
}
|
||
if let Some(u) = url {
|
||
info!(" - URL: {}", u);
|
||
}
|
||
}
|
||
}
|
||
|
||
info!("Tauri 应用设置完成");
|
||
Ok(())
|
||
})
|
||
.on_window_event(|window, event| {
|
||
// 处理窗口事件
|
||
match event {
|
||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||
info!(" [窗口] 关闭请求 - 隐藏窗口到托盘");
|
||
// 隐藏窗口而不是关闭
|
||
window.hide().unwrap();
|
||
api.prevent_close();
|
||
}
|
||
tauri::WindowEvent::Focused(focused) => {
|
||
if *focused {
|
||
info!(" [窗口] 获得焦点");
|
||
} else {
|
||
info!(" [窗口] 失去焦点");
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
})
|
||
.on_page_load(|window, payload| {
|
||
info!("[页面加载] URL: {}", payload.url());
|
||
match payload.event() {
|
||
tauri::webview::PageLoadEvent::Started => {
|
||
info!(" - 页面开始加载");
|
||
}
|
||
tauri::webview::PageLoadEvent::Finished => {
|
||
info!(" - 页面加载完成 ✓");
|
||
}
|
||
}
|
||
})
|
||
.build(context)
|
||
.expect("构建 Tauri 应用失败");
|
||
|
||
eprintln!(" ✓ Tauri 应用构建成功");
|
||
info!("Tauri 应用启动成功");
|
||
|
||
// 立即检查窗口 - 在 run() 之前
|
||
info!("========================================");
|
||
info!("[窗口] build 后立即检查窗口...");
|
||
let windows = app.webview_windows();
|
||
info!(" - 窗口数量:{}", windows.len());
|
||
for (label, _) in windows.iter() {
|
||
info!(" - 窗口标签:label='{}'", label);
|
||
}
|
||
|
||
// 尝试获取主窗口
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
info!("[窗口] 主窗口信息:");
|
||
match window.is_visible() {
|
||
Ok(true) => info!(" - 可见性:可见 ✓"),
|
||
Ok(false) => info!(" - 可见性:隐藏 ⚠"),
|
||
Err(e) => info!(" - 可见性:检查失败 ({})", e),
|
||
}
|
||
match window.is_minimized() {
|
||
Ok(true) => info!(" - 最小化:是 ⚠"),
|
||
Ok(false) => info!(" - 最小化:否 ✓"),
|
||
Err(e) => info!(" - 最小化:检查失败 ({})", e),
|
||
}
|
||
info!(" - 焦点:可设置 ✓");
|
||
} else {
|
||
info!(" ⚠ [窗口] 主窗口 (label='main') 未找到!");
|
||
warn!("主窗口未找到");
|
||
|
||
// 尝试获取任意窗口
|
||
if let Some(first_window) = windows.keys().next() {
|
||
info!(" - 但找到其他窗口:label='{}'", first_window);
|
||
if let Some(win) = app.get_webview_window(first_window) {
|
||
let _ = win.show();
|
||
let _ = win.set_focus();
|
||
info!(" ✓ 已显示该窗口");
|
||
}
|
||
}
|
||
}
|
||
|
||
eprintln!();
|
||
eprintln!(" [运行] 进入事件循环...");
|
||
info!("进入应用事件循环");
|
||
|
||
app.run(|app_handle, event| {
|
||
// 处理全局事件
|
||
match event {
|
||
tauri::RunEvent::Exit => {
|
||
info!(" [事件] 应用退出");
|
||
}
|
||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||
// 检查是否允许完全退出
|
||
let state = app_handle.state::<AppState>();
|
||
if state.is_exit_allowed() {
|
||
info!(" [事件] 退出请求 - 允许完全退出");
|
||
} else {
|
||
info!(" [事件] 退出请求 - 阻止退出(隐藏窗口到托盘)");
|
||
api.prevent_exit();
|
||
}
|
||
}
|
||
tauri::RunEvent::Ready => {
|
||
info!(" [事件] 应用已就绪");
|
||
}
|
||
_ => {}
|
||
}
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 设置系统托盘
|
||
fn setup_tray(app: &tauri::App) -> Result<()> {
|
||
info!(" [托盘] 创建菜单项...");
|
||
|
||
let show = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
|
||
info!(" - '显示窗口' 菜单项已创建");
|
||
|
||
let record = MenuItem::with_id(app, "record", "开始录音", true, None::<&str>)?;
|
||
info!(" - '开始录音' 菜单项已创建");
|
||
|
||
let settings = MenuItem::with_id(app, "settings", "设置", true, None::<&str>)?;
|
||
info!(" - '设置' 菜单项已创建");
|
||
|
||
// 主题子菜单
|
||
let theme_light = MenuItem::with_id(app, "theme_light", "浅色主题", true, None::<&str>)?;
|
||
let theme_dark = MenuItem::with_id(app, "theme_dark", "深色主题", true, None::<&str>)?;
|
||
let theme_system = MenuItem::with_id(app, "theme_system", "跟随系统", true, None::<&str>)?;
|
||
let theme_menu = Menu::with_items(app, &[&theme_light, &theme_dark, &theme_system])?;
|
||
info!(" - '主题' 子菜单已创建(浅色/深色/跟随系统)");
|
||
|
||
// 完全退出选项
|
||
let quit_now = MenuItem::with_id(app, "quit_now", "完全退出", true, None::<&str>)?;
|
||
info!(" - '完全退出' 菜单项已创建");
|
||
|
||
info!(" [托盘] 组合菜单...");
|
||
let menu = Menu::with_items(app, &[&show, &record, &settings, &theme_menu, &quit_now])?;
|
||
info!(" ✓ 菜单创建成功 (5 项 + 主题子菜单)");
|
||
|
||
info!(" [托盘] 加载图标...");
|
||
let icon = app.default_window_icon().cloned();
|
||
match icon {
|
||
Some(_) => info!(" ✓ 窗口图标加载成功"),
|
||
None => {
|
||
info!(" ⚠ 窗口图标未找到,使用默认图标");
|
||
warn!("窗口图标未找到");
|
||
}
|
||
}
|
||
|
||
info!(" [托盘] 创建托盘图标...");
|
||
let _tray = TrayIconBuilder::new()
|
||
.icon(app.default_window_icon().cloned().unwrap_or_else(|| {
|
||
warn!("使用空图标");
|
||
// 创建一个 1x1 的透明像素作为默认图标
|
||
tauri::image::Image::new_owned(vec![0u8; 4], 1, 1)
|
||
}))
|
||
.menu(&menu)
|
||
.show_menu_on_left_click(false)
|
||
.on_menu_event(|app, event| {
|
||
info!("托盘菜单事件:{:?}", event.id);
|
||
match event.id.as_ref() {
|
||
"show" => {
|
||
info!(" [托盘] '显示窗口' 被点击");
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
let _ = window.show();
|
||
let _ = window.set_focus();
|
||
info!(" ✓ 窗口已显示并聚焦");
|
||
}
|
||
}
|
||
"record" => {
|
||
info!(" [托盘] '开始录音' 被点击");
|
||
info!("从托盘启动录音");
|
||
}
|
||
"settings" => {
|
||
info!(" [托盘] '设置' 被点击");
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
let _ = window.show();
|
||
let _ = window.set_focus();
|
||
info!(" ✓ 窗口已显示并聚焦");
|
||
}
|
||
}
|
||
"theme_light" => {
|
||
info!(" [托盘] '浅色主题' 被点击");
|
||
let state = app.state::<AppState>();
|
||
state.set_theme(AppTheme::Light);
|
||
// 通知前端切换主题
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
let _ = window.emit("theme-change", "light");
|
||
}
|
||
}
|
||
"theme_dark" => {
|
||
info!(" [托盘] '深色主题' 被点击");
|
||
let state = app.state::<AppState>();
|
||
state.set_theme(AppTheme::Dark);
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
let _ = window.emit("theme-change", "dark");
|
||
}
|
||
}
|
||
"theme_system" => {
|
||
info!(" [托盘] '跟随系统' 被点击");
|
||
let state = app.state::<AppState>();
|
||
state.set_theme(AppTheme::System);
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
let _ = window.emit("theme-change", "system");
|
||
}
|
||
}
|
||
"quit_now" => {
|
||
info!(" [托盘] '完全退出' 被点击");
|
||
// 允许完全退出
|
||
let state = app.state::<AppState>();
|
||
state.allow_exit();
|
||
// 关闭窗口
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
let _ = window.close();
|
||
}
|
||
// 退出应用
|
||
app.exit(0);
|
||
}
|
||
_ => {}
|
||
}
|
||
})
|
||
.build(app)?;
|
||
|
||
info!(" ✓ 托盘图标创建成功");
|
||
info!("系统托盘设置完成");
|
||
|
||
Ok(())
|
||
}
|