import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import '../services/database_helper.dart'; import '../models/palm_record.dart'; /// Simple detection data parsed from the stored Map in the database. class _DetectionData { final String className; final double confidence; final Rect normalizedBox; const _DetectionData({ required this.className, required this.confidence, required this.normalizedBox, }); factory _DetectionData.fromMap(Map m) { return _DetectionData( className: (m['className'] ?? m['class'] ?? 'Unknown').toString(), confidence: (m['confidence'] as num?)?.toDouble() ?? 0.0, normalizedBox: Rect.fromLTRB( (m['x1'] as num?)?.toDouble() ?? 0.0, (m['y1'] as num?)?.toDouble() ?? 0.0, (m['x2'] as num?)?.toDouble() ?? 1.0, (m['y2'] as num?)?.toDouble() ?? 1.0, ), ); } } class HistoryScreen extends StatefulWidget { const HistoryScreen({super.key}); @override State createState() => _HistoryScreenState(); } class _HistoryScreenState extends State { final DatabaseHelper _dbHelper = DatabaseHelper(); List _records = []; bool _isLoading = true; @override void initState() { super.initState(); _loadHistory(); } Future _loadHistory() async { final records = await _dbHelper.getAllRecords(); setState(() { _records = records; _isLoading = false; }); } Future _resetHistory() async { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("Reset History"), content: const Text("Are you sure you want to clear all analysis records? This action is irreversible."), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text("Reset", style: TextStyle(color: Colors.red)), ), ], ), ); if (confirm == true) { await _dbHelper.clearAllRecords(); _loadHistory(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("History cleared successfully.")), ); } } } Color _getStatusColor(String label) { if (label == 'Empty_Bunch' || label == 'Abnormal') return Colors.red; if (label == 'Ripe' || label == 'Overripe') return Colors.green; return Colors.orange; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('History Vault'), actions: [ if (_records.isNotEmpty) IconButton( icon: const Icon(Icons.delete_sweep), tooltip: 'Reset History', onPressed: _resetHistory, ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _records.isEmpty ? const Center(child: Text("No records found yet.")) : ListView.builder( itemCount: _records.length, padding: const EdgeInsets.all(12), itemBuilder: (context, index) { final record = _records[index]; final statusColor = _getStatusColor(record.ripenessClass); final fileName = p.basename(record.imagePath); return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 3, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), child: ListTile( leading: CircleAvatar( backgroundColor: statusColor.withValues(alpha: 0.2), child: Icon(Icons.spa, color: statusColor), ), title: Text( record.ripenessClass, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Text( "File: $fileName\nConf: ${(record.confidence * 100).toStringAsFixed(1)}% | ${record.timestamp.toString().split('.')[0]}", style: const TextStyle(fontSize: 12), ), trailing: const Icon(Icons.chevron_right), isThreeLine: true, onTap: () => _showDetails(record), ), ); }, ), ); } void _showDetails(PalmRecord record) { final fileName = p.basename(record.imagePath); final List<_DetectionData> detections = record.detections .map((d) => _DetectionData.fromMap(d)) .toList(); showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) { return DraggableScrollableSheet( initialChildSize: 0.7, minChildSize: 0.5, maxChildSize: 0.95, expand: false, builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text("Record Details", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close)), ], ), const Divider(), const SizedBox(height: 16), // Image with Bounding Boxes AspectRatio( aspectRatio: 1, // Assuming squared input 640x640 child: Container( decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12), ), child: record.imagePath.isNotEmpty && File(record.imagePath).existsSync() ? ClipRRect( borderRadius: BorderRadius.circular(12), child: Stack( children: [ // The Image Positioned.fill( child: Image.file(File(record.imagePath), fit: BoxFit.contain), ), // The Overlays Positioned.fill( child: LayoutBuilder( builder: (context, constraints) { return Stack( children: detections.map((d) { final rect = d.normalizedBox; final color = _getStatusColor(d.className); 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: 2), borderRadius: BorderRadius.circular(4), ), ), ); }).toList(), ); }, ), ), ], ), ) : const Center( child: Icon(Icons.image, size: 64, color: Colors.grey), ), ), ), const SizedBox(height: 24), _buildDetailItem("File Name", fileName, Colors.blueGrey), _buildDetailItem("Primary Grade", record.ripenessClass, _getStatusColor(record.ripenessClass)), _buildDetailItem("Total Detections", detections.length.toString(), Colors.black), _buildDetailItem("Timestamp", record.timestamp.toString().split('.')[0], Colors.black87), const Divider(height: 32), const Text("Analytical Breakdown", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 12), ...detections.map((d) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: _getStatusColor(d.className), shape: BoxShape.circle, ), ), const SizedBox(width: 8), Text(d.className, style: const TextStyle(fontWeight: FontWeight.w500)), ], ), Text("${(d.confidence * 100).toStringAsFixed(1)}%", style: const TextStyle(color: Colors.grey)), ], ), )), const SizedBox(height: 24), const Text( "Industrial Summary: This image has been verified for harvest quality. All detected bunches are categorized according to ripeness standards.", style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey), ), const SizedBox(height: 24), ], ), ); }, ); }, ); } Widget _buildDetailItem(String label, String value, Color valueColor) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("$label: ", style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)), Expanded( child: Text(value, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: valueColor)), ), ], ), ); } }