From 3e1c34d1ce51241ed9009ec66e7df95fa922cb91 Mon Sep 17 00:00:00 2001 From: a2nr Date: Sat, 23 Aug 2025 22:44:16 +0700 Subject: [PATCH] feat: Implement logo printing to thermal printer and add loading indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • 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 --- lib/screens/receipt_screen.dart | 135 +++--- lib/services/esc_pos_print_service.dart | 437 ++++++++++++------ lib/services/struk_text_generator.dart | 89 +++- .../bluetooth_print/BluetoothPrintPlugin.java | 57 +++ plugins/lib/bluetooth_print.dart | 4 + pubspec.yaml | 7 +- 6 files changed, 492 insertions(+), 237 deletions(-) diff --git a/lib/screens/receipt_screen.dart b/lib/screens/receipt_screen.dart index a04fc3a..1e20ef1 100644 --- a/lib/screens/receipt_screen.dart +++ b/lib/screens/receipt_screen.dart @@ -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 { 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 { } 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 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 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 { 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 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 { 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, ), ], diff --git a/lib/services/esc_pos_print_service.dart b/lib/services/esc_pos_print_service.dart index 2ca70e3..fdd1aeb 100644 --- a/lib/services/esc_pos_print_service.dart +++ b/lib/services/esc_pos_print_service.dart @@ -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 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 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; + } } \ No newline at end of file diff --git a/lib/services/struk_text_generator.dart b/lib/services/struk_text_generator.dart index f4f5d6c..357ffae 100644 --- a/lib/services/struk_text_generator.dart +++ b/lib/services/struk_text_generator.dart @@ -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 generateStrukText({ required List 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 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; } -} \ No newline at end of file +} diff --git a/plugins/android/src/main/java/com/example/bluetooth_print/BluetoothPrintPlugin.java b/plugins/android/src/main/java/com/example/bluetooth_print/BluetoothPrintPlugin.java index 73aa4da..a788ed6 100644 --- a/plugins/android/src/main/java/com/example/bluetooth_print/BluetoothPrintPlugin.java +++ b/plugins/android/src/main/java/com/example/bluetooth_print/BluetoothPrintPlugin.java @@ -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 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 args = call.arguments(); diff --git a/plugins/lib/bluetooth_print.dart b/plugins/lib/bluetooth_print.dart index ac96371..e7406f0 100644 --- a/plugins/lib/bluetooth_print.dart +++ b/plugins/lib/bluetooth_print.dart @@ -156,4 +156,8 @@ class BluetoothPrint { } Future printTest() => _channel.invokeMethod('printTest'); + + Future printRawData(Uint8List data) { + return _channel.invokeMethod('printRawData', {'data': data}); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 56e8b46..d98d103 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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