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.
This commit is contained in:
vjrj 2026-07-18 12:26:27 +02:00
parent bb5b3d4a43
commit f1aa8f8e66
18 changed files with 1130 additions and 42 deletions

View file

@ -69,6 +69,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$abundance$es abundance = _Translations$abundance$es._(_root);
@override late final _Translations$share$es share = _Translations$share$es._(_root);
@override late final _Translations$printLabels$es printLabels = _Translations$printLabels$es._(_root);
@override late final _Translations$scan$es scan = _Translations$scan$es._(_root);
@override late final _Translations$cropCalendar$es cropCalendar = _Translations$cropCalendar$es._(_root);
@override late final _Translations$needsReproduction$es needsReproduction = _Translations$needsReproduction$es._(_root);
@override late final _Translations$preservation$es preservation = _Translations$preservation$es._(_root);
@ -650,6 +651,22 @@ class _Translations$printLabels$es extends Translations$printLabels$en {
@override String get cancelled => 'Cancelado';
}
// Path: scan
class _Translations$scan$es extends Translations$scan$en {
_Translations$scan$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get action => 'Escanear una etiqueta';
@override String get title => 'Escanear etiqueta';
@override String get notALabel => 'Ese código no es una etiqueta de semillas';
@override String get addTitle => 'No está en tu colección';
@override String addBody({required Object label}) => '¿Añadir «${label}» a tus semillas?';
@override String get add => 'Añadirla';
@override String get added => 'Añadida a tu colección';
}
// Path: cropCalendar
class _Translations$cropCalendar$es extends Translations$cropCalendar$en {
_Translations$cropCalendar$es._(TranslationsEs root) : this._root = root, super.internal(root);
@ -1795,6 +1812,13 @@ extension on TranslationsEs {
'printLabels.save' => 'Guardar etiquetas',
'printLabels.saved' => 'Etiquetas guardadas',
'printLabels.cancelled' => 'Cancelado',
'scan.action' => 'Escanear una etiqueta',
'scan.title' => 'Escanear etiqueta',
'scan.notALabel' => 'Ese código no es una etiqueta de semillas',
'scan.addTitle' => 'No está en tu colección',
'scan.addBody' => ({required Object label}) => '¿Añadir «${label}» a tus semillas?',
'scan.add' => 'Añadirla',
'scan.added' => 'Añadida a tu colección',
'cropCalendar.add' => 'Calendario de cultivo',
'cropCalendar.title' => 'Calendario de cultivo',
'cropCalendar.sow' => 'Siembra',
@ -2006,6 +2030,8 @@ extension on TranslationsEs {
'plantare.openSection' => 'Pendientes',
'plantare.settledSection' => 'Hechos',
'plantare.removeConfirm' => '¿Quitar este compromiso?',
_ => null,
} ?? switch (path) {
'plantare.returnBy' => ({required Object date}) => 'Devolver antes del ${date}',
'plantare.overdue' => 'vencido',
'plantare.dueByLabel' => 'Devolver antes de (opcional)',
@ -2013,8 +2039,6 @@ extension on TranslationsEs {
'plantare.pickDate' => 'Elegir fecha',
'plantare.clearDate' => 'Quitar fecha',
'plantare.sectionTitle' => 'Compromisos',
_ => null,
} ?? switch (path) {
'plantare.propose' => 'Proponer un Plantaré firmado',
'plantare.proposeHelp' => 'Los dos guardáis la misma promesa, firmada por ambos: prueba de que esta semilla cambió de manos y se cultivará y devolverá.',
'plantare.proposeTo' => ({required Object name}) => 'Con ${name}',