|
|
@@ -26,6 +26,15 @@ class _BatchGroup {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/// A single record paired with the batch group it belongs to, so a flat
|
|
|
+/// (ungrouped) list item can still open [RecordDetailScreen] with the full
|
|
|
+/// batch as navigable frames.
|
|
|
+class _FlatRecord {
|
|
|
+ final PalmRecord record;
|
|
|
+ final _BatchGroup group;
|
|
|
+ _FlatRecord({required this.record, required this.group});
|
|
|
+}
|
|
|
+
|
|
|
class HistoryScreen extends StatefulWidget {
|
|
|
const HistoryScreen({super.key});
|
|
|
|
|
|
@@ -41,6 +50,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|
|
|
|
|
bool _sortAscending = false; // false = latest first (default)
|
|
|
DateTimeRange? _dateRange;
|
|
|
+ bool _flatView = false;
|
|
|
|
|
|
@override
|
|
|
void initState() {
|
|
|
@@ -146,6 +156,22 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|
|
_loadHistory();
|
|
|
}
|
|
|
|
|
|
+ List<_FlatRecord> get _flatRecords {
|
|
|
+ final all = <_FlatRecord>[
|
|
|
+ for (final g in _groups)
|
|
|
+ for (final r in g.records) _FlatRecord(record: r, group: g),
|
|
|
+ ];
|
|
|
+ all.sort((a, b) => _sortAscending
|
|
|
+ ? a.record.timestamp.compareTo(b.record.timestamp)
|
|
|
+ : b.record.timestamp.compareTo(a.record.timestamp));
|
|
|
+ return all;
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _deleteSingleRecord(PalmRecord record) async {
|
|
|
+ await _dbHelper.deleteRecord(record);
|
|
|
+ _loadHistory();
|
|
|
+ }
|
|
|
+
|
|
|
bool get _hasActiveFilter => _dateRange != null || _sortAscending;
|
|
|
|
|
|
Future<void> _openFilterSheet() async {
|
|
|
@@ -277,6 +303,11 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|
|
appBar: AppBar(
|
|
|
title: Text(AppStrings.t('history_vault_title')),
|
|
|
actions: [
|
|
|
+ IconButton(
|
|
|
+ icon: Icon(_flatView ? Icons.collections_bookmark : Icons.view_list),
|
|
|
+ tooltip: _flatView ? AppStrings.t('view_toggle_batches_tooltip') : AppStrings.t('view_toggle_flat_tooltip'),
|
|
|
+ onPressed: () => setState(() => _flatView = !_flatView),
|
|
|
+ ),
|
|
|
IconButton(
|
|
|
icon: Icon(_hasActiveFilter ? Icons.filter_alt : Icons.filter_list),
|
|
|
tooltip: AppStrings.t('filter_sort_title'),
|
|
|
@@ -294,11 +325,67 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|
|
? const Center(child: CircularProgressIndicator())
|
|
|
: _groups.isEmpty
|
|
|
? _buildEmptyState()
|
|
|
- : ListView.builder(
|
|
|
- itemCount: _groups.length,
|
|
|
- padding: const EdgeInsets.all(12),
|
|
|
- itemBuilder: (context, index) => _buildGroupCard(_groups[index]),
|
|
|
- ),
|
|
|
+ : _flatView
|
|
|
+ ? _buildFlatList()
|
|
|
+ : ListView.builder(
|
|
|
+ itemCount: _groups.length,
|
|
|
+ padding: const EdgeInsets.all(12),
|
|
|
+ itemBuilder: (context, index) => _buildGroupCard(_groups[index]),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _buildFlatList() {
|
|
|
+ final flat = _flatRecords;
|
|
|
+ return ListView.builder(
|
|
|
+ itemCount: flat.length,
|
|
|
+ padding: const EdgeInsets.all(12),
|
|
|
+ itemBuilder: (context, index) => _buildFlatTile(flat[index]),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _buildFlatTile(_FlatRecord entry) {
|
|
|
+ final record = entry.record;
|
|
|
+ final statusColor = getStatusColor(record.ripenessClass);
|
|
|
+ return Dismissible(
|
|
|
+ key: ValueKey('flat_${record.id}'),
|
|
|
+ direction: DismissDirection.endToStart,
|
|
|
+ background: _buildDeleteBackground(),
|
|
|
+ confirmDismiss: (_) => _confirmDelete(AppStrings.t('history_delete_record_confirm')),
|
|
|
+ onDismissed: (_) => _deleteSingleRecord(record),
|
|
|
+ child: 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(
|
|
|
+ AppStrings.className(record.ripenessClass),
|
|
|
+ style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
|
|
+ ),
|
|
|
+ subtitle: Text(
|
|
|
+ AppStrings.tp('record_conf_timestamp', {
|
|
|
+ 'percent': (record.confidence * 100).toStringAsFixed(1),
|
|
|
+ 'timestamp': record.timestamp.toString().split('.')[0],
|
|
|
+ }),
|
|
|
+ style: const TextStyle(fontSize: 12),
|
|
|
+ ),
|
|
|
+ trailing: const Icon(Icons.chevron_right),
|
|
|
+ isThreeLine: true,
|
|
|
+ onTap: () => Navigator.push(
|
|
|
+ context,
|
|
|
+ MaterialPageRoute(
|
|
|
+ builder: (context) => RecordDetailScreen(
|
|
|
+ frames: entry.group.records,
|
|
|
+ initialIndex: entry.group.records.indexOf(record),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
);
|
|
|
}
|
|
|
|