aboutsummaryrefslogtreecommitdiff
path: root/src/core/print/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/print/ui')
-rw-r--r--src/core/print/ui/mod.rs3
-rw-r--r--src/core/print/ui/print_dialog.rs999
2 files changed, 1002 insertions, 0 deletions
diff --git a/src/core/print/ui/mod.rs b/src/core/print/ui/mod.rs
new file mode 100644
index 0000000..b134f6e
--- /dev/null
+++ b/src/core/print/ui/mod.rs
@@ -0,0 +1,3 @@
+pub mod print_dialog;
+
+// PrintDialog is re-exported at crate::core::print level
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<i64>,
+ pub printer_name: String,
+ pub label_template_id: Option<i64>,
+ 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<String, String>,
+ printers: Vec<Value>,
+ templates: Vec<Value>,
+ renderer: Option<LabelRenderer>,
+ preview_scale: f32,
+ error_message: Option<String>,
+ loading: bool,
+ // Promise for handling async PDF export
+ pdf_export_promise: Option<Promise<Option<PathBuf>>>,
+ // OS printer fallback popup
+ os_popup_visible: bool,
+ os_printers: Vec<PrinterInfo>,
+ os_selected_index: usize,
+ os_print_path: Option<PathBuf>,
+ os_error_message: Option<String>,
+ os_base_settings: Option<PrinterSettings>,
+ os_renderer: Option<LabelRenderer>,
+ 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<String, String>) -> 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<i64>,
+ label_template_id: Option<i64>,
+ last_printer_id: Option<i64>,
+ ) -> 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 { &current })
+ .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<String, String> {
+ &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<bool> {
+ 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<bool, String> {
+ // 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<PathBuf> {
+ 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)
+ }
+}