import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/core/theme/app_colors.dart'; import 'package:tugas_akhir_supabase/screens/community/models/message.dart'; import 'package:tugas_akhir_supabase/screens/community/services/message_service.dart'; import 'package:tugas_akhir_supabase/services/gemini_disease_service.dart'; import 'package:tugas_akhir_supabase/data/models/diagnosis_result_model.dart'; import 'package:tugas_akhir_supabase/utils/pdf_generator.dart'; import 'package:image/image.dart' as img; // API key for Gemini (in a real app, should be stored securely) const String GEMINI_API_KEY = 'AIzaSyDEH7z1IoTZ3J10EProCSTSetTMDqbBXn4'; // App colors class PlantScannerColors { static final primary = Color(0xFF0A8754); static final secondary = Color(0xFF39B686); static final accent = Color(0xFF2C7873); static final background = Color(0xFFF5F9F6); static final cardBackground = Colors.white; static final error = Color(0xFFD83A3A); static final warning = Color(0xFFFF9800); static final success = Color(0xFF4CAF50); static final lightGreen = Color(0xFFE8F5E9); static final darkText = Color(0xFF2C3333); static final lightText = Color(0xFF6B7280); static final disabledText = Color(0xFFAEB0B6); static final divider = Color(0xFFEAECF0); } // Application states enum ScanState { empty, loading, result, error, notPlant } class PlantScannerScreen extends StatefulWidget { const PlantScannerScreen({super.key}); @override State createState() => _PlantScannerScreenState(); } class _PlantScannerScreenState extends State with SingleTickerProviderStateMixin { final _picker = ImagePicker(); File? _image; Uint8List? _webImage; ScanState _scanState = ScanState.empty; String _errorMessage = ''; late AnimationController _animationController; bool _isAnalyzing = false; bool _isLoading = false; // Gemini API service late GeminiDiseaseDiagnosisService _geminiService; DiagnosisResultModel? _diagnosisResult; final MessageService _messageService = MessageService(); final List _messages = []; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(); // Initialize Gemini API service with error handling try { // Check if Supabase is initialized _geminiService = GeminiDiseaseDiagnosisService( apiKey: GEMINI_API_KEY, supabaseClient: Supabase.instance.client, ); // Log API key for debugging (remove in production) debugPrint('Using Gemini API key: ${GEMINI_API_KEY.substring(0, 5)}...'); debugPrint('Gemini service initialized successfully'); } catch (e) { debugPrint('Error initializing Gemini service: $e'); _errorMessage = 'Gagal menginisialisasi layanan. Silakan coba lagi.'; _scanState = ScanState.error; } } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 360; return Scaffold( appBar: AppBar( backgroundColor: AppColors.primary, foregroundColor: Colors.white, title: Text( 'Analisis Tanaman', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), elevation: 0, actions: [ IconButton( icon: const Icon(Icons.help_outline, size: 22), onPressed: _showHelpDialog, ), ], ), body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.primary.withOpacity(0.9), AppColors.scaffoldBackground, ], stops: const [0.0, 0.2], ), ), child: SafeArea(child: _getContentForState()), ), ); } // Content based on state Widget _getContentForState() { switch (_scanState) { case ScanState.empty: return _buildEmptyState(); case ScanState.loading: return _buildLoadingState(); case ScanState.result: return _buildResultView(); case ScanState.error: return _buildErrorState(); case ScanState.notPlant: return _buildNotPlantState(); } } // UI for empty state Widget _buildEmptyState() { return Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Logo or illustration Container( width: 70, height: 70, decoration: BoxDecoration( color: PlantScannerColors.lightGreen, shape: BoxShape.circle, ), child: Icon( Icons.eco_rounded, size: 44, color: PlantScannerColors.primary, ), ), const SizedBox(height: 20), // Middle container with border Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: PlantScannerColors.primary.withOpacity(0.3), width: 1.5, ), ), child: Column( children: [ Row( children: [ Icon( Icons.eco_outlined, color: PlantScannerColors.primary, size: 22, ), const SizedBox(width: 10), Expanded( child: Text( 'Ambil foto yang jelas dari bagian tanaman yang terlihat bermasalah', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: PlantScannerColors.darkText, ), ), ), ], ), const SizedBox(height: 12), Row( children: [ Icon( Icons.wb_sunny_outlined, color: PlantScannerColors.warning, size: 22, ), const SizedBox(width: 10), Expanded( child: Text( 'Pastikan pencahayaan cukup dan tidak berbayang', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: PlantScannerColors.darkText, ), ), ), ], ), const SizedBox(height: 12), Row( children: [ Icon( Icons.zoom_in, color: PlantScannerColors.accent, size: 22, ), const SizedBox(width: 10), Expanded( child: Text( 'Dekatkan kamera agar detail gejala terlihat jelas', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: PlantScannerColors.darkText, ), ), ), ], ), ], ), ), // Image upload options with improved UI Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: Column( children: [ Text( 'Unggah Foto Tanaman', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), const SizedBox(height: 20), Row( children: [ Expanded( child: _buildUploadOption( icon: Icons.camera_alt_rounded, label: 'Kamera', description: 'Ambil foto baru', onTap: () => _pickImage(ImageSource.camera), ), ), const SizedBox(width: 12), Expanded( child: _buildUploadOption( icon: Icons.photo_library_rounded, label: 'Galeri', description: 'Pilih dari galeri', onTap: _pickImageFromGallery, ), ), ], ), ], ), ), ], ), ), ); } // UI for loading state Widget _buildLoadingState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 80, height: 80, padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation( PlantScannerColors.primary, ), ), ), const SizedBox(height: 32), Text( _isAnalyzing ? 'Menganalisis Tanaman' : 'Menghubungkan ke API', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), const SizedBox(height: 12), Text( _isAnalyzing ? 'Mencari pola penyakit dan kondisi kesehatan...' : 'Memproses gambar tanaman...', textAlign: TextAlign.center, style: TextStyle(fontSize: 15, color: PlantScannerColors.lightText), ), const SizedBox(height: 40), // Loading animation for indication of progress SizedBox( width: 240, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: LinearProgressIndicator( backgroundColor: PlantScannerColors.lightGreen, valueColor: AlwaysStoppedAnimation( PlantScannerColors.secondary, ), ), ), ), ], ), ); } // Result view - Shows diagnosis results Widget _buildResultView() { if (_diagnosisResult == null) { return Center(child: Text('Tidak ada hasil diagnosis')); } return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image preview if (_image != null) ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( _image!, width: double.infinity, height: 200, fit: BoxFit.cover, ), ), const SizedBox(height: 16), // Tanaman Terdeteksi section Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Tanaman Terdeteksi', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), const SizedBox(height: 12), Text( _diagnosisResult!.plantSpecies, style: TextStyle( fontSize: 23, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), const SizedBox(height: 8), Row( children: [ Icon(Icons.eco, color: AppColors.primary, size: 20), const SizedBox(width: 8), Text( 'Fase: ${_diagnosisResult!.plantData['growthStage'] ?? 'Tidak dapat ditentukan dari gambar.'}', style: TextStyle(fontSize: 11, color: Colors.black), ), ], ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), decoration: BoxDecoration( color: _diagnosisResult!.isHealthy ? PlantScannerColors.success.withOpacity(0.2) : PlantScannerColors.error.withOpacity(0.2), borderRadius: BorderRadius.circular(20), ), child: Text( _diagnosisResult!.isHealthy ? 'healthy_plant' : 'unhealthy_plant', style: TextStyle( color: _diagnosisResult!.isHealthy ? PlantScannerColors.success : PlantScannerColors.error, fontWeight: FontWeight.w500, ), ), ), ], ), ), const SizedBox(height: 16), // Tingkat Keparahan & Dampak if (!_diagnosisResult!.isHealthy) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Tingkat Keparahan & Dampak', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Keparahan', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), const SizedBox(height: 8), LinearProgressIndicator( value: _diagnosisResult!.confidenceValue, backgroundColor: Colors.red.withOpacity(0.2), valueColor: AlwaysStoppedAnimation( Colors.red, ), minHeight: 10, borderRadius: BorderRadius.circular(5), ), const SizedBox(height: 4), Text( '${(_diagnosisResult!.confidenceValue * 100).toInt()}%', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.red, ), ), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Area Terinfeksi', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), const SizedBox(height: 8), LinearProgressIndicator( value: () { final infectedAreaRaw = _diagnosisResult!.plantData['infectedArea']; if (infectedAreaRaw is num) { return infectedAreaRaw.toDouble() / 100; } else if (infectedAreaRaw is String) { final infectedArea = num.tryParse( infectedAreaRaw, ); if (infectedArea != null) { return infectedArea.toDouble() / 100; } } return 0.0; }(), backgroundColor: Colors.orange.withOpacity(0.2), valueColor: AlwaysStoppedAnimation( Colors.orange, ), minHeight: 10, borderRadius: BorderRadius.circular(5), ), const SizedBox(height: 4), Text( '${(() { final infectedAreaRaw = _diagnosisResult!.plantData['infectedArea']; if (infectedAreaRaw is num) { return infectedAreaRaw.toInt(); } else if (infectedAreaRaw is String) { final infectedArea = num.tryParse(infectedAreaRaw); if (infectedArea != null) { return infectedArea.toInt(); } } return 0; })()}%', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.orange, ), ), ], ), ), ], ), const SizedBox(height: 16), Text( 'Potensi Kerugian: ${_diagnosisResult!.economicImpact['estimatedLoss'] ?? 'Tidak dapat ditentukan dari gambar. Kerugian bergantung pada luas area yang terinfeksi.'}', style: TextStyle(fontSize: 14, color: Colors.red[700]), ), ], ), ), const SizedBox(height: 16), // Informasi Penyakit if (!_diagnosisResult!.isHealthy) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.bug_report, color: Colors.red[700], size: 22), const SizedBox(width: 8), Text( 'Informasi Penyakit', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), ], ), const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _diagnosisResult!.diseaseName, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.red[700], ), ), if (_diagnosisResult!.scientificName.isNotEmpty) Text( '*${_diagnosisResult!.scientificName}*', style: TextStyle( fontSize: 16, fontStyle: FontStyle.italic, color: Colors.red[700], ), ), ], ), ), const SizedBox(height: 16), // Gejala Row( children: [ Icon( Icons.warning_amber_rounded, color: Colors.amber, size: 20, ), const SizedBox(width: 8), Text( 'Gejala', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Text( _diagnosisResult!.symptoms, style: TextStyle( fontSize: 14, height: 1.5, color: PlantScannerColors.darkText, ), ), ), const SizedBox(height: 16), // Penyebab Row( children: [ Icon( Icons.science_outlined, color: Colors.blue, size: 20, ), const SizedBox(width: 8), Text( 'Penyebab', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Text( _diagnosisResult!.causes, style: TextStyle( fontSize: 14, height: 1.5, color: PlantScannerColors.darkText, ), ), ), const SizedBox(height: 16), // Bagian yang Terpengaruh Row( children: [ Icon(Icons.eco_outlined, color: Colors.green, size: 20), const SizedBox(width: 8), Text( 'Bagian yang Terpengaruh', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Text( _diagnosisResult!.additionalInfo.affectedParts.isNotEmpty ? _diagnosisResult!.additionalInfo.affectedParts.join( ', ', ) : 'Daun', style: TextStyle( fontSize: 14, height: 1.5, color: PlantScannerColors.darkText, ), ), ), const SizedBox(height: 16), // Kondisi Lingkungan Row( children: [ Icon( Icons.wb_sunny_outlined, color: Colors.orange, size: 20, ), const SizedBox(width: 8), Text( 'Kondisi Lingkungan', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Text( _diagnosisResult!.additionalInfo.environmentalConditions, style: TextStyle( fontSize: 14, height: 1.5, color: PlantScannerColors.darkText, ), ), ), ], ), ), const SizedBox(height: 16), // Pengobatan & Pencegahan if (!_diagnosisResult!.isHealthy) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.medical_services_outlined, color: Colors.green, size: 22, ), const SizedBox(width: 8), Text( 'Pengobatan & Pencegahan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), ], ), const SizedBox(height: 16), // Pengobatan Organik Row( children: [ Icon(Icons.eco_outlined, color: Colors.green, size: 20), const SizedBox(width: 8), Text( 'Pengobatan Organik', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Text( _diagnosisResult!.organicTreatment, style: TextStyle( fontSize: 14, height: 1.5, color: PlantScannerColors.darkText, ), ), ), const SizedBox(height: 16), // Pengobatan Kimia Row( children: [ Icon( Icons.science_outlined, color: Colors.blue, size: 20, ), const SizedBox(width: 8), Text( 'Pengobatan Kimia', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Text( _diagnosisResult!.chemicalTreatment, style: TextStyle( fontSize: 14, height: 1.5, color: PlantScannerColors.darkText, ), ), ), ], ), ), const SizedBox(height: 16), // Langkah Pencegahan if (!_diagnosisResult!.isHealthy) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.shield_outlined, color: Colors.green[700], size: 22, ), const SizedBox(width: 8), Text( 'Langkah Pencegahan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), ], ), const SizedBox(height: 16), ..._diagnosisResult!.preventionMeasures.map( (measure) => Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.check_circle_outline, color: Colors.green, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( measure, style: TextStyle( fontSize: 14, height: 1.5, color: PlantScannerColors.darkText, ), ), ), ], ), ), ), ], ), ), const SizedBox(height: 16), // Jadwal Perawatan if (!_diagnosisResult!.isHealthy && _diagnosisResult!.treatmentSchedule.isNotEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Jadwal Perawatan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), const SizedBox(height: 16), // Penyiraman if (_diagnosisResult!.treatmentSchedule['wateringSchedule'] != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.water_drop_outlined, color: Colors.blue, size: 20, ), const SizedBox(width: 8), Text( 'Penyiraman', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 4), Text( _diagnosisResult! .treatmentSchedule['wateringSchedule'] .toString(), style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), const SizedBox(height: 12), ], ), // Pemupukan if (_diagnosisResult! .treatmentSchedule['fertilizingSchedule'] != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.eco_outlined, color: Colors.green, size: 20, ), const SizedBox(width: 8), Text( 'Pemupukan', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 4), Text( _diagnosisResult! .treatmentSchedule['fertilizingSchedule'] .toString(), style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), const SizedBox(height: 12), ], ), // Pestisida if (_diagnosisResult! .treatmentSchedule['pesticideSchedule'] != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.pest_control_outlined, color: Colors.orange, size: 20, ), const SizedBox(width: 8), Text( 'Pestisida', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 4), Text( _diagnosisResult! .treatmentSchedule['pesticideSchedule'] .toString(), style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ], ), ], ), ), const SizedBox(height: 16), // Varietas Alternatif if (!_diagnosisResult!.isHealthy && _diagnosisResult!.alternativeVarieties.isNotEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Varietas Alternatif', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), const SizedBox(height: 12), Row( children: [ Icon(Icons.eco_outlined, color: Colors.green, size: 20), const SizedBox(width: 8), Text( 'Varietas padi tahan penyakit bercak daun bakteri', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: PlantScannerColors.darkText, ), ), ], ), const SizedBox(height: 8), Text( 'Cari informasi varietas padi yang direkomendasikan untuk daerah Anda yang memiliki tingkat ketahanan terhadap penyakit bercak daun bakteri.', style: TextStyle( fontSize: 14, color: Colors.grey[600], height: 1.5, ), ), ], ), ), const SizedBox(height: 24), // Action buttons Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => setState(() => _scanState = ScanState.empty), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: const Icon(Icons.refresh), label: const Text('Analisis Ulang'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: _generatePdfReport, style: ElevatedButton.styleFrom( backgroundColor: PlantScannerColors.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: const Icon(Icons.download_rounded), label: const Text('Simpan Laporan'), ), ), ], ), ], ), ); } // Generate PDF report Future _generatePdfReport() async { if (_diagnosisResult == null || _image == null) return; try { setState(() => _isLoading = true); // Convert image to bytes final imageBytes = await _image!.readAsBytes(); // Decode image (apapun formatnya) lalu encode ke PNG final decoded = img.decodeImage(imageBytes); Uint8List safeBytes; if (decoded != null) { safeBytes = Uint8List.fromList(img.encodePng(decoded)); } else { safeBytes = imageBytes; // fallback, meski kemungkinan error } final pdfGenerator = HarvestPdfGenerator(); final pdfFile = await pdfGenerator.generateDiagnosisReportPdf( diagnosisResult: _diagnosisResult!, imageBytes: safeBytes, ); setState(() => _isLoading = false); if (!mounted) return; showDialog( context: context, builder: (context) => AlertDialog( title: const Text('PDF Berhasil Dibuat'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Laporan PDF diagnosis tanaman telah berhasil dibuat.', ), const SizedBox(height: 8), Text( 'Lokasi: \\n${pdfFile.path}', style: const TextStyle( fontSize: 12, fontStyle: FontStyle.italic, ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Tutup'), ), TextButton( onPressed: () async { Navigator.pop(context); await pdfGenerator.sharePdf(pdfFile); }, child: const Text('Bagikan'), ), TextButton( onPressed: () async { Navigator.pop(context); await pdfGenerator.openPdf(pdfFile); }, child: const Text('Buka'), ), ], ), ); } catch (e) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal membuat laporan: \\n${e.toString()}'), backgroundColor: PlantScannerColors.error, ), ); } } // UI for error state Widget _buildErrorState() { return Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: PlantScannerColors.error.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.error_outline_rounded, size: 56, color: PlantScannerColors.error, ), ), const SizedBox(height: 32), Text( 'Terjadi Kesalahan', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: PlantScannerColors.error.withOpacity(0.3), width: 1, ), ), child: Text( _errorMessage.isNotEmpty ? _errorMessage : 'Terjadi kesalahan saat menganalisis gambar. Silakan coba lagi.', textAlign: TextAlign.center, style: TextStyle( fontSize: 15, color: PlantScannerColors.darkText, ), ), ), const SizedBox(height: 32), ElevatedButton.icon( onPressed: () => setState(() => _scanState = ScanState.empty), style: ElevatedButton.styleFrom( backgroundColor: PlantScannerColors.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 0, ), icon: const Icon(Icons.refresh), label: Text('Coba Lagi'), ), ], ), ), ); } // UI for not plant state Widget _buildNotPlantState() { return Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: PlantScannerColors.warning.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.warning_amber_rounded, size: 56, color: PlantScannerColors.warning, ), ), const SizedBox(height: 32), Text( 'Bukan Tanaman Terdeteksi', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: PlantScannerColors.darkText, ), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: PlantScannerColors.warning.withOpacity(0.3), width: 1, ), ), child: Text( 'Kami tidak dapat mendeteksi tanaman dalam gambar ini. Pastikan gambar yang Anda kirim menampilkan tanaman dengan jelas.', textAlign: TextAlign.center, style: TextStyle( fontSize: 15, color: PlantScannerColors.darkText, ), ), ), const SizedBox(height: 32), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => setState(() => _scanState = ScanState.empty), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), side: BorderSide(color: PlantScannerColors.accent), ), icon: const Icon(Icons.arrow_back), label: Text('Kembali'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: _pickImageFromGallery, style: ElevatedButton.styleFrom( backgroundColor: PlantScannerColors.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), icon: const Icon(Icons.photo_library), label: Text('Unggah Ulang'), ), ), ], ), ], ), ), ); } // Help dialog void _showHelpDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: Text( 'Bantuan Analisis Tanaman', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( contentPadding: EdgeInsets.zero, leading: Icon( Icons.check, color: AppColors.primary, size: 18, ), minLeadingWidth: 24, title: Text( 'Ambil foto yang jelas dari bagian tanaman yang terlihat bermasalah', style: TextStyle( fontSize: 13, color: PlantScannerColors.darkText, ), ), ), ListTile( contentPadding: EdgeInsets.zero, leading: Icon( Icons.check, color: AppColors.primary, size: 18, ), minLeadingWidth: 24, title: Text( 'Fokuskan pada bagian yang terinfeksi (daun, batang, buah)', style: TextStyle( fontSize: 13, color: PlantScannerColors.darkText, ), ), ), ListTile( contentPadding: EdgeInsets.zero, leading: Icon( Icons.check, color: AppColors.primary, size: 18, ), minLeadingWidth: 24, title: Text( 'Pastikan pencahayaan cukup dan foto tidak buram', style: TextStyle( fontSize: 13, color: PlantScannerColors.darkText, ), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text( 'Mengerti', style: TextStyle(color: AppColors.primary), ), ), ], ), ); } // Upload option item with improved UI Widget _buildUploadOption({ required IconData icon, required String label, required String description, required VoidCallback onTap, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: PlantScannerColors.background, borderRadius: BorderRadius.circular(16), border: Border.all(color: PlantScannerColors.divider, width: 1), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: PlantScannerColors.primary.withOpacity(0.2), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Icon(icon, color: PlantScannerColors.primary, size: 24), ), const SizedBox(height: 12), Text( label, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: PlantScannerColors.darkText, ), ), const SizedBox(height: 4), Text( description, textAlign: TextAlign.center, style: TextStyle( fontSize: 12, color: PlantScannerColors.lightText, ), ), ], ), ), ); } // Camera image picking Future _pickImage(ImageSource source) async { try { final pickedFile = await _picker.pickImage(source: source); if (pickedFile == null) return; setState(() { _scanState = ScanState.loading; _errorMessage = ''; }); final imageFile = File(pickedFile.path); setState(() { _image = imageFile; _webImage = null; }); // Analyze the plant image setState(() { _isAnalyzing = true; }); try { // Attempt to analyze the image using Gemini service final result = await _geminiService.diagnosePlant(_image!.path); setState(() { _diagnosisResult = result; _scanState = ScanState.result; _isAnalyzing = false; }); } catch (e) { debugPrint('Error analyzing image: $e'); setState(() { _isAnalyzing = false; _scanState = ScanState.error; _errorMessage = 'Terjadi kesalahan saat menganalisis gambar: ${e.toString()}'; }); } } catch (e) { setState(() { _scanState = ScanState.error; _errorMessage = e.toString(); }); } } // Gallery picking Future _pickImageFromGallery() async { try { final pickedFile = await _picker.pickImage(source: ImageSource.gallery); if (pickedFile == null) return; setState(() { _scanState = ScanState.loading; _errorMessage = ''; }); final imageFile = File(pickedFile.path); setState(() { _image = imageFile; _webImage = null; }); // Analyze the plant image setState(() { _isAnalyzing = true; }); try { // Attempt to analyze the image using Gemini service final result = await _geminiService.diagnosePlant(_image!.path); setState(() { _diagnosisResult = result; _scanState = ScanState.result; _isAnalyzing = false; }); } catch (e) { debugPrint('Error analyzing image: $e'); setState(() { _isAnalyzing = false; _scanState = ScanState.error; _errorMessage = 'Terjadi kesalahan saat menganalisis gambar: ${e.toString()}'; }); } } catch (e) { setState(() { _scanState = ScanState.error; _errorMessage = e.toString(); }); } } Future _deleteMessageForEveryone(Message message) async { setState(() { _messages.removeWhere((m) => m.id == message.id); }); try { await _messageService.deleteMessage(message); // ...snackbar... } catch (e) { // ...error handling... } } }