import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../domain/label_document.dart'; import '../domain/dynamic_field_definition.dart'; import '../services/template_data_resolver.dart'; import '../services/zpl_compiler.dart'; import '../../inventory/domain/product.dart'; import '../../inventory/domain/inventory_item.dart'; class BarcodeDesignerState { final LabelDocument document; final String? selectedElementId; final Product? sampleProduct; final InventoryItem? sampleInventoryItem; final bool isPreviewMode; final double zoom; final bool showGrid; final bool snapToGrid; final double gridStepMm; final int? savedTemplateId; final List undoStack; final List redoStack; final String liveCommands; const BarcodeDesignerState({ required this.document, this.selectedElementId, this.sampleProduct, this.sampleInventoryItem, this.isPreviewMode = false, this.zoom = 2.4, // Good default zoom for 50x25mm labels on desktop/mobile this.showGrid = true, this.snapToGrid = true, this.gridStepMm = 1.0, this.savedTemplateId, this.undoStack = const [], this.redoStack = const [], this.liveCommands = '', }); LabelElement? get selectedElement { if (selectedElementId == null) return null; return document.elements.where((e) => e.id == selectedElementId).firstOrNull; } LabelDocument get resolvedDocument { if (sampleProduct == null && sampleInventoryItem == null) { return document; } return TemplateDataResolver.resolveDocument( document: document, product: sampleProduct, inventoryItem: sampleInventoryItem, ); } BarcodeDesignerState copyWith({ LabelDocument? document, String? selectedElementId, bool clearSelectedElement = false, Product? sampleProduct, bool clearSampleProduct = false, InventoryItem? sampleInventoryItem, bool clearSampleInventoryItem = false, bool? isPreviewMode, double? zoom, bool? showGrid, bool? snapToGrid, double? gridStepMm, int? savedTemplateId, List? undoStack, List? redoStack, String? liveCommands, }) { return BarcodeDesignerState( document: document ?? this.document, selectedElementId: clearSelectedElement ? null : (selectedElementId ?? this.selectedElementId), sampleProduct: clearSampleProduct ? null : (sampleProduct ?? this.sampleProduct), sampleInventoryItem: clearSampleInventoryItem ? null : (sampleInventoryItem ?? this.sampleInventoryItem), isPreviewMode: isPreviewMode ?? this.isPreviewMode, zoom: zoom ?? this.zoom, showGrid: showGrid ?? this.showGrid, snapToGrid: snapToGrid ?? this.snapToGrid, gridStepMm: gridStepMm ?? this.gridStepMm, savedTemplateId: savedTemplateId ?? this.savedTemplateId, undoStack: undoStack ?? this.undoStack, redoStack: redoStack ?? this.redoStack, liveCommands: liveCommands ?? this.liveCommands, ); } } class BarcodeDesignerNotifier extends Notifier { @override BarcodeDesignerState build() { final defaultDoc = LabelDocument.createDefault(); final cmds = PrintCompiler.compile(document: defaultDoc); return BarcodeDesignerState( document: defaultDoc, liveCommands: cmds, ); } void _pushUndo(LabelDocument oldDoc) { final newUndo = List.from(state.undoStack); if (newUndo.length >= 30) newUndo.removeAt(0); newUndo.add(oldDoc); state = state.copyWith(undoStack: newUndo, redoStack: []); } void undo() { if (state.undoStack.isEmpty) return; final newUndo = List.from(state.undoStack); final previousDoc = newUndo.removeLast(); final newRedo = List.from(state.redoStack)..add(state.document); state = state.copyWith( document: previousDoc, undoStack: newUndo, redoStack: newRedo, ); _updateLiveCommands(); } void redo() { if (state.redoStack.isEmpty) return; final newRedo = List.from(state.redoStack); final nextDoc = newRedo.removeLast(); final newUndo = List.from(state.undoStack)..add(state.document); state = state.copyWith( document: nextDoc, undoStack: newUndo, redoStack: newRedo, ); _updateLiveCommands(); } void loadDocument(LabelDocument doc, [int? templateId]) { final cmds = PrintCompiler.compile(document: doc); state = BarcodeDesignerState( document: doc, savedTemplateId: templateId, sampleProduct: state.sampleProduct, sampleInventoryItem: state.sampleInventoryItem, isPreviewMode: state.isPreviewMode, zoom: state.zoom, showGrid: state.showGrid, snapToGrid: state.snapToGrid, liveCommands: cmds, ); } void resetToNew([double widthMm = 50.0, double heightMm = 25.0]) { final newDoc = LabelDocument( id: 'template_${DateTime.now().millisecondsSinceEpoch}', name: '${widthMm.round()}x${heightMm.round()} Barcode Template', config: LabelConfiguration( widthMm: widthMm, heightMm: heightMm, ), elements: [], ); loadDocument(newDoc, null); } void updateTemplateName(String name) { state = state.copyWith( document: state.document.copyWith(name: name), ); } void updateTemplateDescription(String desc) { state = state.copyWith( document: state.document.copyWith(description: desc), ); } void setZoom(double z) { state = state.copyWith(zoom: z.clamp(0.8, 6.0)); } void toggleGrid() { state = state.copyWith(showGrid: !state.showGrid); } void toggleSnap() { state = state.copyWith(snapToGrid: !state.snapToGrid); } void togglePreviewMode([bool? force]) { final nextMode = force ?? !state.isPreviewMode; state = state.copyWith(isPreviewMode: nextMode); _updateLiveCommands(); } void setSampleProduct(Product? product, [InventoryItem? item]) { state = state.copyWith( sampleProduct: product, clearSampleProduct: product == null, sampleInventoryItem: item, clearSampleInventoryItem: item == null, ); _updateLiveCommands(); } void selectElement(String? id) { state = state.copyWith( selectedElementId: id, clearSelectedElement: id == null, ); } void updateLabelConfig(LabelConfiguration newConfig) { _pushUndo(state.document); state = state.copyWith( document: state.document.copyWith(config: newConfig), ); _updateLiveCommands(); } double _snap(double valueMm) { if (!state.snapToGrid) return valueMm; final step = state.gridStepMm; return (valueMm / step).round() * step; } void addElement(LabelElement element) { _pushUndo(state.document); final elements = List.from(state.document.elements)..add(element); state = state.copyWith( document: state.document.copyWith(elements: elements), selectedElementId: element.id, ); _updateLiveCommands(); } void updateElement(LabelElement element) { _pushUndo(state.document); final elements = state.document.elements.map((e) { return e.id == element.id ? element : e; }).toList(); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } void recordUndo() { _pushUndo(state.document); } void setElementPosition(String id, double xMm, double yMm) { final elem = state.document.elements.where((e) => e.id == id).firstOrNull; if (elem == null || elem.isLocked) return; double newX = xMm; double newY = yMm; if (state.snapToGrid) { newX = _snap(newX); newY = _snap(newY); } newX = newX.clamp(0.0, state.document.config.widthMm - 1.0); newY = newY.clamp(0.0, state.document.config.heightMm - 1.0); final updated = elem.copyWithPosition(xMm: newX, yMm: newY); final elements = state.document.elements.map((e) => e.id == id ? updated : e).toList(); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } void setElementSize(String id, double widthMm, double heightMm) { final elem = state.document.elements.where((e) => e.id == id).firstOrNull; if (elem == null || elem.isLocked) return; double w = widthMm; double h = heightMm; if (state.snapToGrid) { w = _snap(w); h = _snap(h); } w = w.clamp(2.0, state.document.config.widthMm); h = h.clamp(1.0, state.document.config.heightMm); final updated = elem.copyWithPosition(widthMm: w, heightMm: h); final elements = state.document.elements.map((e) => e.id == id ? updated : e).toList(); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } void moveElement(String id, double deltaXMm, double deltaYMm) { final elem = state.document.elements.where((e) => e.id == id).firstOrNull; if (elem == null || elem.isLocked) return; double newX = (elem.xMm + deltaXMm); double newY = (elem.yMm + deltaYMm); if (state.snapToGrid) { newX = _snap(newX); newY = _snap(newY); } newX = newX.clamp(0.0, state.document.config.widthMm - 1.0); newY = newY.clamp(0.0, state.document.config.heightMm - 1.0); final updated = elem.copyWithPosition(xMm: newX, yMm: newY); final elements = state.document.elements.map((e) => e.id == id ? updated : e).toList(); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } void resizeElement(String id, double newWidthMm, double newHeightMm) { final elem = state.document.elements.where((e) => e.id == id).firstOrNull; if (elem == null || elem.isLocked) return; double w = newWidthMm; double h = newHeightMm; if (state.snapToGrid) { w = _snap(w); h = _snap(h); } w = w.clamp(2.0, state.document.config.widthMm); h = h.clamp(1.0, state.document.config.heightMm); final updated = elem.copyWithPosition(widthMm: w, heightMm: h); final elements = state.document.elements.map((e) => e.id == id ? updated : e).toList(); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } void deleteElement(String id) { _pushUndo(state.document); final elements = state.document.elements.where((e) => e.id != id).toList(); state = state.copyWith( document: state.document.copyWith(elements: elements), clearSelectedElement: state.selectedElementId == id, ); _updateLiveCommands(); } void duplicateElement(String id) { final elem = state.document.elements.where((e) => e.id == id).firstOrNull; if (elem == null) return; _pushUndo(state.document); final newId = 'elem_${DateTime.now().millisecondsSinceEpoch}'; final duplicated = elem.copyWithPosition( xMm: (elem.xMm + 2.0).clamp(0.0, state.document.config.widthMm - 2.0), yMm: (elem.yMm + 2.0).clamp(0.0, state.document.config.heightMm - 2.0), ); final json = duplicated.toJson(); json['id'] = newId; final finalElem = LabelElement.fromJson(json); final elements = List.from(state.document.elements)..add(finalElem); state = state.copyWith( document: state.document.copyWith(elements: elements), selectedElementId: newId, ); _updateLiveCommands(); } void bringToFront(String id) { final index = state.document.elements.indexWhere((e) => e.id == id); if (index == -1 || index == state.document.elements.length - 1) return; _pushUndo(state.document); final elements = List.from(state.document.elements); final elem = elements.removeAt(index); elements.add(elem); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } void sendToBack(String id) { final index = state.document.elements.indexWhere((e) => e.id == id); if (index == -1 || index == 0) return; _pushUndo(state.document); final elements = List.from(state.document.elements); final elem = elements.removeAt(index); elements.insert(0, elem); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } void bringForward(String id) { final index = state.document.elements.indexWhere((e) => e.id == id); if (index == -1 || index >= state.document.elements.length - 1) return; _pushUndo(state.document); final elements = List.from(state.document.elements); final elem = elements.removeAt(index); elements.insert(index + 1, elem); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } void sendBackward(String id) { final index = state.document.elements.indexWhere((e) => e.id == id); if (index <= 0) return; _pushUndo(state.document); final elements = List.from(state.document.elements); final elem = elements.removeAt(index); elements.insert(index - 1, elem); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } /// Binds a product field directly to an existing DynamicTextElement or BarcodeElement void bindFieldToElement(String elementId, String source, String fieldKey) { final elem = state.document.elements.where((e) => e.id == elementId).firstOrNull; if (elem == null) return; _pushUndo(state.document); final fieldDef = DynamicFieldDefinition.find(source, fieldKey); LabelElement updated; if (elem is DynamicTextElement) { updated = elem.copyWithPosition( source: source, fieldKey: fieldKey, prefix: fieldDef?.defaultPrefix ?? elem.prefix, suffix: fieldDef?.defaultSuffix ?? elem.suffix, ); } else if (elem is BarcodeElement) { updated = elem.copyWithPosition( valueType: 'dynamic', source: source, fieldKey: fieldKey, ); } else if (elem is QrCodeElement) { updated = elem.copyWithPosition( valueType: 'dynamic', source: source, fieldKey: fieldKey, ); } else { return; } final elements = state.document.elements.map((e) => e.id == elementId ? updated : e).toList(); state = state.copyWith( document: state.document.copyWith(elements: elements), ); _updateLiveCommands(); } /// Adds a new element directly bound from a dragged DynamicFieldDefinition void addBoundFieldElement({ required DynamicFieldDefinition fieldDef, required double dropXMm, required double dropYMm, }) { _pushUndo(state.document); final id = 'elem_${DateTime.now().millisecondsSinceEpoch}'; LabelElement newElem; if (fieldDef.fieldKey == 'barcode' || (fieldDef.isBarcodeCompatible && fieldDef.fieldKey == 'sku')) { newElem = BarcodeElement( id: id, xMm: dropXMm.clamp(0.0, state.document.config.widthMm - 30.0), yMm: dropYMm.clamp(0.0, state.document.config.heightMm - 8.0), widthMm: 38.0, heightMm: 8.0, valueType: 'dynamic', source: fieldDef.source, fieldKey: fieldDef.fieldKey, showText: true, ); } else { newElem = DynamicTextElement( id: id, xMm: dropXMm.clamp(0.0, state.document.config.widthMm - 20.0), yMm: dropYMm.clamp(0.0, state.document.config.heightMm - 4.0), widthMm: 28.0, heightMm: 4.0, source: fieldDef.source, fieldKey: fieldDef.fieldKey, prefix: fieldDef.defaultPrefix, suffix: fieldDef.defaultSuffix, fontSizePt: 8.0, ); } addElement(newElem); } void _updateLiveCommands() { try { final docToCompile = state.isPreviewMode ? state.resolvedDocument : state.document; final cmds = PrintCompiler.compile(document: docToCompile); state = state.copyWith(liveCommands: cmds); } catch (_) { // Ignore intermediate compilation errors while typing } } } final barcodeDesignerProvider = NotifierProvider(() { return BarcodeDesignerNotifier(); });