feat(labels): print seed labels with QR from an inventory selection
Add multi-select to the inventory and a "Print labels" flow that renders a PDF sheet of labels (small stickers or large cards), each carrying the seed's common name, variety, scientific name, year/origin and a tanemaki://seed QR that holds the seed's data offline (future scan-to-import). - SeedLabelCodec: compact, versioned tanemaki://seed URI (encode/decode) - LabelSheetService: pure-Dart PDF grid (pdf + barcode + DejaVu), RTL-aware - VarietyRepository.labelRows: one row per lot, locale-picked common name - InventoryCubit selection mode; contextual app bar + label print sheet - i18n en/es/pt (printLabels.*) Tests: codec, PDF service (both formats + RTL), labelRows, selection cubit, inventory widget selection.
This commit is contained in:
parent
432acded29
commit
efb369eb81
19 changed files with 1465 additions and 116 deletions
|
|
@ -262,6 +262,23 @@
|
|||
"catalogSaved": "Catalog saved",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"printLabels": {
|
||||
"action": "Print labels",
|
||||
"title": "Print labels",
|
||||
"selectHint": "Pick the seeds to print labels for",
|
||||
"selectAll": "Select all",
|
||||
"selected": "{n} selected",
|
||||
"none": "Select seeds first",
|
||||
"format": "Label size",
|
||||
"formatStickers": "Small stickers",
|
||||
"formatStickersHint": "Many small labels per page — to peel onto packets",
|
||||
"formatCards": "Large cards",
|
||||
"formatCardsHint": "Fewer, bigger labels — for jars and boxes",
|
||||
"count": "{n} labels",
|
||||
"save": "Save labels",
|
||||
"saved": "Labels saved",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"cropCalendar": {
|
||||
"add": "Crop calendar",
|
||||
"title": "Crop calendar",
|
||||
|
|
|
|||
|
|
@ -262,6 +262,23 @@
|
|||
"catalogSaved": "Catálogo guardado",
|
||||
"cancelled": "Cancelado"
|
||||
},
|
||||
"printLabels": {
|
||||
"action": "Imprimir etiquetas",
|
||||
"title": "Imprimir etiquetas",
|
||||
"selectHint": "Elige las semillas para imprimir sus etiquetas",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"selected": "{n} seleccionadas",
|
||||
"none": "Selecciona semillas primero",
|
||||
"format": "Tamaño de etiqueta",
|
||||
"formatStickers": "Pegatinas pequeñas",
|
||||
"formatStickersHint": "Muchas etiquetas pequeñas por hoja — para pegar en sobres",
|
||||
"formatCards": "Tarjetas grandes",
|
||||
"formatCardsHint": "Menos etiquetas, más grandes — para botes y cajas",
|
||||
"count": "{n} etiquetas",
|
||||
"save": "Guardar etiquetas",
|
||||
"saved": "Etiquetas guardadas",
|
||||
"cancelled": "Cancelado"
|
||||
},
|
||||
"cropCalendar": {
|
||||
"add": "Calendario de cultivo",
|
||||
"title": "Calendario de cultivo",
|
||||
|
|
|
|||
|
|
@ -258,6 +258,23 @@
|
|||
"catalogSaved": "Catálogo guardado",
|
||||
"cancelled": "Cancelado"
|
||||
},
|
||||
"printLabels": {
|
||||
"action": "Imprimir etiquetas",
|
||||
"title": "Imprimir etiquetas",
|
||||
"selectHint": "Escolhe as sementes para imprimir as etiquetas",
|
||||
"selectAll": "Selecionar tudo",
|
||||
"selected": "{n} selecionadas",
|
||||
"none": "Seleciona sementes primeiro",
|
||||
"format": "Tamanho da etiqueta",
|
||||
"formatStickers": "Autocolantes pequenos",
|
||||
"formatStickersHint": "Muitas etiquetas pequenas por folha — para colar em envelopes",
|
||||
"formatCards": "Cartões grandes",
|
||||
"formatCardsHint": "Menos etiquetas, maiores — para frascos e caixas",
|
||||
"count": "{n} etiquetas",
|
||||
"save": "Guardar etiquetas",
|
||||
"saved": "Etiquetas guardadas",
|
||||
"cancelled": "Cancelado"
|
||||
},
|
||||
"cropCalendar": {
|
||||
"add": "Calendário de cultivo",
|
||||
"title": "Calendário de cultivo",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 3
|
||||
/// Strings: 998 (332 per locale)
|
||||
/// Strings: 1043 (347 per locale)
|
||||
///
|
||||
/// Built on 2026-07-10 at 10:26 UTC
|
||||
/// Built on 2026-07-10 at 15:14 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||
late final Translations$provenance$en provenance = Translations$provenance$en.internal(_root);
|
||||
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$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);
|
||||
|
|
@ -872,6 +873,60 @@ class Translations$share$en {
|
|||
String get cancelled => 'Cancelled';
|
||||
}
|
||||
|
||||
// Path: printLabels
|
||||
class Translations$printLabels$en {
|
||||
Translations$printLabels$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Print labels'
|
||||
String get action => 'Print labels';
|
||||
|
||||
/// en: 'Print labels'
|
||||
String get title => 'Print labels';
|
||||
|
||||
/// en: 'Pick the seeds to print labels for'
|
||||
String get selectHint => 'Pick the seeds to print labels for';
|
||||
|
||||
/// en: 'Select all'
|
||||
String get selectAll => 'Select all';
|
||||
|
||||
/// en: '{n} selected'
|
||||
String selected({required Object n}) => '${n} selected';
|
||||
|
||||
/// en: 'Select seeds first'
|
||||
String get none => 'Select seeds first';
|
||||
|
||||
/// en: 'Label size'
|
||||
String get format => 'Label size';
|
||||
|
||||
/// en: 'Small stickers'
|
||||
String get formatStickers => 'Small stickers';
|
||||
|
||||
/// en: 'Many small labels per page — to peel onto packets'
|
||||
String get formatStickersHint => 'Many small labels per page — to peel onto packets';
|
||||
|
||||
/// en: 'Large cards'
|
||||
String get formatCards => 'Large cards';
|
||||
|
||||
/// en: 'Fewer, bigger labels — for jars and boxes'
|
||||
String get formatCardsHint => 'Fewer, bigger labels — for jars and boxes';
|
||||
|
||||
/// en: '{n} labels'
|
||||
String count({required Object n}) => '${n} labels';
|
||||
|
||||
/// en: 'Save labels'
|
||||
String get save => 'Save labels';
|
||||
|
||||
/// en: 'Labels saved'
|
||||
String get saved => 'Labels saved';
|
||||
|
||||
/// en: 'Cancelled'
|
||||
String get cancelled => 'Cancelled';
|
||||
}
|
||||
|
||||
// Path: cropCalendar
|
||||
class Translations$cropCalendar$en {
|
||||
Translations$cropCalendar$en.internal(this._root);
|
||||
|
|
@ -1876,6 +1931,21 @@ extension on Translations {
|
|||
'share.catalogTitle' => 'What I share',
|
||||
'share.catalogSaved' => 'Catalog saved',
|
||||
'share.cancelled' => 'Cancelled',
|
||||
'printLabels.action' => 'Print labels',
|
||||
'printLabels.title' => 'Print labels',
|
||||
'printLabels.selectHint' => 'Pick the seeds to print labels for',
|
||||
'printLabels.selectAll' => 'Select all',
|
||||
'printLabels.selected' => ({required Object n}) => '${n} selected',
|
||||
'printLabels.none' => 'Select seeds first',
|
||||
'printLabels.format' => 'Label size',
|
||||
'printLabels.formatStickers' => 'Small stickers',
|
||||
'printLabels.formatStickersHint' => 'Many small labels per page — to peel onto packets',
|
||||
'printLabels.formatCards' => 'Large cards',
|
||||
'printLabels.formatCardsHint' => 'Fewer, bigger labels — for jars and boxes',
|
||||
'printLabels.count' => ({required Object n}) => '${n} labels',
|
||||
'printLabels.save' => 'Save labels',
|
||||
'printLabels.saved' => 'Labels saved',
|
||||
'printLabels.cancelled' => 'Cancelled',
|
||||
'cropCalendar.add' => 'Crop calendar',
|
||||
'cropCalendar.title' => 'Crop calendar',
|
||||
'cropCalendar.sow' => 'Sow',
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@override late final _Translations$provenance$es provenance = _Translations$provenance$es._(_root);
|
||||
@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$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);
|
||||
|
|
@ -487,6 +488,30 @@ class _Translations$share$es extends Translations$share$en {
|
|||
@override String get cancelled => 'Cancelado';
|
||||
}
|
||||
|
||||
// Path: printLabels
|
||||
class _Translations$printLabels$es extends Translations$printLabels$en {
|
||||
_Translations$printLabels$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get action => 'Imprimir etiquetas';
|
||||
@override String get title => 'Imprimir etiquetas';
|
||||
@override String get selectHint => 'Elige las semillas para imprimir sus etiquetas';
|
||||
@override String get selectAll => 'Seleccionar todo';
|
||||
@override String selected({required Object n}) => '${n} seleccionadas';
|
||||
@override String get none => 'Selecciona semillas primero';
|
||||
@override String get format => 'Tamaño de etiqueta';
|
||||
@override String get formatStickers => 'Pegatinas pequeñas';
|
||||
@override String get formatStickersHint => 'Muchas etiquetas pequeñas por hoja — para pegar en sobres';
|
||||
@override String get formatCards => 'Tarjetas grandes';
|
||||
@override String get formatCardsHint => 'Menos etiquetas, más grandes — para botes y cajas';
|
||||
@override String count({required Object n}) => '${n} etiquetas';
|
||||
@override String get save => 'Guardar etiquetas';
|
||||
@override String get saved => 'Etiquetas guardadas';
|
||||
@override String get cancelled => 'Cancelado';
|
||||
}
|
||||
|
||||
// Path: cropCalendar
|
||||
class _Translations$cropCalendar$es extends Translations$cropCalendar$en {
|
||||
_Translations$cropCalendar$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
|
@ -1228,6 +1253,21 @@ extension on TranslationsEs {
|
|||
'share.catalogTitle' => 'Lo que comparto',
|
||||
'share.catalogSaved' => 'Catálogo guardado',
|
||||
'share.cancelled' => 'Cancelado',
|
||||
'printLabels.action' => 'Imprimir etiquetas',
|
||||
'printLabels.title' => 'Imprimir etiquetas',
|
||||
'printLabels.selectHint' => 'Elige las semillas para imprimir sus etiquetas',
|
||||
'printLabels.selectAll' => 'Seleccionar todo',
|
||||
'printLabels.selected' => ({required Object n}) => '${n} seleccionadas',
|
||||
'printLabels.none' => 'Selecciona semillas primero',
|
||||
'printLabels.format' => 'Tamaño de etiqueta',
|
||||
'printLabels.formatStickers' => 'Pegatinas pequeñas',
|
||||
'printLabels.formatStickersHint' => 'Muchas etiquetas pequeñas por hoja — para pegar en sobres',
|
||||
'printLabels.formatCards' => 'Tarjetas grandes',
|
||||
'printLabels.formatCardsHint' => 'Menos etiquetas, más grandes — para botes y cajas',
|
||||
'printLabels.count' => ({required Object n}) => '${n} etiquetas',
|
||||
'printLabels.save' => 'Guardar etiquetas',
|
||||
'printLabels.saved' => 'Etiquetas guardadas',
|
||||
'printLabels.cancelled' => 'Cancelado',
|
||||
'cropCalendar.add' => 'Calendario de cultivo',
|
||||
'cropCalendar.title' => 'Calendario de cultivo',
|
||||
'cropCalendar.sow' => 'Siembra',
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class TranslationsPt extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@override late final _Translations$provenance$pt provenance = _Translations$provenance$pt._(_root);
|
||||
@override late final _Translations$abundance$pt abundance = _Translations$abundance$pt._(_root);
|
||||
@override late final _Translations$share$pt share = _Translations$share$pt._(_root);
|
||||
@override late final _Translations$printLabels$pt printLabels = _Translations$printLabels$pt._(_root);
|
||||
@override late final _Translations$cropCalendar$pt cropCalendar = _Translations$cropCalendar$pt._(_root);
|
||||
@override late final _Translations$needsReproduction$pt needsReproduction = _Translations$needsReproduction$pt._(_root);
|
||||
@override late final _Translations$preservation$pt preservation = _Translations$preservation$pt._(_root);
|
||||
|
|
@ -483,6 +484,30 @@ class _Translations$share$pt extends Translations$share$en {
|
|||
@override String get cancelled => 'Cancelado';
|
||||
}
|
||||
|
||||
// Path: printLabels
|
||||
class _Translations$printLabels$pt extends Translations$printLabels$en {
|
||||
_Translations$printLabels$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsPt _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get action => 'Imprimir etiquetas';
|
||||
@override String get title => 'Imprimir etiquetas';
|
||||
@override String get selectHint => 'Escolhe as sementes para imprimir as etiquetas';
|
||||
@override String get selectAll => 'Selecionar tudo';
|
||||
@override String selected({required Object n}) => '${n} selecionadas';
|
||||
@override String get none => 'Seleciona sementes primeiro';
|
||||
@override String get format => 'Tamanho da etiqueta';
|
||||
@override String get formatStickers => 'Autocolantes pequenos';
|
||||
@override String get formatStickersHint => 'Muitas etiquetas pequenas por folha — para colar em envelopes';
|
||||
@override String get formatCards => 'Cartões grandes';
|
||||
@override String get formatCardsHint => 'Menos etiquetas, maiores — para frascos e caixas';
|
||||
@override String count({required Object n}) => '${n} etiquetas';
|
||||
@override String get save => 'Guardar etiquetas';
|
||||
@override String get saved => 'Etiquetas guardadas';
|
||||
@override String get cancelled => 'Cancelado';
|
||||
}
|
||||
|
||||
// Path: cropCalendar
|
||||
class _Translations$cropCalendar$pt extends Translations$cropCalendar$en {
|
||||
_Translations$cropCalendar$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||
|
|
@ -1220,6 +1245,21 @@ extension on TranslationsPt {
|
|||
'share.catalogTitle' => 'O que partilho',
|
||||
'share.catalogSaved' => 'Catálogo guardado',
|
||||
'share.cancelled' => 'Cancelado',
|
||||
'printLabels.action' => 'Imprimir etiquetas',
|
||||
'printLabels.title' => 'Imprimir etiquetas',
|
||||
'printLabels.selectHint' => 'Escolhe as sementes para imprimir as etiquetas',
|
||||
'printLabels.selectAll' => 'Selecionar tudo',
|
||||
'printLabels.selected' => ({required Object n}) => '${n} selecionadas',
|
||||
'printLabels.none' => 'Seleciona sementes primeiro',
|
||||
'printLabels.format' => 'Tamanho da etiqueta',
|
||||
'printLabels.formatStickers' => 'Autocolantes pequenos',
|
||||
'printLabels.formatStickersHint' => 'Muitas etiquetas pequenas por folha — para colar em envelopes',
|
||||
'printLabels.formatCards' => 'Cartões grandes',
|
||||
'printLabels.formatCardsHint' => 'Menos etiquetas, maiores — para frascos e caixas',
|
||||
'printLabels.count' => ({required Object n}) => '${n} etiquetas',
|
||||
'printLabels.save' => 'Guardar etiquetas',
|
||||
'printLabels.saved' => 'Etiquetas guardadas',
|
||||
'printLabels.cancelled' => 'Cancelado',
|
||||
'cropCalendar.add' => 'Calendário de cultivo',
|
||||
'cropCalendar.title' => 'Calendário de cultivo',
|
||||
'cropCalendar.sow' => 'Sementeira',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue