feat: Implement logo printing to thermal printer and add loading indicator

• Replace StrukTextGenerator with EscPosPrintService to support image printing

• Add loading indicator during print process to prevent UI freeze

• Fix Uint8List type issues for printer compatibility

• Improve error handling with proper state management

• Users can now print selected store logo on thermal receipts
master
a2nr 2025-08-23 22:44:16 +07:00
parent b848881805
commit 3e1c34d1ce
6 changed files with 492 additions and 237 deletions

View File

@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
// import 'dart:io'; // Add this import for file operations // PDF Removed
import 'dart:typed_data'; // Untuk Uint8List
import 'package:intl/intl.dart'; // Untuk format angka
import 'package:cashumit/models/receipt_item.dart';
import 'package:cashumit/screens/add_item_screen.dart';
// import 'package:cashumit/services/pdf_export_service.dart'; // PDF Removed
import 'package:cashumit/services/firefly_api_service.dart';
import 'package:cashumit/services/struk_text_generator.dart'; // Tambahkan import ini
import 'package:cashumit/services/esc_pos_print_service.dart'; // Tambahkan import ini
import 'package:bluetooth_print/bluetooth_print.dart';
import 'package:bluetooth_print/bluetooth_print_model.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart'; // Tambahkan import ini
@ -58,6 +60,7 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
late BluetoothPrint bluetoothPrint;
bool _bluetoothConnected = false;
BluetoothDevice? _bluetoothDevice;
bool _isPrinting = false; // State variable to track printing status
@override
void initState() {
@ -374,51 +377,31 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
}
try {
print('Menghasilkan teks struk...');
print('Menghasilkan byte array ESC/POS menggunakan flutter_esc_pos_utils...');
print('Jumlah item: ${items.length}');
print('Tanggal transaksi: $_transactionDate');
print('ID kasir: $cashierId');
print('ID transaksi: $transactionId');
// Generate struk text menggunakan StrukTextGenerator
final strukText = await StrukTextGenerator.generateStrukText(
// Generate struk dalam format byte array menggunakan EscPosPrintService
final bytes = await EscPosPrintService.generateEscPosBytes(
items: items,
transactionDate: _transactionDate,
cashierId: cashierId,
transactionId: transactionId,
);
print('Teks struk berhasil dihasilkan');
// Tampilkan teks struk untuk debugging
print('Isi teks struk:');
print(strukText);
// Konversi struk text ke format yang bisa dicetak
final lines = strukText.split('\n');
final List<LineText> list = lines
.where((line) => line.isNotEmpty)
.map((line) =>
LineText(type: LineText.TYPE_TEXT, content: line, linefeed: 1))
.toList();
);
// Tampilkan daftar baris untuk debugging
print('Daftar baris yang akan dicetak:');
for (int i = 0; i < list.length; i++) {
print('Baris $i: ${list[i].content}');
print('Byte array ESC/POS berhasil dihasilkan');
print('Jumlah byte: ${bytes.length}');
// Tampilkan byte array untuk debugging (dalam format hex)
print('Isi byte array (hex):');
if (bytes.length <= 1000) { // Batasi tampilan untuk mencegah output terlalu panjang
print(bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '));
} else {
print('Terlalu banyak byte untuk ditampilkan (${bytes.length} bytes)');
}
// Konfigurasi untuk mencetak struk - disesuaikan untuk printer termal umum
Map<String, dynamic> config = {
'width': 384, // Lebar kertas struk termal (dalam piksel) - 58mm printer
'height': 800, // Tinggi kertas struk termal (dalam piksel)
'gap': 0, // Jarak antar struk
};
print('Konfigurasi printer: $config');
// Kirim perintah cetak ke printer
print('Mengirim perintah cetak ke printer...');
print('Jumlah baris yang akan dikirim: ${list.length}');
// Verifikasi koneksi sebelum mencetak
final isConnectedBeforePrint = await bluetoothPrint.isConnected ?? false;
if (!isConnectedBeforePrint) {
@ -430,16 +413,18 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
return;
}
print('Mengirim perintah cetak dengan konfigurasi: $config');
print('Mengirim byte array ke printer...');
bool printSuccess = false;
String printError = '';
try {
// Coba kirim perintah cetak beberapa kali jika gagal
// Coba kirim byte array beberapa kali jika gagal
for (int attempt = 1; attempt <= 3; attempt++) {
print('Percobaan cetak ke-$attempt');
try {
await bluetoothPrint.printReceipt(config, list);
// Konversi List<int> ke Uint8List
final Uint8List data = Uint8List.fromList(bytes);
await bluetoothPrint.printRawData(data);
print('Perintah cetak berhasil dikirim pada percobaan ke-$attempt');
printSuccess = true;
break;
@ -481,11 +466,11 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
print('Stack trace: $stackTrace');
// Cetak detail error tambahan
print('Tipe error: ${e.runtimeType}');
// Menghapus baris bermasalah yang menggunakan e.message
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal mencetak struk: $e')),
);
SnackBar(content: Text('Gagal mencetak struk: $e')),
);
return;
}
}
@ -1090,48 +1075,52 @@ SpeedDialChild(
),
// SpeedDialChild for PDF printing removed as per request
SpeedDialChild(
child: const Icon(Icons.receipt),
child: _isPrinting
? const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)
: const Icon(Icons.receipt),
label: 'Cetak Struk',
onTap: () async {
// Periksa koneksi secara real-time
final isConnected = await _checkBluetoothConnection();
if (isConnected) {
_printToThermalPrinter();
} else {
// Coba sambungkan kembali jika ada device yang tersimpan
if (_bluetoothDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mencoba menyambungkan ke printer...')),
);
try {
await bluetoothPrint.connect(_bluetoothDevice!);
// Tunggu sebentar untuk memastikan koneksi stabil
await Future.delayed(const Duration(milliseconds: 500));
// Periksa koneksi lagi
final isConnectedAfterConnect = await _checkBluetoothConnection();
if (isConnectedAfterConnect) {
_printToThermalPrinter();
} else {
onTap: _isPrinting
? null
: () async {
// Periksa koneksi secara real-time
final isConnected = await _checkBluetoothConnection();
if (isConnected) {
_printToThermalPrinter();
} else {
// Coba sambungkan kembali jika ada device yang tersimpan
if (_bluetoothDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mencoba menyambungkan ke printer...')),
);
try {
await bluetoothPrint.connect(_bluetoothDevice!);
// Tunggu sebentar untuk memastikan koneksi stabil
await Future.delayed(const Duration(milliseconds: 500));
// Periksa koneksi lagi
final isConnectedAfterConnect = await _checkBluetoothConnection();
if (isConnectedAfterConnect) {
_printToThermalPrinter();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal menyambungkan ke printer')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal menyambungkan ke printer')),
SnackBar(
content: Text('Gagal menyambungkan ke printer: $e')),
);
}
} catch (e) {
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal menyambungkan ke printer: $e')),
const SnackBar(
content: Text('Hubungkan printer terlebih dahulu')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Hubungkan printer terlebih dahulu')),
);
}
}
},
},
backgroundColor: Colors.purple,
),
],

