diff --git a/PRINTING_CARD_IMPLEMENTATION.md b/PRINTING_CARD_IMPLEMENTATION.md new file mode 100644 index 0000000..a5a4fc3 --- /dev/null +++ b/PRINTING_CARD_IMPLEMENTATION.md @@ -0,0 +1,36 @@ +# Printing Status Card Implementation + +I've successfully implemented a floating card with animations that shows the printing status when the user presses the print button. + +## Features + +1. **Animated Appearance/Disappearance**: + - Smooth scale and fade animations when showing/hiding + - Elastic entrance animation for a polished feel + +2. **Visual Design**: + - Beautiful gradient background (purple to blue) + - Clear icon and text indicators + - Indeterminate progress bar + - Rounded corners and shadow for depth + +3. **Functionality**: + - Automatically appears when printing starts + - Automatically disappears when printing completes + - Manual dismiss option with the close button + +## Files Modified + +1. Created new widget: `lib/widgets/printing_status_card.dart` +2. Modified: `lib/screens/receipt_screen.dart` to integrate the widget + +## How It Works + +When the user presses the "Cetak Struk" button in the speed dial: +1. The `_isPrinting` state is set to `true` +2. The `PrintingStatusCard` becomes visible with animations +3. The printing process begins +4. When printing completes (success or failure), `_isPrinting` is set to `false` +5. The card smoothly animates out of view + +The card also allows manual dismissal by the user if needed. \ No newline at end of file diff --git a/assets/images/store_logo.png b/assets/images/store_logo.png deleted file mode 100644 index 5d2ef65..0000000 Binary files a/assets/images/store_logo.png and /dev/null differ diff --git a/lib/main.dart b/lib/main.dart index 6545acb..42f28fe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,6 @@ import 'package:cashumit/screens/config_screen.dart'; import 'package:cashumit/screens/transaction_screen.dart'; import 'package:flutter/material.dart'; import 'package:cashumit/screens/receipt_screen.dart'; -import 'package:cashumit/utils/store_logo_utils.dart'; import 'package:provider/provider.dart'; import 'package:cashumit/providers/receipt_provider.dart'; @@ -10,9 +9,6 @@ void main() async { // Ensure WidgetsFlutterBinding is initialized for async operations WidgetsFlutterBinding.ensureInitialized(); - // Initialize the store logo from asset - await copyAndSaveStoreLogoFromAsset('assets/images/store_logo.png'); - runApp( MultiProvider( providers: [ diff --git a/lib/services/esc_pos_print_service.dart b/lib/services/esc_pos_print_service.dart index 96dc2dc..5efd7dc 100644 --- a/lib/services/esc_pos_print_service.dart +++ b/lib/services/esc_pos_print_service.dart @@ -30,13 +30,11 @@ class EscPosPrintService { 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 HH:mm'); @@ -64,93 +62,14 @@ class EscPosPrintService { // Mulai dengan inisialisasi printer List bytes = []; - // Tambahkan logo jika ada path-nya - if (logoPath != null && logoPath.isNotEmpty) { - try { - // Membaca file gambar dari path lokal dengan timeout - final file = File(logoPath); - 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 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 { - 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'); - // Jika gagal memuat logo, gunakan teks sebagai fallback - bytes += generator.text(storeName, - styles: PosStyles( - bold: true, - height: PosTextSize.size1, - width: PosTextSize.size1, - align: PosAlign.center, - )); - } - } else { - // Jika tidak ada logo, gunakan nama toko sebagai header - bytes += generator.text(storeName, - styles: PosStyles( - bold: true, - height: PosTextSize.size1, - width: PosTextSize.size1, - align: PosAlign.center, - )); - } + // Tambahkan nama toko sebagai header + bytes += generator.text(storeName, + styles: PosStyles( + bold: true, + height: PosTextSize.size1, + width: PosTextSize.size1, + align: PosAlign.center, + )); try { bytes += generator.text(storeAddress, diff --git a/lib/services/struk_text_generator.dart b/lib/services/struk_text_generator.dart index 357ffae..5b9c292 100644 --- a/lib/services/struk_text_generator.dart +++ b/lib/services/struk_text_generator.dart @@ -17,25 +17,6 @@ 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, @@ -55,13 +36,11 @@ 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'); @@ -77,9 +56,8 @@ class StrukTextGenerator { // Bangun struk dalam format teks final buffer = StringBuffer(); - // Header toko dengan logo ASCII - menggunakan lebar 32 karakter untuk kompatibilitas printer termal - buffer.write(createAsciiLogo(storeName, 32)); - buffer.writeln(''); + // Header toko - menggunakan lebar 32 karakter untuk kompatibilitas printer termal + buffer.writeln(centerText(storeName, 32)); buffer.writeln(centerText(storeAddress, 32)); buffer.writeln(centerText('Admin: $adminName', 32)); buffer.writeln(centerText('Telp: $adminPhone', 32)); diff --git a/lib/utils/image_validator.dart b/lib/utils/image_validator.dart new file mode 100644 index 0000000..d446501 --- /dev/null +++ b/lib/utils/image_validator.dart @@ -0,0 +1,67 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show instantiateImageCodec; +import 'package:path_provider/path_provider.dart'; + +/// Utility to validate image files +class ImageValidator { + /// Validate if a file is a valid image by trying to decode it + static Future validateImageFile(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + print('File does not exist: $filePath'); + return false; + } + + final bytes = await file.readAsBytes(); + print('File size: ${bytes.length} bytes'); + + if (bytes.isEmpty) { + print('File is empty: $filePath'); + return false; + } + + // Try to decode as image + final codec = await instantiateImageCodec(bytes); + final frameInfo = await codec.getNextFrame(); + final image = frameInfo.image; + + print('Image dimensions: ${image.width}x${image.height}'); + await image.dispose(); + await codec.dispose(); + + return true; + } catch (e) { + print('Failed to validate image file $filePath: $e'); + return false; + } + } + + /// Validate images in the documents directory + static Future validateStoredImages() async { + try { + final dir = await getApplicationDocumentsDirectory(); + final logoDir = Directory('${dir.path}/logos'); + + if (!await logoDir.exists()) { + print('Logo directory does not exist'); + return; + } + + final files = logoDir.listSync(); + print('Found ${files.length} files in logo directory'); + + for (final file in files) { + if (file is File) { + print('Validating ${file.path}...'); + final isValid = await validateImageFile(file.path); + print('Validation result: $isValid'); + } + } + } catch (e) { + print('Error validating stored images: $e'); + } + } +} \ No newline at end of file diff --git a/lib/utils/store_logo_utils.dart b/lib/utils/store_logo_utils.dart deleted file mode 100644 index 661c797..0000000 --- a/lib/utils/store_logo_utils.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:path_provider/path_provider.dart'; -import 'dart:io'; -import 'package:flutter/services.dart' show rootBundle; - -/// Fungsi untuk menyimpan path logo toko ke shared preferences -Future saveStoreLogoPath(String logoPath) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('store_logo_path', logoPath); -} - -/// Fungsi untuk menghapus path logo toko dari shared preferences -Future removeStoreLogoPath() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('store_logo_path'); -} - -/// Fungsi untuk mengambil path logo toko dari shared preferences -Future getStoreLogoPath() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString('store_logo_path'); -} - -/// Fungsi untuk menyalin logo dari asset ke direktori dokumen aplikasi -/// dan menyimpan path-nya ke shared preferences -Future copyAndSaveStoreLogoFromAsset(String assetPath) async { - try { - // Dapatkan direktori dokumen aplikasi - final dir = await getApplicationDocumentsDirectory(); - final logoDir = Directory('${dir.path}/logos'); - - // Buat direktori jika belum ada - if (!await logoDir.exists()) { - await logoDir.create(recursive: true); - } - - // Path file logo di direktori dokumen - final logoFile = File('${logoDir.path}/store_logo.png'); - - // Baca data dari asset - final data = await rootBundle.load(assetPath); - - // Tulis data ke file di direktori dokumen - await logoFile.writeAsBytes(data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes)); - - // Simpan path ke shared preferences - await saveStoreLogoPath(logoFile.path); - } catch (e) { - print('Error copying logo from asset: $e'); - // Jika gagal, hapus path yang mungkin tersimpan - await removeStoreLogoPath(); - } -} \ No newline at end of file diff --git a/lib/widgets/printing_status_card.dart b/lib/widgets/printing_status_card.dart new file mode 100644 index 0000000..e0e8398 --- /dev/null +++ b/lib/widgets/printing_status_card.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; + +class PrintingStatusCard extends StatefulWidget { + final bool isVisible; + final VoidCallback? onDismiss; + + const PrintingStatusCard({ + Key? key, + required this.isVisible, + this.onDismiss, + }) : super(key: key); + + @override + State createState() => _PrintingStatusCardState(); +} + +class _PrintingStatusCardState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.elasticOut, + )); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + + // Start animation when widget is visible + if (widget.isVisible) { + _controller.forward(); + } + } + + @override + void didUpdateWidget(covariant PrintingStatusCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isVisible && !oldWidget.isVisible) { + _controller.forward(); + } else if (!widget.isVisible && oldWidget.isVisible) { + _controller.reverse(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Positioned( + top: 100, + left: MediaQuery.of(context).size.width * 0.1, + right: MediaQuery.of(context).size.width * 0.1, + child: IgnorePointer( + ignoring: !widget.isVisible, + child: Opacity( + opacity: _fadeAnimation.value, + child: Transform.scale( + scale: _scaleAnimation.value, + child: Card( + elevation: 12, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF6A11CB), + Color(0xFF2575FC), + ], + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 8), + const Icon( + Icons.print, + color: Colors.white, + size: 36, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Mencetak Struk', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + const Text( + 'Mohon tunggu...', + style: TextStyle( + fontSize: 16, + color: Colors.white70, + ), + ), + const SizedBox(height: 12), + LinearProgressIndicator( + backgroundColor: Colors.white30, + color: Colors.white, + minHeight: 6, + value: null, + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white70), + onPressed: widget.onDismiss, + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/store_info_config_dialog.dart b/lib/widgets/store_info_config_dialog.dart index a96ec66..cd61ac0 100644 --- a/lib/widgets/store_info_config_dialog.dart +++ b/lib/widgets/store_info_config_dialog.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:image_picker/image_picker.dart'; -import 'dart:io'; /// Widget untuk dialog konfigurasi informasi toko class StoreInfoConfigDialog extends StatefulWidget { @@ -19,10 +17,6 @@ class _StoreInfoConfigDialogState extends State { final TextEditingController _storeAddressController = TextEditingController(); final TextEditingController _adminNameController = TextEditingController(); final TextEditingController _adminPhoneController = TextEditingController(); - - // Variabel untuk logo - String? _logoPath; - final ImagePicker _picker = ImagePicker(); @override void initState() { @@ -40,7 +34,6 @@ class _StoreInfoConfigDialogState extends State { _storeAddressController.text = prefs.getString('store_address') ?? 'Jl. Merdeka No. 123'; _adminNameController.text = prefs.getString('admin_name') ?? 'Budi Santoso'; _adminPhoneController.text = prefs.getString('admin_phone') ?? '08123456789'; - _logoPath = prefs.getString('store_logo_path'); // Load logo path }); } @@ -53,12 +46,6 @@ class _StoreInfoConfigDialogState extends State { await prefs.setString('store_address', _storeAddressController.text); await prefs.setString('admin_name', _adminNameController.text); await prefs.setString('admin_phone', _adminPhoneController.text); - // Save logo path - if (_logoPath != null) { - await prefs.setString('store_logo_path', _logoPath!); - } else { - await prefs.remove('store_logo_path'); - } if (mounted) { Navigator.of(context).pop(true); // Kembali dengan nilai true jika berhasil disimpan @@ -66,17 +53,6 @@ class _StoreInfoConfigDialogState extends State { } } - /// Memilih logo dari galeri - Future _pickImage() async { - final XFile? image = await _picker.pickImage(source: ImageSource.gallery); - - if (image != null && mounted) { - setState(() { - _logoPath = image.path; - }); - } - } - @override void dispose() { _storeNameController.dispose(); @@ -96,28 +72,6 @@ class _StoreInfoConfigDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Display selected logo or a placeholder - GestureDetector( - onTap: _pickImage, - child: Container( - height: 100, - width: 100, - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8), - ), - child: _logoPath != null - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.file(File(_logoPath!), fit: BoxFit.cover), - ) - : const Icon(Icons.add_a_photo, size: 40, color: Colors.grey), - ), - ), - const SizedBox(height: 8), - const Text('Ketuk untuk memilih logo toko', style: TextStyle(fontSize: 12)), - const SizedBox(height: 16), - TextFormField( controller: _storeNameController, decoration: const InputDecoration( diff --git a/lib/widgets/store_info_widget.dart b/lib/widgets/store_info_widget.dart index 3b8bfb4..0093fde 100644 --- a/lib/widgets/store_info_widget.dart +++ b/lib/widgets/store_info_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:io'; import 'package:intl/intl.dart'; /// Widget untuk menampilkan informasi toko dan admin @@ -20,7 +19,6 @@ class _StoreInfoWidgetState extends State { String storeAddress = 'Jl. Merdeka No. 123'; String adminName = 'Budi Santoso'; String adminPhone = '08123456789'; - String? _logoPath; // Path to the store logo @override void initState() { @@ -37,7 +35,6 @@ class _StoreInfoWidgetState extends State { storeAddress = prefs.getString('store_address') ?? 'Jl. Merdeka No. 123'; adminName = prefs.getString('admin_name') ?? 'Budi Santoso'; adminPhone = prefs.getString('admin_phone') ?? '08123456789'; - _logoPath = prefs.getString('store_logo_path'); // Load logo path }); } @@ -61,17 +58,6 @@ class _StoreInfoWidgetState extends State { color: Colors.white, child: Column( children: [ - // Display store logo if available - if (_logoPath != null && _logoPath!.isNotEmpty) - Container( - margin: const EdgeInsets.only(bottom: 8.0), - child: Image.file( - File(_logoPath!), - height: 60, - width: 60, - fit: BoxFit.contain, - ), - ), Text( storeName, style: courierPrime.copyWith(