242 lines
7.0 KiB
Dart
242 lines
7.0 KiB
Dart
|
|
import 'dart:async';
|
||
|
|
import 'dart:ui' as ui;
|
||
|
|
|
||
|
|
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
|
import 'package:image/image.dart' as img;
|
||
|
|
import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';
|
||
|
|
|
||
|
|
import '../../constant.dart';
|
||
|
|
import '../model/print_transaction_model.dart';
|
||
|
|
|
||
|
|
final printerPurchaseProviderNotifier = ChangeNotifierProvider((ref) => PrinterPurchase());
|
||
|
|
|
||
|
|
class PrinterPurchase extends ChangeNotifier {
|
||
|
|
List<BluetoothInfo> availableBluetoothDevices = [];
|
||
|
|
|
||
|
|
Future<void> getBluetooth() async {
|
||
|
|
final List<BluetoothInfo> bluetooths = await PrintBluetoothThermal.pairedBluetooths;
|
||
|
|
availableBluetoothDevices = bluetooths;
|
||
|
|
notifyListeners();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<bool> setConnect(String mac) async {
|
||
|
|
bool status = false;
|
||
|
|
final bool result = await PrintBluetoothThermal.connect(macPrinterAddress: mac);
|
||
|
|
if (result == true) {
|
||
|
|
connected = true;
|
||
|
|
status = true;
|
||
|
|
}
|
||
|
|
notifyListeners();
|
||
|
|
return status;
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<bool> printCustomTicket(
|
||
|
|
{required PrintPurchaseTransactionModel printTransactionModel,
|
||
|
|
required String data,
|
||
|
|
required String paperSize}) async {
|
||
|
|
bool isPrinted = false;
|
||
|
|
bool? isConnected = await PrintBluetoothThermal.connectionStatus;
|
||
|
|
if (isConnected == true) {
|
||
|
|
List<int> bytes = await customPrintTicket(
|
||
|
|
printTransactionModel: printTransactionModel,
|
||
|
|
data: data,
|
||
|
|
paperSize: paperSize,
|
||
|
|
);
|
||
|
|
await PrintBluetoothThermal.writeBytes(bytes);
|
||
|
|
isPrinted = true;
|
||
|
|
} else {
|
||
|
|
isPrinted = false;
|
||
|
|
}
|
||
|
|
notifyListeners();
|
||
|
|
return isPrinted;
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<List<int>> customPrintTicket(
|
||
|
|
{required PrintPurchaseTransactionModel printTransactionModel,
|
||
|
|
required String data,
|
||
|
|
required String paperSize}) async {
|
||
|
|
List<int> bytes = [];
|
||
|
|
PaperSize? size;
|
||
|
|
if (paperSize == '2 inch 58mm') {
|
||
|
|
size = PaperSize.mm58;
|
||
|
|
} else {
|
||
|
|
size = PaperSize.mm80;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
CapabilityProfile profile = await CapabilityProfile.load();
|
||
|
|
final generator = Generator(PaperSize.mm58, profile);
|
||
|
|
|
||
|
|
Future<void> addText(String text, {PosStyles? styles, int linesAfter = 0}) async {
|
||
|
|
if (_isAscii(text)) {
|
||
|
|
bytes += generator.text(
|
||
|
|
text,
|
||
|
|
linesAfter: linesAfter,
|
||
|
|
styles: const PosStyles(
|
||
|
|
align: PosAlign.center,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
final imageBytes = await _textToImageBytes(
|
||
|
|
generator,
|
||
|
|
text,
|
||
|
|
styles: const PosStyles(
|
||
|
|
align: PosAlign.center,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
bytes += imageBytes;
|
||
|
|
if (linesAfter > 0) {
|
||
|
|
bytes += generator.feed(linesAfter);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add company name
|
||
|
|
final companyNameText = printTransactionModel.personalInformationModel.data?.companyName ?? '';
|
||
|
|
bytes += generator.text(
|
||
|
|
companyNameText,
|
||
|
|
styles: const PosStyles(
|
||
|
|
align: PosAlign.center,
|
||
|
|
height: PosTextSize.size2,
|
||
|
|
width: PosTextSize.size2,
|
||
|
|
),
|
||
|
|
linesAfter: 1,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Add address
|
||
|
|
final address = printTransactionModel.personalInformationModel.data?.address ?? '';
|
||
|
|
if (address.isNotEmpty) {
|
||
|
|
bytes += generator.text(
|
||
|
|
address,
|
||
|
|
styles: const PosStyles(align: PosAlign.center),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add phone number
|
||
|
|
final phoneNumber = printTransactionModel.personalInformationModel.data?.phoneNumber ?? '';
|
||
|
|
if (phoneNumber.isNotEmpty) {
|
||
|
|
bytes += generator.text(
|
||
|
|
'Tel: $phoneNumber',
|
||
|
|
styles: const PosStyles(align: PosAlign.center),
|
||
|
|
linesAfter: printTransactionModel.personalInformationModel.data?.vatNo?.trim().isNotEmpty == true ? 0 : 1,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add VAT information if available
|
||
|
|
final vatNumber = printTransactionModel.personalInformationModel.data?.vatNo;
|
||
|
|
if (vatNumber != null &&
|
||
|
|
vatNumber.trim().isNotEmpty &&
|
||
|
|
printTransactionModel.personalInformationModel.data?.meta?.showVat == 1) {
|
||
|
|
final vatName = printTransactionModel.personalInformationModel.data?.vatName;
|
||
|
|
final label = vatName != null ? '$vatName:' : 'Shop GST:';
|
||
|
|
bytes += generator.text(
|
||
|
|
'$label $vatNumber',
|
||
|
|
styles: const PosStyles(align: PosAlign.center),
|
||
|
|
linesAfter: 1,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
await addText(
|
||
|
|
data,
|
||
|
|
styles: const PosStyles(
|
||
|
|
align: PosAlign.center,
|
||
|
|
),
|
||
|
|
linesAfter: 1,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Add footer
|
||
|
|
bytes += generator.text('Thank you!', styles: const PosStyles(align: PosAlign.center, bold: true));
|
||
|
|
bytes += generator.text(
|
||
|
|
'Note: Goods once sold will not be taken back or exchanged.',
|
||
|
|
styles: const PosStyles(align: PosAlign.center, bold: false),
|
||
|
|
linesAfter: 1,
|
||
|
|
);
|
||
|
|
|
||
|
|
bytes += generator.text(
|
||
|
|
'Developed By: $companyName',
|
||
|
|
styles: const PosStyles(align: PosAlign.center),
|
||
|
|
linesAfter: 1,
|
||
|
|
);
|
||
|
|
|
||
|
|
bytes += generator.cut();
|
||
|
|
return bytes;
|
||
|
|
} catch (e) {
|
||
|
|
print('Error generating print ticket: $e');
|
||
|
|
rethrow;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool _isAscii(String input) {
|
||
|
|
for (final c in input.runes) {
|
||
|
|
if (c > 127) return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<List<int>> _textToImageBytes(
|
||
|
|
Generator generator,
|
||
|
|
String text, {
|
||
|
|
PosStyles? styles,
|
||
|
|
}) async {
|
||
|
|
try {
|
||
|
|
const double fontSize = 26.0;
|
||
|
|
const double horizontalPadding = 10.0;
|
||
|
|
const double lineSpacing = 1.2;
|
||
|
|
|
||
|
|
const double printerWidthMm = 58.0;
|
||
|
|
const double printerDpi = 203.0;
|
||
|
|
|
||
|
|
final double printerWidthPx = (printerWidthMm * printerDpi / 25.4) - (horizontalPadding * 2);
|
||
|
|
const String fallbackFont = 'Arial Unicode MS';
|
||
|
|
|
||
|
|
final textStyle = TextStyle(
|
||
|
|
fontSize: fontSize,
|
||
|
|
fontWeight: styles?.bold == true ? FontWeight.bold : FontWeight.normal,
|
||
|
|
color: Colors.black,
|
||
|
|
fontFamily: fallbackFont,
|
||
|
|
height: lineSpacing,
|
||
|
|
);
|
||
|
|
|
||
|
|
final textPainter = TextPainter(
|
||
|
|
text: TextSpan(text: text, style: textStyle),
|
||
|
|
textDirection: TextDirection.ltr,
|
||
|
|
maxLines: 100,
|
||
|
|
ellipsis: '...',
|
||
|
|
);
|
||
|
|
|
||
|
|
textPainter.layout(maxWidth: printerWidthPx);
|
||
|
|
|
||
|
|
final double imageWidth = printerWidthPx + (horizontalPadding * 2);
|
||
|
|
final double imageHeight = textPainter.height + 20.0;
|
||
|
|
|
||
|
|
final recorder = ui.PictureRecorder();
|
||
|
|
final canvas = Canvas(
|
||
|
|
recorder,
|
||
|
|
Rect.fromLTWH(0, 0, imageWidth, imageHeight),
|
||
|
|
);
|
||
|
|
|
||
|
|
textPainter.paint(
|
||
|
|
canvas,
|
||
|
|
Offset(horizontalPadding, 10.0),
|
||
|
|
);
|
||
|
|
|
||
|
|
final picture = recorder.endRecording();
|
||
|
|
final uiImage = await picture.toImage(
|
||
|
|
imageWidth.toInt(),
|
||
|
|
imageHeight.toInt(),
|
||
|
|
);
|
||
|
|
|
||
|
|
final byteData = await uiImage.toByteData(format: ui.ImageByteFormat.png);
|
||
|
|
final pngBytes = byteData!.buffer.asUint8List();
|
||
|
|
final image = img.decodePng(pngBytes)!;
|
||
|
|
|
||
|
|
return generator.image(image);
|
||
|
|
} catch (e) {
|
||
|
|
print('Error in _textToImageBytes: $e');
|
||
|
|
rethrow;
|
||
|
|
}
|
||
|
|
}
|