aboutsummaryrefslogtreecommitdiff
path: root/src/ui/printers.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/printers.rs')
-rw-r--r--src/ui/printers.rs943
1 files changed, 943 insertions, 0 deletions
diff --git a/src/ui/printers.rs b/src/ui/printers.rs
new file mode 100644
index 0000000..bafc445
--- /dev/null
+++ b/src/ui/printers.rs
@@ -0,0 +1,943 @@
+use crate::api::ApiClient;
+use crate::core::components::form_builder::FormBuilder;
+use crate::core::components::interactions::ConfirmDialog;
+use crate::core::{ColumnConfig, TableEventHandler, TableRenderer};
+use crate::core::{EditorField, FieldType};
+use crate::ui::ribbon::RibbonUI;
+use eframe::egui;
+use serde_json::Value;
+
+const SYSTEM_PRINTER_SETTINGS_TEMPLATE: &str = r#"{
+ "paper_size": "Custom",
+ "orientation": "landscape",
+ "margins": {
+ "top": 0.0,
+ "right": 0.0,
+ "bottom": 0.0,
+ "left": 0.0
+ },
+ "color": false,
+ "quality": "high",
+ "copies": 1,
+ "duplex": false,
+ "center": "both",
+ "center_disabled": false,
+ "scale_mode": "fit",
+ "scale_factor": 1.0,
+ "custom_width_mm": 50.8,
+ "custom_height_mm": 76.2,
+ "printer_name": null,
+ "show_dialog_if_unfound": true
+}"#;
+
+const PDF_PRINTER_SETTINGS_TEMPLATE: &str = SYSTEM_PRINTER_SETTINGS_TEMPLATE;
+
+const SYSTEM_PRINTER_JSON_HELP: &str = r#"# System printer JSON
+
+Use this payload when registering the `System` printer plugin. Leave fields out to fall back to BeepZone's legacy sizing.
+
+## Core fields
+- `paper_size` *(string)* — Named stock such as `A4`, `Letter`, `A5`, or `Custom`.
+- `orientation` *(string)* — Either `portrait` or `landscape`. Selecting `landscape` rotates the page 90°; any custom width/height you supply are interpreted in the stock's natural (portrait) orientation and the app flips them automatically while printing.
+- `margins` *(object in millimetres)* — Trim space on each edge with `top`, `right`, `bottom`, `left` properties.
+- `scale_mode` *(string)* — Scaling behavior: `fit` (proportional fit), `fit-x` (fit width), `fit-y` (fit height), `max-both`, `max-x`, `max-y`, or `manual`.
+- `scale_factor` *(number ≥ 0)* — Manual multiplier applied according to scale_mode.
+- `duplex`, `color`, `quality` *(optional)* — Mirrors the underlying OS print options.
+- `copies` *(number)* — Number of copies to print.
+- `custom_width_mm` / `custom_height_mm` *(numbers)* — Provide both to describe bespoke media using the printer's normal portrait orientation.
+
+## Layout control
+- `center` *("none" | "horizontal" | "vertical" | "both" | null)* — Centers content when not disabled.
+- `center_disabled` *(bool)* — When `true`, ignores the `center` setting while keeping the last chosen mode for later.
+
+## Direct print (optional)
+- `printer_name` *(string | null)* — If set, the System plugin will attempt to print directly to this OS printer by name.
+- `show_dialog_if_unfound` *(bool, default: true)* — When `true` (or omitted) and the named printer can't be resolved, a lightweight popup chooser appears. Set to `false` to skip the chooser and only open the PDF viewer.
+- `compatibility_mode` *(bool, default: false)* — When `true`, sends NO CUPS job options at all - only the raw PDF. Use this for severely broken printer filters (e.g., Kyocera network printers with crashing filters). The printer will use its default settings.
+
+## Examples
+
+### Custom Label Printer (e.g., ZQ510)
+```json
+{
+ "paper_size": "Custom",
+ "orientation": "landscape",
+ "margins": {
+ "top": 0.0,
+ "right": 0.0,
+ "bottom": 0.0,
+ "left": 0.0
+ },
+ "scale_mode": "fit",
+ "scale_factor": 1.0,
+ "color": false,
+ "quality": "high",
+ "copies": 1,
+ "duplex": false,
+ "custom_width_mm": 50.8,
+ "custom_height_mm": 76.2,
+ "center": "both",
+ "center_disabled": false,
+ "printer_name": "ZQ510",
+ "show_dialog_if_unfound": true
+}
+```
+
+### Standard A4 Office Printer
+```json
+{
+ "paper_size": "A4",
+ "orientation": "portrait",
+ "margins": {
+ "top": 12.7,
+ "right": 12.7,
+ "bottom": 12.7,
+ "left": 12.7
+ },
+ "scale_mode": "fit",
+ "scale_factor": 1.0,
+ "color": true,
+ "quality": "high",
+ "copies": 1,
+ "duplex": false,
+ "center": "both",
+ "center_disabled": false,
+ "printer_name": "HP LaserJet Pro",
+ "show_dialog_if_unfound": true
+}
+```
+"#;
+
+const PDF_PRINTER_JSON_HELP: &str = r#"# PDF export JSON
+
+The PDF plugin understands the same shape as the System printer. Use the optional flags only when you want the enhanced layout controls; otherwise omit them for the classic renderer settings.
+
+## Typical usage
+- Provide `paper_size` / `orientation` or include `custom_width_mm` + `custom_height_mm` for bespoke sheets. Enter the measurements in the stock's natural portrait orientation; landscape output is handled automatically.
+- Reuse the `margins` block from your system printers so labels line up identically.
+- `scale_mode`, `scale_factor`, `center`, `center_disabled` behave exactly the same as the System plugin.
+- The exported file path is still chosen through the PDF save dialog; these settings only influence page geometry.
+
+## Available scale modes
+- `fit` — Proportionally fit the design within the printable area
+- `fit-x` — Fit to page width only
+- `fit-y` — Fit to page height only
+- `max-both` — Maximum size that fits both dimensions
+- `max-x` — Maximum width scaling
+- `max-y` — Maximum height scaling
+- `manual` — Use exact `scale_factor` value
+
+## Example
+
+```json
+{
+ "paper_size": "Letter",
+ "orientation": "portrait",
+ "margins": { "top": 5.0, "right": 5.0, "bottom": 5.0, "left": 5.0 },
+ "scale_mode": "manual",
+ "scale_factor": 0.92,
+ "center": "horizontal",
+ "center_disabled": false
+}
+```
+"#;
+
+pub struct PrintersView {
+ printers: Vec<serde_json::Value>,
+ is_loading: bool,
+ last_error: Option<String>,
+ initial_load_done: bool,
+
+ // Table renderer
+ table_renderer: TableRenderer,
+
+ // Editor dialogs
+ edit_dialog: FormBuilder,
+ add_dialog: FormBuilder,
+ delete_dialog: ConfirmDialog,
+
+ // Pending operations
+ pending_delete_id: Option<i64>,
+ pending_edit_id: Option<i64>,
+
+ // Navigation
+ pub switch_to_print_history: bool,
+
+ // Track last selected plugin to detect changes
+ last_add_dialog_plugin: Option<String>,
+}
+
+impl PrintersView {
+ pub fn new() -> Self {
+ let edit_dialog = Self::create_edit_dialog();
+ let add_dialog = Self::create_add_dialog();
+
+ // Define columns for printer_settings table
+ let columns = vec![
+ ColumnConfig::new("ID", "id").with_width(60.0).hidden(),
+ ColumnConfig::new("Printer Name", "printer_name").with_width(150.0),
+ ColumnConfig::new("Description", "description").with_width(200.0),
+ ColumnConfig::new("Plugin", "printer_plugin").with_width(100.0),
+ ColumnConfig::new("Log Prints", "log").with_width(90.0),
+ ColumnConfig::new("Use for Reports", "can_be_used_for_reports").with_width(120.0),
+ ColumnConfig::new("Min Power Level", "min_powerlevel_to_use").with_width(110.0),
+ ColumnConfig::new("Settings JSON", "printer_settings")
+ .with_width(150.0)
+ .hidden(),
+ ];
+
+ Self {
+ printers: Vec::new(),
+ is_loading: false,
+ last_error: None,
+ initial_load_done: false,
+ table_renderer: TableRenderer::new()
+ .with_columns(columns)
+ .with_default_sort("printer_name", true)
+ .with_search_fields(vec![
+ "printer_name".to_string(),
+ "description".to_string(),
+ "printer_plugin".to_string(),
+ ]),
+ edit_dialog,
+ add_dialog,
+ delete_dialog: ConfirmDialog::new(
+ "Delete Printer",
+ "Are you sure you want to delete this printer configuration?",
+ ),
+ pending_delete_id: None,
+ pending_edit_id: None,
+ switch_to_print_history: false,
+ last_add_dialog_plugin: None,
+ }
+ }
+
+ fn plugin_help_text(plugin: &str) -> Option<&'static str> {
+ match plugin {
+ "System" => Some(SYSTEM_PRINTER_JSON_HELP),
+ "PDF" => Some(PDF_PRINTER_JSON_HELP),
+ _ => None,
+ }
+ }
+
+ fn apply_plugin_help(editor: &mut FormBuilder, plugin: Option<&str>) {
+ if let Some(plugin) = plugin {
+ if let Some(help) = Self::plugin_help_text(plugin) {
+ editor.form_help_text = Some(help.to_string());
+ return;
+ }
+ }
+ editor.form_help_text = None;
+ }
+
+ fn create_edit_dialog() -> FormBuilder {
+ let plugin_options = vec![
+ ("Ptouch".to_string(), "Brother P-Touch".to_string()),
+ ("Brother".to_string(), "Brother (Generic)".to_string()),
+ ("Zebra".to_string(), "Zebra".to_string()),
+ ("System".to_string(), "System Printer".to_string()),
+ ("PDF".to_string(), "PDF Export".to_string()),
+ ("Network".to_string(), "Network Printer".to_string()),
+ ("Custom".to_string(), "Custom".to_string()),
+ ];
+
+ FormBuilder::new(
+ "Edit Printer",
+ vec![
+ EditorField {
+ name: "id".into(),
+ label: "ID".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: true,
+ },
+ EditorField {
+ name: "printer_name".into(),
+ label: "Printer Name".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "description".into(),
+ label: "Description".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "printer_plugin".into(),
+ label: "Printer Plugin".into(),
+ field_type: FieldType::Dropdown(plugin_options.clone()),
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "log".into(),
+ label: "Log Print Jobs".into(),
+ field_type: FieldType::Checkbox,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "can_be_used_for_reports".into(),
+ label: "Can Print Reports".into(),
+ field_type: FieldType::Checkbox,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "min_powerlevel_to_use".into(),
+ label: "Minimum Power Level".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "printer_settings".into(),
+ label: "Printer Settings Required".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ ],
+ )
+ }
+
+ fn create_add_dialog() -> FormBuilder {
+ let plugin_options = vec![
+ ("Ptouch".to_string(), "Brother P-Touch".to_string()),
+ ("Brother".to_string(), "Brother (Generic)".to_string()),
+ ("Zebra".to_string(), "Zebra".to_string()),
+ ("System".to_string(), "System Printer".to_string()),
+ ("PDF".to_string(), "PDF Export".to_string()),
+ ("Network".to_string(), "Network Printer".to_string()),
+ ("Custom".to_string(), "Custom".to_string()),
+ ];
+
+ FormBuilder::new(
+ "Add Printer",
+ vec![
+ EditorField {
+ name: "printer_name".into(),
+ label: "Printer Name".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "description".into(),
+ label: "Description".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "printer_plugin".into(),
+ label: "Printer Plugin".into(),
+ field_type: FieldType::Dropdown(plugin_options),
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "log".into(),
+ label: "Log Print Jobs".into(),
+ field_type: FieldType::Checkbox,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "can_be_used_for_reports".into(),
+ label: "Can Print Reports".into(),
+ field_type: FieldType::Checkbox,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "min_powerlevel_to_use".into(),
+ label: "Minimum Power Level".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "printer_settings".into(),
+ label: "Printer Settings (JSON)".into(),
+ field_type: FieldType::MultilineText,
+ required: true,
+ read_only: false,
+ },
+ ],
+ )
+ }
+
+ fn ensure_loaded(&mut self, api_client: Option<&ApiClient>) {
+ if self.is_loading || self.initial_load_done {
+ return;
+ }
+ if let Some(client) = api_client {
+ self.load_printers(client);
+ }
+ }
+
+ fn load_printers(&mut self, api_client: &ApiClient) {
+ use crate::core::tables::get_printers;
+
+ self.is_loading = true;
+ self.last_error = None;
+ match get_printers(api_client) {
+ Ok(list) => {
+ self.printers = list;
+ self.is_loading = false;
+ self.initial_load_done = true;
+ }
+ Err(e) => {
+ self.last_error = Some(e.to_string());
+ self.is_loading = false;
+ self.initial_load_done = true;
+ }
+ }
+ }
+
+ pub fn render(
+ &mut self,
+ ui: &mut egui::Ui,
+ api_client: Option<&ApiClient>,
+ ribbon_ui: Option<&mut RibbonUI>,
+ session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) {
+ self.ensure_loaded(api_client);
+
+ // Get search query from ribbon first (before mutable borrow)
+ let search_query = ribbon_ui
+ .as_ref()
+ .and_then(|r| r.search_texts.get("printers_search"))
+ .map(|s| s.clone())
+ .unwrap_or_default();
+
+ // Apply search to table renderer
+ self.table_renderer.search_query = search_query;
+
+ // Handle ribbon actions and default printer dropdown
+ if let Some(ribbon) = ribbon_ui {
+ if ribbon
+ .checkboxes
+ .get("printers_action_add")
+ .copied()
+ .unwrap_or(false)
+ {
+ // Provide default values - printer_settings will get plugin-specific template
+ let default_data = serde_json::json!({
+ "printer_settings": "{}",
+ "log": true,
+ "can_be_used_for_reports": false,
+ "min_powerlevel_to_use": "0"
+ });
+ self.add_dialog.open(&default_data);
+ }
+ if ribbon
+ .checkboxes
+ .get("printers_action_refresh")
+ .copied()
+ .unwrap_or(false)
+ {
+ if let Some(client) = api_client {
+ self.load_printers(client);
+ }
+ }
+ if ribbon
+ .checkboxes
+ .get("printers_view_print_history")
+ .copied()
+ .unwrap_or(false)
+ {
+ self.switch_to_print_history = true;
+ }
+
+ // Handle default printer dropdown (will be rendered in Settings group)
+ // Store selected printer ID change flag
+ if ribbon
+ .checkboxes
+ .get("printers_default_changed")
+ .copied()
+ .unwrap_or(false)
+ {
+ if let Some(printer_id_str) = ribbon.search_texts.get("printers_default_id") {
+ if printer_id_str.is_empty() {
+ // Clear default printer
+ if let Ok(mut session) = session_manager.try_lock() {
+ if let Err(e) = session.update_default_printer(None) {
+ log::error!("Failed to clear default printer: {}", e);
+ } else {
+ log::info!("Default printer cleared");
+ }
+ }
+ } else if let Ok(printer_id) = printer_id_str.parse::<i64>() {
+ // Set default printer
+ if let Ok(mut session) = session_manager.try_lock() {
+ if let Err(e) = session.update_default_printer(Some(printer_id)) {
+ log::error!("Failed to update default printer: {}", e);
+ } else {
+ log::info!("Default printer set to ID: {}", printer_id);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Error message
+ let mut clear_error = false;
+ if let Some(err) = &self.last_error {
+ ui.horizontal(|ui| {
+ ui.colored_label(egui::Color32::RED, format!("Error: {}", err));
+ if ui.button("Close").clicked() {
+ clear_error = true;
+ }
+ });
+ ui.separator();
+ }
+ if clear_error {
+ self.last_error = None;
+ }
+
+ // Loading indicator
+ if self.is_loading {
+ ui.spinner();
+ ui.label("Loading printers...");
+ return;
+ }
+
+ // Render table with event handling
+ self.render_table_with_events(ui, api_client);
+
+ // Handle dialogs
+ self.handle_dialogs(ui, api_client);
+
+ // Process deferred actions from context menus
+ self.process_deferred_actions(ui, api_client);
+ }
+
+ /// Called before rendering to inject printer dropdown data into ribbon
+ pub fn inject_dropdown_into_ribbon(
+ &self,
+ ribbon_ui: &mut RibbonUI,
+ session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) {
+ // Try to get current default printer ID without blocking (avoid Tokio panic)
+ let current_default = session_manager
+ .try_lock()
+ .ok()
+ .and_then(|s| s.get_default_printer_id());
+
+ // Store current default for ribbon rendering
+ if let Some(id) = current_default {
+ ribbon_ui
+ .search_texts
+ .insert("_printers_current_default".to_string(), id.to_string());
+ } else {
+ ribbon_ui
+ .search_texts
+ .insert("_printers_current_default".to_string(), "".to_string());
+ }
+
+ // Store printer list as JSON string for ribbon to parse
+ let printers_json = serde_json::to_string(&self.printers).unwrap_or_default();
+ ribbon_ui
+ .search_texts
+ .insert("_printers_list".to_string(), printers_json);
+ }
+
+ fn render_table_with_events(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) {
+ let printers_clone = self.printers.clone();
+ let prepared_data = self.table_renderer.prepare_json_data(&printers_clone);
+
+ let mut deferred_actions: Vec<DeferredAction> = Vec::new();
+ let mut temp_handler = TempPrintersEventHandler {
+ api_client,
+ deferred_actions: &mut deferred_actions,
+ };
+
+ self.table_renderer
+ .render_json_table(ui, &prepared_data, Some(&mut temp_handler));
+
+ self.process_temp_deferred_actions(deferred_actions, api_client);
+ }
+
+ fn process_temp_deferred_actions(
+ &mut self,
+ actions: Vec<DeferredAction>,
+ _api_client: Option<&ApiClient>,
+ ) {
+ for action in actions {
+ match action {
+ DeferredAction::DoubleClick(printer) => {
+ log::info!(
+ "Processing double-click edit for printer: {:?}",
+ printer.get("printer_name")
+ );
+ self.edit_dialog.open(&printer);
+ if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
+ self.pending_edit_id = Some(id);
+ }
+ }
+ DeferredAction::ContextEdit(printer) => {
+ log::info!(
+ "Processing context menu edit for printer: {:?}",
+ printer.get("printer_name")
+ );
+ self.edit_dialog.open(&printer);
+ if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
+ self.pending_edit_id = Some(id);
+ }
+ }
+ DeferredAction::ContextDelete(printer) => {
+ let name = printer
+ .get("printer_name")
+ .and_then(|v| v.as_str())
+ .unwrap_or("Unknown");
+ let id = printer.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
+ log::info!("Processing context menu delete for printer: {}", name);
+ self.pending_delete_id = Some(id);
+ self.delete_dialog.open(name.to_string(), id.to_string());
+ }
+ DeferredAction::ContextClone(printer) => {
+ log::info!(
+ "Processing context menu clone for printer: {:?}",
+ printer.get("printer_name")
+ );
+ let mut cloned = crate::core::components::prepare_cloned_value(
+ &printer,
+ &["id"],
+ Some("printer_name"),
+ Some(""),
+ );
+ if let Some(obj) = cloned.as_object_mut() {
+ if let Some(ps) = obj.get("printer_settings") {
+ let as_str = if ps.is_string() {
+ ps.as_str().unwrap_or("{}").to_string()
+ } else {
+ serde_json::to_string_pretty(ps)
+ .unwrap_or_else(|_| "{}".to_string())
+ };
+ obj.insert(
+ "printer_settings".to_string(),
+ serde_json::Value::String(as_str),
+ );
+ }
+ self.add_dialog.open_new(Some(obj));
+ }
+ }
+ }
+ }
+ }
+
+ fn handle_dialogs(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) {
+ // BEFORE showing add dialog, check if printer_plugin changed and auto-populate printer_settings
+ if self.add_dialog.show {
+ let current_plugin = self
+ .add_dialog
+ .data
+ .get("printer_plugin")
+ .map(|s| s.clone());
+
+ // Detect if plugin changed to "System"
+ if current_plugin != self.last_add_dialog_plugin {
+ if let Some(ref plugin) = current_plugin {
+ let template = match plugin.as_str() {
+ "System" => Some(SYSTEM_PRINTER_SETTINGS_TEMPLATE),
+ "PDF" => Some(PDF_PRINTER_SETTINGS_TEMPLATE),
+ _ => None,
+ };
+
+ if let Some(template) = template {
+ let current_settings = self
+ .add_dialog
+ .data
+ .get("printer_settings")
+ .map(|s| s.as_str())
+ .unwrap_or("{}");
+
+ if current_settings.trim().is_empty() || current_settings.trim() == "{}" {
+ self.add_dialog
+ .data
+ .insert("printer_settings".to_string(), template.to_string());
+ }
+ }
+ }
+ self.last_add_dialog_plugin = current_plugin.clone();
+ }
+
+ Self::apply_plugin_help(&mut self.add_dialog, current_plugin.as_deref());
+ } else {
+ // Reset tracking when dialog closes
+ self.last_add_dialog_plugin = None;
+ self.add_dialog.form_help_text = None;
+ }
+
+ if self.edit_dialog.show {
+ let edit_plugin = self
+ .edit_dialog
+ .data
+ .get("printer_plugin")
+ .map(|s| s.clone());
+ Self::apply_plugin_help(&mut self.edit_dialog, edit_plugin.as_deref());
+ } else {
+ self.edit_dialog.form_help_text = None;
+ }
+
+ // Delete confirmation dialog
+ if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) {
+ if confirmed {
+ if let (Some(id), Some(client)) = (self.pending_delete_id, api_client) {
+ let where_clause = serde_json::json!({"id": id});
+ match client.delete("printer_settings", where_clause) {
+ Ok(_) => {
+ log::info!("Printer {} deleted successfully", id);
+ self.load_printers(client);
+ }
+ Err(e) => {
+ self.last_error = Some(format!("Failed to delete printer: {}", e));
+ log::error!("Failed to delete printer: {}", e);
+ }
+ }
+ }
+ self.pending_delete_id = None;
+ }
+ }
+
+ // Edit dialog
+ if let Some(Some(updated)) = self.edit_dialog.show_editor(ui.ctx()) {
+ if let (Some(id), Some(client)) = (self.pending_edit_id, api_client) {
+ let where_clause = serde_json::json!({"id": id});
+ // Ensure printer_settings field is valid JSON and send as JSON object
+ let mut to_update = updated;
+ // Remove generic editor metadata keys (avoid backend invalid column errors)
+ let mut meta_keys: Vec<String> = to_update
+ .keys()
+ .filter(|k| k.starts_with("__editor_"))
+ .cloned()
+ .collect();
+ // Also remove __editor_item_id specifically
+ if to_update.contains_key("__editor_item_id") {
+ meta_keys.push("__editor_item_id".to_string());
+ }
+ for k in meta_keys {
+ to_update.remove(&k);
+ }
+ if let Some(val) = to_update.get_mut("printer_settings") {
+ if let Some(s) = val.as_str() {
+ match serde_json::from_str::<serde_json::Value>(s) {
+ Ok(json_val) => {
+ // Send as actual JSON object, not base64 string
+ *val = json_val;
+ }
+ Err(e) => {
+ self.last_error =
+ Some(format!("Printer Settings JSON is invalid: {}", e));
+ return;
+ }
+ }
+ }
+ }
+ match client.update(
+ "printer_settings",
+ serde_json::Value::Object(to_update.clone()),
+ where_clause,
+ ) {
+ Ok(resp) => {
+ if resp.success {
+ log::info!("Printer {} updated successfully", id);
+ self.load_printers(client);
+ } else {
+ self.last_error = Some(format!("Update failed: {:?}", resp.error));
+ log::error!("Update failed: {:?}", resp.error);
+ }
+ }
+ Err(e) => {
+ self.last_error = Some(format!("Failed to update printer: {}", e));
+ log::error!("Failed to update printer: {}", e);
+ }
+ }
+ self.pending_edit_id = None;
+ }
+ }
+
+ // Add dialog
+ if let Some(Some(new_data)) = self.add_dialog.show_editor(ui.ctx()) {
+ if let Some(client) = api_client {
+ // Parse printer_settings JSON and send as JSON object
+ let mut payload = new_data;
+ // Strip any editor metadata that may have leaked in
+ let meta_strip: Vec<String> = payload
+ .keys()
+ .filter(|k| k.starts_with("__editor_"))
+ .cloned()
+ .collect();
+ for k in meta_strip {
+ payload.remove(&k);
+ }
+ if let Some(val) = payload.get_mut("printer_settings") {
+ if let Some(s) = val.as_str() {
+ match serde_json::from_str::<serde_json::Value>(s) {
+ Ok(json_val) => {
+ // Send as actual JSON object, not base64 string
+ *val = json_val;
+ }
+ Err(e) => {
+ self.last_error =
+ Some(format!("Printer Settings JSON is invalid: {}", e));
+ return;
+ }
+ }
+ }
+ }
+ match client.insert(
+ "printer_settings",
+ serde_json::Value::Object(payload.clone()),
+ ) {
+ Ok(resp) => {
+ if resp.success {
+ log::info!("Printer added successfully");
+ self.load_printers(client);
+ } else {
+ self.last_error = Some(format!("Insert failed: {:?}", resp.error));
+ log::error!("Insert failed: {:?}", resp.error);
+ }
+ }
+ Err(e) => {
+ self.last_error = Some(format!("Failed to add printer: {}", e));
+ log::error!("Failed to add printer: {}", e);
+ }
+ }
+ }
+ }
+ }
+
+ fn process_deferred_actions(&mut self, ui: &mut egui::Ui, _api_client: Option<&ApiClient>) {
+ // Handle double-click edit
+ if let Some(printer) = ui
+ .ctx()
+ .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_double_click_edit")))
+ {
+ log::info!(
+ "Processing double-click edit for printer: {:?}",
+ printer.get("printer_name")
+ );
+ self.edit_dialog.open(&printer);
+ if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
+ self.pending_edit_id = Some(id);
+ }
+ }
+
+ // Handle context menu actions
+ if let Some(printer) = ui
+ .ctx()
+ .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_context_menu_edit")))
+ {
+ log::info!(
+ "Processing context menu edit for printer: {:?}",
+ printer.get("printer_name")
+ );
+ self.edit_dialog.open(&printer);
+ if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
+ self.pending_edit_id = Some(id);
+ }
+ }
+
+ if let Some(printer) = ui
+ .ctx()
+ .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_context_menu_delete")))
+ {
+ let name = printer
+ .get("printer_name")
+ .and_then(|v| v.as_str())
+ .unwrap_or("Unknown");
+ let id = printer.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
+ log::info!("Processing context menu delete for printer: {}", name);
+ self.pending_delete_id = Some(id);
+ self.delete_dialog.open(name.to_string(), id.to_string());
+ }
+ }
+}
+
+impl Default for PrintersView {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[derive(Clone)]
+enum DeferredAction {
+ DoubleClick(Value),
+ ContextEdit(Value),
+ ContextDelete(Value),
+ ContextClone(Value),
+}
+
+// Temporary event handler that collects actions for later processing
+struct TempPrintersEventHandler<'a> {
+ #[allow(dead_code)]
+ api_client: Option<&'a ApiClient>,
+ deferred_actions: &'a mut Vec<DeferredAction>,
+}
+
+impl<'a> TableEventHandler<Value> for TempPrintersEventHandler<'a> {
+ fn on_double_click(&mut self, item: &Value, _row_index: usize) {
+ log::info!(
+ "Double-click detected on printer: {:?}",
+ item.get("printer_name")
+ );
+ self.deferred_actions
+ .push(DeferredAction::DoubleClick(item.clone()));
+ }
+
+ fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
+ if ui
+ .button(format!("{} Edit Printer", egui_phosphor::regular::PENCIL))
+ .clicked()
+ {
+ log::info!(
+ "Context menu edit clicked for printer: {:?}",
+ item.get("printer_name")
+ );
+ self.deferred_actions
+ .push(DeferredAction::ContextEdit(item.clone()));
+ ui.close();
+ }
+
+ ui.separator();
+
+ if ui
+ .button(format!("{} Clone Printer", egui_phosphor::regular::COPY))
+ .clicked()
+ {
+ log::info!(
+ "Context menu clone clicked for printer: {:?}",
+ item.get("printer_name")
+ );
+ self.deferred_actions
+ .push(DeferredAction::ContextClone(item.clone()));
+ ui.close();
+ }
+
+ ui.separator();
+
+ if ui
+ .button(format!("{} Delete Printer", egui_phosphor::regular::TRASH))
+ .clicked()
+ {
+ log::info!(
+ "Context menu delete clicked for printer: {:?}",
+ item.get("printer_name")
+ );
+ self.deferred_actions
+ .push(DeferredAction::ContextDelete(item.clone()));
+ ui.close();
+ }
+ }
+
+ fn on_selection_changed(&mut self, selected_indices: &[usize]) {
+ log::debug!("Printer selection changed: {:?}", selected_indices);
+ }
+}