print_bluetooth_thermal 1.2.2
print_bluetooth_thermal: ^1.2.2 copied to clipboard
Ticket printing for android, location permission is not requested to connect the printer
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:flutter_esc_pos_utils/flutter_esc_pos_utils.dart';
import 'package:print_bluetooth_thermal/post_code.dart';
import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';
import 'package:image/image.dart' as img;
import 'package:print_bluetooth_thermal/print_bluetooth_thermal_windows.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
String _info = "Loading system info...";
String _msj = '';
bool connected = false;
List<BluetoothInfo> items = [];
// Opciones del menú corregidas gramaticalmente
final List<String> _options = ["Check Bluetooth Permission", "Check Bluetooth State", "Check Connection Status", "Refresh Platform Info"];
String _selectSize = "2";
final _txtText = TextEditingController(text: "Hello Developer!");
bool _progress = false;
String _msjprogress = "";
String optionprinttype = "58 mm";
List<String> options = ["58 mm", "80 mm"];
@override
void initState() {
super.initState();
initPlatformState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.light,
),
),
home: Scaffold(
appBar: AppBar(
title: const Text('Thermal Printer Utility'),
centerTitle: true,
actions: [
PopupMenuButton(
elevation: 4,
tooltip: 'More Options',
onSelected: (Object select) async {
String sel = select as String;
if (sel == "Check Bluetooth Permission") {
bool status = await PrintBluetoothThermal.isPermissionBluetoothGranted;
setState(() {
_info = "Bluetooth Permission Granted: $status";
});
} else if (sel == "Check Bluetooth State") {
bool state = await PrintBluetoothThermal.bluetoothEnabled;
setState(() {
_info = "Bluetooth Enabled: $state";
});
} else if (sel == "Refresh Platform Info") {
initPlatformState();
} else if (sel == "Check Connection Status") {
final bool result = await PrintBluetoothThermal.connectionStatus;
connected = result;
setState(() {
_info = "Connection Status: $result";
});
}
},
itemBuilder: (BuildContext context) {
return _options.map((String option) {
return PopupMenuItem(
value: option,
child: Text(option),
);
}).toList();
},
)
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// CARD 1: Status & Info
Card(
elevation: 0,
color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Printer Status",
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: connected ? Colors.green.withValues(alpha: 0.15) : Colors.grey.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
connected ? Icons.check_circle : Icons.cancel,
color: connected ? Colors.green : Colors.grey[700],
size: 16,
),
const SizedBox(width: 4),
Text(
connected ? "CONNECTED" : "DISCONNECTED",
style: TextStyle(
color: connected ? Colors.green[800] : Colors.grey[800],
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
),
),
],
),
const Divider(height: 24),
Text(
_msj,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
"Info: $_info",
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]),
),
],
),
),
),
const SizedBox(height: 12),
// CARD 2: Printing Preferences (Width)
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.settings_ethernet, color: Theme.of(context).colorScheme.secondary),
const SizedBox(width: 8),
Text(
"Paper Width:",
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
),
],
),
DropdownButton<String>(
value: optionprinttype,
underline: const SizedBox(), // Removes the default line
items: options.map((String option) {
return DropdownMenuItem<String>(
value: option,
child: Text(option, style: const TextStyle(fontWeight: FontWeight.bold)),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
optionprinttype = newValue!;
});
},
),
],
),
),
),
const SizedBox(height: 12),
// CARD 3: Bluetooth Search & Devices List
Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Available Printers",
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
FilledButton.icon(
onPressed: _progress ? null : getBluetoots,
icon: _progress
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.search, size: 18),
label: Text(_progress ? _msjprogress : "Search"),
),
],
),
const SizedBox(height: 12),
Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surfaceContainerLowest,
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5),
),
child: items.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.print_disabled, size: 40, color: Colors.grey[400]),
const SizedBox(height: 8),
Text(
"No paired devices found",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600], fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
"Link the printer in settings, then press Search.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
],
),
),
)
: ListView.separated(
padding: const EdgeInsets.all(8),
itemCount: items.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Icon(Icons.print, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 20),
),
title: Text(
item.name.isNotEmpty ? item.name : "Unknown Device",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"MAC: ${item.macAdress}",
style: TextStyle(fontFamily: 'monospace', fontSize: 12, color: Colors.grey[600]),
),
trailing: const Icon(Icons.chevron_right, size: 18),
onTap: () => connect(item.macAdress),
);
},
),
),
if (connected) ...[
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: disconnect,
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
side: BorderSide(color: Theme.of(context).colorScheme.error),
),
icon: const Icon(Icons.power_settings_new, size: 18),
label: const Text("Disconnect"),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton.icon(
onPressed: printTest,
icon: const Icon(Icons.receipt_long, size: 18),
label: const Text("Test Print"),
),
),
],
)
],
],
),
),
),
const SizedBox(height: 12),
// CARD 4: Custom Raw Text Printing
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Direct Raw Printing",
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
"Send raw text natively without external templates.",
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _txtText,
decoration: InputDecoration(
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
labelText: "Message",
hintText: "Write something...",
),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).colorScheme.outline, width: 0.8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
hint: const Text('Size'),
value: _selectSize,
items: <String>['1', '2', '3', '4', '5'].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text("Size $value", style: const TextStyle(fontSize: 14)),
);
}).toList(),
onChanged: (String? select) {
setState(() {
_selectSize = select.toString();
});
},
),
),
)
],
),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: connected ? printWithoutPackage : null,
icon: const Icon(Icons.send_rounded, size: 18),
label: const Text("Send Plain Text"),
),
],
),
),
),
const SizedBox(height: 24),
],
),
),
),
);
}
Future<void> initPlatformState() async {
String platformVersion;
int porcentbatery = 0;
try {
platformVersion = await PrintBluetoothThermal.platformVersion;
porcentbatery = await PrintBluetoothThermal.batteryLevel;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
final bool result = await PrintBluetoothThermal.bluetoothEnabled;
debugPrint("bluetooth enabled: $result");
if (result) {
_msj = "Bluetooth enabled, search for devices to connect.";
} else {
_msj = "Bluetooth is disabled. Please turn it on.";
}
setState(() {
_info = "$platformVersion ($porcentbatery% Battery)";
});
}
Future<void> getBluetoots() async {
setState(() {
_progress = true;
_msjprogress = "Searching...";
items = [];
});
final List<BluetoothInfo> listResult = await PrintBluetoothThermal.pairedBluetooths;
setState(() {
_progress = false;
});
if (listResult.isEmpty) {
_msj = "No paired devices found. Please link the printer in settings.";
} else {
_msj = "Select a printer from the list to connect.";
}
setState(() {
items = listResult;
});
}
Future<void> connect(String mac) async {
setState(() {
_progress = true;
_msjprogress = "Connecting...";
connected = false;
});
final bool result = await PrintBluetoothThermal.connect(macPrinterAddress: mac);
debugPrint("state connected: $result");
if (result) connected = true;
setState(() {
_progress = false;
});
}
Future<void> disconnect() async {
final bool status = await PrintBluetoothThermal.disconnect;
setState(() {
connected = false;
});
debugPrint("status disconnect: $status");
}
Future<void> printTest() async {
bool conexionStatus = await PrintBluetoothThermal.connectionStatus;
if (conexionStatus) {
bool result = false;
if (Platform.isWindows) {
List<int> ticket = await testWindows();
result = await PrintBluetoothThermalWindows.writeBytes(bytes: ticket);
} else {
List<int> ticket = await testTicket();
result = await PrintBluetoothThermal.writeBytes(ticket);
}
debugPrint("print test result: $result");
} else {
debugPrint("print test connectionStatus: $conexionStatus");
setState(() {
disconnect();
});
}
}
Future<void> printString() async {
bool conexionStatus = await PrintBluetoothThermal.connectionStatus;
if (conexionStatus) {
String enter = '\n';
await PrintBluetoothThermal.writeBytes(enter.codeUnits);
String text = "Hello";
await PrintBluetoothThermal.writeString(printText: PrintTextSize(size: 1, text: text));
await PrintBluetoothThermal.writeString(printText: PrintTextSize(size: 2, text: "$text size 2"));
await PrintBluetoothThermal.writeString(printText: PrintTextSize(size: 3, text: "$text size 3"));
} else {
debugPrint("Bluetooth disconnected $conexionStatus");
}
}
Future<List<int>> testTicket() async {
List<int> bytes = [];
final profile = await CapabilityProfile.load();
final generator = Generator(optionprinttype == "58 mm" ? PaperSize.mm58 : PaperSize.mm80, profile);
bytes += generator.reset();
final ByteData data = await rootBundle.load('assets/mylogo.jpg');
final Uint8List bytesImg = data.buffer.asUint8List();
img.Image? image = img.decodeImage(bytesImg);
if (Platform.isIOS) {
final resizedImage = img.copyResize(image!, width: image.width ~/ 1.3, height: image.height ~/ 1.3, interpolation: img.Interpolation.nearest);
final bytesimg = Uint8List.fromList(img.encodeJpg(resizedImage));
}
bytes += generator.text('Regular: aA bB cC dD eE fF gG hH iI jJ kK lL mM nN oO pP qQ rR sS tT uU vV wW xX yY zZ');
bytes += generator.text('Special 1: ñÑ àÀ èÈ éÉ üÜ çÇ ôÔ', styles: const PosStyles(codeTable: 'CP1252'));
bytes += generator.text('Special 2: blåbærgrød', styles: const PosStyles(codeTable: 'CP1252'));
bytes += generator.text('Bold text', styles: const PosStyles(bold: true));
bytes += generator.text('Reverse text', styles: const PosStyles(reverse: true));
bytes += generator.text('Underlined text', styles: const PosStyles(underline: true), linesAfter: 1);
bytes += generator.text('Align left', styles: const PosStyles(align: PosAlign.left));
bytes += generator.text('Align center', styles: const PosStyles(align: PosAlign.center));
bytes += generator.text('Align right', styles: const PosStyles(align: PosAlign.right), linesAfter: 1);
bytes += generator.row([
PosColumn(
text: 'col3',
width: 3,
styles: const PosStyles(align: PosAlign.center, underline: true),
),
PosColumn(
text: 'col6',
width: 6,
styles: const PosStyles(align: PosAlign.center, underline: true),
),
PosColumn(
text: 'col3',
width: 3,
styles: const PosStyles(align: PosAlign.center, underline: true),
),
]);
final List<int> barData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 4];
bytes += generator.barcode(Barcode.upcA(barData));
bytes += generator.qrcode('example.com');
bytes += generator.text(
'Text size 50%',
styles: const PosStyles(
fontType: PosFontType.fontB,
),
);
bytes += generator.text(
'Text size 100%',
styles: const PosStyles(
fontType: PosFontType.fontA,
),
);
bytes += generator.text(
'Text size 200%',
styles: const PosStyles(
height: PosTextSize.size2,
width: PosTextSize.size2,
),
);
bytes += generator.feed(2);
return bytes;
}
Future<List<int>> testWindows() async {
List<int> bytes = [];
bytes += PostCode.text(text: "Size compressed", fontSize: FontSize.compressed);
bytes += PostCode.text(text: "Size normal", fontSize: FontSize.normal);
bytes += PostCode.text(text: "Bold", bold: true);
bytes += PostCode.text(text: "Inverse", inverse: true);
bytes += PostCode.text(text: "AlignPos right", align: AlignPos.right);
bytes += PostCode.text(text: "Size big", fontSize: FontSize.big);
bytes += PostCode.enter();
bytes += PostCode.row(texts: ["PRODUCT", "VALUE"], proportions: [60, 40], fontSize: FontSize.compressed);
for (int i = 0; i < 3; i++) {
bytes += PostCode.row(texts: ["Item $i", "$i,00"], proportions: [60, 40], fontSize: FontSize.compressed);
}
bytes += PostCode.line();
bytes += PostCode.barcode(barcodeData: "123456789");
bytes += PostCode.qr("123456789");
bytes += PostCode.enter(nEnter: 5);
return bytes;
}
Future<void> printWithoutPackage() async {
bool connectionStatus = await PrintBluetoothThermal.connectionStatus;
if (connectionStatus) {
String text = "${_txtText.text}\n";
bool result = await PrintBluetoothThermal.writeString(printText: PrintTextSize(size: int.parse(_selectSize), text: text));
debugPrint("print raw text result: $result");
setState(() {
_msj = "Printed successfully: $result";
});
} else {
setState(() {
_msj = "Error: No connected device found.";
});
debugPrint("Not connected");
}
}
}