easy_thermal_printer 1.0.1
easy_thermal_printer: ^1.0.1 copied to clipboard
A simple Flutter library for Bluetooth thermal printing using ESC/POS commands. Supports text, images, QR codes, barcodes, and HTML receipt preview.
import 'dart:convert';
import 'dart:math' as math;
import 'package:easy_thermal_printer/easy_thermal_printer.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:intl/intl.dart';
import 'int_extension.dart';
void main() => runApp(const EasyPrinterDemoApp());
class EasyPrinterDemoApp extends StatelessWidget {
const EasyPrinterDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Easy Thermal Printer Demo',
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.blue,
),
home: const PrinterHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class PrinterHomePage extends StatefulWidget {
const PrinterHomePage({super.key});
@override
State<PrinterHomePage> createState() => _PrinterHomePageState();
}
class _PrinterHomePageState extends State<PrinterHomePage> {
bool _isBusy = false;
List<FluetoothDevice> _devices = [];
List<FluetoothDevice> _connectedDevices = [];
final _printer = EasyThermalPrinter.instance;
String _htmlPreview = '';
@override
void initState() {
super.initState();
_refreshPrinters();
_printer.setPaperSize(PaperSize.mm58);
}
Future<void> _refreshPrinters() async {
if (_isBusy) return;
setState(() => _isBusy = true);
final devices = await _printer.scan();
final connected = await _printer.getConnectedDevices();
setState(() {
_devices = devices;
_connectedDevices = connected;
_isBusy = false;
});
}
Future<void> _connect(FluetoothDevice device) async {
if (_isBusy) return;
setState(() => _isBusy = true);
await _printer.connect(device);
final connected = await _printer.getConnectedDevices();
setState(() {
_connectedDevices = connected;
_isBusy = false;
});
}
Future<void> _disconnect(FluetoothDevice device) async {
if (_isBusy) return;
setState(() => _isBusy = true);
await _printer.disconnect(device.id);
final connected = await _printer.getConnectedDevices();
setState(() {
_connectedDevices = connected;
_isBusy = false;
});
}
bool _isConnected(FluetoothDevice device) =>
_connectedDevices.any((d) => d.id == device.id);
/// Build example receipt
Future<ReceiptSectionText> _buildReceipt({bool useLogo = true}) async {
final logoBytes = await rootBundle.load('assets/image.png');
final receipt = ReceiptSectionText();
if (useLogo) {
receipt.addImage(
base64.encode(Uint8List.view(logoBytes.buffer)),
width: 330,
);
receipt.addSpacer();
}
receipt.addText('EASY STORE',
size: ReceiptTextSizeType.large, style: ReceiptTextStyleType.bold);
receipt.addText('Wisma 46, Jakarta, Indonesia',
size: ReceiptTextSizeType.small);
receipt.addSpacer();
receipt.addLeftRightText('Order No', '123456');
receipt.addLeftRightText(
'Time',
DateFormat('H:mm, dd/MM/yy').format(DateTime.now()),
);
receipt.addSpacer(useDashed: true);
int total = 0;
for (int i = 0; i < 5; i++) {
final qty = math.Random().nextInt(5) + 1;
final price = 1000 + (i * 500);
receipt.addText('Item ${i + 1}', style: ReceiptTextStyleType.bold);
receipt.addLeftRightText('$qty × ${price.inIDR}', (qty * price).inIDR);
total += qty * price;
}
receipt.addSpacer(useDashed: true);
receipt.addLeftRightText('Total', total.inIDR,
rightStyle: ReceiptTextStyleType.bold);
receipt.addLeftRightText('Payment', 'Cash');
receipt.addSpacer();
receipt.addText('Scan QR to pay:');
receipt.addQR('easythermalprinter', size: 300);
receipt.addText('Thank you for your purchase!');
return receipt;
}
Future<void> _showPreview(ReceiptSectionText receipt) async {
final html = _printer.convertToHtml(receipt.getContent());
setState(() => _htmlPreview = html);
}
Future<void> _printNow(FluetoothDevice device) async {
final receipt = await _buildReceipt();
await _printer.printReceiptText(receipt, device.id, feedCount: 2);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Easy Thermal Printer'),
actions: [
IconButton(
onPressed: _isBusy ? null : _refreshPrinters,
icon: const Icon(Icons.refresh),
),
],
),
body: Row(
children: [
// Printer List Panel
Expanded(
flex: 1,
child: _isBusy
? const Center(child: CircularProgressIndicator())
: _devices.isEmpty
? const Center(child: Text('No printers found'))
: ListView.separated(
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) => const Divider(height: 1),
itemCount: _devices.length,
itemBuilder: (context, index) {
final device = _devices[index];
final isConnected = _isConnected(device);
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
child: ListTile(
leading: Icon(
isConnected
? Icons.print
: Icons.print_disabled,
color: isConnected ? Colors.green : Colors.grey,
),
title: Text(device.name),
subtitle: Text(device.id),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.visibility),
tooltip: 'Preview Receipt',
onPressed: () async {
final receipt = await _buildReceipt();
_showPreview(receipt);
},
),
IconButton(
icon: const Icon(Icons.print),
tooltip: 'Print Receipt',
onPressed: isConnected
? () => _printNow(device)
: null,
),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: isConnected
? Colors.redAccent
: Colors.blueAccent,
),
onPressed: isConnected
? () => _disconnect(device)
: () => _connect(device),
child: Text(
isConnected ? 'Disconnect' : 'Connect'),
),
],
),
),
);
},
),
),
// HTML Preview Panel
Expanded(
flex: 1,
child: Column(
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
padding: const EdgeInsets.all(8),
child: const Row(
children: [
Icon(Icons.receipt_long),
SizedBox(width: 8),
Text('Receipt Preview',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16)),
],
),
),
Expanded(
child: _htmlPreview.isEmpty
? const Center(child: Text('No preview available'))
: SingleChildScrollView(
child: Html(
data: _htmlPreview,
style: {
"body": Style(
backgroundColor: Colors.white,
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontFamily: 'Courier New',
fontSize: FontSize.medium,
),
"img": Style(
alignment: Alignment.center,
margin: Margins.symmetric(vertical: 8),
),
"hr": Style(
border: const Border(
top: BorderSide(
color: Colors.black,
width: 1.5,
style: BorderStyle.solid),
),
),
".text-center":
Style(textAlign: TextAlign.center),
".text-left": Style(textAlign: TextAlign.left),
".text-right": Style(textAlign: TextAlign.right),
},
),
),
),
],
),
),
],
),
);
}
}