import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:printing/printing.dart'; import '../../../app/providers.dart'; import '../../../data/local/app_database.dart'; /// Hardware preferences for this terminal. /// /// Persisted in `app_meta` rather than held in widget state, so the choice /// survives a restart — a cashier should not have to reselect the printer /// every morning. class PrinterSettings { const PrinterSettings({ this.printerUrl, this.printerName, this.autoPrint = false, this.openDrawer = true, this.drawerHost, this.drawerPort = 9100, }); /// Target passed to `directPrintPdf`. Null means "use the system default". final String? printerUrl; /// Shown in Settings. Kept alongside the url because the url is opaque. final String? printerName; /// Print silently the moment a sale completes. /// /// Defaults to **off**: with no printer attached, auto-printing fails /// invisibly and looks like a bug. final bool autoPrint; final bool openDrawer; /// IP address of the receipt printer the drawer is wired to. /// /// Separate from [printerUrl] because that is an opaque platform handle for /// the PDF driver, which cannot carry raw ESC/POS bytes. The drawer kick /// needs a socket, so it needs an address. final String? drawerHost; final int drawerPort; bool get hasPrinter => printerUrl != null; bool get hasDrawer => (drawerHost ?? '').isNotEmpty; PrinterSettings copyWith({ String? printerUrl, String? printerName, bool clearPrinter = false, bool? autoPrint, bool? openDrawer, String? drawerHost, int? drawerPort, }) { return PrinterSettings( printerUrl: clearPrinter ? null : (printerUrl ?? this.printerUrl), printerName: clearPrinter ? null : (printerName ?? this.printerName), autoPrint: autoPrint ?? this.autoPrint, openDrawer: openDrawer ?? this.openDrawer, drawerHost: drawerHost ?? this.drawerHost, drawerPort: drawerPort ?? this.drawerPort, ); } } class PrinterSettingsController extends StateNotifier { PrinterSettingsController(this._ref) : super(const PrinterSettings()) { _load(); } final Ref _ref; Future _load() async { final store = _ref.read(localStoreProvider); if (!store.isReady) return; final dao = store.catalogue; // Read every value first, then assign. Written inline the four awaits still // run before the assignment, so leaving Settings mid-read threw // "used after dispose" — which reaches the cashier as a red screen. final url = await dao.meta(MetaKeys.printerUrl); final name = await dao.meta(MetaKeys.printerName); final autoPrint = await dao.meta(MetaKeys.autoPrint); final openDrawer = await dao.meta(MetaKeys.openDrawer); final drawerHost = await dao.meta(MetaKeys.drawerHost); final drawerPort = await dao.meta(MetaKeys.drawerPort); if (!mounted) return; state = PrinterSettings( printerUrl: url, printerName: name, autoPrint: autoPrint == '1', openDrawer: openDrawer != '0', drawerHost: drawerHost, drawerPort: int.tryParse(drawerPort ?? '') ?? 9100, ); } Future selectPrinter(Printer? printer) async { final dao = _ref.read(localStoreProvider).catalogue; if (printer == null) { await dao.setMeta(MetaKeys.printerUrl, ''); await dao.setMeta(MetaKeys.printerName, ''); if (!mounted) return; state = state.copyWith(clearPrinter: true, autoPrint: false); return; } await dao.setMeta(MetaKeys.printerUrl, printer.url); await dao.setMeta(MetaKeys.printerName, printer.name); if (!mounted) return; state = state.copyWith( printerUrl: printer.url, printerName: printer.name, ); } Future setAutoPrint(bool value) async { // Auto-print with nothing selected would fail silently on every sale. if (value && !state.hasPrinter) return; await _ref .read(localStoreProvider) .catalogue .setMeta(MetaKeys.autoPrint, value ? '1' : '0'); if (!mounted) return; state = state.copyWith(autoPrint: value); } Future setOpenDrawer(bool value) async { await _ref .read(localStoreProvider) .catalogue .setMeta(MetaKeys.openDrawer, value ? '1' : '0'); if (!mounted) return; state = state.copyWith(openDrawer: value); } /// Points the drawer kick at a printer. /// /// A blank host disables it — which is the honest state for a USB printer, /// since there is no raw path to one from Flutter. Future setDrawerAddress(String host, int port) async { final dao = _ref.read(localStoreProvider).catalogue; await dao.setMeta(MetaKeys.drawerHost, host.trim()); await dao.setMeta(MetaKeys.drawerPort, '$port'); if (!mounted) return; state = state.copyWith(drawerHost: host.trim(), drawerPort: port); } } final printerSettingsProvider = StateNotifierProvider( (ref) => PrinterSettingsController(ref), ); /// Printers the OS can currently see. Re-read whenever Settings is opened. final availablePrintersProvider = FutureProvider>( (ref) => ref.watch(receiptServiceProvider).availablePrinters(), );