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:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
// import 'dart:io'; // Add this import for file operations // PDF Removed
|
// 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:intl/intl.dart'; // Untuk format angka
|
||||||
import 'package:cashumit/models/receipt_item.dart';
|
import 'package:cashumit/models/receipt_item.dart';
|
||||||
import 'package:cashumit/screens/add_item_screen.dart';
|
import 'package:cashumit/screens/add_item_screen.dart';
|
||||||
// import 'package:cashumit/services/pdf_export_service.dart'; // PDF Removed
|
// import 'package:cashumit/services/pdf_export_service.dart'; // PDF Removed
|
||||||
import 'package:cashumit/services/firefly_api_service.dart';
|
import 'package:cashumit/services/firefly_api_service.dart';
|
||||||
import 'package:cashumit/services/struk_text_generator.dart'; // Tambahkan import ini
|
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.dart';
|
||||||
import 'package:bluetooth_print/bluetooth_print_model.dart';
|
import 'package:bluetooth_print/bluetooth_print_model.dart';
|
||||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart'; // Tambahkan import ini
|
import 'package:flutter_speed_dial/flutter_speed_dial.dart'; // Tambahkan import ini
|
||||||
|
@ -58,6 +60,7 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
late BluetoothPrint bluetoothPrint;
|
late BluetoothPrint bluetoothPrint;
|
||||||
bool _bluetoothConnected = false;
|
bool _bluetoothConnected = false;
|
||||||
BluetoothDevice? _bluetoothDevice;
|
BluetoothDevice? _bluetoothDevice;
|
||||||
|
bool _isPrinting = false; // State variable to track printing status
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -374,51 +377,31 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('Menghasilkan teks struk...');
|
print('Menghasilkan byte array ESC/POS menggunakan flutter_esc_pos_utils...');
|
||||||
print('Jumlah item: ${items.length}');
|
print('Jumlah item: ${items.length}');
|
||||||
print('Tanggal transaksi: $_transactionDate');
|
print('Tanggal transaksi: $_transactionDate');
|
||||||
print('ID kasir: $cashierId');
|
print('ID kasir: $cashierId');
|
||||||
print('ID transaksi: $transactionId');
|
print('ID transaksi: $transactionId');
|
||||||
|
|
||||||
// Generate struk text menggunakan StrukTextGenerator
|
// Generate struk dalam format byte array menggunakan EscPosPrintService
|
||||||
final strukText = await StrukTextGenerator.generateStrukText(
|
final bytes = await EscPosPrintService.generateEscPosBytes(
|
||||||
items: items,
|
items: items,
|
||||||
transactionDate: _transactionDate,
|
transactionDate: _transactionDate,
|
||||||
cashierId: cashierId,
|
cashierId: cashierId,
|
||||||
transactionId: transactionId,
|
transactionId: transactionId,
|
||||||
);
|
);
|
||||||
print('Teks struk berhasil dihasilkan');
|
|
||||||
|
|
||||||
// Tampilkan teks struk untuk debugging
|
print('Byte array ESC/POS berhasil dihasilkan');
|
||||||
print('Isi teks struk:');
|
print('Jumlah byte: ${bytes.length}');
|
||||||
print(strukText);
|
|
||||||
|
|
||||||
// Konversi struk text ke format yang bisa dicetak
|
// Tampilkan byte array untuk debugging (dalam format hex)
|
||||||
final lines = strukText.split('\n');
|
print('Isi byte array (hex):');
|
||||||
final List<LineText> list = lines
|
if (bytes.length <= 1000) { // Batasi tampilan untuk mencegah output terlalu panjang
|
||||||
.where((line) => line.isNotEmpty)
|
print(bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '));
|
||||||
.map((line) =>
|
} else {
|
||||||
LineText(type: LineText.TYPE_TEXT, content: line, linefeed: 1))
|
print('Terlalu banyak byte untuk ditampilkan (${bytes.length} bytes)');
|
||||||
.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}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Verifikasi koneksi sebelum mencetak
|
||||||
final isConnectedBeforePrint = await bluetoothPrint.isConnected ?? false;
|
final isConnectedBeforePrint = await bluetoothPrint.isConnected ?? false;
|
||||||
if (!isConnectedBeforePrint) {
|
if (!isConnectedBeforePrint) {
|
||||||
|
@ -430,16 +413,18 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('Mengirim perintah cetak dengan konfigurasi: $config');
|
print('Mengirim byte array ke printer...');
|
||||||
bool printSuccess = false;
|
bool printSuccess = false;
|
||||||
String printError = '';
|
String printError = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Coba kirim perintah cetak beberapa kali jika gagal
|
// Coba kirim byte array beberapa kali jika gagal
|
||||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
for (int attempt = 1; attempt <= 3; attempt++) {
|
||||||
print('Percobaan cetak ke-$attempt');
|
print('Percobaan cetak ke-$attempt');
|
||||||
try {
|
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');
|
print('Perintah cetak berhasil dikirim pada percobaan ke-$attempt');
|
||||||
printSuccess = true;
|
printSuccess = true;
|
||||||
break;
|
break;
|
||||||
|
@ -481,11 +466,11 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
print('Stack trace: $stackTrace');
|
print('Stack trace: $stackTrace');
|
||||||
// Cetak detail error tambahan
|
// Cetak detail error tambahan
|
||||||
print('Tipe error: ${e.runtimeType}');
|
print('Tipe error: ${e.runtimeType}');
|
||||||
// Menghapus baris bermasalah yang menggunakan e.message
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Gagal mencetak struk: $e')),
|
SnackBar(content: Text('Gagal mencetak struk: $e')),
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1090,9 +1075,13 @@ SpeedDialChild(
|
||||||
),
|
),
|
||||||
// SpeedDialChild for PDF printing removed as per request
|
// SpeedDialChild for PDF printing removed as per request
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: const Icon(Icons.receipt),
|
child: _isPrinting
|
||||||
|
? const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)
|
||||||
|
: const Icon(Icons.receipt),
|
||||||
label: 'Cetak Struk',
|
label: 'Cetak Struk',
|
||||||
onTap: () async {
|
onTap: _isPrinting
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
// Periksa koneksi secara real-time
|
// Periksa koneksi secara real-time
|
||||||
final isConnected = await _checkBluetoothConnection();
|
final isConnected = await _checkBluetoothConnection();
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
|
|
|
@ -5,6 +5,8 @@ import 'package:cashumit/models/receipt_item.dart';
|
||||||
import 'package:flutter_esc_pos_utils/flutter_esc_pos_utils.dart';
|
import 'package:flutter_esc_pos_utils/flutter_esc_pos_utils.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
/// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils
|
/// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils
|
||||||
class EscPosPrintService {
|
class EscPosPrintService {
|
||||||
|
@ -46,8 +48,16 @@ class EscPosPrintService {
|
||||||
return "Rp ${formatter.format(amount)}";
|
return "Rp ${formatter.format(amount)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load capability profile
|
// Load capability profile with timeout
|
||||||
final profile = await CapabilityProfile.load();
|
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);
|
final generator = Generator(PaperSize.mm58, profile);
|
||||||
|
|
||||||
// Mulai dengan inisialisasi printer
|
// Mulai dengan inisialisasi printer
|
||||||
|
@ -56,18 +66,68 @@ class EscPosPrintService {
|
||||||
// Tambahkan logo jika ada path-nya
|
// Tambahkan logo jika ada path-nya
|
||||||
if (logoPath != null && logoPath.isNotEmpty) {
|
if (logoPath != null && logoPath.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
// Membaca file gambar dari path lokal
|
// Membaca file gambar dari path lokal dengan timeout
|
||||||
final file = File(logoPath);
|
final file = File(logoPath);
|
||||||
if (await file.exists()) {
|
bool fileExists = false;
|
||||||
final Uint8List imageBytes = await file.readAsBytes();
|
try {
|
||||||
|
fileExists = await file.exists();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saat memeriksa keberadaan file logo: $e');
|
||||||
|
}
|
||||||
|
|
||||||
// Decode gambar (format yang didukung: JPEG, PNG)
|
if (fileExists) {
|
||||||
final img.Image? image = img.decodeImage(imageBytes);
|
// 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 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) {
|
if (image != null) {
|
||||||
bytes += generator.image(image);
|
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 {
|
} else {
|
||||||
throw Exception('File logo tidak ditemukan');
|
// 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 {
|
||||||
|
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) {
|
} catch (e) {
|
||||||
print('Gagal memuat logo: $e');
|
print('Gagal memuat logo: $e');
|
||||||
|
@ -91,6 +151,7 @@ class EscPosPrintService {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
bytes += generator.text(storeAddress,
|
bytes += generator.text(storeAddress,
|
||||||
styles: PosStyles(align: PosAlign.center));
|
styles: PosStyles(align: PosAlign.center));
|
||||||
bytes += generator.text('Admin: $adminName',
|
bytes += generator.text('Admin: $adminName',
|
||||||
|
@ -132,9 +193,10 @@ class EscPosPrintService {
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Item list
|
// Item list dengan penanganan error
|
||||||
print('Memulai iterasi item...');
|
print('Memulai iterasi item...');
|
||||||
for (int i = 0; i < items.length; i++) {
|
for (int i = 0; i < items.length; i++) {
|
||||||
|
try {
|
||||||
var item = items[i];
|
var item = items[i];
|
||||||
print('Item $i: ${item.description}, qty: ${item.quantity}, price: ${item.price}, total: ${item.total}');
|
print('Item $i: ${item.description}, qty: ${item.quantity}, price: ${item.price}, total: ${item.total}');
|
||||||
|
|
||||||
|
@ -159,6 +221,11 @@ class EscPosPrintService {
|
||||||
styles: PosStyles(align: PosAlign.right),
|
styles: PosStyles(align: PosAlign.right),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saat memproses item $i: $e');
|
||||||
|
// Lanjutkan ke item berikutnya jika ada error
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
print('Selesai iterasi item');
|
print('Selesai iterasi item');
|
||||||
|
|
||||||
|
@ -167,7 +234,14 @@ class EscPosPrintService {
|
||||||
styles: PosStyles(align: PosAlign.center));
|
styles: PosStyles(align: PosAlign.center));
|
||||||
|
|
||||||
// Total
|
// Total
|
||||||
final totalAmount = items.fold(0.0, (sum, item) => sum + item.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');
|
print('Total amount: $totalAmount');
|
||||||
|
|
||||||
bytes += generator.row([
|
bytes += generator.row([
|
||||||
|
@ -188,20 +262,41 @@ class EscPosPrintService {
|
||||||
styles: PosStyles(align: PosAlign.center));
|
styles: PosStyles(align: PosAlign.center));
|
||||||
|
|
||||||
// Memuat teks kustom dari shared preferences
|
// Memuat teks kustom dari shared preferences
|
||||||
final customDisclaimer = prefs.getString('store_disclaimer_text') ??
|
String customDisclaimer;
|
||||||
|
try {
|
||||||
|
customDisclaimer = prefs.getString('store_disclaimer_text') ??
|
||||||
'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. '
|
'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. '
|
||||||
'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.';
|
'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.';
|
||||||
final customThankYou = prefs.getString('thank_you_text') ?? '*** TERIMA KASIH ***';
|
} catch (e) {
|
||||||
final customPantun = prefs.getString('pantun_text') ??
|
print('Error saat memuat disclaimer: $e');
|
||||||
'Belanja di toko kami, hemat dan nyaman,\n'
|
customDisclaimer = 'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar.';
|
||||||
'Dengan penuh semangat, kami siap melayani,\n'
|
}
|
||||||
'Harapan kami, Anda selalu puas,\n'
|
|
||||||
|
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.';
|
'Sampai jumpa lagi, selamat tinggal.';
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saat memuat pantun: $e');
|
||||||
|
customPantun = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Menambahkan disclaimer
|
// Menambahkan disclaimer
|
||||||
bytes += generator.feed(1);
|
bytes += generator.feed(1);
|
||||||
|
|
||||||
// Memecah disclaimer menjadi beberapa baris jika terlalu panjang
|
// Memecah disclaimer menjadi beberapa baris jika terlalu panjang
|
||||||
|
try {
|
||||||
final disclaimerLines = customDisclaimer.split(' ');
|
final disclaimerLines = customDisclaimer.split(' ');
|
||||||
final List<String> wrappedDisclaimer = [];
|
final List<String> wrappedDisclaimer = [];
|
||||||
String currentLine = '';
|
String currentLine = '';
|
||||||
|
@ -222,26 +317,78 @@ class EscPosPrintService {
|
||||||
bytes += generator.text(line,
|
bytes += generator.text(line,
|
||||||
styles: PosStyles(align: PosAlign.center));
|
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
|
// Menambahkan ucapan terima kasih
|
||||||
bytes += generator.feed(1);
|
bytes += generator.feed(1);
|
||||||
bytes += generator.text(customThankYou,
|
bytes += generator.text(customThankYou,
|
||||||
styles: PosStyles(align: PosAlign.center, bold: true));
|
styles: PosStyles(align: PosAlign.center, bold: true));
|
||||||
|
|
||||||
// Menambahkan pantun
|
// Menambahkan pantun jika ada
|
||||||
|
if (customPantun.isNotEmpty) {
|
||||||
|
try {
|
||||||
bytes += generator.feed(1);
|
bytes += generator.feed(1);
|
||||||
final pantunLines = customPantun.split('\n');
|
final pantunLines = customPantun.split('\n');
|
||||||
for (final line in pantunLines) {
|
for (final line in pantunLines) {
|
||||||
bytes += generator.text(line,
|
bytes += generator.text(line,
|
||||||
styles: PosStyles(align: PosAlign.center));
|
styles: PosStyles(align: PosAlign.center));
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saat menambahkan pantun: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Spasi di akhir dan pemotongan kertas
|
// Spasi di akhir dan pemotongan kertas
|
||||||
bytes += generator.feed(2);
|
bytes += generator.feed(2);
|
||||||
bytes += generator.cut();
|
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}');
|
print('Jumlah byte yang dihasilkan: ${bytes.length}');
|
||||||
|
|
||||||
return bytes;
|
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;
|
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
|
/// Menghasilkan struk dalam format teks berdasarkan data transaksi
|
||||||
static Future<String> generateStrukText({
|
static Future<String> generateStrukText({
|
||||||
required List<ReceiptItem> items,
|
required List<ReceiptItem> items,
|
||||||
|
@ -36,11 +55,13 @@ class StrukTextGenerator {
|
||||||
final storeAddress = prefs.getString('store_address') ?? 'Jl. Merdeka No. 123';
|
final storeAddress = prefs.getString('store_address') ?? 'Jl. Merdeka No. 123';
|
||||||
final adminName = prefs.getString('admin_name') ?? 'Budi Santoso';
|
final adminName = prefs.getString('admin_name') ?? 'Budi Santoso';
|
||||||
final adminPhone = prefs.getString('admin_phone') ?? '08123456789';
|
final adminPhone = prefs.getString('admin_phone') ?? '08123456789';
|
||||||
|
final logoPath = prefs.getString('store_logo_path'); // Load logo path
|
||||||
|
|
||||||
print('Nama toko: $storeName');
|
print('Nama toko: $storeName');
|
||||||
print('Alamat toko: $storeAddress');
|
print('Alamat toko: $storeAddress');
|
||||||
print('Nama admin: $adminName');
|
print('Nama admin: $adminName');
|
||||||
print('Telepon admin: $adminPhone');
|
print('Telepon admin: $adminPhone');
|
||||||
|
print('Logo path: $logoPath');
|
||||||
|
|
||||||
// Format tanggal
|
// Format tanggal
|
||||||
final dateFormatter = DateFormat('dd/MM/yyyy');
|
final dateFormatter = DateFormat('dd/MM/yyyy');
|
||||||
|
@ -56,17 +77,13 @@ class StrukTextGenerator {
|
||||||
// Bangun struk dalam format teks
|
// Bangun struk dalam format teks
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
// Header toko - menggunakan lebar 32 karakter untuk kompatibilitas printer termal
|
// Header toko dengan logo ASCII - menggunakan lebar 32 karakter untuk kompatibilitas printer termal
|
||||||
buffer.writeln(centerText(storeName, 32));
|
buffer.write(createAsciiLogo(storeName, 32));
|
||||||
|
buffer.writeln('');
|
||||||
buffer.writeln(centerText(storeAddress, 32));
|
buffer.writeln(centerText(storeAddress, 32));
|
||||||
buffer.writeln(centerText('Admin: $adminName', 32));
|
buffer.writeln(centerText('Admin: $adminName', 32));
|
||||||
buffer.writeln(centerText('Telp: $adminPhone', 32));
|
buffer.writeln(centerText('Telp: $adminPhone', 32));
|
||||||
buffer.writeln(createSeparator(32, '='));
|
buffer.writeln(centerText(' $formattedDate ', 32));
|
||||||
|
|
||||||
// Info transaksi
|
|
||||||
buffer.writeln('TANGGAL : $formattedDate');
|
|
||||||
buffer.writeln('DARI : Kas Tunai');
|
|
||||||
buffer.writeln('KE : Pendapatan Harian');
|
|
||||||
buffer.writeln(createSeparator(32, '='));
|
buffer.writeln(createSeparator(32, '='));
|
||||||
|
|
||||||
// Header tabel - disesuaikan lebar kolom untuk printer termal
|
// Header tabel - disesuaikan lebar kolom untuk printer termal
|
||||||
|
@ -108,10 +125,50 @@ class StrukTextGenerator {
|
||||||
// Garis pemisah setelah total
|
// Garis pemisah setelah total
|
||||||
buffer.writeln(createSeparator(32, '='));
|
buffer.writeln(createSeparator(32, '='));
|
||||||
|
|
||||||
// Footer
|
// Memuat teks kustom dari shared preferences
|
||||||
buffer.writeln(centerText('*** TERIMA KASIH ***', 32));
|
final customDisclaimer = prefs.getString('store_disclaimer_text') ??
|
||||||
buffer.writeln(centerText('Barang yang sudah dibeli', 32));
|
'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. '
|
||||||
buffer.writeln(centerText('tidak dapat dikembalikan', 32));
|
'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
|
// Spasi di akhir
|
||||||
buffer.writeln('');
|
buffer.writeln('');
|
||||||
|
|
|
@ -216,6 +216,9 @@ public class BluetoothPrintPlugin implements FlutterPlugin, ActivityAware, Metho
|
||||||
case "printTest":
|
case "printTest":
|
||||||
printTest(result);
|
printTest(result);
|
||||||
break;
|
break;
|
||||||
|
case "printRawData":
|
||||||
|
printRawData(call, result);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
result.notImplemented();
|
result.notImplemented();
|
||||||
break;
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private void print(MethodCall call, Result result) {
|
private void print(MethodCall call, Result result) {
|
||||||
Map<String, Object> args = call.arguments();
|
Map<String, Object> args = call.arguments();
|
||||||
|
|
|
@ -156,4 +156,8 @@ class BluetoothPrint {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> printTest() => _channel.invokeMethod('printTest');
|
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
|
http_auth: ^1.0.4
|
||||||
bluetooth_print:
|
bluetooth_print:
|
||||||
path: ./plugins
|
path: ./plugins
|
||||||
|
flutter_esc_pos_utils: ^0.0.1
|
||||||
|
image: ^3.0.1
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
pdf: ^3.8.1
|
pdf: ^3.8.1
|
||||||
path_provider: ^2.0.14
|
path_provider: ^2.0.14
|
||||||
|
@ -71,9 +73,8 @@ flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
assets:
|
||||||
# - images/a_dot_burr.jpeg
|
- assets/images/
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
Loading…
Reference in New Issue