feat(inventory): photo-first drafts + on-device OCR (digitization R2+R4)
Lower the bulk-digitization cliff with two more routes on top of the already-landed CSV import and "save and add another": - Photo-first drafts (capture now, catalogue later): burst-capture photos (camera or multi-gallery) into unnamed draft varieties, shown in a "to catalogue" tray, hidden from the main list until named. Adds Variety.isDraft (schema), addDraftVariety/watchDrafts/nameDraft, the triage sheet and the inventory banner. - On-device OCR label suggestion (Tesseract, offline, no Google): a "Suggest name from photo" button in the naming dialog behind a LabelTextExtractor interface (Tesseract on Android/iOS, no-op elsewhere). Reads the largest print via hOCR bounding boxes, drops boilerplate/low-confidence noise, preprocesses (grayscale, contrast, upscale) and sweeps rotations (0-315 deg) so tilted packets still read. Bundles tessdata_fast eng+spa; validated on-device against real packets. The photo is written to a temp file deleted immediately in a finally block (the plugin needs a path) - a bounded, documented exception to no-plaintext-at-rest. This commit also carries the co-developed schema evolution v5 to v8 that shares these files (organic flag, species viability years, crop calendar, lot provenance/abundance/preservation format, condition checks) plus their exports/migrations and i18n. Tests: CSV/draft/OCR unit + widget + migration green in isolation. Note: the full widget suite currently hangs (>10 min) - under investigation.
This commit is contained in:
parent
12a2ee2d64
commit
6809dc6143
89 changed files with 17141 additions and 228 deletions
|
|
@ -49,14 +49,23 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@override late final _Translations$about$es about = _Translations$about$es._(_root);
|
||||
@override late final _Translations$intro$es intro = _Translations$intro$es._(_root);
|
||||
@override late final _Translations$inventory$es inventory = _Translations$inventory$es._(_root);
|
||||
@override late final _Translations$draft$es draft = _Translations$draft$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$germination$es germination = _Translations$germination$es._(_root);
|
||||
@override late final _Translations$viability$es viability = _Translations$viability$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$harvest$es harvest = _Translations$harvest$es._(_root);
|
||||
@override late final _Translations$lotType$es lotType = _Translations$lotType$es._(_root);
|
||||
@override late final _Translations$presentation$es presentation = _Translations$presentation$es._(_root);
|
||||
@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$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);
|
||||
@override late final _Translations$conditionCheck$es conditionCheck = _Translations$conditionCheck$es._(_root);
|
||||
@override late final _Translations$desiccant$es desiccant = _Translations$desiccant$es._(_root);
|
||||
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
|
||||
}
|
||||
|
||||
|
|
@ -154,23 +163,23 @@ class _Translations$backup$es extends Translations$backup$en {
|
|||
|
||||
// Translations
|
||||
@override String get title => 'Copia de seguridad';
|
||||
@override String get exportCsv => 'Exportar a CSV';
|
||||
@override String get exportCsvSubtitle => 'Para hojas de cálculo — sin fotos';
|
||||
@override String get exportJson => 'Exportar a JSON';
|
||||
@override String get exportJsonSubtitle => 'Copia completa, se puede volver a importar';
|
||||
@override String get importJson => 'Importar desde JSON';
|
||||
@override String get importJsonSubtitle => 'Fusiona una copia guardada con este inventario';
|
||||
@override String get importCsv => 'Importar desde CSV';
|
||||
@override String get importCsvSubtitle => 'Añade una lista desde una hoja de cálculo';
|
||||
@override String get importConfirmTitle => '¿Importar una copia guardada?';
|
||||
@override String get exportJson => 'Guardar una copia de seguridad';
|
||||
@override String get exportJsonSubtitle => 'Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo';
|
||||
@override String get importJson => 'Restaurar una copia';
|
||||
@override String get importJsonSubtitle => 'Recupera una copia guardada — no se duplica nada';
|
||||
@override String get exportCsv => 'Exportar a una hoja de cálculo';
|
||||
@override String get exportCsvSubtitle => 'Una lista sencilla para Excel o LibreOffice — sin fotos';
|
||||
@override String get importCsv => 'Importar una lista';
|
||||
@override String get importCsvSubtitle => 'Añade entradas desde una hoja de cálculo';
|
||||
@override String get importConfirmTitle => '¿Restaurar una copia?';
|
||||
@override String get importConfirmBody => 'Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.';
|
||||
@override String get importCsvConfirmTitle => '¿Importar una lista CSV?';
|
||||
@override String get importCsvConfirmTitle => '¿Importar una lista?';
|
||||
@override String get importCsvConfirmBody => 'Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.';
|
||||
@override String get importAction => 'Importar';
|
||||
@override String get exportSaved => 'Copia guardada';
|
||||
@override String get cancelled => 'Cancelado';
|
||||
@override String importDone({required Object added, required Object updated}) => 'Importado: ${added} nuevas, ${updated} actualizadas';
|
||||
@override String importCsvDone({required Object count}) => 'Importadas ${count} desde CSV';
|
||||
@override String importCsvDone({required Object count}) => 'Añadidas ${count} entradas';
|
||||
@override String get importFailed => 'Este fichero no se pudo leer como una copia de Tanemaki';
|
||||
@override String get failed => 'Algo ha salido mal';
|
||||
}
|
||||
|
|
@ -222,6 +231,25 @@ class _Translations$inventory$es extends Translations$inventory$en {
|
|||
@override String get noMatches => 'Ninguna semilla coincide con los filtros.';
|
||||
@override String get clearFilters => 'Quitar filtros';
|
||||
@override String get uncategorized => 'Sin categoría';
|
||||
@override String get needsReproductionFilter => 'Por reproducir';
|
||||
}
|
||||
|
||||
// Path: draft
|
||||
class _Translations$draft$es extends Translations$draft$en {
|
||||
_Translations$draft$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get capture => 'Capturar fotos';
|
||||
@override String captured({required Object n}) => '${n} capturadas por catalogar';
|
||||
@override String get triageTitle => 'Por catalogar';
|
||||
@override String triageCount({required Object n}) => '${n} por catalogar';
|
||||
@override String get untitled => 'Sin nombre';
|
||||
@override String get nameField => 'Nombra esta semilla';
|
||||
@override String get nameHint => '¿Qué es?';
|
||||
@override String get suggestFromPhoto => 'Sugerir nombre de la foto';
|
||||
@override String get discard => 'Descartar';
|
||||
}
|
||||
|
||||
// Path: quickAdd
|
||||
|
|
@ -282,6 +310,19 @@ class _Translations$germination$es extends Translations$germination$en {
|
|||
@override String result({required Object percent}) => '${percent}%';
|
||||
}
|
||||
|
||||
// Path: viability
|
||||
class _Translations$viability$es extends Translations$viability$en {
|
||||
_Translations$viability$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get expiringSoon => 'Úsala o multiplícala esta temporada';
|
||||
@override String expiringSoonYears({required Object years}) => 'Úsala o multiplícala esta temporada · dura ~${years} años';
|
||||
@override String get expired => 'Supera su viabilidad típica — a multiplicar';
|
||||
@override String expiredYears({required Object years}) => 'Supera su viabilidad típica (~${years} años) — a multiplicar';
|
||||
}
|
||||
|
||||
// Path: editVariety
|
||||
class _Translations$editVariety$es extends Translations$editVariety$en {
|
||||
_Translations$editVariety$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
|
@ -295,6 +336,8 @@ class _Translations$editVariety$es extends Translations$editVariety$en {
|
|||
@override String get notes => 'Notas';
|
||||
@override String get species => 'Especie (del catálogo)';
|
||||
@override String get speciesHint => 'Buscar una especie…';
|
||||
@override String get organic => 'Ecológica';
|
||||
@override String get organicHint => 'Cultivada de forma ecológica (eco)';
|
||||
}
|
||||
|
||||
// Path: addLot
|
||||
|
|
@ -367,6 +410,114 @@ class _Translations$presentation$es extends Translations$presentation$en {
|
|||
@override String get rootBall => 'Cepellón';
|
||||
}
|
||||
|
||||
// Path: provenance
|
||||
class _Translations$provenance$es extends Translations$provenance$en {
|
||||
_Translations$provenance$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get section => 'De dónde viene';
|
||||
@override String get seedsFrom => 'Semillas de';
|
||||
@override String get seedsFromHint => 'Quién las cultivó o las dio';
|
||||
@override String get place => 'Lugar';
|
||||
@override String get placeHint => 'De dónde vienen (con provincia)';
|
||||
@override String get addSeedsFrom => 'Semillas de';
|
||||
@override String get addPlace => 'Lugar';
|
||||
}
|
||||
|
||||
// Path: abundance
|
||||
class _Translations$abundance$es extends Translations$abundance$en {
|
||||
_Translations$abundance$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get add => 'Cuánta tengo';
|
||||
@override String get title => 'Cuánta tengo';
|
||||
@override String get none => 'Sin indicar';
|
||||
@override String get plentyToShare => 'De sobra para compartir';
|
||||
@override String get enoughToShare => 'Bastante, para compartir con moderación';
|
||||
@override String get enoughForMe => 'Suficiente para mí';
|
||||
@override String get runningLow => 'Queda poca';
|
||||
}
|
||||
|
||||
// Path: cropCalendar
|
||||
class _Translations$cropCalendar$es extends Translations$cropCalendar$en {
|
||||
_Translations$cropCalendar$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get add => 'Calendario de cultivo';
|
||||
@override String get title => 'Calendario de cultivo';
|
||||
@override String get sow => 'Siembra';
|
||||
@override String get transplant => 'Trasplante';
|
||||
@override String get flowering => 'Floración';
|
||||
@override String get fruiting => 'Fructificación';
|
||||
@override String get seedHarvest => 'Cosecha de semilla';
|
||||
@override String get unset => '—';
|
||||
}
|
||||
|
||||
// Path: needsReproduction
|
||||
class _Translations$needsReproduction$es extends Translations$needsReproduction$en {
|
||||
_Translations$needsReproduction$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get label => 'Reproducir esta temporada';
|
||||
@override String get hint => 'Cultívala antes de que se acabe la semilla';
|
||||
@override String get badge => 'Por reproducir';
|
||||
}
|
||||
|
||||
// Path: preservation
|
||||
class _Translations$preservation$es extends Translations$preservation$en {
|
||||
_Translations$preservation$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get add => 'Cómo se conserva';
|
||||
@override String get title => 'Cómo se conserva';
|
||||
@override String get none => 'Sin especificar';
|
||||
@override String get jarWithDesiccant => 'Bote con desecante';
|
||||
@override String get glassJar => 'Bote de cristal';
|
||||
@override String get paperEnvelope => 'Sobre de papel';
|
||||
@override String get paperBag => 'Bolsa de papel';
|
||||
@override String get plasticBag => 'Bolsa de plástico';
|
||||
}
|
||||
|
||||
// Path: conditionCheck
|
||||
class _Translations$conditionCheck$es extends Translations$conditionCheck$en {
|
||||
_Translations$conditionCheck$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get advanced => 'Conservación y detalles del banco';
|
||||
@override String get title => 'Revisiones de conservación';
|
||||
@override String get add => 'Añadir revisión';
|
||||
@override String get containers => 'Botes / recipientes';
|
||||
@override String get desiccant => 'Desecante';
|
||||
@override String get none => 'Aún no hay revisiones.';
|
||||
@override String summary({required Object count, required Object state}) => '${count} bote(s) · ${state}';
|
||||
}
|
||||
|
||||
// Path: desiccant
|
||||
class _Translations$desiccant$es extends Translations$desiccant$en {
|
||||
_Translations$desiccant$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get none => 'No tiene';
|
||||
@override String get add => 'Se pone';
|
||||
@override String get replace => 'Se cambia';
|
||||
@override String get dry => 'Azul — seco';
|
||||
@override String get fresh => 'Recién puesto';
|
||||
}
|
||||
|
||||
// Path: unit
|
||||
class _Translations$unit$es extends Translations$unit$en {
|
||||
_Translations$unit$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
|
@ -778,23 +929,23 @@ extension on TranslationsEs {
|
|||
'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.',
|
||||
'settings.aboutOpen' => 'Acerca de Tanemaki',
|
||||
'backup.title' => 'Copia de seguridad',
|
||||
'backup.exportCsv' => 'Exportar a CSV',
|
||||
'backup.exportCsvSubtitle' => 'Para hojas de cálculo — sin fotos',
|
||||
'backup.exportJson' => 'Exportar a JSON',
|
||||
'backup.exportJsonSubtitle' => 'Copia completa, se puede volver a importar',
|
||||
'backup.importJson' => 'Importar desde JSON',
|
||||
'backup.importJsonSubtitle' => 'Fusiona una copia guardada con este inventario',
|
||||
'backup.importCsv' => 'Importar desde CSV',
|
||||
'backup.importCsvSubtitle' => 'Añade una lista desde una hoja de cálculo',
|
||||
'backup.importConfirmTitle' => '¿Importar una copia guardada?',
|
||||
'backup.exportJson' => 'Guardar una copia de seguridad',
|
||||
'backup.exportJsonSubtitle' => 'Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo',
|
||||
'backup.importJson' => 'Restaurar una copia',
|
||||
'backup.importJsonSubtitle' => 'Recupera una copia guardada — no se duplica nada',
|
||||
'backup.exportCsv' => 'Exportar a una hoja de cálculo',
|
||||
'backup.exportCsvSubtitle' => 'Una lista sencilla para Excel o LibreOffice — sin fotos',
|
||||
'backup.importCsv' => 'Importar una lista',
|
||||
'backup.importCsvSubtitle' => 'Añade entradas desde una hoja de cálculo',
|
||||
'backup.importConfirmTitle' => '¿Restaurar una copia?',
|
||||
'backup.importConfirmBody' => 'Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.',
|
||||
'backup.importCsvConfirmTitle' => '¿Importar una lista CSV?',
|
||||
'backup.importCsvConfirmTitle' => '¿Importar una lista?',
|
||||
'backup.importCsvConfirmBody' => 'Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.',
|
||||
'backup.importAction' => 'Importar',
|
||||
'backup.exportSaved' => 'Copia guardada',
|
||||
'backup.cancelled' => 'Cancelado',
|
||||
'backup.importDone' => ({required Object added, required Object updated}) => 'Importado: ${added} nuevas, ${updated} actualizadas',
|
||||
'backup.importCsvDone' => ({required Object count}) => 'Importadas ${count} desde CSV',
|
||||
'backup.importCsvDone' => ({required Object count}) => 'Añadidas ${count} entradas',
|
||||
'backup.importFailed' => 'Este fichero no se pudo leer como una copia de Tanemaki',
|
||||
'backup.failed' => 'Algo ha salido mal',
|
||||
'about.title' => 'Acerca de',
|
||||
|
|
@ -828,6 +979,16 @@ extension on TranslationsEs {
|
|||
'inventory.noMatches' => 'Ninguna semilla coincide con los filtros.',
|
||||
'inventory.clearFilters' => 'Quitar filtros',
|
||||
'inventory.uncategorized' => 'Sin categoría',
|
||||
'inventory.needsReproductionFilter' => 'Por reproducir',
|
||||
'draft.capture' => 'Capturar fotos',
|
||||
'draft.captured' => ({required Object n}) => '${n} capturadas por catalogar',
|
||||
'draft.triageTitle' => 'Por catalogar',
|
||||
'draft.triageCount' => ({required Object n}) => '${n} por catalogar',
|
||||
'draft.untitled' => 'Sin nombre',
|
||||
'draft.nameField' => 'Nombra esta semilla',
|
||||
'draft.nameHint' => '¿Qué es?',
|
||||
'draft.suggestFromPhoto' => 'Sugerir nombre de la foto',
|
||||
'draft.discard' => 'Descartar',
|
||||
'quickAdd.title' => 'Añadir una semilla',
|
||||
'quickAdd.labelField' => 'Nombre',
|
||||
'quickAdd.labelRequired' => 'Ponle un nombre',
|
||||
|
|
@ -859,12 +1020,18 @@ extension on TranslationsEs {
|
|||
'germination.germinated' => 'Germinadas',
|
||||
'germination.none' => 'Aún no hay pruebas de germinación.',
|
||||
'germination.result' => ({required Object percent}) => '${percent}%',
|
||||
'viability.expiringSoon' => 'Úsala o multiplícala esta temporada',
|
||||
'viability.expiringSoonYears' => ({required Object years}) => 'Úsala o multiplícala esta temporada · dura ~${years} años',
|
||||
'viability.expired' => 'Supera su viabilidad típica — a multiplicar',
|
||||
'viability.expiredYears' => ({required Object years}) => 'Supera su viabilidad típica (~${years} años) — a multiplicar',
|
||||
'editVariety.title' => 'Editar semilla',
|
||||
'editVariety.name' => 'Nombre',
|
||||
'editVariety.category' => 'Categoría',
|
||||
'editVariety.notes' => 'Notas',
|
||||
'editVariety.species' => 'Especie (del catálogo)',
|
||||
'editVariety.speciesHint' => 'Buscar una especie…',
|
||||
'editVariety.organic' => 'Ecológica',
|
||||
'editVariety.organicHint' => 'Cultivada de forma ecológica (eco)',
|
||||
'addLot.title' => 'Añadir lote',
|
||||
'addLot.year' => 'Fecha de cosecha',
|
||||
'addLot.quantity' => '¿Cuánta?',
|
||||
|
|
@ -897,6 +1064,51 @@ extension on TranslationsEs {
|
|||
'presentation.plug' => 'Alvéolo',
|
||||
'presentation.bareRoot' => 'Raíz desnuda',
|
||||
'presentation.rootBall' => 'Cepellón',
|
||||
'provenance.section' => 'De dónde viene',
|
||||
'provenance.seedsFrom' => 'Semillas de',
|
||||
'provenance.seedsFromHint' => 'Quién las cultivó o las dio',
|
||||
'provenance.place' => 'Lugar',
|
||||
'provenance.placeHint' => 'De dónde vienen (con provincia)',
|
||||
'provenance.addSeedsFrom' => 'Semillas de',
|
||||
'provenance.addPlace' => 'Lugar',
|
||||
'abundance.add' => 'Cuánta tengo',
|
||||
'abundance.title' => 'Cuánta tengo',
|
||||
'abundance.none' => 'Sin indicar',
|
||||
'abundance.plentyToShare' => 'De sobra para compartir',
|
||||
'abundance.enoughToShare' => 'Bastante, para compartir con moderación',
|
||||
'abundance.enoughForMe' => 'Suficiente para mí',
|
||||
'abundance.runningLow' => 'Queda poca',
|
||||
'cropCalendar.add' => 'Calendario de cultivo',
|
||||
'cropCalendar.title' => 'Calendario de cultivo',
|
||||
'cropCalendar.sow' => 'Siembra',
|
||||
'cropCalendar.transplant' => 'Trasplante',
|
||||
'cropCalendar.flowering' => 'Floración',
|
||||
'cropCalendar.fruiting' => 'Fructificación',
|
||||
'cropCalendar.seedHarvest' => 'Cosecha de semilla',
|
||||
'cropCalendar.unset' => '—',
|
||||
'needsReproduction.label' => 'Reproducir esta temporada',
|
||||
'needsReproduction.hint' => 'Cultívala antes de que se acabe la semilla',
|
||||
'needsReproduction.badge' => 'Por reproducir',
|
||||
'preservation.add' => 'Cómo se conserva',
|
||||
'preservation.title' => 'Cómo se conserva',
|
||||
'preservation.none' => 'Sin especificar',
|
||||
'preservation.jarWithDesiccant' => 'Bote con desecante',
|
||||
'preservation.glassJar' => 'Bote de cristal',
|
||||
'preservation.paperEnvelope' => 'Sobre de papel',
|
||||
'preservation.paperBag' => 'Bolsa de papel',
|
||||
'preservation.plasticBag' => 'Bolsa de plástico',
|
||||
'conditionCheck.advanced' => 'Conservación y detalles del banco',
|
||||
'conditionCheck.title' => 'Revisiones de conservación',
|
||||
'conditionCheck.add' => 'Añadir revisión',
|
||||
'conditionCheck.containers' => 'Botes / recipientes',
|
||||
'conditionCheck.desiccant' => 'Desecante',
|
||||
'conditionCheck.none' => 'Aún no hay revisiones.',
|
||||
'conditionCheck.summary' => ({required Object count, required Object state}) => '${count} bote(s) · ${state}',
|
||||
'desiccant.none' => 'No tiene',
|
||||
'desiccant.add' => 'Se pone',
|
||||
'desiccant.replace' => 'Se cambia',
|
||||
'desiccant.dry' => 'Azul — seco',
|
||||
'desiccant.fresh' => 'Recién puesto',
|
||||
'unit.aFew' => 'unas pocas',
|
||||
'unit.some' => 'algunas',
|
||||
'unit.plenty' => 'muchas',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue