From 8323fdd73272a2882781aba3c499ba0be3dff2a6 Mon Sep 17 00:00:00 2001 From: UMTS at Teleco Date: Sat, 13 Dec 2025 02:51:15 +0100 Subject: committing to insanity --- src/core/print/ui/print_dialog.rs | 999 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 999 insertions(+) create mode 100644 src/core/print/ui/print_dialog.rs (limited to 'src/core/print/ui/print_dialog.rs') diff --git a/src/core/print/ui/print_dialog.rs b/src/core/print/ui/print_dialog.rs new file mode 100644 index 0000000..8ac503a --- /dev/null +++ b/src/core/print/ui/print_dialog.rs @@ -0,0 +1,999 @@ +use anyhow::Result; +use eframe::egui; +use serde_json::Value; +use std::collections::HashMap; + +use crate::api::ApiClient; +use crate::core::print::parsing::{parse_layout_json, parse_printer_settings, PrinterSettings}; +use crate::core::print::plugins::pdf::PdfPlugin; +use crate::core::print::printer_manager::{PrinterInfo, SharedPrinterManager}; +use crate::core::print::renderer::LabelRenderer; +use poll_promise::Promise; +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PaperSizeOverride { + UseSaved, + A4, + Letter, + Custom, +} + +/// Print options selected by user +#[derive(Debug, Clone)] +pub struct PrintOptions { + pub printer_id: Option, + pub printer_name: String, + pub label_template_id: Option, + pub label_template_name: String, + pub copies: i32, +} + +impl Default for PrintOptions { + fn default() -> Self { + Self { + printer_id: None, + printer_name: String::new(), + label_template_id: None, + label_template_name: String::new(), + copies: 1, + } + } +} + +/// Print dialog for selecting printer, template, and preview +pub struct PrintDialog { + options: PrintOptions, + pub asset_data: HashMap, + printers: Vec, + templates: Vec, + renderer: Option, + preview_scale: f32, + error_message: Option, + loading: bool, + // Promise for handling async PDF export + pdf_export_promise: Option>>, + // OS printer fallback popup + os_popup_visible: bool, + os_printers: Vec, + os_selected_index: usize, + os_print_path: Option, + os_error_message: Option, + os_base_settings: Option, + os_renderer: Option, + os_size_override: PaperSizeOverride, + os_custom_width_mm: f32, + os_custom_height_mm: f32, +} + +impl PrintDialog { + /// Create new print dialog with asset data + pub fn new(asset_data: HashMap) -> Self { + Self { + options: PrintOptions::default(), + asset_data, + printers: Vec::new(), + templates: Vec::new(), + renderer: None, + preview_scale: 3.78, // Default scale: 1mm = 3.78px at 96 DPI + error_message: None, + loading: false, + pdf_export_promise: None, + os_popup_visible: false, + os_printers: Vec::new(), + os_selected_index: 0, + os_print_path: None, + os_error_message: None, + os_base_settings: None, + os_renderer: None, + os_size_override: PaperSizeOverride::UseSaved, + os_custom_width_mm: 0.0, + os_custom_height_mm: 0.0, + } + } + + /// Initialize with default printer and template if available + pub fn with_defaults( + mut self, + default_printer_id: Option, + label_template_id: Option, + last_printer_id: Option, + ) -> Self { + // Prefer last-used printer if available, otherwise fall back to default + self.options.printer_id = last_printer_id.or(default_printer_id); + // Label template is *not* persisted across sessions; if none is set on the asset, + // the dialog will require the user to choose one. + self.options.label_template_id = label_template_id; + self + } + + /// Load printers and templates from API + pub fn load_data(&mut self, api_client: &ApiClient) -> Result<()> { + self.loading = true; + self.error_message = None; + + // Load printers + match crate::core::tables::get_printers(api_client) { + Ok(printers) => self.printers = printers, + Err(e) => { + self.error_message = Some(format!("Failed to load printers: {}", e)); + return Err(e); + } + } + + // Load templates + match crate::core::tables::get_label_templates(api_client) { + Ok(templates) => self.templates = templates, + Err(e) => { + self.error_message = Some(format!("Failed to load templates: {}", e)); + return Err(e); + } + } + + // Set default selections if IDs provided + if let Some(printer_id) = self.options.printer_id { + if let Some(printer) = self + .printers + .iter() + .find(|p| p.get("id").and_then(|v| v.as_i64()) == Some(printer_id)) + { + self.options.printer_name = printer + .get("printer_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + // Fetch printer_settings for preview sizing/orientation + let resp = api_client.select( + "printer_settings", + Some(vec!["printer_settings".into()]), + Some(serde_json::json!({"id": printer_id})), + None, + Some(1), + )?; + if let Some(first) = resp.data.as_ref().and_then(|d| d.get(0)) { + if let Some(ps_val) = first.get("printer_settings") { + if let Ok(ps) = parse_printer_settings(ps_val) { + self.os_base_settings = Some(ps); + } + } + } + } + } + + if let Some(template_id) = self.options.label_template_id { + if let Some(template) = self + .templates + .iter() + .find(|t| t.get("id").and_then(|v| v.as_i64()) == Some(template_id)) + { + let template_name = template + .get("template_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + self.options.label_template_name = template_name.clone(); + + // Load renderer for preview + if let Some(layout_json) = template.get("layout_json").and_then(|v| v.as_str()) { + if layout_json.trim().is_empty() { + log::warn!("Label template '{}' has empty layout_json", template_name); + self.error_message = Some("This label template has no layout defined. Please edit the template in Label Templates view.".to_string()); + } else { + match LabelRenderer::from_json(layout_json) { + Ok(renderer) => self.renderer = Some(renderer), + Err(e) => { + log::warn!( + "Failed to parse label layout for '{}': {}", + template_name, + e + ); + self.error_message = Some(format!("Invalid template layout JSON. Please fix in Label Templates view.\n\nError: {}", e)); + } + } + } + } else { + log::warn!( + "Label template '{}' missing layout_json field", + template_name + ); + self.error_message = Some( + "This label template is missing layout data. Please edit the template." + .to_string(), + ); + } + } + } + + self.loading = false; + Ok(()) + } + + /// Show the dialog and return true if user clicked Print and the action is complete + pub fn show( + &mut self, + ctx: &egui::Context, + open: &mut bool, + api_client: Option<&ApiClient>, + ) -> bool { + let mut print_action_complete = false; + let mut close_dialog = false; + + if let Some(_response) = egui::Window::new("Print Label") + .open(open) + .resizable(true) + .default_width(600.0) + .default_height(500.0) + .show(ctx, |ui| { + // Load data if not loaded yet + if self.printers.is_empty() && !self.loading && api_client.is_some() { + if let Err(e) = self.load_data(api_client.unwrap()) { + log::error!("Failed to load print data: {}", e); + } + } + + // Show error if any + if let Some(error) = &self.error_message { + ui.colored_label(egui::Color32::RED, error); + ui.add_space(8.0); + } + + // Show loading spinner + if self.loading { + ui.horizontal(|ui| { + ui.spinner(); + ui.label("Loading printers and templates..."); + }); + return; + } + + // Options panel + egui::ScrollArea::vertical() + .id_salt("print_options_scroll") + .show(ui, |ui| { + self.show_options(ui); + ui.add_space(12.0); + self.show_preview(ui); + }); + + // Handle PDF export promise + if let Some(promise) = &self.pdf_export_promise { + if let Some(result) = promise.ready() { + match result { + Some(path) => { + log::info!("PDF export promise ready, path: {:?}", path); + // The file dialog is done, now we can save the file. + // We need the ApiClient and other details again. + if let Some(client) = api_client { + if let Err(e) = self.execute_pdf_export(path, client) { + self.error_message = + Some(format!("Failed to export PDF: {}", e)); + } else { + // Successfully exported, close dialog + print_action_complete = true; + close_dialog = true; + } + } else { + self.error_message = Some( + "API client not available for PDF export.".to_string(), + ); + } + } + None => { + // User cancelled the dialog + log::info!("PDF export cancelled by user."); + } + } + self.pdf_export_promise = None; // Consume the promise + } else { + ui.spinner(); + ui.label("Waiting for file path..."); + } + } + + // Bottom buttons + ui.add_space(8.0); + ui.separator(); + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + close_dialog = true; + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let can_print = self.options.printer_id.is_some() + && self.options.label_template_id.is_some() + && self.options.copies > 0 + && self.pdf_export_promise.is_none(); // Disable while waiting for path + + ui.add_enabled_ui(can_print, |ui| { + if ui.button("Print").clicked() { + if let Some(client) = api_client { + match self.execute_print(client) { + Ok(completed) => { + if completed { + print_action_complete = true; + close_dialog = true; + } + // if not completed, dialog stays open for promise + } + Err(e) => { + self.error_message = + Some(format!("Print error: {}", e)); + } + } + } else { + self.error_message = + Some("API Client not available.".to_string()); + } + } + }); + }); + }); + }) + { + // Window was shown + } + + // Render OS printer fallback popup if requested + if self.os_popup_visible { + let mut close_os_popup = false; + let mut keep_open_flag = true; + egui::Window::new("Select System Printer") + .collapsible(false) + .resizable(true) + .default_width(420.0) + .open(&mut keep_open_flag) + .show(ctx, |ui| { + if let Some(err) = &self.os_error_message { + ui.colored_label(egui::Color32::RED, err); + ui.separator(); + } + if self.os_printers.is_empty() { + let mgr = SharedPrinterManager::new(); + self.os_printers = mgr.get_printers(); + if let Some(base) = &self.os_base_settings { + if let Some(target_name) = &base.printer_name { + if let Some((idx, _)) = self + .os_printers + .iter() + .enumerate() + .find(|(_, p)| &p.name == target_name) + { + self.os_selected_index = idx; + } + } + } + } + if self.os_printers.is_empty() { + ui.label("No system printers found."); + } else { + if self.os_selected_index >= self.os_printers.len() { + self.os_selected_index = 0; + } + let current = self + .os_printers + .get(self.os_selected_index) + .map(|p| p.name.clone()) + .unwrap_or_default(); + egui::ComboBox::from_id_salt("os_printers_combo") + .selected_text(if current.is_empty() { "Select printer" } else { ¤t }) + .show_ui(ui, |ui| { + for (i, p) in self.os_printers.iter().enumerate() { + if ui + .selectable_label(i == self.os_selected_index, &p.name) + .clicked() + { + self.os_selected_index = i; + } + } + }); + } + ui.separator(); + + if let Some(base) = &self.os_base_settings { + let saved_label = format!( + "Use saved ({})", + base.paper_size.as_str() + ); + + egui::ComboBox::from_id_salt("os_size_override") + .selected_text(match self.os_size_override { + PaperSizeOverride::UseSaved => saved_label.clone(), + PaperSizeOverride::A4 => "A4 (210×297 mm)".into(), + PaperSizeOverride::Letter => "Letter (215.9×279.4 mm)".into(), + PaperSizeOverride::Custom => "Custom size".into(), + }) + .show_ui(ui, |ui| { + if ui + .selectable_value( + &mut self.os_size_override, + PaperSizeOverride::UseSaved, + saved_label, + ) + .clicked() + { + self.os_error_message = None; + } + if ui + .selectable_value( + &mut self.os_size_override, + PaperSizeOverride::A4, + "A4 (210×297 mm)", + ) + .clicked() + { + self.os_error_message = None; + } + if ui + .selectable_value( + &mut self.os_size_override, + PaperSizeOverride::Letter, + "Letter (215.9×279.4 mm)", + ) + .clicked() + { + self.os_error_message = None; + } + if ui + .selectable_value( + &mut self.os_size_override, + PaperSizeOverride::Custom, + "Custom size", + ) + .clicked() + { + if base.custom_width_mm.is_some() + && base.custom_height_mm.is_some() + { + let (w, h) = base.get_dimensions_mm(); + self.os_custom_width_mm = w; + self.os_custom_height_mm = h; + } else { + self.os_custom_width_mm = 0.0; + self.os_custom_height_mm = 0.0; + } + self.os_error_message = None; + } + }); + + if matches!(self.os_size_override, PaperSizeOverride::Custom) { + ui.vertical(|ui| { + ui.label("Custom page size (mm)"); + ui.horizontal(|ui| { + ui.label("Width:"); + ui.add( + egui::DragValue::new(&mut self.os_custom_width_mm) + .range(10.0..=600.0) + .suffix(" mm"), + ); + ui.label("Height:"); + ui.add( + egui::DragValue::new(&mut self.os_custom_height_mm) + .range(10.0..=600.0) + .suffix(" mm"), + ); + }); + }); + } + } + + ui.separator(); + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + self.os_print_path = None; + self.os_base_settings = None; + close_os_popup = true; + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let can_print = !self.os_printers.is_empty() + && self + .os_printers + .get(self.os_selected_index) + .is_some(); + ui.add_enabled_ui(can_print, |ui| { + if ui.button("Print").clicked() { + let selected_name = self + .os_printers + .get(self.os_selected_index) + .map(|p| p.name.clone()); + if let Some(name) = selected_name { + match self.print_via_os_popup(&name) { + Ok(true) => { + self.os_print_path = None; + self.os_base_settings = None; + close_os_popup = true; + print_action_complete = true; + close_dialog = true; + } + Ok(false) => { /* not used: function only returns true on success */ } + Err(e) => { + self.os_error_message = Some(e); + } + } + } + } + }); + }); + }); + }); + // Apply window close state after rendering + if !keep_open_flag { + close_os_popup = true; + } + if close_os_popup { + self.os_popup_visible = false; + self.os_base_settings = None; + } + } + + if close_dialog { + *open = false; + } + + print_action_complete + } + + /// Show options section + fn show_options(&mut self, ui: &mut egui::Ui) { + ui.heading("Print Options"); + ui.add_space(8.0); + + egui::Grid::new("print_options_grid") + .num_columns(2) + .spacing([8.0, 8.0]) + .show(ui, |ui| { + // Printer selection + ui.label("Printer:"); + egui::ComboBox::from_id_salt("printer_select") + .selected_text(&self.options.printer_name) + .width(300.0) + .show_ui(ui, |ui| { + for printer in &self.printers { + let printer_id = printer.get("id").and_then(|v| v.as_i64()); + let printer_name = printer + .get("printer_name") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + + if ui + .selectable_label( + self.options.printer_id == printer_id, + printer_name, + ) + .clicked() + { + self.options.printer_id = printer_id; + self.options.printer_name = printer_name.to_string(); + // Try to parse printer settings for preview (if provided by the DB row) + if let Some(ps_val) = printer.get("printer_settings") { + match parse_printer_settings(ps_val) { + Ok(ps) => { + self.os_base_settings = Some(ps); + } + Err(e) => { + log::warn!( + "Failed to parse printer_settings for preview: {}", + e + ); + self.os_base_settings = None; + } + } + } else { + self.os_base_settings = None; + } + } + } + }); + ui.end_row(); + + // Template selection + ui.label("Label Template:"); + egui::ComboBox::from_id_salt("template_select") + .selected_text(&self.options.label_template_name) + .width(300.0) + .show_ui(ui, |ui| { + for template in &self.templates { + let template_id = template.get("id").and_then(|v| v.as_i64()); + let template_name = template + .get("template_name") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + + if ui + .selectable_label( + self.options.label_template_id == template_id, + template_name, + ) + .clicked() + { + self.options.label_template_id = template_id; + self.options.label_template_name = template_name.to_string(); + + // Update renderer + if let Some(layout_json) = + template.get("layout_json").and_then(|v| v.as_str()) + { + match LabelRenderer::from_json(layout_json) { + Ok(renderer) => { + self.renderer = Some(renderer); + self.error_message = None; + } + Err(e) => { + log::warn!("Failed to parse label layout: {}", e); + self.error_message = + Some(format!("Invalid template: {}", e)); + self.renderer = None; + } + } + } + } + } + }); + ui.end_row(); + + // Number of copies + ui.label("Copies:"); + ui.add(egui::DragValue::new(&mut self.options.copies).range(1..=99)); + ui.end_row(); + }); + } + + /// Show preview section + fn show_preview(&mut self, ui: &mut egui::Ui) { + ui.add_space(8.0); + ui.heading("Preview"); + ui.add_space(8.0); + + // Preview scale control + ui.horizontal(|ui| { + ui.label("Scale:"); + ui.add(egui::Slider::new(&mut self.preview_scale, 2.0..=8.0).suffix("x")); + }); + + ui.add_space(8.0); + + // Render preview + if let Some(renderer) = &self.renderer { + egui::ScrollArea::both() // Enable both horizontal and vertical scrolling + .max_height(300.0) + .auto_shrink([false, false]) // Don't shrink in either direction + .show(ui, |ui| { + egui::Frame::new() + .fill(egui::Color32::from_gray(240)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200))) + .inner_margin(16.0) + .show(ui, |ui| { + if let Err(e) = renderer.render_preview( + ui, + &self.asset_data, + self.preview_scale, + self.os_base_settings.as_ref(), + ) { + ui.colored_label( + egui::Color32::RED, + format!("Preview error: {}", e), + ); + } + }); + }); + } else { + ui.colored_label( + egui::Color32::from_gray(150), + "Select a label template to see preview", + ); + } + } + + /// Get asset data reference + pub fn asset_data(&self) -> &HashMap { + &self.asset_data + } + + /// Get current print options + pub fn options(&self) -> &PrintOptions { + &self.options + } + + /// Executes the actual PDF file saving. This is called after the promise resolves. + fn execute_pdf_export(&self, path: &PathBuf, api_client: &ApiClient) -> Result<()> { + let template_id = self + .options + .label_template_id + .ok_or_else(|| anyhow::anyhow!("No template selected"))?; + let printer_id = self + .options + .printer_id + .ok_or_else(|| anyhow::anyhow!("No printer selected"))?; + + // Fetch template + let template_resp = api_client.select( + "label_templates", + Some(vec!["layout_json".into()]), + Some(serde_json::json!({"id": template_id})), + None, + Some(1), + )?; + let template_data = template_resp + .data + .as_ref() + .and_then(|d| d.get(0)) + .ok_or_else(|| anyhow::anyhow!("Template not found"))?; + let layout_json = template_data + .get("layout_json") + .ok_or_else(|| anyhow::anyhow!("No layout JSON"))?; + let layout = parse_layout_json(layout_json)?; + + // Fetch printer settings + let printer_resp = api_client.select( + "printer_settings", + Some(vec!["printer_settings".into()]), + Some(serde_json::json!({"id": printer_id})), + None, + Some(1), + )?; + let printer_data = printer_resp + .data + .as_ref() + .and_then(|d| d.get(0)) + .ok_or_else(|| anyhow::anyhow!("Printer settings not found"))?; + let printer_settings_value = printer_data + .get("printer_settings") + .ok_or_else(|| anyhow::anyhow!("No printer settings JSON"))?; + let printer_settings = parse_printer_settings(printer_settings_value)?; + + // Generate and save PDF + let renderer = LabelRenderer::new(layout); + let (doc, _, _) = + renderer.generate_pdf_with_settings(&self.asset_data, &printer_settings)?; + let pdf_plugin = PdfPlugin::new(); + pdf_plugin.export_pdf(doc, path) + } + + /// Execute print job - handles all the loading, parsing, and printing. + /// Returns Ok(true) if the job is complete, Ok(false) if it's pending (e.g., PDF export). + pub fn execute_print(&mut self, api_client: &ApiClient) -> Result { + let printer_id = self + .options + .printer_id + .ok_or_else(|| anyhow::anyhow!("No printer selected"))?; + let template_id = self + .options + .label_template_id + .ok_or_else(|| anyhow::anyhow!("No template selected"))?; + + log::info!( + "Executing print: printer_id={}, template_id={}, copies={}", + printer_id, + template_id, + self.options.copies + ); + + // 1. Load printer settings and plugin info + let printer_resp = api_client.select( + "printer_settings", + Some(vec![ + "printer_name".into(), + "printer_settings".into(), + "printer_plugin".into(), + ]), + Some(serde_json::json!({ "id": printer_id })), + None, + Some(1), + )?; + + let printer_data = printer_resp + .data + .as_ref() + .and_then(|d| if !d.is_empty() { Some(d) } else { None }) + .ok_or_else(|| anyhow::anyhow!("Printer {} not found", printer_id))?; + + let printer_plugin = printer_data[0] + .get("printer_plugin") + .and_then(|v| v.as_str()) + .unwrap_or("System"); + + let printer_settings_value = printer_data[0] + .get("printer_settings") + .ok_or_else(|| anyhow::anyhow!("printer_settings field not found"))?; + let printer_settings = parse_printer_settings(printer_settings_value)?; + + // 2. Load label template layout + let template_resp = api_client.select( + "label_templates", + Some(vec!["layout_json".into()]), + Some(serde_json::json!({"id": template_id})), + None, + Some(1), + )?; + + let template_data = template_resp + .data + .as_ref() + .and_then(|d| if !d.is_empty() { Some(d) } else { None }) + .ok_or_else(|| anyhow::anyhow!("Label template {} not found", template_id))?; + + let layout_json_value = template_data[0] + .get("layout_json") + .ok_or_else(|| anyhow::anyhow!("layout_json field not found in template"))?; + let layout = parse_layout_json(layout_json_value)?; + + // 3. Dispatch to appropriate plugin based on the printer_plugin field + match printer_plugin { + "PDF" => { + // Use a promise to handle the blocking file dialog in a background thread + let promise = Promise::spawn_thread("pdf_export_dialog", || { + rfd::FileDialog::new() + .add_filter("PDF Document", &["pdf"]) + .set_file_name("label.pdf") + .save_file() + }); + self.pdf_export_promise = Some(promise); + } + "System" | _ => { + // Use SystemPrintPlugin for system printing + use crate::core::print::plugins::system::SystemPrintPlugin; + let renderer = LabelRenderer::new(layout); + let (doc, _, _) = + renderer.generate_pdf_with_settings(&self.asset_data, &printer_settings)?; + + let system_plugin = SystemPrintPlugin::new() + .map_err(|e| anyhow::anyhow!("Failed to initialize system print: {}", e))?; + + // Save PDF first since doc can't be cloned + let pdf_path = system_plugin.save_pdf_to_temp(doc)?; + + // Try direct print to named system printer if provided + if let Some(name) = printer_settings.printer_name.clone() { + let mgr = SharedPrinterManager::new(); + match mgr.print_pdf_to(&name, pdf_path.as_path(), Some(&printer_settings)) { + Ok(()) => { + return Ok(true); + } + Err(e) => { + log::warn!("Direct system print failed: {}", e); + let fallback = printer_settings.show_dialog_if_unfound.unwrap_or(true); + if fallback { + // Show OS printer chooser popup + self.os_print_path = Some(pdf_path); + self.os_popup_visible = true; + self.os_error_message = Some(format!( + "Named printer '{}' not found. Please select a system printer.", + name + )); + // Provide base settings and renderer so overrides can regenerate PDF + self.os_base_settings = Some(printer_settings.clone()); + self.os_renderer = Some(renderer.clone()); + self.os_size_override = PaperSizeOverride::UseSaved; + return Ok(false); + } else { + // Fallback to opening in viewer using SystemPrintPlugin + system_plugin.open_print_dialog(&pdf_path)?; + return Ok(true); + } + } + } + } else { + // No printer_name provided: either show chooser or open viewer + if printer_settings.show_dialog_if_unfound.unwrap_or(true) { + self.os_print_path = Some(pdf_path); + self.os_popup_visible = true; + self.os_error_message = None; + // Provide base settings and renderer so overrides can regenerate PDF + self.os_base_settings = Some(printer_settings.clone()); + self.os_renderer = Some(renderer.clone()); + self.os_size_override = PaperSizeOverride::UseSaved; + return Ok(false); + } else { + system_plugin.open_print_dialog(&pdf_path)?; + return Ok(true); + } + } + } + } + + log::info!("Print job for plugin '{}' dispatched.", printer_plugin); + Ok(false) // Dialog should remain open for PDF export + } + + /// Print via the OS popup selection with optional paper size overrides. + /// Returns Ok(true) if a job was sent, Err(message) on failure. + fn print_via_os_popup(&mut self, target_printer_name: &str) -> Result { + // Determine the PDF to print: reuse existing if no override, or regenerate if overridden + let (path_to_print, job_settings) = match self.os_size_override { + PaperSizeOverride::UseSaved => { + let mut settings = self + .os_base_settings + .clone() + .unwrap_or_else(|| PrinterSettings::default()); + settings.canonicalize_dimensions(); + let path = self + .os_print_path + .clone() + .ok_or_else(|| "No PDF available to print".to_string())?; + (path, settings) + } + PaperSizeOverride::A4 | PaperSizeOverride::Letter | PaperSizeOverride::Custom => { + let base = self + .os_base_settings + .clone() + .ok_or_else(|| "Missing base printer settings for override".to_string())?; + let renderer = self + .os_renderer + .clone() + .ok_or_else(|| "Missing renderer for override".to_string())?; + + let mut settings = base.clone(); + match self.os_size_override { + PaperSizeOverride::A4 => { + settings.paper_size = "A4".to_string(); + settings.custom_width_mm = None; + settings.custom_height_mm = None; + } + PaperSizeOverride::Letter => { + settings.paper_size = "Letter".to_string(); + settings.custom_width_mm = None; + settings.custom_height_mm = None; + } + PaperSizeOverride::Custom => { + let w = self.os_custom_width_mm.max(0.0); + let h = self.os_custom_height_mm.max(0.0); + if w <= 0.0 || h <= 0.0 { + return Err("Please enter a valid custom size in mm".into()); + } + settings.custom_width_mm = Some(w); + settings.custom_height_mm = Some(h); + } + PaperSizeOverride::UseSaved => unreachable!(), + } + + settings.canonicalize_dimensions(); + + // Regenerate the PDF with overridden settings + let (doc, _, _) = renderer + .generate_pdf_with_settings(&self.asset_data, &settings) + .map_err(|e| format!("Failed to generate PDF: {}", e))?; + let new_path = Self::save_pdf_to_temp(doc) + .map_err(|e| format!("Failed to save PDF: {}", e))?; + // Update stored state for potential re-prints + self.os_print_path = Some(new_path.clone()); + self.os_base_settings = Some(settings.clone()); + (new_path, settings) + } + }; + + // Send to the selected OS printer + let mgr = SharedPrinterManager::new(); + let job_settings_owned = job_settings; + let result = mgr.print_pdf_to( + target_printer_name, + path_to_print.as_path(), + Some(&job_settings_owned), + ); + + if result.is_ok() { + // Ensure latest settings persist for future retries when using saved path + self.os_base_settings = Some(job_settings_owned.clone()); + self.os_print_path = Some(path_to_print.clone()); + } + + result.map(|_| true) + } + + fn save_pdf_to_temp(doc: printpdf::PdfDocumentReference) -> Result { + use anyhow::Context; + use std::fs::File; + use std::io::BufWriter; + let temp_dir = std::env::temp_dir().join("beepzone_labels"); + std::fs::create_dir_all(&temp_dir).context("Failed to create temp directory for labels")?; + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let pdf_path = temp_dir.join(format!("label_{}.pdf", timestamp)); + let file = File::create(&pdf_path).context("Failed to create temp PDF file")?; + let mut writer = BufWriter::new(file); + doc.save(&mut writer).context("Failed to save temp PDF")?; + Ok(pdf_path) + } +} -- cgit v1.2.3-70-g09d2