import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; import '../services/tflite_service.dart'; import '../services/database_helper.dart'; import '../models/palm_record.dart'; class AnalysisScreen extends StatefulWidget { const AnalysisScreen({super.key}); @override State createState() => _AnalysisScreenState(); } class _AnalysisScreenState extends State with SingleTickerProviderStateMixin { final TfliteService _tfliteService = TfliteService(); final DatabaseHelper _dbHelper = DatabaseHelper(); late AnimationController _scanningController; bool _isAnalyzing = false; File? _selectedImage; List? _detections; String? _errorMessage; @override void initState() { super.initState(); _scanningController = AnimationController( vsync: this, duration: const Duration(seconds: 2), ); _initModel(); } Future _initModel() async { try { await _tfliteService.initModel(); } catch (e) { if (mounted) { setState(() { _errorMessage = "Failed to initialize AI model: $e"; }); } } } Future _processGalleryImage() async { final image = await _tfliteService.pickImage(); if (image == null) return; if (mounted) { setState(() { _isAnalyzing = true; _selectedImage = File(image.path); _detections = null; _errorMessage = null; }); _scanningController.repeat(reverse: true); // Give the UI a frame to paint the loading state await Future.delayed(const Duration(milliseconds: 50)); } try { // 1. Run Inference final detections = await _tfliteService.runInference(image.path); if (detections.isNotEmpty) { // 2. Persist Image to Documents Directory final appDocDir = await getApplicationDocumentsDirectory(); final fileName = p.basename(image.path); final persistentPath = p.join(appDocDir.path, 'palm_${DateTime.now().millisecondsSinceEpoch}_$fileName'); await File(image.path).copy(persistentPath); // 3. Update UI if (mounted) { setState(() { _detections = detections; _selectedImage = File(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); } else { if (mounted) { setState(() { _errorMessage = "No palm bunches detected."; }); } } } catch (e) { if (mounted) { setState(() { _errorMessage = "Analysis error: $e"; }); } } finally { if (mounted) { setState(() { _isAnalyzing = false; }); _scanningController.stop(); _scanningController.reset(); } } } bool _isHealthAlert(String label) { return label == 'Empty_Bunch' || label == 'Abnormal'; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('AI Analysis')), body: Stack( children: [ Column( children: [ Expanded( child: Center( child: _selectedImage == null ? const Text("Select a photo to start AI analysis") : AspectRatio( aspectRatio: 1.0, child: Padding( padding: const EdgeInsets.all(8.0), child: Stack( children: [ // Main Image ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( _selectedImage!, fit: BoxFit.contain, width: double.infinity, height: double.infinity, ), ), // Bounding Box Overlays if (_detections != null) Positioned.fill( child: LayoutBuilder( builder: (context, constraints) { return Stack( children: _detections! .map((d) => _buildBoundingBox(d, constraints)) .toList(), ); }, ), ), // Scanning Animation if (_isAnalyzing) _ScanningOverlay(animation: _scanningController), ], ), ), ), ), ), if (_detections != null) _buildResultSummary(), Padding( padding: const EdgeInsets.all(32.0), child: ElevatedButton.icon( onPressed: _isAnalyzing ? null : _processGalleryImage, icon: const Icon(Icons.add_a_photo), label: const Text("Pick Image from Gallery"), style: ElevatedButton.styleFrom( minimumSize: const Size(double.infinity, 54), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ], ), if (_isAnalyzing) _buildLoadingOverlay(), if (_errorMessage != null) _buildErrorToast(), ], ), ); } 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), ), ), ), ), ); } // Removed _getStatusColor local method in favor of DetectionResult.getStatusColor() Widget _buildResultSummary() { final best = _detections!.first; return Container( padding: const EdgeInsets.all(16), margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [const BoxShadow(color: Colors.black12, blurRadius: 10)], ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_circle, color: best.getStatusColor(), size: 32), const SizedBox(width: 12), Text( best.className, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), ), ], ), Text( "Confidence: ${(best.confidence * 100).toStringAsFixed(1)}%", style: const TextStyle(fontSize: 14, color: Colors.grey), ), if (_isHealthAlert(best.className)) const Padding( padding: EdgeInsets.only(top: 8.0), child: Text( "HEALTH ALERT: Abnormal detected!", style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), ), ), if (_detections!.length > 1) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( "${_detections!.length} bunches detected", style: const TextStyle(fontSize: 13, color: Colors.grey), ), ), ], ), ); } Widget _buildLoadingOverlay() { return Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: Container( color: Colors.black.withOpacity(0.3), 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, letterSpacing: 1.2, ), ), Text( "Optimizing detection parameters", style: TextStyle(color: Colors.white70, fontSize: 14), ), ], ), ), ), ), ); } Widget _buildErrorToast() { return Positioned( bottom: 20, left: 20, right: 20, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.red.shade800, borderRadius: BorderRadius.circular(12), boxShadow: [const BoxShadow(color: Colors.black26, blurRadius: 8)], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.error_outline, color: Colors.white), SizedBox(width: 8), Text("Error Details", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 8), Text( _errorMessage!, style: const TextStyle(color: Colors.white, fontSize: 12), ), const SizedBox(height: 8), TextButton( onPressed: () => setState(() => _errorMessage = null), child: const Text("Dismiss", style: TextStyle(color: Colors.white)), ), ], ), ), ); } @override void dispose() { _scanningController.dispose(); _tfliteService.dispose(); super.dispose(); } } class _ScanningOverlay extends StatelessWidget { final Animation animation; const _ScanningOverlay({required this.animation}); @override Widget build(BuildContext context) { return AnimatedBuilder( animation: animation, builder: (context, child) { return Stack( children: [ Positioned( top: animation.value * MediaQuery.of(context).size.width, // Rough estimation since AspectRatio is 1.0 left: 0, right: 0, child: Container( height: 4, decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.transparent, Colors.greenAccent.withOpacity(0.8), Colors.transparent, ], ), boxShadow: [ BoxShadow( color: Colors.greenAccent.withOpacity(0.6), blurRadius: 15, spreadRadius: 2, ), ], ), ), ), ], ); }, ); } }