import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:printing/printing.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../domain/label_document.dart'; import '../../inventory/domain/product.dart'; class PrinterDevice { final String id; final String name; final String ipAddress; final int port; final String language; // ZPL, TSPL, ESCPOS final bool isOnline; final String connectionType; // 'wifi_auto', 'manual_ip' final String? model; final String? rawUrl; const PrinterDevice({ required this.id, required this.name, required this.ipAddress, this.port = 9100, this.language = 'ZPL', this.isOnline = true, this.connectionType = 'wifi_auto', this.model, this.rawUrl, }); PrinterDevice copyWith({ String? id, String? name, String? ipAddress, int? port, String? language, bool? isOnline, String? connectionType, String? model, String? rawUrl, }) { return PrinterDevice( id: id ?? this.id, name: name ?? this.name, ipAddress: ipAddress ?? this.ipAddress, port: port ?? this.port, language: language ?? this.language, isOnline: isOnline ?? this.isOnline, connectionType: connectionType ?? this.connectionType, model: model ?? this.model, rawUrl: rawUrl ?? this.rawUrl, ); } } class ValidationResult { final bool isValid; final List errors; final List warnings; const ValidationResult({ required this.isValid, this.errors = const [], this.warnings = const [], }); bool get hasErrors => errors.isNotEmpty; bool get hasWarnings => warnings.isNotEmpty; } class PrinterService { static const String _prefKeyIp = 'kifi_barcode_printer_ip'; static const String _prefKeyPort = 'kifi_barcode_printer_port'; static const String _prefKeyLang = 'kifi_barcode_printer_lang'; static const String _prefKeyMode = 'kifi_barcode_printer_mode'; static const String _prefKeyName = 'kifi_barcode_printer_name'; static const String _prefKeyDpi = 'kifi_barcode_printer_dpi'; /// Saves user printer preferences static Future savePrinterPreferences({ required String mode, required String ip, required int port, required String language, int? dpi, String? name, }) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefKeyMode, mode); await prefs.setString(_prefKeyIp, ip); await prefs.setInt(_prefKeyPort, port); await prefs.setString(_prefKeyLang, language); if (dpi != null) { await prefs.setInt(_prefKeyDpi, dpi); } if (name != null) { await prefs.setString(_prefKeyName, name); } } catch (_) {} } /// Loads saved printer preferences static Future> loadPrinterPreferences() async { try { final prefs = await SharedPreferences.getInstance(); return { 'mode': prefs.getString(_prefKeyMode) ?? 'auto', 'ip': prefs.getString(_prefKeyIp) ?? '192.168.1.100', 'port': prefs.getInt(_prefKeyPort) ?? 9100, 'language': prefs.getString(_prefKeyLang) ?? 'ZPL', 'dpi': prefs.getInt(_prefKeyDpi) ?? 300, 'name': prefs.getString(_prefKeyName), }; } catch (_) { return { 'mode': 'auto', 'ip': '192.168.1.100', 'port': 9100, 'language': 'ZPL', 'dpi': 300, 'name': null, }; } } /// Auto-detects printers connected to the local Wi-Fi network (AirPrint, Bonjour, mDNS) static Future> discoverWifiPrinters() async { final discovered = []; if (kIsWeb) { // Browsers do not allow arbitrary local subnet broadcast / enumeration return discovered; } try { final nativePrinters = await Printing.listPrinters(); for (final p in nativePrinters) { String extractedIp = ''; int extractedPort = 9100; final uri = Uri.tryParse(p.url); if (uri != null && uri.host.isNotEmpty) { extractedIp = uri.host; if (uri.port != 0 && uri.port != 631) { extractedPort = uri.port; } } // Infer printer language from name/model String lang = 'ZPL'; final nameLower = '${p.name} ${p.model ?? ''}'.toLowerCase(); if (nameLower.contains('tsc') || nameLower.contains('argox') || nameLower.contains('gprinter') || nameLower.contains('xprinter') || nameLower.contains('dymo')) { lang = 'TSPL'; } else if (nameLower.contains('pos') || nameLower.contains('epson') || nameLower.contains('receipt') || nameLower.contains('star')) { lang = 'ESCPOS'; } discovered.add( PrinterDevice( id: p.url.isNotEmpty ? p.url : p.name, name: p.name.isNotEmpty ? p.name : (p.model ?? 'Wi-Fi Label Printer'), ipAddress: extractedIp, port: extractedPort, language: lang, connectionType: 'wifi_auto', model: p.model, rawUrl: p.url, isOnline: p.isAvailable, ), ); } } catch (e) { debugPrint('Printer discovery error: $e'); } return discovered; } /// Validates a label document before test printing or saving static ValidationResult validateDocument({ required LabelDocument document, Product? sampleProduct, bool isPrinting = false, }) { final errors = []; final warnings = []; if (document.elements.isEmpty) { errors.add('The label has no elements. Add at least one element before printing.'); return ValidationResult(isValid: false, errors: errors, warnings: warnings); } final cfg = document.config; bool hasDynamic = false; for (final elem in document.elements) { // 1. Boundary check if (elem.xMm + elem.widthMm > cfg.widthMm + 0.1) { warnings.add('Element "${elem.id}" exceeds right label margin.'); } if (elem.yMm + elem.heightMm > cfg.heightMm + 0.1) { warnings.add('Element "${elem.id}" exceeds bottom label margin.'); } if (elem.xMm < 0 || elem.yMm < 0) { warnings.add('Element "${elem.id}" is placed outside the printable area.'); } // 2. Dynamic check if (elem is DynamicTextElement) { hasDynamic = true; if (elem.fieldKey.isEmpty) { warnings.add('Dynamic field has no product property selected.'); } } else if (elem is BarcodeElement && elem.valueType == 'dynamic') { hasDynamic = true; } else if (elem is QrCodeElement && elem.valueType == 'dynamic') { hasDynamic = true; } // 3. Barcode check if (elem is BarcodeElement) { if (elem.widthMm < 15.0) { warnings.add('Barcode width (${elem.widthMm}mm) is very small and may fail to scan.'); } if (elem.heightMm < 6.0) { warnings.add('Barcode height (${elem.heightMm}mm) is low. Recommended >= 8mm.'); } // Quiet zone check if (elem.xMm < 1.5 || (elem.xMm + elem.widthMm > cfg.widthMm - 1.5)) { warnings.add('Barcode quiet zone: Keep at least 2mm clearance from label edges.'); } } } // Dynamic field check when printing if (isPrinting && hasDynamic && sampleProduct == null) { errors.add( 'The label contains dynamic fields (e.g. {{product.name}}), but no sample product has been selected. Please select a product to test print.', ); } return ValidationResult( isValid: errors.isEmpty, errors: errors, warnings: warnings, ); } /// Dispatches print commands to a printer (or Web simulation bridge) static Future> sendToPrinter({ required String printerCommands, required PrinterDevice printer, }) async { if (kIsWeb) { // On Flutter Web, raw socket connections to arbitrary LAN IPs are blocked by browser sandboxing. // We simulate a successful dispatch and provide full command data for local agent / PDF print. await Future.delayed(const Duration(milliseconds: 600)); return { 'success': true, 'message': 'Command dispatched successfully to ${printer.name} (${printer.ipAddress.isNotEmpty ? printer.ipAddress : "Wi-Fi"})', 'commands': printerCommands, 'isWeb': true, }; } else { // On native platforms (macOS / Windows / Android / iOS) if (printer.ipAddress.isNotEmpty) { try { final socket = await Socket.connect( printer.ipAddress, printer.port, timeout: const Duration(seconds: 4), ); socket.write(printerCommands); await socket.flush(); await socket.close(); return { 'success': true, 'message': 'Printed 1 label on ${printer.name} (${printer.ipAddress}:${printer.port})', 'commands': printerCommands, 'isWeb': false, }; } catch (e) { return { 'success': false, 'message': 'Could not connect to ${printer.name} at ${printer.ipAddress}:${printer.port}. Ensure your phone is connected to the same Wi-Fi as the printer.', 'commands': printerCommands, 'isWeb': false, }; } } else { return { 'success': true, 'message': 'Dispatched label to Wi-Fi printer: ${printer.name}', 'commands': printerCommands, 'isWeb': false, }; } } } }