history_screen.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import 'dart:io';
  2. import 'dart:ui';
  3. import 'package:flutter/material.dart';
  4. import 'package:path/path.dart' as p;
  5. import '../services/database_helper.dart';
  6. import '../models/palm_record.dart';
  7. /// Simple detection data parsed from the stored Map in the database.
  8. class _DetectionData {
  9. final String className;
  10. final double confidence;
  11. final Rect normalizedBox;
  12. const _DetectionData({
  13. required this.className,
  14. required this.confidence,
  15. required this.normalizedBox,
  16. });
  17. factory _DetectionData.fromMap(Map<String, dynamic> m) {
  18. return _DetectionData(
  19. className: (m['className'] ?? m['class'] ?? 'Unknown').toString(),
  20. confidence: (m['confidence'] as num?)?.toDouble() ?? 0.0,
  21. normalizedBox: Rect.fromLTRB(
  22. (m['x1'] as num?)?.toDouble() ?? 0.0,
  23. (m['y1'] as num?)?.toDouble() ?? 0.0,
  24. (m['x2'] as num?)?.toDouble() ?? 1.0,
  25. (m['y2'] as num?)?.toDouble() ?? 1.0,
  26. ),
  27. );
  28. }
  29. }
  30. class HistoryScreen extends StatefulWidget {
  31. const HistoryScreen({super.key});
  32. @override
  33. State<HistoryScreen> createState() => _HistoryScreenState();
  34. }
  35. class _HistoryScreenState extends State<HistoryScreen> {
  36. final DatabaseHelper _dbHelper = DatabaseHelper();
  37. List<PalmRecord> _records = [];
  38. bool _isLoading = true;
  39. @override
  40. void initState() {
  41. super.initState();
  42. _loadHistory();
  43. }
  44. Future<void> _loadHistory() async {
  45. final records = await _dbHelper.getAllRecords();
  46. setState(() {
  47. _records = records;
  48. _isLoading = false;
  49. });
  50. }
  51. Future<void> _resetHistory() async {
  52. final confirm = await showDialog<bool>(
  53. context: context,
  54. builder: (context) => AlertDialog(
  55. title: const Text("Reset History"),
  56. content: const Text("Are you sure you want to clear all analysis records? This action is irreversible."),
  57. actions: [
  58. TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")),
  59. TextButton(
  60. onPressed: () => Navigator.pop(context, true),
  61. child: const Text("Reset", style: TextStyle(color: Colors.red)),
  62. ),
  63. ],
  64. ),
  65. );
  66. if (confirm == true) {
  67. await _dbHelper.clearAllRecords();
  68. _loadHistory();
  69. if (mounted) {
  70. ScaffoldMessenger.of(context).showSnackBar(
  71. const SnackBar(content: Text("History cleared successfully.")),
  72. );
  73. }
  74. }
  75. }
  76. Color _getStatusColor(String label) {
  77. if (label == 'Empty_Bunch' || label == 'Abnormal') return Colors.red;
  78. if (label == 'Ripe' || label == 'Overripe') return Colors.green;
  79. return Colors.orange;
  80. }
  81. @override
  82. Widget build(BuildContext context) {
  83. return Scaffold(
  84. appBar: AppBar(
  85. title: const Text('History Vault'),
  86. actions: [
  87. if (_records.isNotEmpty)
  88. IconButton(
  89. icon: const Icon(Icons.delete_sweep),
  90. tooltip: 'Reset History',
  91. onPressed: _resetHistory,
  92. ),
  93. ],
  94. ),
  95. body: _isLoading
  96. ? const Center(child: CircularProgressIndicator())
  97. : _records.isEmpty
  98. ? const Center(child: Text("No records found yet."))
  99. : ListView.builder(
  100. itemCount: _records.length,
  101. padding: const EdgeInsets.all(12),
  102. itemBuilder: (context, index) {
  103. final record = _records[index];
  104. final statusColor = _getStatusColor(record.ripenessClass);
  105. final fileName = p.basename(record.imagePath);
  106. return Card(
  107. margin: const EdgeInsets.only(bottom: 12),
  108. elevation: 3,
  109. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
  110. child: ListTile(
  111. leading: CircleAvatar(
  112. backgroundColor: statusColor.withValues(alpha: 0.2),
  113. child: Icon(Icons.spa, color: statusColor),
  114. ),
  115. title: Text(
  116. record.ripenessClass,
  117. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
  118. ),
  119. subtitle: Text(
  120. "File: $fileName\nConf: ${(record.confidence * 100).toStringAsFixed(1)}% | ${record.timestamp.toString().split('.')[0]}",
  121. style: const TextStyle(fontSize: 12),
  122. ),
  123. trailing: const Icon(Icons.chevron_right),
  124. isThreeLine: true,
  125. onTap: () => _showDetails(record),
  126. ),
  127. );
  128. },
  129. ),
  130. );
  131. }
  132. void _showDetails(PalmRecord record) {
  133. final fileName = p.basename(record.imagePath);
  134. final List<_DetectionData> detections = record.detections
  135. .map((d) => _DetectionData.fromMap(d))
  136. .toList();
  137. showModalBottomSheet(
  138. context: context,
  139. isScrollControlled: true,
  140. shape: const RoundedRectangleBorder(
  141. borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
  142. ),
  143. builder: (context) {
  144. return DraggableScrollableSheet(
  145. initialChildSize: 0.7,
  146. minChildSize: 0.5,
  147. maxChildSize: 0.95,
  148. expand: false,
  149. builder: (context, scrollController) {
  150. return SingleChildScrollView(
  151. controller: scrollController,
  152. padding: const EdgeInsets.all(24),
  153. child: Column(
  154. crossAxisAlignment: CrossAxisAlignment.start,
  155. children: [
  156. Row(
  157. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  158. children: [
  159. const Text("Record Details", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
  160. IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close)),
  161. ],
  162. ),
  163. const Divider(),
  164. const SizedBox(height: 16),
  165. // Image with Bounding Boxes
  166. AspectRatio(
  167. aspectRatio: 1, // Assuming squared input 640x640
  168. child: Container(
  169. decoration: BoxDecoration(
  170. color: Colors.grey.shade200,
  171. borderRadius: BorderRadius.circular(12),
  172. ),
  173. child: record.imagePath.isNotEmpty && File(record.imagePath).existsSync()
  174. ? ClipRRect(
  175. borderRadius: BorderRadius.circular(12),
  176. child: Stack(
  177. children: [
  178. // The Image
  179. Positioned.fill(
  180. child: Image.file(File(record.imagePath), fit: BoxFit.contain),
  181. ),
  182. // The Overlays
  183. Positioned.fill(
  184. child: LayoutBuilder(
  185. builder: (context, constraints) {
  186. return Stack(
  187. children: detections.map<Widget>((d) {
  188. final rect = d.normalizedBox;
  189. final color = _getStatusColor(d.className);
  190. return Positioned(
  191. left: rect.left * constraints.maxWidth,
  192. top: rect.top * constraints.maxHeight,
  193. width: rect.width * constraints.maxWidth,
  194. height: rect.height * constraints.maxHeight,
  195. child: Container(
  196. decoration: BoxDecoration(
  197. border: Border.all(color: color, width: 2),
  198. borderRadius: BorderRadius.circular(4),
  199. ),
  200. ),
  201. );
  202. }).toList(),
  203. );
  204. },
  205. ),
  206. ),
  207. ],
  208. ),
  209. )
  210. : const Center(
  211. child: Icon(Icons.image, size: 64, color: Colors.grey),
  212. ),
  213. ),
  214. ),
  215. const SizedBox(height: 24),
  216. _buildDetailItem("File Name", fileName, Colors.blueGrey),
  217. _buildDetailItem("Primary Grade", record.ripenessClass, _getStatusColor(record.ripenessClass)),
  218. _buildDetailItem("Total Detections", detections.length.toString(), Colors.black),
  219. _buildDetailItem("Timestamp", record.timestamp.toString().split('.')[0], Colors.black87),
  220. const Divider(height: 32),
  221. const Text("Analytical Breakdown", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
  222. const SizedBox(height: 12),
  223. ...detections.map((d) => Padding(
  224. padding: const EdgeInsets.only(bottom: 8),
  225. child: Row(
  226. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  227. children: [
  228. Row(
  229. children: [
  230. Container(
  231. width: 12,
  232. height: 12,
  233. decoration: BoxDecoration(
  234. color: _getStatusColor(d.className),
  235. shape: BoxShape.circle,
  236. ),
  237. ),
  238. const SizedBox(width: 8),
  239. Text(d.className, style: const TextStyle(fontWeight: FontWeight.w500)),
  240. ],
  241. ),
  242. Text("${(d.confidence * 100).toStringAsFixed(1)}%", style: const TextStyle(color: Colors.grey)),
  243. ],
  244. ),
  245. )),
  246. const SizedBox(height: 24),
  247. const Text(
  248. "Industrial Summary: This image has been verified for harvest quality. All detected bunches are categorized according to ripeness standards.",
  249. style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
  250. ),
  251. const SizedBox(height: 24),
  252. ],
  253. ),
  254. );
  255. },
  256. );
  257. },
  258. );
  259. }
  260. Widget _buildDetailItem(String label, String value, Color valueColor) {
  261. return Padding(
  262. padding: const EdgeInsets.only(bottom: 12),
  263. child: Row(
  264. crossAxisAlignment: CrossAxisAlignment.start,
  265. children: [
  266. Text("$label: ", style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
  267. Expanded(
  268. child: Text(value, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: valueColor)),
  269. ),
  270. ],
  271. ),
  272. );
  273. }
  274. }