card-recog-demo/lib/services/feedback_store.dart
2026-07-14 22:51:07 -07:00

175 lines
5.0 KiB
Dart

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<String, dynamic> 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<String, dynamic> 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<Directory> _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<File> _indexFile() async {
final dir = await _dir();
return File(p.join(dir.path, 'index.json'));
}
Future<List<FeedbackEntry>> 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>()
.map((e) => FeedbackEntry.fromJson(Map<String, dynamic>.from(e), dir.path))
.toList();
items.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return items;
} catch (_) {
return const [];
}
}
Future<FeedbackEntry> 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<void> 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<void> clearAll() async {
final dir = await _dir();
if (await dir.exists()) {
await dir.delete(recursive: true);
}
}
}