import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:camera/camera.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; import 'package:permission_handler/permission_handler.dart'; import '../services/tflite_service.dart'; import '../services/database_helper.dart'; import '../models/palm_record.dart'; class StaticCaptureScreen extends StatefulWidget { const StaticCaptureScreen({super.key}); @override State createState() => _StaticCaptureScreenState(); } class _StaticCaptureScreenState extends State { CameraController? _controller; final TfliteService _tfliteService = TfliteService(); final DatabaseHelper _dbHelper = DatabaseHelper(); bool _isInitialized = false; bool _isAnalyzing = false; XFile? _capturedPhoto; List? _detections; String? _errorMessage; @override void initState() { super.initState(); _initializeCamera(); } Future _initializeCamera() async { final status = await Permission.camera.request(); if (status.isDenied) { if (mounted) { setState(() => _errorMessage = "Camera permission denied."); } return; } final cameras = await availableCameras(); if (cameras.isEmpty) { if (mounted) { setState(() => _errorMessage = "No cameras found."); } return; } _controller = CameraController( cameras[0], ResolutionPreset.high, enableAudio: false, ); try { await _controller!.initialize(); await _tfliteService.initModel(); if (mounted) { setState(() => _isInitialized = true); } } catch (e) { if (mounted) { setState(() => _errorMessage = "Camera init error: $e"); } } } Future _takeAndAnalyze() async { if (_controller == null || !_controller!.value.isInitialized || _isAnalyzing) return; setState(() { _isAnalyzing = true; _errorMessage = null; _detections = null; }); try { // 1. Capture Photo final XFile photo = await _controller!.takePicture(); if (mounted) { setState(() => _capturedPhoto = photo); } // 2. Run Inference final detections = await _tfliteService.runInference(photo.path); if (detections.isNotEmpty) { // 3. Persist Image final appDocDir = await getApplicationDocumentsDirectory(); final fileName = p.basename(photo.path); final persistentPath = p.join(appDocDir.path, 'palm_static_${DateTime.now().millisecondsSinceEpoch}_$fileName'); await File(photo.path).copy(persistentPath); // 4. Save to Database final best = detections.first; final record = PalmRecord( imagePath: persistentPath, ripenessClass: best.className, confidence: best.confidence, timestamp: DateTime.now(), x1: best.normalizedBox.left, y1: best.normalizedBox.top, x2: best.normalizedBox.right, y2: best.normalizedBox.bottom, detections: detections.map((d) => { 'className': d.className, 'classIndex': d.classIndex, 'confidence': d.confidence, 'x1': d.normalizedBox.left, 'y1': d.normalizedBox.top, 'x2': d.normalizedBox.right, 'y2': d.normalizedBox.bottom, }).toList(), ); await _dbHelper.insertRecord(record); if (mounted) { setState(() { _detections = detections; _capturedPhoto = XFile(persistentPath); }); _showResultSheet(record); } } else { if (mounted) { setState(() => _errorMessage = "No palm bunches detected."); } } } catch (e) { if (mounted) { setState(() => _errorMessage = "Error during capture/analysis: $e"); } } finally { if (mounted) { setState(() => _isAnalyzing = false); } } } Future _showResultSheet(PalmRecord record) async { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => _ResultSheet(record: record), ); // Auto-reset once dismissed if (mounted) { setState(() { _capturedPhoto = null; _detections = null; }); } } @override Widget build(BuildContext context) { if (_errorMessage != null) { return Scaffold( appBar: AppBar(title: const Text('Capture & Analyze')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_errorMessage!, style: const TextStyle(color: Colors.red)), const SizedBox(height: 16), ElevatedButton(onPressed: _initializeCamera, child: const Text("Retry")), ], ), ), ); } if (!_isInitialized || _controller == null) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ // Camera Preview or Captured Image Center( child: _capturedPhoto != null && _detections != null ? AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: Stack( children: [ Image.file(File(_capturedPhoto!.path), fit: BoxFit.cover, width: double.infinity), Positioned.fill( child: LayoutBuilder( builder: (context, constraints) { return Stack( children: _detections! .map((d) => _buildBoundingBox(d, constraints)) .toList(), ); }, ), ), ], ), ) : CameraPreview(_controller!), ), // Top UI Positioned( top: 40, left: 20, right: 20, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ CircleAvatar( backgroundColor: Colors.black54, child: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), ), if (_capturedPhoto != null) CircleAvatar( backgroundColor: Colors.black54, child: IconButton( icon: const Icon(Icons.refresh, color: Colors.white), onPressed: () => setState(() { _capturedPhoto = null; _detections = null; }), ), ), ], ), ), // Capture Button if (_capturedPhoto == null && !_isAnalyzing) Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 48.0), child: GestureDetector( onTap: _takeAndAnalyze, child: Container( height: 80, width: 80, padding: const EdgeInsets.all(4), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 4), ), child: Container( decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ), ), ), ), // Analyzing Overlay if (_isAnalyzing) _buildLoadingOverlay(), ], ), ); } Widget _buildBoundingBox(DetectionResult detection, BoxConstraints constraints) { final rect = detection.normalizedBox; final color = detection.getStatusColor(); return Positioned( left: rect.left * constraints.maxWidth, top: rect.top * constraints.maxHeight, width: rect.width * constraints.maxWidth, height: rect.height * constraints.maxHeight, child: Container( decoration: BoxDecoration( border: Border.all(color: color, width: 3), borderRadius: BorderRadius.circular(4), ), child: Align( alignment: Alignment.topLeft, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), color: color, child: Text( "${detection.className} ${(detection.confidence * 100).toStringAsFixed(0)}%", style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ), ), ), ); } Widget _buildLoadingOverlay() { return Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: Container( color: Colors.black.withOpacity(0.5), child: const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: Colors.white, strokeWidth: 6), SizedBox(height: 24), Text( "AI is Analyzing...", style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold), ), ], ), ), ), ), ); } @override void dispose() { _controller?.dispose(); _tfliteService.dispose(); super.dispose(); } } class _ResultSheet extends StatelessWidget { final PalmRecord record; const _ResultSheet({required this.record}); @override Widget build(BuildContext context) { Color statusColor = const Color(0xFFFF9800); // Default orange if (record.ripenessClass == 'Empty_Bunch' || record.ripenessClass == 'Abnormal') { statusColor = const Color(0xFFF44336); } else if (record.ripenessClass == 'Ripe' || record.ripenessClass == 'Overripe') { statusColor = const Color(0xFF4CAF50); } final bool isWarning = record.ripenessClass == 'Unripe' || record.ripenessClass == 'Underripe'; return Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)), ), const SizedBox(height: 24), Row( children: [ Icon(Icons.analytics, color: statusColor, size: 32), const SizedBox(width: 12), const Text("Analysis Result", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 24), _buildInfoRow("Ripeness", record.ripenessClass, statusColor), _buildInfoRow("Confidence", "${(record.confidence * 100).toStringAsFixed(1)}%", Colors.grey[700]!), if (record.ripenessClass == 'Empty_Bunch' || record.ripenessClass == 'Abnormal') _buildAlertCard("HEALTH ALERT", "Abnormal features detected. Check palm health.", Colors.red), if (isWarning) _buildAlertCard("YIELD WARNING", "Potential Yield Loss due to premature harvest.", Colors.orange), const SizedBox(height: 32), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: statusColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: const Text("Acknowledge", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ), const SizedBox(height: 12), ], ), ); } Widget _buildInfoRow(String label, String value, Color color) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontSize: 16, color: Colors.grey)), Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color)), ], ), ); } Widget _buildAlertCard(String title, String message, Color color) { return Container( margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3)), ), child: Row( children: [ Icon(Icons.warning_amber_rounded, color: color), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 14)), Text(message, style: TextStyle(color: color.withOpacity(0.8), fontSize: 12)), ], ), ), ], ), ); } }