feat(block1): variety detail + edit screen with reactive lots

Tapping an inventory item now opens its detail (the /variety/:id route was a
placeholder). Read + edit one variety end-to-end:

- Detail view: photo, category, "also known as" names, notes and lots, driven
  by a reactive VarietyDetailCubit.
- Edit core fields (label/category/notes, LWW) via a bottom sheet.
- Add a lot (harvest year + quantity) — the list refreshes reactively.
- Soft-delete (tombstone) with a confirm dialog.

Repository:
- watchVariety(id): merges Drift table-change streams (variety, lots, names,
  attachments) via StreamGroup and reloads the full detail on any change, so a
  lot added to a *different* table still refreshes the view.
- updateVariety (LWW), addLot, softDeleteVariety.

i18n: detail/edit/addLot strings (ES/EN); switched slang to {param} braces
interpolation (translator- and Weblate-friendly).

Tests: repository (reactive watchVariety, update, soft-delete) and widget tests
(render, edit updates title, add-lot appears, delete shows not-found). Full
suite green: 23 passing, 1 skipped (SQLCipher-only encryption guard).
This commit is contained in:
vjrj 2026-07-07 15:56:03 +02:00
parent 040f15a898
commit 7ff4e38a15
15 changed files with 1093 additions and 18 deletions

View file

