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.
115 lines
3.9 KiB
Dart
115 lines
3.9 KiB
Dart
import 'package:commons_core/commons_core.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:material_symbols_icons/symbols.dart';
|
||
|
||
import '../db/enums.dart';
|
||
|
||
/// Renders a glyph from the bundled `seedks` icon font, whose characters `a`–`k`
|
||
/// map to seed-themed pictograms (jar, packet, spoon…). See
|
||
/// docs/mockups/seedks-glyphs.png.
|
||
class SeedGlyph extends StatelessWidget {
|
||
const SeedGlyph(this.char, {this.size = 24, this.color, super.key});
|
||
|
||
final String char;
|
||
final double size;
|
||
final Color? color;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Text(
|
||
char,
|
||
style: TextStyle(
|
||
fontFamily: 'seedks',
|
||
fontSize: size,
|
||
height: 1,
|
||
color: color ?? Theme.of(context).colorScheme.primary,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Named seedks glyphs (the character each pictogram is mapped to).
|
||
abstract final class SeedGlyphs {
|
||
static const jar = 'a';
|
||
static const jars = 'b';
|
||
static const search = 'c';
|
||
static const share = 'd';
|
||
static const sack = 'e';
|
||
static const smallSpoon = 'f';
|
||
static const bigSpoon = 'g';
|
||
static const mug = 'h';
|
||
static const envelope = 'i';
|
||
static const pouring = 'j';
|
||
static const scattered = 'k';
|
||
}
|
||
|
||
/// The seedks character for a quantity [kind], or null for plant-form/precise
|
||
/// kinds that have no seed pictogram (they fall back to a Material icon).
|
||
String? seedGlyphForKind(QuantityKind kind) => switch (kind) {
|
||
QuantityKind.aFew => SeedGlyphs.scattered,
|
||
QuantityKind.some => SeedGlyphs.smallSpoon,
|
||
QuantityKind.plenty => SeedGlyphs.jars,
|
||
QuantityKind.pinch => SeedGlyphs.scattered,
|
||
QuantityKind.handful => SeedGlyphs.bigSpoon,
|
||
QuantityKind.teaspoon => SeedGlyphs.smallSpoon,
|
||
QuantityKind.spoon => SeedGlyphs.bigSpoon,
|
||
QuantityKind.cup => SeedGlyphs.mug,
|
||
QuantityKind.jar => SeedGlyphs.jar,
|
||
QuantityKind.sack => SeedGlyphs.sack,
|
||
QuantityKind.packet => SeedGlyphs.pouring,
|
||
_ => null,
|
||
};
|
||
|
||
IconData _materialIconForKind(QuantityKind kind) => switch (kind) {
|
||
QuantityKind.grams => Icons.scale_outlined,
|
||
QuantityKind.count => Icons.tag,
|
||
// Seed/fruit forms — each a distinct icon so they're told apart at a glance.
|
||
QuantityKind.cob => Symbols.grass, // mazorca
|
||
QuantityKind.ear => Symbols.grain, // espiga
|
||
QuantityKind.head => Symbols.filter_vintage, // cabezuela
|
||
QuantityKind.seedHead => Symbols.spa,
|
||
QuantityKind.fruit => Symbols.nutrition, // fruto (apple)
|
||
QuantityKind.bulb => Symbols.spa, // bulbo
|
||
QuantityKind.tuber => Symbols.egg, // tubérculo
|
||
QuantityKind.bunch => Symbols.local_florist, // manojo
|
||
// Living material counted as individuals.
|
||
QuantityKind.seedling => Symbols.potted_plant,
|
||
QuantityKind.plant => Symbols.yard,
|
||
QuantityKind.tree => Symbols.park,
|
||
QuantityKind.cutting => Symbols.content_cut,
|
||
QuantityKind.pot => Symbols.yard,
|
||
QuantityKind.tray => Symbols.grid_on,
|
||
_ => Icons.eco_outlined,
|
||
};
|
||
|
||
/// The icon for a lot's [LotType] form (used in the form selector and lot chip),
|
||
/// distinct across all forms so the selector reads clearly.
|
||
IconData iconForLotType(LotType type) => switch (type) {
|
||
LotType.seed => Symbols.eco, // overridden by a seed glyph where shown
|
||
LotType.seedling => Symbols.potted_plant,
|
||
LotType.plant => Symbols.local_florist,
|
||
LotType.tree => Symbols.park,
|
||
LotType.bulb => Symbols.spa,
|
||
LotType.cutting => Symbols.content_cut,
|
||
};
|
||
|
||
/// A consistent icon for a [QuantityKind]: the seed glyph where one exists,
|
||
/// otherwise a Material fallback.
|
||
class QuantityKindIcon extends StatelessWidget {
|
||
const QuantityKindIcon(this.kind, {this.size = 24, this.color, super.key});
|
||
|
||
final QuantityKind kind;
|
||
final double size;
|
||
final Color? color;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final glyph = seedGlyphForKind(kind);
|
||
if (glyph != null) return SeedGlyph(glyph, size: size, color: color);
|
||
return Icon(
|
||
_materialIconForKind(kind),
|
||
size: size,
|
||
color: color ?? Theme.of(context).colorScheme.primary,
|
||
);
|
||
}
|
||
}
|