Font import: - Migration M6 adds user_fonts; files are stored content-addressed under the local data dir. New Rust commands inspect, import, list, read and remove fonts, refusing WOFF2, collections, variable fonts and restricted fsType, and unpacking WOFF1 to plain sfnt. - Imported fonts register in the render worker as user:<Face> families with an italic alias, and are pinned in the frozen render prefs so issued invoices can still re-render. Fonts referenced by issued invoices are hidden, never deleted. - Size and tracking compensations tuned for the substitutes apply only while the face is not overridden; default output and fingerprint goldens are unchanged. - Settings gets a Fonts section with a licence acknowledgement. Void packaging: - Install fonts to /usr/share/fonts/voiced with per-family OFL licences, set font_dirs, and add INSTALL/REMOVE scripts that run fc-cache. xbps-create carries both scripts (verified in a void-glibc container). - Fix xbps-create dependency specs in build-xbps.sh.
163 lines
6.2 KiB
Rust
163 lines
6.2 KiB
Rust
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<Connection>,
|
|
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<AppState, Box<dyn std::error::Error>> {
|
|
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);
|
|
}
|
|
}
|