import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; class FeedbackEntry { const FeedbackEntry({ required this.id, required this.accurate, required this.createdAt, required this.photoPath, required this.cropPath, this.matchCardId, this.matchScore, this.matchDistance, this.matchFilename, this.matchConfident, }); final String id; final bool accurate; final DateTime createdAt; final String photoPath; final String cropPath; final String? matchCardId; final double? matchScore; final int? matchDistance; final String? matchFilename; final bool? matchConfident; String? get matchAssetPath => matchFilename == null ? null : 'test_assets/$matchFilename'; Map toJson() => { 'id': id, 'accurate': accurate, 'createdAt': createdAt.toIso8601String(), 'photoFile': p.basename(photoPath), 'cropFile': p.basename(cropPath), 'matchCardId': matchCardId, 'matchScore': matchScore, 'matchDistance': matchDistance, 'matchFilename': matchFilename, 'matchConfident': matchConfident, }; static FeedbackEntry fromJson(Map json, String dir) { return FeedbackEntry( id: json['id'] as String, accurate: json['accurate'] as bool? ?? false, createdAt: DateTime.tryParse(json['createdAt'] as String? ?? '') ?? DateTime.fromMillisecondsSinceEpoch(0), photoPath: p.join(dir, json['photoFile'] as String), cropPath: p.join(dir, json['cropFile'] as String), matchCardId: json['matchCardId'] as String?, matchScore: (json['matchScore'] as num?)?.toDouble(), matchDistance: json['matchDistance'] as int?, matchFilename: json['matchFilename'] as String?, matchConfident: json['matchConfident'] as bool?, ); } } /// Persists scan feedback under app documents (`feedback/`). class FeedbackStore { FeedbackStore._(); static final FeedbackStore instance = FeedbackStore._(); Future _dir() async { final root = await getApplicationDocumentsDirectory(); final dir = Directory(p.join(root.path, 'feedback')); if (!await dir.exists()) { await dir.create(recursive: true); } return dir; } Future _indexFile() async { final dir = await _dir(); return File(p.join(dir.path, 'index.json')); } Future> list() async { final dir = await _dir(); final index = await _indexFile(); if (!await index.exists()) return const []; try { final raw = jsonDecode(await index.readAsString()); if (raw is! List) return const []; final items = raw .whereType() .map((e) => FeedbackEntry.fromJson(Map.from(e), dir.path)) .toList(); items.sort((a, b) => b.createdAt.compareTo(a.createdAt)); return items; } catch (_) { return const []; } } Future add({ required bool accurate, required String photoSourcePath, required Uint8List cropBytes, String? matchCardId, double? matchScore, int? matchDistance, String? matchFilename, bool? matchConfident, }) async { final dir = await _dir(); final id = DateTime.now().millisecondsSinceEpoch.toString(); final photoPath = p.join(dir.path, '${id}_photo.jpg'); final cropPath = p.join(dir.path, '${id}_crop.jpg'); await File(photoSourcePath).copy(photoPath); await File(cropPath).writeAsBytes(cropBytes, flush: true); final entry = FeedbackEntry( id: id, accurate: accurate, createdAt: DateTime.now(), photoPath: photoPath, cropPath: cropPath, matchCardId: matchCardId, matchScore: matchScore, matchDistance: matchDistance, matchFilename: matchFilename, matchConfident: matchConfident, ); final existing = await list(); final next = [entry, ...existing]; final index = await _indexFile(); await index.writeAsString( const JsonEncoder.withIndent(' ').convert( next.map((e) => e.toJson()).toList(), ), flush: true, ); return entry; } Future delete(String id) async { final dir = await _dir(); final existing = await list(); final keep = existing.where((e) => e.id != id).toList(); // Remove files for this entry. for (final suffix in ['_photo.jpg', '_crop.jpg']) { final f = File(p.join(dir.path, '$id$suffix')); try { await f.delete(); } catch (_) {} } final index = await _indexFile(); if (keep.isEmpty) { try { await index.delete(); } catch (_) {} return; } await index.writeAsString( const JsonEncoder.withIndent(' ').convert( keep.map((e) => e.toJson()).toList(), ), flush: true, ); } Future clearAll() async { final dir = await _dir(); if (await dir.exists()) { await dir.delete(recursive: true); } } }