371 lines
11 KiB
Dart
371 lines
11 KiB
Dart
import 'dart:async';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../../models/user_model.dart';
|
||
import '../../services/ocr_service.dart';
|
||
import '../../services/gemini_service.dart';
|
||
import '../../services/firebase_service.dart';
|
||
import '../../models/recommendation_model.dart';
|
||
|
||
import '../recommendation/recommendation_screen.dart';
|
||
|
||
class CameraScreen extends ConsumerStatefulWidget {
|
||
/// Satu atau lebih path foto menu (maks 3).
|
||
final List<String> imagePaths;
|
||
final RecommendationModel? previousScan;
|
||
|
||
const CameraScreen({super.key, required this.imagePaths, this.previousScan});
|
||
|
||
@override
|
||
ConsumerState<CameraScreen> createState() => _CameraScreenState();
|
||
}
|
||
|
||
class _CameraScreenState extends ConsumerState<CameraScreen> {
|
||
bool _isProcessing = false;
|
||
double _progress = 0.0;
|
||
|
||
// Cycling loading text
|
||
int _cycleIndex = 0;
|
||
Timer? _cycleTimer;
|
||
|
||
static const _cycleMessages = [
|
||
(
|
||
title: 'Tunggu Sebentar',
|
||
subtitle: 'Biarkan AI Memberikanmu Rekomendasi Terbaiknya',
|
||
),
|
||
(title: 'Sebentar Lagi', subtitle: 'Ini Membutuhkan Waktu Sebentar Lagi'),
|
||
];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_processImage();
|
||
_cycleTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_cycleIndex = (_cycleIndex + 1) % _cycleMessages.length;
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_cycleTimer?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _processImage() async {
|
||
setState(() {
|
||
_isProcessing = true;
|
||
_progress = 0.1;
|
||
});
|
||
|
||
try {
|
||
// Step 1: OCR – gabungkan teks dari semua foto (Atau ambil dari riwayat)
|
||
setState(() => _progress = 0.3);
|
||
|
||
final allMenuItems = <MenuItemOCR>[];
|
||
String combinedText = '';
|
||
|
||
if (widget.previousScan != null) {
|
||
// Lewati OCR, gunakan data dari riwayat sebelumnya
|
||
combinedText = widget.previousScan!.originalMenuText;
|
||
for (var rec in widget.previousScan!.recommendations) {
|
||
allMenuItems.add(MenuItemOCR(
|
||
name: rec.menuName,
|
||
price: rec.price,
|
||
description: rec.description,
|
||
category: rec.category,
|
||
));
|
||
}
|
||
} else {
|
||
final ocrService = OCRService();
|
||
|
||
for (final path in widget.imagePaths) {
|
||
final ocrResult = await ocrService.extractTextFromImage(path);
|
||
if (ocrResult.isSuccess) {
|
||
allMenuItems.addAll(ocrResult.menuItems);
|
||
if (combinedText.isNotEmpty) combinedText += '\n';
|
||
combinedText += ocrResult.processedText;
|
||
}
|
||
}
|
||
|
||
// Validasi setidaknya ada 1 item menu
|
||
if (allMenuItems.isEmpty) {
|
||
// Fallback: gunakan hasil OCR foto pertama walaupun tidak ideal
|
||
final firstResult = await ocrService.extractTextFromImage(
|
||
widget.imagePaths.first,
|
||
);
|
||
if (!firstResult.isSuccess ||
|
||
!ocrService.validateOCRResult(firstResult)) {
|
||
throw Exception(
|
||
'Gagal membaca menu. Pastikan gambar jelas dan berisi teks menu.',
|
||
);
|
||
}
|
||
allMenuItems.addAll(firstResult.menuItems);
|
||
combinedText = firstResult.processedText;
|
||
}
|
||
}
|
||
|
||
// Step 2: Get user data (preferences + daily check-in)
|
||
setState(() => _progress = 0.5);
|
||
|
||
final firebaseService = FirebaseService();
|
||
final currentUser = firebaseService.currentUser;
|
||
|
||
if (currentUser == null) {
|
||
throw Exception('Pengguna tidak terautentikasi');
|
||
}
|
||
|
||
final userData = await firebaseService.getUserData(currentUser.uid);
|
||
final userPreferences = userData?.preferences ?? UserPreferences();
|
||
|
||
// Step 3: Generate recommendations — semua data (profil + daily) dikirim sekaligus
|
||
setState(() => _progress = 0.7);
|
||
|
||
final geminiService = GeminiService();
|
||
final recommendation = await geminiService.generateRecommendations(
|
||
menuItems: allMenuItems,
|
||
userPreferences: userPreferences,
|
||
userId: currentUser.uid,
|
||
menuImageUrl: widget.previousScan?.menuImageUrl ?? (widget.imagePaths.isNotEmpty ? widget.imagePaths.first : null),
|
||
originalMenuText: combinedText,
|
||
userModel: userData,
|
||
menuImagePaths: widget.previousScan?.menuImagePaths ?? widget.imagePaths,
|
||
);
|
||
|
||
// Step 4: Save as Draft
|
||
setState(() => _progress = 0.9);
|
||
await firebaseService.saveRecommendation(recommendation);
|
||
|
||
// Step 5: Navigate
|
||
setState(() => _progress = 1.0);
|
||
await Future.delayed(const Duration(milliseconds: 500));
|
||
|
||
if (mounted) {
|
||
Navigator.of(context).pushReplacement(
|
||
MaterialPageRoute(
|
||
builder: (context) => RecommendationScreen(
|
||
recommendation: recommendation,
|
||
ocrResult: null,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} catch (e) {
|
||
debugPrint('Image processing error: $e');
|
||
if (mounted) {
|
||
_showErrorDialog(e.toString());
|
||
}
|
||
}
|
||
}
|
||
|
||
void _showErrorDialog(String error) {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (context) => AlertDialog(
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||
title: Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
color: Colors.red.shade50,
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Icon(
|
||
Icons.error_outline,
|
||
color: Colors.red.shade600,
|
||
size: 24,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
const Expanded(
|
||
child: Text(
|
||
'Gagal Memproses',
|
||
style: TextStyle(
|
||
fontFamily: 'Poppins',
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text(
|
||
'Terjadi kesalahan saat memproses gambar menu:',
|
||
style: TextStyle(
|
||
fontFamily: 'Poppins',
|
||
fontSize: 14,
|
||
color: Colors.grey,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.red.shade50,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: Colors.red.shade200),
|
||
),
|
||
child: Text(
|
||
error.length > 200 ? '${error.substring(0, 200)}...' : error,
|
||
style: TextStyle(
|
||
fontFamily: 'Poppins',
|
||
fontSize: 12,
|
||
color: Colors.red.shade700,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
'Tips:',
|
||
style: TextStyle(
|
||
fontFamily: 'Poppins',
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
_buildTipItem('Pastikan gambar menu terlihat jelas'),
|
||
_buildTipItem('Foto dari berbagai sudut bisa membantu'),
|
||
_buildTipItem('Pastikan koneksi internet aktif'),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
Navigator.of(context).pop();
|
||
},
|
||
child: Text(
|
||
'Kembali',
|
||
style: TextStyle(
|
||
fontFamily: 'Poppins',
|
||
color: Colors.red.shade600,
|
||
),
|
||
),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
setState(() {
|
||
_isProcessing = true;
|
||
_progress = 0.0;
|
||
});
|
||
_processImage();
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF8B5E3C),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
),
|
||
child: const Text(
|
||
'Coba Lagi',
|
||
style: TextStyle(fontFamily: 'Poppins', color: Colors.white),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTipItem(String text) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Icon(
|
||
Icons.check_circle_outline,
|
||
size: 16,
|
||
color: Colors.green.shade600,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
text,
|
||
style: const TextStyle(
|
||
fontFamily: 'Poppins',
|
||
fontSize: 12,
|
||
color: Colors.grey,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final msg = _cycleMessages[_cycleIndex];
|
||
|
||
return Scaffold(
|
||
backgroundColor: const Color(0xFFFAF6F1),
|
||
body: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Image.asset(
|
||
'assets/images/Loading Scan.gif',
|
||
width: 220,
|
||
height: 220,
|
||
fit: BoxFit.contain,
|
||
),
|
||
const SizedBox(height: 32),
|
||
AnimatedSwitcher(
|
||
duration: const Duration(milliseconds: 400),
|
||
child: Column(
|
||
key: ValueKey(_cycleIndex),
|
||
children: [
|
||
Text(
|
||
msg.title,
|
||
style: const TextStyle(
|
||
fontFamily: 'Poppins',
|
||
fontSize: 22,
|
||
fontWeight: FontWeight.bold,
|
||
color: Color(0xFF4A3C3C),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||
child: Text(
|
||
msg.subtitle,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(
|
||
fontFamily: 'Poppins',
|
||
fontSize: 13,
|
||
color: Color(0xFF8B6F6F),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (widget.imagePaths.length > 1) ...[
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'${widget.imagePaths.length} foto sedang dianalisis',
|
||
style: const TextStyle(
|
||
fontFamily: 'Poppins',
|
||
fontSize: 12,
|
||
color: Color(0xFFB0A0A0),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|