crosssubtitle-ai/src-tauri/src/lib.rs
2026-05-02 16:10:27 +08:00

235 lines
7.4 KiB
Rust

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<SubtitleTask, String> {
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<Vec<SubtitleTask>, 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<SubtitleTask, String> {
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<String, String> {
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<DefaultModelPaths, String> {
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<SubtitleTask, String> {
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::<NSWindow>();
let aspect_ratio = NSSize::new(WINDOW_RATIO_WIDTH, WINDOW_RATIO_HEIGHT);
unsafe {
let ns_window = &*ns_window;
ns_window.setContentAspectRatio(aspect_ratio);
}
Ok(())
}