import 'dart:io'; import 'dart:math'; 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'; import '../constants/mpob_colors.dart'; import '../services/app_strings.dart'; import '../theme/locale_controller.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; // One batch groups every capture made during this screen visit. late final String _batchId = 'batch_${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(1 << 32)}'; @override void initState() { super.initState(); _initializeCamera(); } Future _initializeCamera() async { final status = await Permission.camera.request(); if (status.isDenied) { if (mounted) { setState(() => _errorMessage = AppStrings.t('camera_permission_denied')); } return; } final cameras = await availableCameras(); if (cameras.isEmpty) { if (mounted) { setState(() => _errorMessage = AppStrings.t('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 = AppStrings.tp('camera_init_error', {'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 stopwatch = Stopwatch()..start(); final detections = await _tfliteService.runInference(photo.path); stopwatch.stop(); 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(), batchId: _batchId, inferenceMs: stopwatch.elapsedMilliseconds, ); await _dbHelper.insertRecord(record); if (mounted) { setState(() { _detections = detections; _capturedPhoto = XFile(persistentPath); }); _showResultSheet(record); } } else { if (mounted) { setState(() => _errorMessage = AppStrings.t('static_no_detections')); } } } catch (e) { if (mounted) { setState(() => _errorMessage = AppStrings.tp('static_capture_error', {'error': '$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) { return ValueListenableBuilder( valueListenable: LocaleController.localeCode, builder: (context, _, _) => _buildScaffold(context), ); } Widget _buildScaffold(BuildContext context) { if (_errorMessage != null) { return Scaffold( appBar: AppBar(title: Text(AppStrings.t('static_appbar_title'))), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_errorMessage!, style: TextStyle(color: Theme.of(context).colorScheme.error)), const SizedBox(height: 16), ElevatedButton(onPressed: _initializeCamera, child: Text(AppStrings.t('retry_button'))), ], ), ), ); } 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( "${AppStrings.className(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.withValues(alpha: 0.5), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(color: Colors.white, strokeWidth: 6), const SizedBox(height: 24), Text( AppStrings.t('ai_analyzing'), style: const 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) { return ValueListenableBuilder( valueListenable: LocaleController.localeCode, builder: (context, _, _) => _buildSheet(context), ); } Widget _buildSheet(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final Color statusColor = getStatusColor(record.ripenessClass); final bool isWarning = record.ripenessClass == 'Unripe' || record.ripenessClass == 'Underripe'; return Container( decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: const 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: colorScheme.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2)), ), const SizedBox(height: 24), Row( children: [ Icon(Icons.analytics, color: statusColor, size: 32), const SizedBox(width: 12), Text(AppStrings.t('result_sheet_title'), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: colorScheme.onSurface)), ], ), const SizedBox(height: 24), _buildInfoRow(AppStrings.t('label_ripeness'), AppStrings.className(record.ripenessClass), statusColor, colorScheme), _buildInfoRow(AppStrings.t('label_confidence'), "${(record.confidence * 100).toStringAsFixed(1)}%", colorScheme.onSurface, colorScheme), if (record.ripenessClass == 'Empty_Bunch' || record.ripenessClass == 'Abnormal') _buildAlertCard(AppStrings.t('health_alert_title'), AppStrings.t('health_alert_message'), Colors.red), if (isWarning) _buildAlertCard(AppStrings.t('yield_warning_title'), AppStrings.t('yield_warning_message'), 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: Text(AppStrings.t('acknowledge_button'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ), const SizedBox(height: 12), ], ), ); } Widget _buildInfoRow(String label, String value, Color color, ColorScheme colorScheme) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: TextStyle(fontSize: 16, color: colorScheme.onSurface.withValues(alpha: 0.6))), 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.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withValues(alpha: 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.withValues(alpha: 0.8), fontSize: 12)), ], ), ), ], ), ); } }