| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- 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';
- import '../widgets/palm_bounding_box.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<String, dynamic> 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<HistoryScreen> createState() => _HistoryScreenState();
- }
- class _HistoryScreenState extends State<HistoryScreen> {
- final DatabaseHelper _dbHelper = DatabaseHelper();
- List<PalmRecord> _records = [];
- bool _isLoading = true;
- @override
- void initState() {
- super.initState();
- _loadHistory();
- }
- Future<void> _loadHistory() async {
- final records = await _dbHelper.getAllRecords();
- setState(() {
- _records = records;
- _isLoading = false;
- });
- }
- Future<void> _resetHistory() async {
- final confirm = await showDialog<bool>(
- 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.withOpacity(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<Widget>((d) {
- return PalmBoundingBox(
- normalizedRect: d.normalizedBox,
- label: d.className,
- confidence: d.confidence,
- constraints: constraints,
- );
- }).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)),
- ),
- ],
- ),
- );
- }
- }
|