@ -2,6 +2,12 @@
"app": {
"title": "Tanemaki"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit"
},
"inventory": {
"title": "Inventory",
"searchHint": "Search seeds",
@ -18,6 +24,28 @@
"save": "Save",
"cancel": "Cancel"
},
"detail": {
"notFound": "This seed is no longer here.",
"lots": "Lots",
"noLots": "No lots yet.",
"names": "Also known as",
"notes": "Notes",
"addLot": "Add lot",
"deleteConfirm": "Delete this seed?",
"year": "Year {year}",
"noYear": "Year unknown"
},
"editVariety": {
"title": "Edit seed",
"name": "Name",
"category": "Category",
"notes": "Notes"
},
"addLot": {
"title": "Add lot",
"year": "Harvest year",
"quantity": "Quantity"
},
"quantityKind": {
"aFew": "a few",
"some": "some",

View file

@ -2,6 +2,12 @@
"app": {
"title": "Tanemaki"
},
"common": {
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"edit": "Editar"
},
"inventory": {
"title": "Inventario",
"searchHint": "Buscar semillas",
@ -18,6 +24,28 @@
"save": "Guardar",
"cancel": "Cancelar"
},
"detail": {
"notFound": "Esta semilla ya no está aquí.",
"lots": "Lotes",
"noLots": "Aún no hay lotes.",
"names": "También conocida como",
"notes": "Notas",
"addLot": "Añadir lote",
"deleteConfirm": "¿Eliminar esta semilla?",
"year": "Año {year}",
"noYear": "Año desconocido"
},
"editVariety": {
"title": "Editar semilla",
"name": "Nombre",
"category": "Categoría",
"notes": "Notas"
},
"addLot": {
"title": "Añadir lote",
"year": "Año de cosecha",
"quantity": "Cantidad"
},
"quantityKind": {
"aFew": "unas pocas",
"some": "algunas",

View file

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 62 (31 per locale)
/// Strings: 102 (51 per locale)
///
/// Built on 2026-07-07 at 13:12 UTC
/// Built on 2026-07-07 at 13:25 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View file

@ -41,8 +41,12 @@ class Translations with BaseTranslations<AppLocale, Translations> {
// Translations
late final Translations$app$en app = Translations$app$en.internal(_root);
late final Translations$common$en common = Translations$common$en.internal(_root);
late final Translations$inventory$en inventory = Translations$inventory$en.internal(_root);
late final Translations$quickAdd$en quickAdd = Translations$quickAdd$en.internal(_root);
late final Translations$detail$en detail = Translations$detail$en.internal(_root);
late final Translations$editVariety$en editVariety = Translations$editVariety$en.internal(_root);
late final Translations$addLot$en addLot = Translations$addLot$en.internal(_root);
late final Translations$quantityKind$en quantityKind = Translations$quantityKind$en.internal(_root);
}
@ -58,6 +62,27 @@ class Translations$app$en {
String get title => 'Tanemaki';
}
// Path: common
class Translations$common$en {
Translations$common$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Save'
String get save => 'Save';
/// en: 'Cancel'
String get cancel => 'Cancel';
/// en: 'Delete'
String get delete => 'Delete';
/// en: 'Edit'
String get edit => 'Edit';
}
// Path: inventory
class Translations$inventory$en {
Translations$inventory$en.internal(this._root);
@ -112,6 +137,81 @@ class Translations$quickAdd$en {
String get cancel => 'Cancel';
}
// Path: detail
class Translations$detail$en {
Translations$detail$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'This seed is no longer here.'
String get notFound => 'This seed is no longer here.';
/// en: 'Lots'
String get lots => 'Lots';
/// en: 'No lots yet.'
String get noLots => 'No lots yet.';
/// en: 'Also known as'
String get names => 'Also known as';
/// en: 'Notes'
String get notes => 'Notes';
/// en: 'Add lot'
String get addLot => 'Add lot';
/// en: 'Delete this seed?'
String get deleteConfirm => 'Delete this seed?';
/// en: 'Year {year}'
String year({required Object year}) => 'Year ${year}';
/// en: 'Year unknown'
String get noYear => 'Year unknown';
}
// Path: editVariety
class Translations$editVariety$en {
Translations$editVariety$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Edit seed'
String get title => 'Edit seed';
/// en: 'Name'
String get name => 'Name';
/// en: 'Category'
String get category => 'Category';
/// en: 'Notes'
String get notes => 'Notes';
}
// Path: addLot
class Translations$addLot$en {
Translations$addLot$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Add lot'
String get title => 'Add lot';
/// en: 'Harvest year'
String get year => 'Harvest year';
/// en: 'Quantity'
String get quantity => 'Quantity';
}
// Path: quantityKind
class Translations$quantityKind$en {
Translations$quantityKind$en.internal(this._root);
@ -184,6 +284,10 @@ extension on Translations {
dynamic _flatMapFunction(String path) {
return switch (path) {
'app.title' => 'Tanemaki',
'common.save' => 'Save',
'common.cancel' => 'Cancel',
'common.delete' => 'Delete',
'common.edit' => 'Edit',
'inventory.title' => 'Inventory',
'inventory.searchHint' => 'Search seeds',
'inventory.empty' => 'No seeds yet. Tap + to add your first.',
@ -196,6 +300,22 @@ extension on Translations {
'quickAdd.more' => 'Add more…',
'quickAdd.save' => 'Save',
'quickAdd.cancel' => 'Cancel',
'detail.notFound' => 'This seed is no longer here.',
'detail.lots' => 'Lots',
'detail.noLots' => 'No lots yet.',
'detail.names' => 'Also known as',
'detail.notes' => 'Notes',
'detail.addLot' => 'Add lot',
'detail.deleteConfirm' => 'Delete this seed?',
'detail.year' => ({required Object year}) => 'Year ${year}',
'detail.noYear' => 'Year unknown',
'editVariety.title' => 'Edit seed',
'editVariety.name' => 'Name',
'editVariety.category' => 'Category',
'editVariety.notes' => 'Notes',
'addLot.title' => 'Add lot',
'addLot.year' => 'Harvest year',
'addLot.quantity' => 'Quantity',
'quantityKind.aFew' => 'a few',
'quantityKind.some' => 'some',
'quantityKind.plenty' => 'plenty',

View file

@ -40,8 +40,12 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
// Translations
@override late final _Translations$app$es app = _Translations$app$es._(_root);
@override late final _Translations$common$es common = _Translations$common$es._(_root);
@override late final _Translations$inventory$es inventory = _Translations$inventory$es._(_root);
@override late final _Translations$quickAdd$es quickAdd = _Translations$quickAdd$es._(_root);
@override late final _Translations$detail$es detail = _Translations$detail$es._(_root);
@override late final _Translations$editVariety$es editVariety = _Translations$editVariety$es._(_root);
@override late final _Translations$addLot$es addLot = _Translations$addLot$es._(_root);
@override late final _Translations$quantityKind$es quantityKind = _Translations$quantityKind$es._(_root);
}
@ -55,6 +59,19 @@ class _Translations$app$es extends Translations$app$en {
@override String get title => 'Tanemaki';
}
// Path: common
class _Translations$common$es extends Translations$common$en {
_Translations$common$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get save => 'Guardar';
@override String get cancel => 'Cancelar';
@override String get delete => 'Eliminar';
@override String get edit => 'Editar';
}
// Path: inventory
class _Translations$inventory$es extends Translations$inventory$en {
_Translations$inventory$es._(TranslationsEs root) : this._root = root, super.internal(root);
@ -85,6 +102,49 @@ class _Translations$quickAdd$es extends Translations$quickAdd$en {
@override String get cancel => 'Cancelar';
}
// Path: detail
class _Translations$detail$es extends Translations$detail$en {
_Translations$detail$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get notFound => 'Esta semilla ya no está aquí.';
@override String get lots => 'Lotes';
@override String get noLots => 'Aún no hay lotes.';
@override String get names => 'También conocida como';
@override String get notes => 'Notas';
@override String get addLot => 'Añadir lote';
@override String get deleteConfirm => '¿Eliminar esta semilla?';
@override String year({required Object year}) => 'Año ${year}';
@override String get noYear => 'Año desconocido';
}
// Path: editVariety
class _Translations$editVariety$es extends Translations$editVariety$en {
_Translations$editVariety$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'Editar semilla';
@override String get name => 'Nombre';
@override String get category => 'Categoría';
@override String get notes => 'Notas';
}
// Path: addLot
class _Translations$addLot$es extends Translations$addLot$en {
_Translations$addLot$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'Añadir lote';
@override String get year => 'Año de cosecha';
@override String get quantity => 'Cantidad';
}
// Path: quantityKind
class _Translations$quantityKind$es extends Translations$quantityKind$en {
_Translations$quantityKind$es._(TranslationsEs root) : this._root = root, super.internal(root);
@ -121,6 +181,10 @@ extension on TranslationsEs {
dynamic _flatMapFunction(String path) {
return switch (path) {
'app.title' => 'Tanemaki',
'common.save' => 'Guardar',
'common.cancel' => 'Cancelar',
'common.delete' => 'Eliminar',
'common.edit' => 'Editar',
'inventory.title' => 'Inventario',
'inventory.searchHint' => 'Buscar semillas',
'inventory.empty' => 'Aún no hay semillas. Toca + para añadir la primera.',
@ -133,6 +197,22 @@ extension on TranslationsEs {
'quickAdd.more' => 'Añadir más…',
'quickAdd.save' => 'Guardar',
'quickAdd.cancel' => 'Cancelar',
'detail.notFound' => 'Esta semilla ya no está aquí.',
'detail.lots' => 'Lotes',
'detail.noLots' => 'Aún no hay lotes.',
'detail.names' => 'También conocida como',
'detail.notes' => 'Notas',
'detail.addLot' => 'Añadir lote',
'detail.deleteConfirm' => '¿Eliminar esta semilla?',
'detail.year' => ({required Object year}) => 'Año ${year}',
'detail.noYear' => 'Año desconocido',
'editVariety.title' => 'Editar semilla',
'editVariety.name' => 'Nombre',
'editVariety.category' => 'Categoría',
'editVariety.notes' => 'Notas',
'addLot.title' => 'Añadir lote',
'addLot.year' => 'Año de cosecha',
'addLot.quantity' => 'Cantidad',
'quantityKind.aFew' => 'unas pocas',
'quantityKind.some' => 'algunas',
'quantityKind.plenty' => 'muchas',