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:
parent
bb5b3d4a43
commit
f1aa8f8e66
18 changed files with 1130 additions and 42 deletions
|
|
@ -368,6 +368,15 @@
|
|||
"saved": "Labels saved",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"scan": {
|
||||
"action": "Scan a seed label",
|
||||
"title": "Scan a label",
|
||||
"notALabel": "That code is not a seed label",
|
||||
"addTitle": "Not in your collection",
|
||||
"addBody": "Add “{label}” to your seeds?",
|
||||
"add": "Add it",
|
||||
"added": "Added to your collection"
|
||||
},
|
||||
"cropCalendar": {
|
||||
"add": "Crop calendar",
|
||||
"title": "Crop calendar",
|
||||
|
|
|
|||
|
|
@ -367,6 +367,15 @@
|
|||
"saved": "Etiquetas guardadas",
|
||||
"cancelled": "Cancelado"
|
||||
},
|
||||
"scan": {
|
||||
"action": "Escanear una etiqueta",
|
||||
"title": "Escanear etiqueta",
|
||||
"notALabel": "Ese código no es una etiqueta de semillas",
|
||||
"addTitle": "No está en tu colección",
|
||||
"addBody": "¿Añadir «{label}» a tus semillas?",
|
||||
"add": "Añadirla",
|
||||
"added": "Añadida a tu colección"
|
||||
},
|
||||
"cropCalendar": {
|
||||
"add": "Calendario de cultivo",
|
||||
"title": "Calendario de cultivo",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 7
|
||||
/// Strings: 3550 (507 per locale)
|
||||
/// Strings: 3564 (509 per locale)
|
||||
///
|
||||
/// Built on 2026-07-18 at 10:13 UTC
|
||||
/// Built on 2026-07-18 at 10:23 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||
late final Translations$abundance$en abundance = Translations$abundance$en.internal(_root);
|
||||
late final Translations$share$en share = Translations$share$en.internal(_root);
|
||||
late final Translations$printLabels$en printLabels = Translations$printLabels$en.internal(_root);
|
||||
late final Translations$scan$en scan = Translations$scan$en.internal(_root);
|
||||
late final Translations$cropCalendar$en cropCalendar = Translations$cropCalendar$en.internal(_root);
|
||||
late final Translations$needsReproduction$en needsReproduction = Translations$needsReproduction$en.internal(_root);
|
||||
late final Translations$preservation$en preservation = Translations$preservation$en.internal(_root);
|
||||
|
|
@ -1200,6 +1201,36 @@ class Translations$printLabels$en {
|
|||
String get cancelled => 'Cancelled';
|
||||
}
|
||||
|
||||
// Path: scan
|
||||
class Translations$scan$en {
|
||||
Translations$scan$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Scan a seed label'
|
||||
String get action => 'Scan a seed label';
|
||||
|
||||
/// en: 'Scan a label'
|
||||
String get title => 'Scan a label';
|
||||
|
||||
/// en: 'That code is not a seed label'
|
||||
String get notALabel => 'That code is not a seed label';
|
||||
|
||||
/// en: 'Not in your collection'
|
||||
String get addTitle => 'Not in your collection';
|
||||
|
||||
/// en: 'Add “{label}” to your seeds?'
|
||||
String addBody({required Object label}) => 'Add “${label}” to your seeds?';
|
||||
|
||||
/// en: 'Add it'
|
||||
String get add => 'Add it';
|
||||
|
||||
/// en: 'Added to your collection'
|
||||
String get added => 'Added to your collection';
|
||||
}
|
||||
|
||||
// Path: cropCalendar
|
||||
class Translations$cropCalendar$en {
|
||||
Translations$cropCalendar$en.internal(this._root);
|
||||
|
|
@ -3039,6 +3070,13 @@ extension on Translations {
|
|||
'printLabels.save' => 'Save labels',
|
||||
'printLabels.saved' => 'Labels saved',
|
||||
'printLabels.cancelled' => 'Cancelled',
|
||||
'scan.action' => 'Scan a seed label',
|
||||
'scan.title' => 'Scan a label',
|
||||
'scan.notALabel' => 'That code is not a seed label',
|
||||
'scan.addTitle' => 'Not in your collection',
|
||||
'scan.addBody' => ({required Object label}) => 'Add “${label}” to your seeds?',
|
||||
'scan.add' => 'Add it',
|
||||
'scan.added' => 'Added to your collection',
|
||||
'cropCalendar.add' => 'Crop calendar',
|
||||
'cropCalendar.title' => 'Crop calendar',
|
||||
'cropCalendar.sow' => 'Sow',
|
||||
|
|
@ -3249,6 +3287,8 @@ extension on Translations {
|
|||
'plantare.statusForgiven' => 'Settled',
|
||||
'plantare.openSection' => 'Open',
|
||||
'plantare.settledSection' => 'Done',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'plantare.removeConfirm' => 'Remove this commitment?',
|
||||
'plantare.returnBy' => ({required Object date}) => 'Return by ${date}',
|
||||
'plantare.overdue' => 'overdue',
|
||||
|
|
@ -3256,8 +3296,6 @@ extension on Translations {
|
|||
'plantare.dueByHint' => 'A gentle reminder, never enforced',
|
||||
'plantare.pickDate' => 'Pick a date',
|
||||
'plantare.clearDate' => 'Clear date',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'plantare.sectionTitle' => 'Commitments',
|
||||
'plantare.propose' => 'Propose a signed Plantaré',
|
||||
'plantare.proposeHelp' => 'Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.',
|
||||
|
|
|
|||
|
|
@ -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}',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue