amoriai/lib/screens/camera/camera_screen.dart

371 lines
11 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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),
),
),
],
],
),
),
);
}
}