Barcode Printing Utility Done
This commit is contained in:
@@ -0,0 +1,531 @@
|
||||
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<LabelDocument> undoStack;
|
||||
final List<LabelDocument> 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<LabelDocument>? undoStack,
|
||||
List<LabelDocument>? 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<BarcodeDesignerState> {
|
||||
@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<LabelDocument>.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<LabelDocument>.from(state.undoStack);
|
||||
final previousDoc = newUndo.removeLast();
|
||||
final newRedo = List<LabelDocument>.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<LabelDocument>.from(state.redoStack);
|
||||
final nextDoc = newRedo.removeLast();
|
||||
final newUndo = List<LabelDocument>.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<LabelElement>.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<LabelElement>.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<LabelElement>.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<LabelElement>.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<LabelElement>.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<LabelElement>.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<BarcodeDesignerNotifier, BarcodeDesignerState>(() {
|
||||
return BarcodeDesignerNotifier();
|
||||
});
|
||||
Reference in New Issue
Block a user