mod commands; mod db; mod gst; mod logo; mod models; mod selftest; use rusqlite::Connection; use std::path::{Path, PathBuf}; use std::sync::Mutex; use tauri::Manager; use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; pub struct AppState { pub db: Mutex, pub data_dir: PathBuf, /// Machine-local, non-roaming data (the PDF archive). On Windows this is /// %LOCALAPPDATA%, unlike `data_dir`, which is the roaming location. pub local_data_dir: PathBuf, } /// Show a native error dialog. Release builds have no console on Windows, so /// without this a startup failure would look like the app silently not opening. fn show_startup_error(app: &tauri::App, error: &dyn std::fmt::Display, data_dir: Option<&Path>) { let location = data_dir .map(|d| format!("\n\nData directory:\n{}", d.display())) .unwrap_or_default(); eprintln!("Voiced could not start: {error}"); app.dialog() .message(format!("{error}{location}")) .title("Voiced could not start") .kind(MessageDialogKind::Error) .blocking_show(); } fn init_state(app: &tauri::App) -> Result> { let data_dir = match app.path().app_data_dir() { Ok(dir) => dir, Err(e) => { show_startup_error(app, &format!("Could not resolve the application data directory: {e}"), None); return Err(e.into()); } }; if let Err(e) = std::fs::create_dir_all(&data_dir) { show_startup_error(app, &format!("Could not create the application data directory: {e}"), Some(&data_dir)); return Err(e.into()); } let local_data_dir = match app.path().app_local_data_dir() { Ok(dir) => dir, Err(e) => { show_startup_error(app, &format!("Could not resolve the local data directory: {e}"), Some(&data_dir)); return Err(e.into()); } }; if let Err(e) = std::fs::create_dir_all(&local_data_dir) { show_startup_error(app, &format!("Could not create the local data directory: {e}"), Some(&local_data_dir)); return Err(e.into()); } let conn = match db::open(&data_dir.join("voiced.db"), &data_dir.join("backups")) { Ok(conn) => conn, Err(e) => { show_startup_error(app, &format!("Could not open the database: {e}"), Some(&data_dir)); return Err(e.into()); } }; if let Err(e) = db::relativize_asset_paths(&conn, &data_dir) { show_startup_error(app, &format!("Could not update stored image paths: {e}"), Some(&data_dir)); return Err(e.into()); } // Best effort: a logo that cannot be derived keeps working untrimmed, and the next start retries. if let Err(e) = commands::logo::ensure_logo_derived_impl(&conn, &data_dir) { eprintln!("Could not derive the logo images: {e}"); } Ok(AppState { db: Mutex::new(conn), data_dir, local_data_dir, }) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // WebKitGTK's DMABUF renderer shows a blank window on several Linux GPU/driver // combinations. Respect an explicit user setting, otherwise turn it off. #[cfg(target_os = "linux")] if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); } let builder = tauri::Builder::default(); // The single-instance plugin must be registered before every other plugin. #[cfg(desktop)] let builder = builder .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); } })) .plugin(tauri_plugin_window_state::Builder::default().build()); let result = builder .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .setup(|app| { let state = init_state(app)?; app.manage(state); Ok(()) }) .invoke_handler(tauri::generate_handler![ commands::settings::get_settings, commands::settings::save_settings, commands::settings::list_banks, commands::settings::save_bank, commands::settings::delete_bank, commands::series::get_active_series, commands::series::list_series, commands::series::start_new_series, commands::clients::list_clients, commands::clients::save_client, commands::clients::delete_client, commands::invoice::peek_next_invoice_number, commands::invoice::issue_invoice, commands::invoice::cancel_invoice, commands::invoice::list_invoices, commands::invoice::get_invoice, commands::invoice::save_draft, commands::invoice::list_drafts, commands::invoice::get_draft, commands::invoice::delete_draft, commands::assets::import_asset, commands::assets::save_asset_bytes, commands::assets::read_asset_data_uri, commands::assets::remove_asset, commands::logo::import_logo, commands::logo::save_logo_bytes, commands::logo::ensure_logo_derived, commands::logo::remove_logo, commands::archive::archive_pdf, commands::archive::read_archive, commands::archive::archive_status, commands::fonts::inspect_font, commands::fonts::import_font, commands::fonts::list_user_fonts, commands::fonts::get_user_font_bytes, commands::fonts::remove_user_font, commands::files::write_export_file, commands::files::get_last_export_dir, commands::files::reveal_in_folder, commands::files::open_file, selftest::selftest_config, selftest::selftest_report, ]) .run(tauri::generate_context!()); if let Err(e) = result { eprintln!("error while running the Voiced application: {e}"); std::process::exit(1); } }