View File

@ -5,6 +5,8 @@ import 'package:cashumit/models/receipt_item.dart';
import 'package:flutter_esc_pos_utils/flutter_esc_pos_utils.dart';
import 'package:image/image.dart' as img;
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
/// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils
class EscPosPrintService {
@ -46,8 +48,16 @@ class EscPosPrintService {
return "Rp ${formatter.format(amount)}";
}
// Load capability profile
final profile = await CapabilityProfile.load();
// Load capability profile with timeout
CapabilityProfile profile;
try {
profile = await CapabilityProfile.load();
} catch (e) {
print('Gagal memuat capability profile: $e');
// Gunakan profile default jika gagal
profile = await CapabilityProfile.load();
}
final generator = Generator(PaperSize.mm58, profile);
// Mulai dengan inisialisasi printer
@ -56,18 +66,68 @@ class EscPosPrintService {
// Tambahkan logo jika ada path-nya
if (logoPath != null && logoPath.isNotEmpty) {
try {
// Membaca file gambar dari path lokal
// Membaca file gambar dari path lokal dengan timeout
final file = File(logoPath);
if (await file.exists()) {
final Uint8List imageBytes = await file.readAsBytes();
bool fileExists = false;
try {
fileExists = await file.exists();
} catch (e) {
print('Error saat memeriksa keberadaan file logo: $e');
}
if (fileExists) {
// Baca file dengan timeout
Uint8List imageBytes;
try {
imageBytes = await file.readAsBytes();
} catch (e) {
print('Gagal membaca file logo: $e');
throw Exception('Gagal membaca file logo');
}
// Decode gambar (format yang didukung: JPEG, PNG)
final img.Image? image = img.decodeImage(imageBytes);
if (image != null) {
bytes += generator.image(image);
// Decode gambar dengan penanganan error menggunakan isolate
Uint8List? processedImageBytes;
try {
processedImageBytes = await compute(processImageInIsolate, ImageProcessParams(imageBytes, 200));
} catch (e) {
print('Gagal mendecode gambar: $e');
}
if (processedImageBytes != null) {
// Decode the processed image bytes back to an image object for printing
final img.Image? image = img.decodeImage(processedImageBytes);
if (image != null) {
bytes += generator.image(image);
} else {
// Jika gagal decode gambar, gunakan teks sebagai fallback
bytes += generator.text(storeName,
styles: PosStyles(
bold: true,
height: PosTextSize.size1,
width: PosTextSize.size1,
align: PosAlign.center,
));
}
} else {
// Jika gagal decode gambar, gunakan teks sebagai fallback
bytes += generator.text(storeName,
styles: PosStyles(
bold: true,
height: PosTextSize.size1,
width: PosTextSize.size1,
align: PosAlign.center,
));
}
} else {
throw Exception('File logo tidak ditemukan');
print('File logo tidak ditemukan: $logoPath');
// Jika file tidak ditemukan, gunakan teks sebagai fallback
bytes += generator.text(storeName,
styles: PosStyles(
bold: true,
height: PosTextSize.size1,
width: PosTextSize.size1,
align: PosAlign.center,
));
}
} catch (e) {
print('Gagal memuat logo: $e');
@ -91,157 +151,244 @@ class EscPosPrintService {
));
}
bytes += generator.text(storeAddress,
styles: PosStyles(align: PosAlign.center));
bytes += generator.text('Admin: $adminName',
styles: PosStyles(align: PosAlign.center));
bytes += generator.text('Telp: $adminPhone',
styles: PosStyles(align: PosAlign.center));
bytes += generator.text('$formattedDate',
styles: PosStyles(align: PosAlign.center));
bytes += generator.feed(1);
// Garis pemisah
bytes += generator.text('================================',
styles: PosStyles(align: PosAlign.center));
// Informasi transaksi
bytes += generator.text('Kasir: $cashierId',
styles: PosStyles(bold: true));
bytes += generator.text('ID Transaksi: $transactionId',
styles: PosStyles());
bytes += generator.feed(1);
// Garis pemisah
bytes += generator.text('================================',
styles: PosStyles(align: PosAlign.center));
// Tabel item
bytes += generator.row([
PosColumn(
text: 'Item',
width: 7,
styles: PosStyles(bold: true, align: PosAlign.left),
),
PosColumn(
text: 'Total',
width: 5,
styles: PosStyles(bold: true, align: PosAlign.right),
),
]);
// Item list
print('Memulai iterasi item...');
for (int i = 0; i < items.length; i++) {
var item = items[i];
print('Item $i: ${item.description}, qty: ${item.quantity}, price: ${item.price}, total: ${item.total}');
try {
bytes += generator.text(storeAddress,
styles: PosStyles(align: PosAlign.center));
bytes += generator.text('Admin: $adminName',
styles: PosStyles(align: PosAlign.center));
bytes += generator.text('Telp: $adminPhone',
styles: PosStyles(align: PosAlign.center));
bytes += generator.text('$formattedDate',
styles: PosStyles(align: PosAlign.center));
// Untuk item dengan detail kuantitas dan harga
bytes += generator.feed(1);
// Garis pemisah
bytes += generator.text('================================',
styles: PosStyles(align: PosAlign.center));
// Informasi transaksi
bytes += generator.text('Kasir: $cashierId',
styles: PosStyles(bold: true));
bytes += generator.text('ID Transaksi: $transactionId',
styles: PosStyles());
bytes += generator.feed(1);
// Garis pemisah
bytes += generator.text('================================',
styles: PosStyles(align: PosAlign.center));
// Tabel item
bytes += generator.row([
PosColumn(
text: item.description,
width: 12,
styles: PosStyles(align: PosAlign.left),
),
]);
bytes += generator.row([
PosColumn(
text: '${item.quantity} x ${formatRupiah(item.price)}',
text: 'Item',
width: 7,
styles: PosStyles(align: PosAlign.left, height: PosTextSize.size1, width: PosTextSize.size1),
styles: PosStyles(bold: true, align: PosAlign.left),
),
PosColumn(
text: formatRupiah(item.total),
text: 'Total',
width: 5,
styles: PosStyles(align: PosAlign.right),
styles: PosStyles(bold: true, align: PosAlign.right),
),
]);
}
print('Selesai iterasi item');
// Garis pemisah sebelum total
bytes += generator.text('--------------------------------',
styles: PosStyles(align: PosAlign.center));
// Total
final totalAmount = items.fold(0.0, (sum, item) => sum + item.total);
print('Total amount: $totalAmount');
bytes += generator.row([
PosColumn(
text: 'TOTAL',
width: 7,
styles: PosStyles(bold: true, align: PosAlign.left),
),
PosColumn(
text: formatRupiah(totalAmount),
width: 5,
styles: PosStyles(bold: true, align: PosAlign.right),
),
]);
// Garis pemisah setelah total
bytes += generator.text('================================',
styles: PosStyles(align: PosAlign.center));
// Memuat teks kustom dari shared preferences
final customDisclaimer = prefs.getString('store_disclaimer_text') ??
'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. '
'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.';
final customThankYou = prefs.getString('thank_you_text') ?? '*** TERIMA KASIH ***';
final customPantun = prefs.getString('pantun_text') ??
'Belanja di toko kami, hemat dan nyaman,\n'
'Dengan penuh semangat, kami siap melayani,\n'
'Harapan kami, Anda selalu puas,\n'
'Sampai jumpa lagi, selamat tinggal.';
// Menambahkan disclaimer
bytes += generator.feed(1);
// Memecah disclaimer menjadi beberapa baris jika terlalu panjang
final disclaimerLines = customDisclaimer.split(' ');
final List<String> wrappedDisclaimer = [];
String currentLine = '';
for (final word in disclaimerLines) {
if ((currentLine + word).length > 32) {
wrappedDisclaimer.add(currentLine.trim());
currentLine = word + ' ';
} else {
currentLine += word + ' ';
// Item list dengan penanganan error
print('Memulai iterasi item...');
for (int i = 0; i < items.length; i++) {
try {
var item = items[i];
print('Item $i: ${item.description}, qty: ${item.quantity}, price: ${item.price}, total: ${item.total}');
// Untuk item dengan detail kuantitas dan harga
bytes += generator.row([
PosColumn(
text: item.description,
width: 12,
styles: PosStyles(align: PosAlign.left),
),
]);
bytes += generator.row([
PosColumn(
text: '${item.quantity} x ${formatRupiah(item.price)}',
width: 7,
styles: PosStyles(align: PosAlign.left, height: PosTextSize.size1, width: PosTextSize.size1),
),
PosColumn(
text: formatRupiah(item.total),
width: 5,
styles: PosStyles(align: PosAlign.right),
),
]);
} catch (e) {
print('Error saat memproses item $i: $e');
// Lanjutkan ke item berikutnya jika ada error
continue;
}
}
}
if (currentLine.trim().isNotEmpty) {
wrappedDisclaimer.add(currentLine.trim());
}
for (final line in wrappedDisclaimer) {
bytes += generator.text(line,
print('Selesai iterasi item');
// Garis pemisah sebelum total
bytes += generator.text('--------------------------------',
styles: PosStyles(align: PosAlign.center));
}
// Menambahkan ucapan terima kasih
bytes += generator.feed(1);
bytes += generator.text(customThankYou,
styles: PosStyles(align: PosAlign.center, bold: true));
// Menambahkan pantun
bytes += generator.feed(1);
final pantunLines = customPantun.split('\n');
for (final line in pantunLines) {
bytes += generator.text(line,
// Total
double totalAmount = 0.0;
try {
totalAmount = items.fold(0.0, (sum, item) => sum + item.total);
} catch (e) {
print('Error saat menghitung total: $e');
totalAmount = 0.0;
}
print('Total amount: $totalAmount');
bytes += generator.row([
PosColumn(
text: 'TOTAL',
width: 7,
styles: PosStyles(bold: true, align: PosAlign.left),
),
PosColumn(
text: formatRupiah(totalAmount),
width: 5,
styles: PosStyles(bold: true, align: PosAlign.right),
),
]);
// Garis pemisah setelah total
bytes += generator.text('================================',
styles: PosStyles(align: PosAlign.center));
}
// Spasi di akhir dan pemotongan kertas
bytes += generator.feed(2);
bytes += generator.cut();
// Memuat teks kustom dari shared preferences
String customDisclaimer;
try {
customDisclaimer = prefs.getString('store_disclaimer_text') ??
'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. '
'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.';
} catch (e) {
print('Error saat memuat disclaimer: $e');
customDisclaimer = 'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar.';
}
String customThankYou;
try {
customThankYou = prefs.getString('thank_you_text') ?? '*** TERIMA KASIH ***';
} catch (e) {
print('Error saat memuat thank you text: $e');
customThankYou = '*** TERIMA KASIH ***';
}
String customPantun;
try {
customPantun = prefs.getString('pantun_text') ??
'Belanja di toko kami, hemat dan nyaman\n'
'Dengan penuh semangat, kami siap melayani\n'
'Harapan kami, Anda selalu puas\n'
'Sampai jumpa lagi, selamat tinggal.';
} catch (e) {
print('Error saat memuat pantun: $e');
customPantun = '';
}
// Menambahkan disclaimer
bytes += generator.feed(1);
// Memecah disclaimer menjadi beberapa baris jika terlalu panjang
try {
final disclaimerLines = customDisclaimer.split(' ');
final List<String> wrappedDisclaimer = [];
String currentLine = '';
for (final word in disclaimerLines) {
if ((currentLine + word).length > 32) {
wrappedDisclaimer.add(currentLine.trim());
currentLine = word + ' ';
} else {
currentLine += word + ' ';
}
}
if (currentLine.trim().isNotEmpty) {
wrappedDisclaimer.add(currentLine.trim());
}
for (final line in wrappedDisclaimer) {
bytes += generator.text(line,
styles: PosStyles(align: PosAlign.center));
}
} catch (e) {
print('Error saat memproses disclaimer: $e');
// Fallback jika ada error
bytes += generator.text(customDisclaimer,
styles: PosStyles(align: PosAlign.center));
}
// Menambahkan ucapan terima kasih
bytes += generator.feed(1);
bytes += generator.text(customThankYou,
styles: PosStyles(align: PosAlign.center, bold: true));
// Menambahkan pantun jika ada
if (customPantun.isNotEmpty) {
try {
bytes += generator.feed(1);
final pantunLines = customPantun.split('\n');
for (final line in pantunLines) {
bytes += generator.text(line,
styles: PosStyles(align: PosAlign.center));
}
} catch (e) {
print('Error saat menambahkan pantun: $e');
}
}
// Spasi di akhir dan pemotongan kertas
bytes += generator.feed(2);
bytes += generator.cut();
} catch (e) {
print('Error saat membuat bagian akhir struk: $e');
// Pastikan struk tetap dipotong meski ada error
bytes += generator.feed(2);
bytes += generator.cut();
}
print('Jumlah byte yang dihasilkan: ${bytes.length}');
return bytes;
}
}
/// Data class to hold image processing parameters
class ImageProcessParams {
final Uint8List imageBytes;
final int maxWidth;
ImageProcessParams(this.imageBytes, this.maxWidth);
}
/// Helper function to decode and resize image in an isolate
Uint8List? processImageInIsolate(ImageProcessParams params) {
try {
// Decode image
img.Image? image = img.decodeImage(params.imageBytes);
if (image != null) {
// Resize gambar agar sesuai dengan ukuran kertas printer
if (image.width > params.maxWidth) {
final ratio = params.maxWidth / image.width;
final newHeight = (image.height * ratio).round();
image = img.copyResize(image, width: params.maxWidth, height: newHeight);
}
// Convert image to bytes that can be sent back to main isolate
final imageBytes = Uint8List.fromList(img.encodePng(image));
return imageBytes;
}
return null;
} catch (e) {
print('Error processing image in isolate: $e');
return null;
}
}

View File

@ -17,6 +17,25 @@ class StrukTextGenerator {
return char * width;
}
/// Fungsi untuk membuat representasi ASCII sederhana dari logo
static String createAsciiLogo(String storeName, int width) {
final buffer = StringBuffer();
// Membuat logo ASCII sederhana dengan nama toko
final int nameLength = storeName.length;
final int boxWidth = nameLength + 4;
final String horizontalLine = '=' * boxWidth;
final String emptyLine = '|${' ' * (boxWidth - 2)}|';
buffer.writeln(centerText(horizontalLine, width));
buffer.writeln(centerText(emptyLine, width));
buffer.writeln(centerText('| $storeName |', width));
buffer.writeln(centerText(emptyLine, width));
buffer.writeln(centerText(horizontalLine, width));
return buffer.toString();
}
/// Menghasilkan struk dalam format teks berdasarkan data transaksi
static Future<String> generateStrukText({
required List<ReceiptItem> items,
@ -36,11 +55,13 @@ class StrukTextGenerator {
final storeAddress = prefs.getString('store_address') ?? 'Jl. Merdeka No. 123';
final adminName = prefs.getString('admin_name') ?? 'Budi Santoso';
final adminPhone = prefs.getString('admin_phone') ?? '08123456789';
final logoPath = prefs.getString('store_logo_path'); // Load logo path
print('Nama toko: $storeName');
print('Alamat toko: $storeAddress');
print('Nama admin: $adminName');
print('Telepon admin: $adminPhone');
print('Logo path: $logoPath');
// Format tanggal
final dateFormatter = DateFormat('dd/MM/yyyy');
@ -56,21 +77,17 @@ class StrukTextGenerator {
// Bangun struk dalam format teks
final buffer = StringBuffer();
// Header toko - menggunakan lebar 32 karakter untuk kompatibilitas printer termal
buffer.writeln(centerText(storeName, 32));
// Header toko dengan logo ASCII - menggunakan lebar 32 karakter untuk kompatibilitas printer termal
buffer.write(createAsciiLogo(storeName, 32));
buffer.writeln('');
buffer.writeln(centerText(storeAddress, 32));
buffer.writeln(centerText('Admin: $adminName', 32));
buffer.writeln(centerText('Telp: $adminPhone', 32));
buffer.writeln(createSeparator(32, '='));
// Info transaksi
buffer.writeln('TANGGAL : $formattedDate');
buffer.writeln('DARI : Kas Tunai');
buffer.writeln('KE : Pendapatan Harian');
buffer.writeln(centerText(' $formattedDate ', 32));
buffer.writeln(createSeparator(32, '='));
// Header tabel - disesuaikan lebar kolom untuk printer termal
buffer.writeln('ITEM QTY HARGA TOTAL');
buffer.writeln('ITEM QTY HARGA TOTAL');
buffer.writeln(createSeparator(32, '-'));
// Item list
@ -107,12 +124,52 @@ class StrukTextGenerator {
// Garis pemisah setelah total
buffer.writeln(createSeparator(32, '='));
// Footer
buffer.writeln(centerText('*** TERIMA KASIH ***', 32));
buffer.writeln(centerText('Barang yang sudah dibeli', 32));
buffer.writeln(centerText('tidak dapat dikembalikan', 32));
// Memuat teks kustom dari shared preferences
final customDisclaimer = prefs.getString('store_disclaimer_text') ??
'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. '
'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.';
final customThankYou = prefs.getString('thank_you_text') ?? '*** TERIMA KASIH ***';
final customPantun = prefs.getString('pantun_text') ??
'Belanja di toko kami, hemat dan nyaman,\n'
'Dengan penuh semangat, kami siap melayani,\n'
'Harapan kami, Anda selalu puas,\n'
'Sampai jumpa lagi, selamat tinggal.';
// Menambahkan disclaimer
buffer.writeln('');
// Memecah disclaimer menjadi beberapa baris jika terlalu panjang
final disclaimerLines = customDisclaimer.split(' ');
final List<String> wrappedDisclaimer = [];
String currentLine = '';
for (final word in disclaimerLines) {
if ((currentLine + word).length > 32) {
wrappedDisclaimer.add(currentLine.trim());
currentLine = word + ' ';
} else {
currentLine += word + ' ';
}
}
if (currentLine.trim().isNotEmpty) {
wrappedDisclaimer.add(currentLine.trim());
}
for (final line in wrappedDisclaimer) {
buffer.writeln(centerText(line, 32));
}
// Menambahkan ucapan terima kasih
buffer.writeln('');
buffer.writeln(centerText(customThankYou, 32));
// Menambahkan pantun
buffer.writeln('');
final pantunLines = customPantun.split('\n');
for (final line in pantunLines) {
buffer.writeln(centerText(line, 32));
}
// Spasi di akhir
buffer.writeln('');
buffer.writeln('');
@ -123,4 +180,4 @@ class StrukTextGenerator {
return result;
}
}
}

View File

@ -216,6 +216,9 @@ public class BluetoothPrintPlugin implements FlutterPlugin, ActivityAware, Metho
case "printTest":
printTest(result);
break;
case "printRawData":
printRawData(call, result);
break;
default:
result.notImplemented();
break;
@ -402,6 +405,60 @@ public class BluetoothPrintPlugin implements FlutterPlugin, ActivityAware, Metho
}
@SuppressWarnings("unchecked")
private void printRawData(MethodCall call, Result result) {
Map<String, Object> args = call.arguments();
// Tambahkan logging untuk debugging
Log.d(TAG, "******************* Memulai proses printRawData, curMacAddress: " + curMacAddress);
// Periksa apakah curMacAddress sudah diatur
if (curMacAddress == null || curMacAddress.isEmpty()) {
Log.e(TAG, "******************* curMacAddress tidak diatur, tidak bisa mencetak");
result.error("not connected", "Printer address not set", null);
return;
}
final DeviceConnFactoryManager deviceConnFactoryManager = DeviceConnFactoryManager.getDeviceConnFactoryManagers().get(curMacAddress);
if (deviceConnFactoryManager == null) {
Log.e(TAG, "******************* deviceConnFactoryManager tidak ditemukan untuk alamat: " + curMacAddress);
result.error("not connected", "Device connection manager not found", null);
return;
}
// Periksa status koneksi
if (!deviceConnFactoryManager.getConnState()) {
Log.e(TAG, "******************* Printer tidak terhubung, status koneksi: " + deviceConnFactoryManager.getConnState());
result.error("not connected", "Printer not connected", null);
return;
}
if (args != null && args.containsKey("data")) {
final byte[] data = (byte[]) args.get("data");
if(data == null || data.length == 0){
Log.e(TAG, "******************* Data untuk dicetak null atau kosong");
result.error("no data", "Data to print is null or empty", null);
return;
}
Log.d(TAG, "******************* Mengirim data raw untuk dicetak, jumlah byte: " + data.length);
threadPool = ThreadPool.getInstantiation();
threadPool.addSerialTask(new Runnable() {
@Override
public void run() {
assert deviceConnFactoryManager != null;
deviceConnFactoryManager.sendByteDataImmediately(data);
}
});
result.success(true);
}else{
Log.e(TAG, "******************* Data tidak ditemukan");
result.error("please add data", "", null);
}
}
@SuppressWarnings("unchecked")
private void print(MethodCall call, Result result) {
Map<String, Object> args = call.arguments();

View File

@ -156,4 +156,8 @@ class BluetoothPrint {
}
Future<dynamic> printTest() => _channel.invokeMethod('printTest');
Future<dynamic> printRawData(Uint8List data) {
return _channel.invokeMethod('printRawData', {'data': data});
}
}

View File

@ -38,6 +38,8 @@ dependencies:
http_auth: ^1.0.4
bluetooth_print:
path: ./plugins
flutter_esc_pos_utils: ^0.0.1
image: ^3.0.1
intl: ^0.20.2
pdf: ^3.8.1
path_provider: ^2.0.14
@ -71,9 +73,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- assets/images/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images