feat: Remove store logo functionality and update printing services

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
master
a2nr 2025-08-25 20:50:46 +07:00
parent c2e6f6b945
commit 64e36aa691
10 changed files with 271 additions and 230 deletions

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 B

View File

@ -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: [

View File

@ -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<int> 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,

View File

@ -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<String> generateStrukText({
required List<ReceiptItem> 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));

View File

@ -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<bool> 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<void> 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');
}
}
}

View File

@ -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<void> 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<void> removeStoreLogoPath() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('store_logo_path');
}
/// Fungsi untuk mengambil path logo toko dari shared preferences
Future<String?> 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<void> 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();
}
}

View File

@ -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<PrintingStatusCard> createState() => _PrintingStatusCardState();
}
class _PrintingStatusCardState extends State<PrintingStatusCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
));
_fadeAnimation = Tween<double>(
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,
),
],
),
),
),
),
),
),
);
},
);
}
}

View File

@ -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<StoreInfoConfigDialog> {
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<StoreInfoConfigDialog> {
_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<StoreInfoConfigDialog> {
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<StoreInfoConfigDialog> {
}
}
/// Memilih logo dari galeri
Future<void> _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<StoreInfoConfigDialog> {
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(

View File

@ -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<StoreInfoWidget> {
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<StoreInfoWidget> {
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<StoreInfoWidget> {
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(