Admin 08f42b7a32
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
feat(desktop): persist auth and install Linux updates in app
2026-09-24 18:36:34 +00:00

236 lines
7.3 KiB
Rust

mod capture;
mod fieldlens;
mod model;
mod printers;
mod state;
mod updates;
mod zpl;
use parking_lot::Mutex;
use tauri::{AppHandle, Manager, State};
use capture::CaptureRuntime;
use model::{AppConfig, AuthStatus, CaptureStatus, JobSummary, LoginPoll, LoginStart, SyncResult};
use printers::{SystemPrinter, VirtualPrinterStatus};
use state::SharedState;
struct AppState {
shared: SharedState,
runtime: Mutex<Option<CaptureRuntime>>,
}
#[tauri::command]
fn get_config(state: State<'_, AppState>) -> AppConfig {
state.shared.config.lock().clone()
}
#[tauri::command]
fn save_config(config: AppConfig, state: State<'_, AppState>) -> Result<AppConfig, String> {
state
.shared
.replace_config(config)
.map_err(|error| error.to_string())
}
#[tauri::command]
fn get_status(state: State<'_, AppState>) -> CaptureStatus {
CaptureStatus {
running: state.runtime.lock().is_some(),
mode: state.shared.config.lock().mode.clone(),
pending: state.shared.pending_count(),
jobs: state.shared.jobs.lock().len(),
}
}
#[tauri::command]
fn recent_jobs(state: State<'_, AppState>) -> Vec<JobSummary> {
state.shared.jobs.lock().iter().cloned().collect()
}
#[tauri::command]
fn start_capture(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
let mut runtime = state.runtime.lock();
if runtime.is_some() {
return Ok(());
}
let config = state.shared.config.lock().clone();
let next = capture::start(config, state.shared.clone(), app).map_err(|error| error.to_string())?;
*runtime = Some(next);
Ok(())
}
#[tauri::command]
fn stop_capture(state: State<'_, AppState>) {
if let Some(runtime) = state.runtime.lock().take() {
runtime.stop();
}
}
#[tauri::command]
fn list_system_printers() -> Result<Vec<SystemPrinter>, String> {
printers::list_system_printers().map_err(|error| error.to_string())
}
#[tauri::command]
fn get_virtual_printer_status() -> Result<VirtualPrinterStatus, String> {
printers::virtual_printer_status().map_err(|error| error.to_string())
}
#[tauri::command]
fn install_virtual_printer(target_printer: String) -> Result<VirtualPrinterStatus, String> {
printers::install_virtual_printer(&target_printer).map_err(|error| error.to_string())
}
#[tauri::command]
fn remove_virtual_printer() -> Result<VirtualPrinterStatus, String> {
printers::remove_virtual_printer().map_err(|error| error.to_string())
}
#[tauri::command]
fn begin_login(state: State<'_, AppState>) -> Result<LoginStart, String> {
fieldlens::begin_login(&state.shared).map_err(|error| error.to_string())
}
#[tauri::command]
fn poll_login(state: State<'_, AppState>) -> Result<LoginPoll, String> {
fieldlens::poll_login(&state.shared).map_err(|error| error.to_string())
}
#[tauri::command]
fn get_auth_status(state: State<'_, AppState>) -> Result<AuthStatus, String> {
fieldlens::auth_status(&state.shared).map_err(|error| error.to_string())
}
#[tauri::command]
fn logout(state: State<'_, AppState>) -> Result<(), String> {
fieldlens::logout(&state.shared).map_err(|error| error.to_string())
}
#[tauri::command]
async fn install_linux_update() -> Result<updates::UpdateInstallResult, String> {
tauri::async_runtime::spawn_blocking(updates::install_linux_update)
.await
.map_err(|error| error.to_string())?
.map_err(|error| error.to_string())
}
#[cfg(target_os = "linux")]
fn build_self_test_pdf() -> Vec<u8> {
let objects = [
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".to_string(),
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n".to_string(),
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>\nendobj\n".to_string(),
{
let stream = "BT /F1 18 Tf 72 720 Td (FieldLens capture self-test) Tj ET\n";
format!("4 0 obj\n<< /Length {} >>\nstream\n{}endstream\nendobj\n", stream.len(), stream)
},
"5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n".to_string(),
];
let mut pdf = b"%PDF-1.4\n".to_vec();
let mut offsets = Vec::new();
for object in &objects {
offsets.push(pdf.len());
pdf.extend_from_slice(object.as_bytes());
}
let xref = pdf.len();
pdf.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
pdf.extend_from_slice(b"0000000000 65535 f \n");
for offset in offsets {
pdf.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
pdf.extend_from_slice(
format!(
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n",
objects.len() + 1,
xref
)
.as_bytes(),
);
pdf
}
#[tauri::command]
fn run_capture_self_test() -> Result<String, String> {
#[cfg(target_os = "linux")]
{
let path = std::env::temp_dir().join(format!(
"fieldlens-capture-self-test-{}.pdf",
uuid::Uuid::new_v4()
));
std::fs::write(&path, build_self_test_pdf()).map_err(|error| error.to_string())?;
let output = std::process::Command::new("lp")
.args(["-d", "FieldLens-Test-Capture"])
.arg(&path)
.output()
.map_err(|error| error.to_string());
let _ = std::fs::remove_file(&path);
let output = output?;
if !output.status.success() {
return Err(format!(
"CUPS self-test submission failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
let detail = String::from_utf8_lossy(&output.stdout).trim().to_string();
return Ok(if detail.is_empty() {
"Self-test submitted to FieldLens Test Capture.".into()
} else {
detail
});
}
#[cfg(not(target_os = "linux"))]
{
Err("Capture self-test is currently available on Linux only.".into())
}
}
#[tauri::command]
fn sync_pending(state: State<'_, AppState>) -> Result<SyncResult, String> {
fieldlens::sync_pending(&state.shared).map_err(|error| error.to_string())
}
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
let data_dir = app.path().app_data_dir()?;
let shared = SharedState::load(data_dir)
.map_err(|error| std::io::Error::other(error.to_string()))?;
app.manage(AppState {
shared,
runtime: Mutex::new(None),
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_config,
save_config,
get_status,
recent_jobs,
start_capture,
stop_capture,
list_system_printers,
get_virtual_printer_status,
install_virtual_printer,
remove_virtual_printer,
run_capture_self_test,
begin_login,
poll_login,
get_auth_status,
logout,
install_linux_update,
sync_pending
])
.run(tauri::generate_context!())
.expect("error while running FieldLens Print");
}