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 receiptsmaster
parent
b848881805
commit
3e1c34d1ce
|
@ -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,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -156,4 +156,8 @@ class BluetoothPrint {
|
|||
}
|
||||
|
||||
Future<dynamic> printTest() => _channel.invokeMethod('printTest');
|
||||
|
||||
Future<dynamic> printRawData(Uint8List data) {
|
||||
return _channel.invokeMethod('printRawData', {'data': data});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue