Files
Kifi/kifi-app/lib/features/barcode_designer/providers/barcode_designer_provider.dart

532 lines
16 KiB
Dart

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();
});