impress_asr_input_rust/src/app/mod.rs
impressionyang 7ad06cc54a
Some checks are pending
Build Windows GUI / build-windows (push) Waiting to run
Build Windows GUI / release (push) Blocked by required conditions
feat: 完善托盘功能、完全退出和主题切换
托盘功能:
- 添加主题子菜单(浅色/深色/跟随系统)
- 添加完全退出菜单项
- 托盘图标使用应用默认图标

完全退出功能:
- 新增 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
2026-05-21 18:07:38 +08:00

368 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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(())
}