mod audio; mod models; mod state; mod subtitle; mod task; mod translate; mod vad; mod whisper; use models::{ DefaultModelPaths, StartTaskPayload, SubtitleSegment, SubtitleTask, TranslationConfig, }; use state::AppState; use tauri::{AppHandle, Manager, PhysicalSize, Size}; #[cfg(target_os = "macos")] use tauri::{ menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, Emitter, }; #[cfg(target_os = "macos")] use objc2_app_kit::NSWindow; #[cfg(target_os = "macos")] use objc2_foundation::NSSize; #[cfg(target_os = "macos")] const WINDOW_RATIO_WIDTH: f64 = 16.0; #[cfg(target_os = "macos")] const WINDOW_RATIO_HEIGHT: f64 = 10.0; const DEFAULT_WINDOW_WIDTH: u32 = 1440; const DEFAULT_WINDOW_HEIGHT: u32 = 900; const MIN_WINDOW_WIDTH: u32 = 1280; const MIN_WINDOW_HEIGHT: u32 = 800; #[tauri::command] async fn start_subtitle_task( app: tauri::AppHandle, window: tauri::Window, state: tauri::State<'_, AppState>, payload: StartTaskPayload, ) -> std::result::Result { task::start_task(app, window, state, payload) .await .map_err(error_to_string) } #[tauri::command] fn list_tasks(state: tauri::State<'_, AppState>) -> std::result::Result, String> { task::list_tasks(state).map_err(error_to_string) } #[tauri::command] fn update_segment_text( state: tauri::State<'_, AppState>, segment: SubtitleSegment, ) -> std::result::Result { task::update_segment_text(state, segment).map_err(error_to_string) } #[tauri::command] fn delete_task( state: tauri::State<'_, AppState>, task_id: String, ) -> std::result::Result<(), String> { task::delete_task(state, task_id).map_err(error_to_string) } #[tauri::command] fn export_subtitles( state: tauri::State<'_, AppState>, task_id: String, format: String, ) -> std::result::Result { task::export_task(state, task_id, format).map_err(error_to_string) } #[tauri::command] fn get_default_model_paths(app: tauri::AppHandle) -> std::result::Result { task::get_default_model_paths(&app).map_err(error_to_string) } #[tauri::command] async fn retry_translation( app: tauri::AppHandle, window: tauri::Window, state: tauri::State<'_, AppState>, task_id: String, translation_config: TranslationConfig, ) -> std::result::Result { task::retry_translation(app, window, state, task_id, translation_config) .await .map_err(error_to_string) } fn error_to_string(error: anyhow::Error) -> String { format!("{error:#}") } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .manage(AppState::default()) .setup(|app| { configure_window(app.handle())?; #[cfg(target_os = "macos")] configure_macos_menu(app.handle())?; Ok(()) }) .invoke_handler(tauri::generate_handler![ start_subtitle_task, list_tasks, update_segment_text, delete_task, export_subtitles, get_default_model_paths, retry_translation ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } fn configure_window(app: &AppHandle) -> tauri::Result<()> { if let Some(window) = app.get_webview_window("main") { window.set_min_size(Some(Size::Physical(PhysicalSize::new( MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, ))))?; window.set_size(Size::Physical(PhysicalSize::new( DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT, )))?; #[cfg(target_os = "macos")] apply_macos_aspect_ratio(&window)?; } Ok(()) } #[cfg(target_os = "macos")] fn configure_macos_menu(app: &AppHandle) -> tauri::Result<()> { let app_name = app.package_info().name.clone(); let app_menu = SubmenuBuilder::new(app, app_name) .item(&PredefinedMenuItem::about(app, None, None)?) .separator() .item(&PredefinedMenuItem::services(app, None)?) .separator() .item(&PredefinedMenuItem::hide(app, None)?) .item(&PredefinedMenuItem::hide_others(app, None)?) .item(&PredefinedMenuItem::show_all(app, None)?) .separator() .item(&PredefinedMenuItem::quit(app, None)?) .build()?; let file_menu = SubmenuBuilder::new(app, "文件") .item(&MenuItemBuilder::with_id("pick_files", "选择媒体文件").accelerator("CmdOrCtrl+O").build(app)?) .separator() .item(&MenuItemBuilder::with_id("export_srt", "导出 SRT").build(app)?) .item(&MenuItemBuilder::with_id("export_vtt", "导出 VTT").build(app)?) .item(&MenuItemBuilder::with_id("export_ass", "导出 ASS").build(app)?) .build()?; let edit_menu = SubmenuBuilder::new(app, "编辑") .item(&PredefinedMenuItem::undo(app, None)?) .item(&PredefinedMenuItem::redo(app, None)?) .separator() .item(&PredefinedMenuItem::cut(app, None)?) .item(&PredefinedMenuItem::copy(app, None)?) .item(&PredefinedMenuItem::paste(app, None)?) .item(&PredefinedMenuItem::select_all(app, None)?) .build()?; let settings_menu = SubmenuBuilder::new(app, "设置") .item(&MenuItemBuilder::with_id("toggle_advanced", "显示或隐藏高级设置").build(app)?) .item(&MenuItemBuilder::with_id("toggle_bilingual", "切换双语导出").build(app)?) .item(&MenuItemBuilder::with_id("reset_models", "恢复默认模型路径").build(app)?) .build()?; let language_menu = SubmenuBuilder::new(app, "语言") .item(&MenuItemBuilder::with_id("set_lang_zh_cn", "中文").build(app)?) .item(&MenuItemBuilder::with_id("set_lang_en", "English").build(app)?) .build()?; let window_menu = SubmenuBuilder::new(app, "窗口") .item(&PredefinedMenuItem::minimize(app, None)?) .item(&PredefinedMenuItem::maximize(app, None)?) .separator() .item(&PredefinedMenuItem::close_window(app, None)?) .build()?; let menu = MenuBuilder::new(app) .item(&app_menu) .item(&file_menu) .item(&edit_menu) .item(&settings_menu) .item(&language_menu) .item(&window_menu) .build()?; app.set_menu(menu)?; app.on_menu_event(|app, event| { let action = match event.id().0.as_str() { "pick_files" => Some("pick-files"), "export_srt" => Some("export-srt"), "export_vtt" => Some("export-vtt"), "export_ass" => Some("export-ass"), "toggle_advanced" => Some("toggle-advanced"), "toggle_bilingual" => Some("toggle-bilingual"), "reset_models" => Some("reset-models"), "set_lang_zh_cn" => Some("set-lang-zh-CN"), "set_lang_en" => Some("set-lang-en"), _ => None, }; if let Some(action) = action { let _ = app.emit("menu:action", action); } }); Ok(()) } #[cfg(target_os = "macos")] fn apply_macos_aspect_ratio(window: &tauri::WebviewWindow) -> tauri::Result<()> { let ns_window = window.ns_window()?; let ns_window = ns_window.cast::(); let aspect_ratio = NSSize::new(WINDOW_RATIO_WIDTH, WINDOW_RATIO_HEIGHT); unsafe { let ns_window = &*ns_window; ns_window.setContentAspectRatio(aspect_ratio); } Ok(()) }