392 lines
12 KiB
Dart
392 lines
12 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:image_cropper/image_cropper.dart';
|
|
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:praresi/presentation/controllers/resi_controller.dart';
|
|
|
|
|
|
class OCRView extends StatefulWidget {
|
|
const OCRView({super.key});
|
|
|
|
@override
|
|
State<OCRView> createState() => _OCRViewState();
|
|
}
|
|
|
|
|
|
|
|
class _OCRViewState extends State<OCRView> {
|
|
File? _imageFile;
|
|
final picker = ImagePicker();
|
|
final TextEditingController _textController = TextEditingController();
|
|
bool _isProcessing = false;
|
|
|
|
bool _isSaving = false;
|
|
|
|
|
|
final ResiController resiController = Get.put(ResiController());
|
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
|
|
|
/// Ambil gambar dan jalankan OCR
|
|
Future<void> _pickImage(ImageSource source) async {
|
|
final pickedFile = await picker.pickImage(source: source, imageQuality: 80);
|
|
if (pickedFile != null) {
|
|
File? cropped = await _cropImage(pickedFile.path);
|
|
if (cropped != null) {
|
|
setState(() {
|
|
_imageFile = cropped;
|
|
});
|
|
await _performOCR(cropped);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_textController.addListener(() {
|
|
setState(() {});
|
|
});
|
|
}
|
|
|
|
/// Crop gambar
|
|
Future<File?> _cropImage(String imagePath) async {
|
|
final croppedFile = await ImageCropper().cropImage(
|
|
sourcePath: imagePath,
|
|
aspectRatioPresets: [
|
|
CropAspectRatioPreset.original,
|
|
CropAspectRatioPreset.square,
|
|
CropAspectRatioPreset.ratio4x3,
|
|
CropAspectRatioPreset.ratio16x9,
|
|
],
|
|
uiSettings: [
|
|
AndroidUiSettings(
|
|
toolbarTitle: 'Crop Gambar',
|
|
toolbarColor: const Color(0xFF1976D2),
|
|
toolbarWidgetColor: Colors.white,
|
|
activeControlsWidgetColor: const Color(0xFF1976D2),
|
|
initAspectRatio: CropAspectRatioPreset.original,
|
|
lockAspectRatio: false, // 🔓 supaya bisa geser & ubah ukuran bebas
|
|
),
|
|
IOSUiSettings(
|
|
title: 'Crop Gambar',
|
|
aspectRatioLockEnabled: false, // 🔓 untuk iPhone/iPad
|
|
),
|
|
],
|
|
);
|
|
return croppedFile != null ? File(croppedFile.path) : null;
|
|
}
|
|
|
|
/// Jalankan OCR pakai Google ML Kit
|
|
Future<void> _performOCR(File imageFile) async {
|
|
setState(() {
|
|
_isProcessing = true;
|
|
});
|
|
|
|
final inputImage = InputImage.fromFile(imageFile);
|
|
final textRecognizer = TextRecognizer();
|
|
final recognizedText = await textRecognizer.processImage(inputImage);
|
|
await textRecognizer.close();
|
|
|
|
setState(() {
|
|
_textController.text = recognizedText.text;
|
|
_isProcessing = false;
|
|
});
|
|
|
|
Get.snackbar(
|
|
'Selesai',
|
|
'Teks berhasil dikenali!',
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
|
|
/// Simpan hasil OCR ke Firestore
|
|
Future<void> _saveResi() async {
|
|
|
|
/// ✅ cegah double klik
|
|
if (_isSaving) return;
|
|
|
|
final user = _auth.currentUser;
|
|
|
|
if (user == null) {
|
|
Get.snackbar(
|
|
'Gagal',
|
|
'User belum login!',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (_textController.text.trim().isEmpty) {
|
|
Get.snackbar(
|
|
'Peringatan',
|
|
'Tidak ada teks untuk disimpan!',
|
|
backgroundColor: Colors.orange,
|
|
colorText: Colors.white,
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isSaving = true;
|
|
});
|
|
|
|
try {
|
|
final storeId = user.uid;
|
|
|
|
await resiController.saveResi(
|
|
_textController.text.trim(),
|
|
storeId,
|
|
);
|
|
|
|
} finally {
|
|
|
|
/// ✅ aktif kembali setelah selesai
|
|
setState(() {
|
|
_isSaving = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
void dispose() {
|
|
_textController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final size = MediaQuery.of(context).size;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text("Pindai Pemesanan"),
|
|
centerTitle: true,
|
|
backgroundColor: const Color(0xFF1976D2),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
body: Container(
|
|
width: size.width,
|
|
height: size.height,
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [Color(0xFF1976D2), Color(0xFFE3F2FD)],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
child: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: 20),
|
|
|
|
// Gambar hasil ambil/crop
|
|
Expanded(
|
|
flex: 4,
|
|
child: Container(
|
|
width: double.infinity,
|
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
)
|
|
],
|
|
),
|
|
child: _imageFile == null
|
|
? Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: const [
|
|
Icon(Icons.camera_alt,
|
|
size: 50, color: Colors.grey),
|
|
SizedBox(height: 12),
|
|
Text(
|
|
"Belum ada gambar",
|
|
style: TextStyle(
|
|
fontSize: 16, color: Colors.grey),
|
|
),
|
|
],
|
|
)
|
|
: ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Image.file(_imageFile!, fit: BoxFit.cover),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Tombol ambil/pilih gambar
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _pickImage(ImageSource.camera),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF1976D2),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.camera, color: Colors.white),
|
|
label: const Text("Kamera",
|
|
style:
|
|
TextStyle(color: Colors.white, fontSize: 16)),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => _pickImage(ImageSource.gallery),
|
|
style: OutlinedButton.styleFrom(
|
|
side: const BorderSide(color: Color(0xFF1976D2), width: 2),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.photo_library,
|
|
color: Color(0xFF1976D2)),
|
|
label: const Text("Galeri",
|
|
style: TextStyle(
|
|
color: Color(0xFF1976D2), fontSize: 16)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Area hasil OCR
|
|
Expanded(
|
|
flex: 3,
|
|
child: Stack(
|
|
children: [
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
)
|
|
],
|
|
),
|
|
child: _isProcessing
|
|
? const Center(
|
|
child: CircularProgressIndicator(
|
|
color: Color(0xFF1976D2),
|
|
),
|
|
)
|
|
: TextField(
|
|
controller: _textController,
|
|
maxLines: null,
|
|
decoration: const InputDecoration(
|
|
border: InputBorder.none,
|
|
hintText: "Hasil OCR akan muncul di sini...",
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===============================
|
|
// ICON HAPUS
|
|
// ===============================
|
|
if (_textController.text.isNotEmpty)
|
|
Positioned(
|
|
top: 8,
|
|
right: 24,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
setState(() {
|
|
_textController.clear();
|
|
});
|
|
},
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200,
|
|
shape: BoxShape.circle,
|
|
),
|
|
padding: const EdgeInsets.all(6),
|
|
child: const Icon(
|
|
Icons.close,
|
|
size: 18,
|
|
color: Colors.black54,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
// Tombol Simpan ke Firestore
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: ElevatedButton.icon(
|
|
|
|
/// ✅ disable saat saving
|
|
onPressed: _isSaving ? null : _saveResi,
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
|
|
/// ✅ loading icon
|
|
icon: _isSaving
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Icon(Icons.save, color: Colors.white),
|
|
|
|
label: Text(
|
|
_isSaving ? "Menyimpan..." : "Simpan",
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |