tane/apps/app_seeds/lib/services/seed_label_scan.dart
vjrj f1aa8f8e66 feat(app): scan a printed seed label back into its record
Closes the physical↔digital loop the labels opened: the QR on a printed
envelope (tane://seed, already encoded by seed_label_codec) can now be
scanned from the inventory bar. A known label opens its variety; an
unknown one asks before adding anything, then creates the variety (species
linked by exact scientific name when bundled) with a lot carrying the
label's year and origin. Scanner is pure ZXing (zxing_barcode_scanner,
MIT) — no ML Kit, no Play Services, F-Droid-safe, the stack Ğ1nkgo ships.
Mobile-only; other platforms don't show the button (OCR precedent).
The post-scan flow is a plain function (handleScannedPayload) so it's
widget-tested without a camera.
2026-07-18 12:26:27 +02:00

66 lines
2.4 KiB
Dart

import 'package:equatable/equatable.dart';
import '../data/export_import/seed_label_codec.dart';
import '../data/species_repository.dart';
import '../data/variety_repository.dart';
/// What scanning a QR yielded: not a seed label at all, a variety already in
/// the collection ([varietyId] set), or a seed not held yet ([data] set — the
/// UI asks before adding anything).
class SeedScanResult extends Equatable {
const SeedScanResult.notALabel() : varietyId = null, data = null;
const SeedScanResult.match(String this.varietyId) : data = null;
const SeedScanResult.unknown(SeedLabelData this.data) : varietyId = null;
final String? varietyId;
final SeedLabelData? data;
@override
List<Object?> get props => [varietyId, data];
}
/// Resolves a scanned QR payload against the collection: decodes the
/// `tane://seed` label and looks the variety up by its label (the identity a
/// keeper printed on the envelope). This is how a physical packet finds its
/// way back to its record — even a re-saved one whose digital thread broke.
Future<SeedScanResult> resolveScannedLabel(
VarietyRepository repository,
String raw,
) async {
final data = SeedLabelCodec.decode(raw);
if (data == null) return const SeedScanResult.notALabel();
final varietyId = await repository.findVarietyIdByLabel(data.varietyLabel);
return varietyId == null
? SeedScanResult.unknown(data)
: SeedScanResult.match(varietyId);
}
/// Adds the scanned seed to the collection — a variety named as the label
/// says, linked to the bundled species when the scientific name matches
/// exactly, with one lot carrying the label's harvest year and origin.
/// Called only after the person confirmed the add prompt.
Future<String> importScannedLabel({
required VarietyRepository repository,
required SpeciesRepository species,
required SeedLabelData data,
}) async {
final varietyId = await repository.addQuickVariety(label: data.varietyLabel);
final scientific = data.scientificName?.trim();
if (scientific != null && scientific.isNotEmpty) {
final matches = await species.search(scientific, limit: 4);
for (final m in matches) {
if (m.scientificName.toLowerCase() == scientific.toLowerCase()) {
await repository.linkSpecies(varietyId, m.id);
break;
}
}
}
await repository.addLot(
varietyId: varietyId,
harvestYear: data.year,
originName: data.origin,
);
return varietyId;
}