feat(inventory): informal quantity scale (number + unit) + seed/plant lots
Rework quantities the way seeds are actually described — a number of an informal
unit — and let a lot hold seeds or plants (plantel).
Quantity model (commons_core):
- Quantity is now { kind, count?, label } — "3 cobs", "2 packets", "a few".
Count is optional; uncountable vibes (a few, some, pinch) carry no number.
- QuantityKind reorganised into an ascending scale: vibes → containers
(teaspoon/spoon/cup/jar/sack) → seed & fruit forms → plant units, each tagged
countable/not. Stored by name; count reuses the existing numeric column.
Lot type (schema v2):
- Lots gain a `type` (seed | plant); germination stays meaningful for seeds.
schemaVersion 2 with a from1To2 migration (adds the column), drift_schema_v2
exported, migration test covers v1→v2.
UI:
- New QuantityPicker: a horizontal scale of large seed-glyph icons + a number
stepper (shown only for countable units), plus a seed/plant SegmentedButton.
Used in quick-add and add-lot. Lot lines render "3 cobs" / "a few".
- i18n: unit singular/plural (ES/EN, Weblate-friendly), lot-type labels.
Build: slang moved to its CLI (disabled in build_runner to avoid a multi-file
collision); CI runs `dart run slang` before build_runner. 38 tests green,
Linux migrates the existing v1 DB and runs.
This commit is contained in:
parent
48e9d15772
commit
3942975dba
26 changed files with 4220 additions and 315 deletions
|
|
@ -42,7 +42,8 @@ test:app_seeds:
|
|||
stage: test
|
||||
script:
|
||||
- cd apps/app_seeds
|
||||
# Generated sources (Drift, slang) must exist before analysis/tests.
|
||||
# slang is generated via its CLI (disabled in build_runner); Drift via build_runner.
|
||||
- dart run slang
|
||||
- dart run build_runner build --delete-conflicting-outputs
|
||||
- flutter test --coverage
|
||||
coverage: '/lines\.*: \d+\.\d+\%/'
|
||||
|
|
|
|||
8
apps/app_seeds/build.yaml
Normal file
8
apps/app_seeds/build.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# slang is generated via the CLI (`dart run slang`), which emits multiple files
|
||||
# (strings.g.dart + per-locale parts). Disable its build_runner integration so
|
||||
# `build_runner build` (Drift only) doesn't collide with those committed files.
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
slang_build_runner:
|
||||
enabled: false
|
||||
1471
apps/app_seeds/drift_schemas/drift_schema_v2.json
Normal file
1471
apps/app_seeds/drift_schemas/drift_schema_v2.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -63,6 +63,7 @@ class GerminationEntry extends Equatable {
|
|||
class VarietyLot extends Equatable {
|
||||
const VarietyLot({
|
||||
required this.id,
|
||||
this.type = LotType.seed,
|
||||
this.harvestYear,
|
||||
this.quantity,
|
||||
this.storageLocation,
|
||||
|
|
@ -70,6 +71,7 @@ class VarietyLot extends Equatable {
|
|||
});
|
||||
|
||||
final String id;
|
||||
final LotType type;
|
||||
final int? harvestYear;
|
||||
final Quantity? quantity;
|
||||
final String? storageLocation;
|
||||
|
|
@ -82,6 +84,7 @@ class VarietyLot extends Equatable {
|
|||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
type,
|
||||
harvestYear,
|
||||
quantity,
|
||||
storageLocation,
|
||||
|
|
@ -216,6 +219,7 @@ class VarietyRepository {
|
|||
Future<String> addQuickVariety({
|
||||
required String label,
|
||||
String? category,
|
||||
LotType lotType = LotType.seed,
|
||||
Quantity? quantity,
|
||||
Uint8List? photoBytes,
|
||||
}) async {
|
||||
|
|
@ -246,8 +250,9 @@ class VarietyRepository {
|
|||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
type: Value(lotType),
|
||||
quantityKind: Value(quantity.kind.name),
|
||||
quantityPrecise: Value(quantity.precise),
|
||||
quantityPrecise: Value(quantity.count?.toDouble()),
|
||||
quantityLabel: Value(quantity.label),
|
||||
),
|
||||
);
|
||||
|
|
@ -423,6 +428,7 @@ class VarietyRepository {
|
|||
/// Adds a lot (a held batch) to a variety. Returns the new lot id.
|
||||
Future<String> addLot({
|
||||
required String varietyId,
|
||||
LotType type = LotType.seed,
|
||||
int? harvestYear,
|
||||
Quantity? quantity,
|
||||
String? storageLocation,
|
||||
|
|
@ -438,9 +444,10 @@ class VarietyRepository {
|
|||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
type: Value(type),
|
||||
harvestYear: Value(harvestYear),
|
||||
quantityKind: Value(quantity?.kind.name),
|
||||
quantityPrecise: Value(quantity?.precise),
|
||||
quantityPrecise: Value(quantity?.count?.toDouble()),
|
||||
quantityLabel: Value(quantity?.label),
|
||||
storageLocation: Value(storageLocation),
|
||||
),
|
||||
|
|
@ -496,13 +503,14 @@ class VarietyRepository {
|
|||
l.quantityLabel != null;
|
||||
return VarietyLot(
|
||||
id: l.id,
|
||||
type: l.type,
|
||||
harvestYear: l.harvestYear,
|
||||
storageLocation: l.storageLocation,
|
||||
germinationTests: germinationTests,
|
||||
quantity: hasQuantity
|
||||
? Quantity(
|
||||
kind: _parseKind(l.quantityKind),
|
||||
precise: l.quantityPrecise,
|
||||
count: l.quantityPrecise,
|
||||
label: l.quantityLabel,
|
||||
)
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -27,12 +27,16 @@ class AppDatabase extends _$AppDatabase {
|
|||
AppDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (m) async => m.createAll(),
|
||||
// Step-by-step upgrades (from1To2, …) are generated into
|
||||
// schema_versions.dart when schemaVersion is bumped. See data-model §5.
|
||||
onUpgrade: (m, from, to) async {
|
||||
// v2: lots can hold seeds or plants/seedlings (plantel).
|
||||
if (from < 2) {
|
||||
await m.addColumn(lots, lots.type);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2610,6 +2610,16 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
|||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<LotType, String> type =
|
||||
GeneratedColumn<String>(
|
||||
'type',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant('seed'),
|
||||
).withConverter<LotType>($LotsTable.$convertertype);
|
||||
static const VerificationMeta _harvestYearMeta = const VerificationMeta(
|
||||
'harvestYear',
|
||||
);
|
||||
|
|
@ -2695,6 +2705,7 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
|||
isDeleted,
|
||||
schemaRowVersion,
|
||||
varietyId,
|
||||
type,
|
||||
harvestYear,
|
||||
quantityKind,
|
||||
quantityPrecise,
|
||||
|
|
@ -2855,6 +2866,12 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
|||
DriftSqlType.string,
|
||||
data['${effectivePrefix}variety_id'],
|
||||
)!,
|
||||
type: $LotsTable.$convertertype.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}type'],
|
||||
)!,
|
||||
),
|
||||
harvestYear: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}harvest_year'],
|
||||
|
|
@ -2893,6 +2910,8 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> {
|
|||
return $LotsTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static JsonTypeConverter2<LotType, String, String> $convertertype =
|
||||
const EnumNameConverter<LotType>(LotType.values);
|
||||
static JsonTypeConverter2<OfferStatus, String, String> $converterofferStatus =
|
||||
const EnumNameConverter<OfferStatus>(OfferStatus.values);
|
||||
}
|
||||
|
|
@ -2905,6 +2924,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
final bool isDeleted;
|
||||
final int schemaRowVersion;
|
||||
final String varietyId;
|
||||
final LotType type;
|
||||
final int? harvestYear;
|
||||
final String? quantityKind;
|
||||
final double? quantityPrecise;
|
||||
|
|
@ -2920,6 +2940,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
required this.isDeleted,
|
||||
required this.schemaRowVersion,
|
||||
required this.varietyId,
|
||||
required this.type,
|
||||
this.harvestYear,
|
||||
this.quantityKind,
|
||||
this.quantityPrecise,
|
||||
|
|
@ -2938,6 +2959,9 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
map['is_deleted'] = Variable<bool>(isDeleted);
|
||||
map['schema_row_version'] = Variable<int>(schemaRowVersion);
|
||||
map['variety_id'] = Variable<String>(varietyId);
|
||||
{
|
||||
map['type'] = Variable<String>($LotsTable.$convertertype.toSql(type));
|
||||
}
|
||||
if (!nullToAbsent || harvestYear != null) {
|
||||
map['harvest_year'] = Variable<int>(harvestYear);
|
||||
}
|
||||
|
|
@ -2973,6 +2997,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
isDeleted: Value(isDeleted),
|
||||
schemaRowVersion: Value(schemaRowVersion),
|
||||
varietyId: Value(varietyId),
|
||||
type: Value(type),
|
||||
harvestYear: harvestYear == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(harvestYear),
|
||||
|
|
@ -3008,6 +3033,9 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
isDeleted: serializer.fromJson<bool>(json['isDeleted']),
|
||||
schemaRowVersion: serializer.fromJson<int>(json['schemaRowVersion']),
|
||||
varietyId: serializer.fromJson<String>(json['varietyId']),
|
||||
type: $LotsTable.$convertertype.fromJson(
|
||||
serializer.fromJson<String>(json['type']),
|
||||
),
|
||||
harvestYear: serializer.fromJson<int?>(json['harvestYear']),
|
||||
quantityKind: serializer.fromJson<String?>(json['quantityKind']),
|
||||
quantityPrecise: serializer.fromJson<double?>(json['quantityPrecise']),
|
||||
|
|
@ -3030,6 +3058,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
'isDeleted': serializer.toJson<bool>(isDeleted),
|
||||
'schemaRowVersion': serializer.toJson<int>(schemaRowVersion),
|
||||
'varietyId': serializer.toJson<String>(varietyId),
|
||||
'type': serializer.toJson<String>($LotsTable.$convertertype.toJson(type)),
|
||||
'harvestYear': serializer.toJson<int?>(harvestYear),
|
||||
'quantityKind': serializer.toJson<String?>(quantityKind),
|
||||
'quantityPrecise': serializer.toJson<double?>(quantityPrecise),
|
||||
|
|
@ -3050,6 +3079,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
bool? isDeleted,
|
||||
int? schemaRowVersion,
|
||||
String? varietyId,
|
||||
LotType? type,
|
||||
Value<int?> harvestYear = const Value.absent(),
|
||||
Value<String?> quantityKind = const Value.absent(),
|
||||
Value<double?> quantityPrecise = const Value.absent(),
|
||||
|
|
@ -3065,6 +3095,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
schemaRowVersion: schemaRowVersion ?? this.schemaRowVersion,
|
||||
varietyId: varietyId ?? this.varietyId,
|
||||
type: type ?? this.type,
|
||||
harvestYear: harvestYear.present ? harvestYear.value : this.harvestYear,
|
||||
quantityKind: quantityKind.present ? quantityKind.value : this.quantityKind,
|
||||
quantityPrecise: quantityPrecise.present
|
||||
|
|
@ -3092,6 +3123,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
? data.schemaRowVersion.value
|
||||
: this.schemaRowVersion,
|
||||
varietyId: data.varietyId.present ? data.varietyId.value : this.varietyId,
|
||||
type: data.type.present ? data.type.value : this.type,
|
||||
harvestYear: data.harvestYear.present
|
||||
? data.harvestYear.value
|
||||
: this.harvestYear,
|
||||
|
|
@ -3126,6 +3158,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
..write('isDeleted: $isDeleted, ')
|
||||
..write('schemaRowVersion: $schemaRowVersion, ')
|
||||
..write('varietyId: $varietyId, ')
|
||||
..write('type: $type, ')
|
||||
..write('harvestYear: $harvestYear, ')
|
||||
..write('quantityKind: $quantityKind, ')
|
||||
..write('quantityPrecise: $quantityPrecise, ')
|
||||
|
|
@ -3146,6 +3179,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
isDeleted,
|
||||
schemaRowVersion,
|
||||
varietyId,
|
||||
type,
|
||||
harvestYear,
|
||||
quantityKind,
|
||||
quantityPrecise,
|
||||
|
|
@ -3165,6 +3199,7 @@ class Lot extends DataClass implements Insertable<Lot> {
|
|||
other.isDeleted == this.isDeleted &&
|
||||
other.schemaRowVersion == this.schemaRowVersion &&
|
||||
other.varietyId == this.varietyId &&
|
||||
other.type == this.type &&
|
||||
other.harvestYear == this.harvestYear &&
|
||||
other.quantityKind == this.quantityKind &&
|
||||
other.quantityPrecise == this.quantityPrecise &&
|
||||
|
|
@ -3182,6 +3217,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
final Value<bool> isDeleted;
|
||||
final Value<int> schemaRowVersion;
|
||||
final Value<String> varietyId;
|
||||
final Value<LotType> type;
|
||||
final Value<int?> harvestYear;
|
||||
final Value<String?> quantityKind;
|
||||
final Value<double?> quantityPrecise;
|
||||
|
|
@ -3198,6 +3234,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
this.isDeleted = const Value.absent(),
|
||||
this.schemaRowVersion = const Value.absent(),
|
||||
this.varietyId = const Value.absent(),
|
||||
this.type = const Value.absent(),
|
||||
this.harvestYear = const Value.absent(),
|
||||
this.quantityKind = const Value.absent(),
|
||||
this.quantityPrecise = const Value.absent(),
|
||||
|
|
@ -3215,6 +3252,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
this.isDeleted = const Value.absent(),
|
||||
this.schemaRowVersion = const Value.absent(),
|
||||
required String varietyId,
|
||||
this.type = const Value.absent(),
|
||||
this.harvestYear = const Value.absent(),
|
||||
this.quantityKind = const Value.absent(),
|
||||
this.quantityPrecise = const Value.absent(),
|
||||
|
|
@ -3236,6 +3274,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
Expression<bool>? isDeleted,
|
||||
Expression<int>? schemaRowVersion,
|
||||
Expression<String>? varietyId,
|
||||
Expression<String>? type,
|
||||
Expression<int>? harvestYear,
|
||||
Expression<String>? quantityKind,
|
||||
Expression<double>? quantityPrecise,
|
||||
|
|
@ -3253,6 +3292,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
if (isDeleted != null) 'is_deleted': isDeleted,
|
||||
if (schemaRowVersion != null) 'schema_row_version': schemaRowVersion,
|
||||
if (varietyId != null) 'variety_id': varietyId,
|
||||
if (type != null) 'type': type,
|
||||
if (harvestYear != null) 'harvest_year': harvestYear,
|
||||
if (quantityKind != null) 'quantity_kind': quantityKind,
|
||||
if (quantityPrecise != null) 'quantity_precise': quantityPrecise,
|
||||
|
|
@ -3272,6 +3312,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
Value<bool>? isDeleted,
|
||||
Value<int>? schemaRowVersion,
|
||||
Value<String>? varietyId,
|
||||
Value<LotType>? type,
|
||||
Value<int?>? harvestYear,
|
||||
Value<String?>? quantityKind,
|
||||
Value<double?>? quantityPrecise,
|
||||
|
|
@ -3289,6 +3330,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
schemaRowVersion: schemaRowVersion ?? this.schemaRowVersion,
|
||||
varietyId: varietyId ?? this.varietyId,
|
||||
type: type ?? this.type,
|
||||
harvestYear: harvestYear ?? this.harvestYear,
|
||||
quantityKind: quantityKind ?? this.quantityKind,
|
||||
quantityPrecise: quantityPrecise ?? this.quantityPrecise,
|
||||
|
|
@ -3324,6 +3366,11 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
if (varietyId.present) {
|
||||
map['variety_id'] = Variable<String>(varietyId.value);
|
||||
}
|
||||
if (type.present) {
|
||||
map['type'] = Variable<String>(
|
||||
$LotsTable.$convertertype.toSql(type.value),
|
||||
);
|
||||
}
|
||||
if (harvestYear.present) {
|
||||
map['harvest_year'] = Variable<int>(harvestYear.value);
|
||||
}
|
||||
|
|
@ -3363,6 +3410,7 @@ class LotsCompanion extends UpdateCompanion<Lot> {
|
|||
..write('isDeleted: $isDeleted, ')
|
||||
..write('schemaRowVersion: $schemaRowVersion, ')
|
||||
..write('varietyId: $varietyId, ')
|
||||
..write('type: $type, ')
|
||||
..write('harvestYear: $harvestYear, ')
|
||||
..write('quantityKind: $quantityKind, ')
|
||||
..write('quantityPrecise: $quantityPrecise, ')
|
||||
|
|
@ -8138,6 +8186,7 @@ typedef $$LotsTableCreateCompanionBuilder =
|
|||
Value<bool> isDeleted,
|
||||
Value<int> schemaRowVersion,
|
||||
required String varietyId,
|
||||
Value<LotType> type,
|
||||
Value<int?> harvestYear,
|
||||
Value<String?> quantityKind,
|
||||
Value<double?> quantityPrecise,
|
||||
|
|
@ -8156,6 +8205,7 @@ typedef $$LotsTableUpdateCompanionBuilder =
|
|||
Value<bool> isDeleted,
|
||||
Value<int> schemaRowVersion,
|
||||
Value<String> varietyId,
|
||||
Value<LotType> type,
|
||||
Value<int?> harvestYear,
|
||||
Value<String?> quantityKind,
|
||||
Value<double?> quantityPrecise,
|
||||
|
|
@ -8209,6 +8259,12 @@ class $$LotsTableFilterComposer extends Composer<_$AppDatabase, $LotsTable> {
|
|||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnWithTypeConverterFilters<LotType, LotType, String> get type =>
|
||||
$composableBuilder(
|
||||
column: $table.type,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<int> get harvestYear => $composableBuilder(
|
||||
column: $table.harvestYear,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
|
|
@ -8289,6 +8345,11 @@ class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> {
|
|||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get type => $composableBuilder(
|
||||
column: $table.type,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<int> get harvestYear => $composableBuilder(
|
||||
column: $table.harvestYear,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
|
|
@ -8359,6 +8420,9 @@ class $$LotsTableAnnotationComposer
|
|||
GeneratedColumn<String> get varietyId =>
|
||||
$composableBuilder(column: $table.varietyId, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<LotType, String> get type =>
|
||||
$composableBuilder(column: $table.type, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get harvestYear => $composableBuilder(
|
||||
column: $table.harvestYear,
|
||||
builder: (column) => column,
|
||||
|
|
@ -8431,6 +8495,7 @@ class $$LotsTableTableManager
|
|||
Value<bool> isDeleted = const Value.absent(),
|
||||
Value<int> schemaRowVersion = const Value.absent(),
|
||||
Value<String> varietyId = const Value.absent(),
|
||||
Value<LotType> type = const Value.absent(),
|
||||
Value<int?> harvestYear = const Value.absent(),
|
||||
Value<String?> quantityKind = const Value.absent(),
|
||||
Value<double?> quantityPrecise = const Value.absent(),
|
||||
|
|
@ -8447,6 +8512,7 @@ class $$LotsTableTableManager
|
|||
isDeleted: isDeleted,
|
||||
schemaRowVersion: schemaRowVersion,
|
||||
varietyId: varietyId,
|
||||
type: type,
|
||||
harvestYear: harvestYear,
|
||||
quantityKind: quantityKind,
|
||||
quantityPrecise: quantityPrecise,
|
||||
|
|
@ -8465,6 +8531,7 @@ class $$LotsTableTableManager
|
|||
Value<bool> isDeleted = const Value.absent(),
|
||||
Value<int> schemaRowVersion = const Value.absent(),
|
||||
required String varietyId,
|
||||
Value<LotType> type = const Value.absent(),
|
||||
Value<int?> harvestYear = const Value.absent(),
|
||||
Value<String?> quantityKind = const Value.absent(),
|
||||
Value<double?> quantityPrecise = const Value.absent(),
|
||||
|
|
@ -8481,6 +8548,7 @@ class $$LotsTableTableManager
|
|||
isDeleted: isDeleted,
|
||||
schemaRowVersion: schemaRowVersion,
|
||||
varietyId: varietyId,
|
||||
type: type,
|
||||
harvestYear: harvestYear,
|
||||
quantityKind: quantityKind,
|
||||
quantityPrecise: quantityPrecise,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
// may only be appended, never renumbered or reused; when sync arrives, readers
|
||||
// must tolerate unknown values (map to a safe default). No sync yet in Block 1.
|
||||
|
||||
/// Whether a lot holds seeds or living plants/seedlings (plantel). Germination
|
||||
/// tests apply only to seed lots.
|
||||
enum LotType { seed, plant }
|
||||
|
||||
/// Per-lot visibility (data-model §2.3). Used by the future sharing layer.
|
||||
enum OfferStatus { private, shared, exchange, sell }
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ class SpeciesCommonNames extends Table with SyncColumns {
|
|||
/// Quantity (commons_core value type) is flattened into columns here.
|
||||
class Lots extends Table with SyncColumns {
|
||||
TextColumn get varietyId => text()();
|
||||
TextColumn get type =>
|
||||
textEnum<LotType>().withDefault(const Constant('seed'))();
|
||||
IntColumn get harvestYear => integer().nullable()();
|
||||
TextColumn get quantityKind => text().nullable()(); // QuantityKind.name
|
||||
RealColumn get quantityPrecise => real().nullable()();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit"
|
||||
"edit": "Edit",
|
||||
"type": "Type"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
|
|
@ -54,26 +55,38 @@
|
|||
"addLot": {
|
||||
"title": "Add lot",
|
||||
"year": "Harvest year",
|
||||
"quantity": "Quantity"
|
||||
"quantity": "How much?",
|
||||
"amount": "Amount"
|
||||
},
|
||||
"quantityKind": {
|
||||
"lotType": {
|
||||
"seed": "Seeds",
|
||||
"plant": "Plants"
|
||||
},
|
||||
"unit": {
|
||||
"aFew": "a few",
|
||||
"some": "some",
|
||||
"plenty": "plenty",
|
||||
"handful": "a handful",
|
||||
"pinch": "a pinch",
|
||||
"jar": "a jar",
|
||||
"packet": "a packet",
|
||||
"cob": "a cob",
|
||||
"head": "a head",
|
||||
"pod": "a pod",
|
||||
"ear": "an ear",
|
||||
"fruit": "a fruit",
|
||||
"bulb": "a bulb",
|
||||
"tuber": "a tuber",
|
||||
"seedHead": "a seed head",
|
||||
"bunch": "a bunch",
|
||||
"grams": "grams",
|
||||
"count": "count"
|
||||
"handful": { "singular": "handful", "plural": "handfuls" },
|
||||
"teaspoon": { "singular": "teaspoon", "plural": "teaspoons" },
|
||||
"spoon": { "singular": "spoon", "plural": "spoons" },
|
||||
"cup": { "singular": "cup", "plural": "cups" },
|
||||
"jar": { "singular": "jar", "plural": "jars" },
|
||||
"sack": { "singular": "sack", "plural": "sacks" },
|
||||
"packet": { "singular": "packet", "plural": "packets" },
|
||||
"cob": { "singular": "cob", "plural": "cobs" },
|
||||
"pod": { "singular": "pod", "plural": "pods" },
|
||||
"ear": { "singular": "ear", "plural": "ears" },
|
||||
"head": { "singular": "head", "plural": "heads" },
|
||||
"fruit": { "singular": "fruit", "plural": "fruits" },
|
||||
"bulb": { "singular": "bulb", "plural": "bulbs" },
|
||||
"tuber": { "singular": "tuber", "plural": "tubers" },
|
||||
"seedHead": { "singular": "seed head", "plural": "seed heads" },
|
||||
"bunch": { "singular": "bunch", "plural": "bunches" },
|
||||
"plant": { "singular": "plant", "plural": "plants" },
|
||||
"pot": { "singular": "pot", "plural": "pots" },
|
||||
"tray": { "singular": "tray", "plural": "trays" },
|
||||
"grams": { "singular": "gram", "plural": "grams" },
|
||||
"count": { "singular": "seed", "plural": "seeds" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar"
|
||||
"edit": "Editar",
|
||||
"type": "Tipo"
|
||||
},
|
||||
"inventory": {
|
||||
"title": "Inventario",
|
||||
|
|
@ -54,26 +55,38 @@
|
|||
"addLot": {
|
||||
"title": "Añadir lote",
|
||||
"year": "Año de cosecha",
|
||||
"quantity": "Cantidad"
|
||||
"quantity": "¿Cuánta?",
|
||||
"amount": "Cantidad"
|
||||
},
|
||||
"quantityKind": {
|
||||
"lotType": {
|
||||
"seed": "Semillas",
|
||||
"plant": "Plantel"
|
||||
},
|
||||
"unit": {
|
||||
"aFew": "unas pocas",
|
||||
"some": "algunas",
|
||||
"plenty": "muchas",
|
||||
"handful": "un puñado",
|
||||
"pinch": "una pizca",
|
||||
"jar": "un tarro",
|
||||
"packet": "un sobre",
|
||||
"cob": "una mazorca",
|
||||
"head": "una cabezuela",
|
||||
"pod": "una vaina",
|
||||
"ear": "una espiga",
|
||||
"fruit": "un fruto",
|
||||
"bulb": "un bulbo",
|
||||
"tuber": "un tubérculo",
|
||||
"seedHead": "una cabeza de semillas",
|
||||
"bunch": "un manojo",
|
||||
"grams": "gramos",
|
||||
"count": "unidades"
|
||||
"handful": { "singular": "puñado", "plural": "puñados" },
|
||||
"teaspoon": { "singular": "cucharadita", "plural": "cucharaditas" },
|
||||
"spoon": { "singular": "cuchara", "plural": "cucharas" },
|
||||
"cup": { "singular": "taza", "plural": "tazas" },
|
||||
"jar": { "singular": "bote", "plural": "botes" },
|
||||
"sack": { "singular": "saco", "plural": "sacos" },
|
||||
"packet": { "singular": "sobre", "plural": "sobres" },
|
||||
"cob": { "singular": "mazorca", "plural": "mazorcas" },
|
||||
"pod": { "singular": "vaina", "plural": "vainas" },
|
||||
"ear": { "singular": "espiga", "plural": "espigas" },
|
||||
"head": { "singular": "cabezuela", "plural": "cabezuelas" },
|
||||
"fruit": { "singular": "fruto", "plural": "frutos" },
|
||||
"bulb": { "singular": "bulbo", "plural": "bulbos" },
|
||||
"tuber": { "singular": "tubérculo", "plural": "tubérculos" },
|
||||
"seedHead": { "singular": "cabeza de semillas", "plural": "cabezas de semillas" },
|
||||
"bunch": { "singular": "manojo", "plural": "manojos" },
|
||||
"plant": { "singular": "planta", "plural": "plantas" },
|
||||
"pot": { "singular": "maceta", "plural": "macetas" },
|
||||
"tray": { "singular": "bandeja", "plural": "bandejas" },
|
||||
"grams": { "singular": "gramo", "plural": "gramos" },
|
||||
"count": { "singular": "semilla", "plural": "semillas" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 2
|
||||
/// Strings: 118 (59 per locale)
|
||||
/// Strings: 182 (91 per locale)
|
||||
///
|
||||
/// Built on 2026-07-07 at 19:58 UTC
|
||||
/// Built on 2026-07-08 at 08:39 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||
late final Translations$germination$en germination = Translations$germination$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);
|
||||
late final Translations$lotType$en lotType = Translations$lotType$en.internal(_root);
|
||||
late final Translations$unit$en unit = Translations$unit$en.internal(_root);
|
||||
}
|
||||
|
||||
// Path: app
|
||||
|
|
@ -82,6 +83,9 @@ class Translations$common$en {
|
|||
|
||||
/// en: 'Edit'
|
||||
String get edit => 'Edit';
|
||||
|
||||
/// en: 'Type'
|
||||
String get type => 'Type';
|
||||
}
|
||||
|
||||
// Path: inventory
|
||||
|
|
@ -242,13 +246,31 @@ class Translations$addLot$en {
|
|||
/// en: 'Harvest year'
|
||||
String get year => 'Harvest year';
|
||||
|
||||
/// en: 'Quantity'
|
||||
String get quantity => 'Quantity';
|
||||
/// en: 'How much?'
|
||||
String get quantity => 'How much?';
|
||||
|
||||
/// en: 'Amount'
|
||||
String get amount => 'Amount';
|
||||
}
|
||||
|
||||
// Path: quantityKind
|
||||
class Translations$quantityKind$en {
|
||||
Translations$quantityKind$en.internal(this._root);
|
||||
// Path: lotType
|
||||
class Translations$lotType$en {
|
||||
Translations$lotType$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Seeds'
|
||||
String get seed => 'Seeds';
|
||||
|
||||
/// en: 'Plants'
|
||||
String get plant => 'Plants';
|
||||
}
|
||||
|
||||
// Path: unit
|
||||
class Translations$unit$en {
|
||||
Translations$unit$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
|
|
@ -263,50 +285,345 @@ class Translations$quantityKind$en {
|
|||
/// en: 'plenty'
|
||||
String get plenty => 'plenty';
|
||||
|
||||
/// en: 'a handful'
|
||||
String get handful => 'a handful';
|
||||
|
||||
/// en: 'a pinch'
|
||||
String get pinch => 'a pinch';
|
||||
|
||||
/// en: 'a jar'
|
||||
String get jar => 'a jar';
|
||||
late final Translations$unit$handful$en handful = Translations$unit$handful$en.internal(_root);
|
||||
late final Translations$unit$teaspoon$en teaspoon = Translations$unit$teaspoon$en.internal(_root);
|
||||
late final Translations$unit$spoon$en spoon = Translations$unit$spoon$en.internal(_root);
|
||||
late final Translations$unit$cup$en cup = Translations$unit$cup$en.internal(_root);
|
||||
late final Translations$unit$jar$en jar = Translations$unit$jar$en.internal(_root);
|
||||
late final Translations$unit$sack$en sack = Translations$unit$sack$en.internal(_root);
|
||||
late final Translations$unit$packet$en packet = Translations$unit$packet$en.internal(_root);
|
||||
late final Translations$unit$cob$en cob = Translations$unit$cob$en.internal(_root);
|
||||
late final Translations$unit$pod$en pod = Translations$unit$pod$en.internal(_root);
|
||||
late final Translations$unit$ear$en ear = Translations$unit$ear$en.internal(_root);
|
||||
late final Translations$unit$head$en head = Translations$unit$head$en.internal(_root);
|
||||
late final Translations$unit$fruit$en fruit = Translations$unit$fruit$en.internal(_root);
|
||||
late final Translations$unit$bulb$en bulb = Translations$unit$bulb$en.internal(_root);
|
||||
late final Translations$unit$tuber$en tuber = Translations$unit$tuber$en.internal(_root);
|
||||
late final Translations$unit$seedHead$en seedHead = Translations$unit$seedHead$en.internal(_root);
|
||||
late final Translations$unit$bunch$en bunch = Translations$unit$bunch$en.internal(_root);
|
||||
late final Translations$unit$plant$en plant = Translations$unit$plant$en.internal(_root);
|
||||
late final Translations$unit$pot$en pot = Translations$unit$pot$en.internal(_root);
|
||||
late final Translations$unit$tray$en tray = Translations$unit$tray$en.internal(_root);
|
||||
late final Translations$unit$grams$en grams = Translations$unit$grams$en.internal(_root);
|
||||
late final Translations$unit$count$en count = Translations$unit$count$en.internal(_root);
|
||||
}
|
||||
|
||||
/// en: 'a packet'
|
||||
String get packet => 'a packet';
|
||||
// Path: unit.handful
|
||||
class Translations$unit$handful$en {
|
||||
Translations$unit$handful$en.internal(this._root);
|
||||
|
||||
/// en: 'a cob'
|
||||
String get cob => 'a cob';
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
/// en: 'a head'
|
||||
String get head => 'a head';
|
||||
// Translations
|
||||
|
||||
/// en: 'a pod'
|
||||
String get pod => 'a pod';
|
||||
/// en: 'handful'
|
||||
String get singular => 'handful';
|
||||
|
||||
/// en: 'an ear'
|
||||
String get ear => 'an ear';
|
||||
/// en: 'handfuls'
|
||||
String get plural => 'handfuls';
|
||||
}
|
||||
|
||||
/// en: 'a fruit'
|
||||
String get fruit => 'a fruit';
|
||||
// Path: unit.teaspoon
|
||||
class Translations$unit$teaspoon$en {
|
||||
Translations$unit$teaspoon$en.internal(this._root);
|
||||
|
||||
/// en: 'a bulb'
|
||||
String get bulb => 'a bulb';
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
/// en: 'a tuber'
|
||||
String get tuber => 'a tuber';
|
||||
// Translations
|
||||
|
||||
/// en: 'a seed head'
|
||||
String get seedHead => 'a seed head';
|
||||
/// en: 'teaspoon'
|
||||
String get singular => 'teaspoon';
|
||||
|
||||
/// en: 'a bunch'
|
||||
String get bunch => 'a bunch';
|
||||
/// en: 'teaspoons'
|
||||
String get plural => 'teaspoons';
|
||||
}
|
||||
|
||||
// Path: unit.spoon
|
||||
class Translations$unit$spoon$en {
|
||||
Translations$unit$spoon$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'spoon'
|
||||
String get singular => 'spoon';
|
||||
|
||||
/// en: 'spoons'
|
||||
String get plural => 'spoons';
|
||||
}
|
||||
|
||||
// Path: unit.cup
|
||||
class Translations$unit$cup$en {
|
||||
Translations$unit$cup$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'cup'
|
||||
String get singular => 'cup';
|
||||
|
||||
/// en: 'cups'
|
||||
String get plural => 'cups';
|
||||
}
|
||||
|
||||
// Path: unit.jar
|
||||
class Translations$unit$jar$en {
|
||||
Translations$unit$jar$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'jar'
|
||||
String get singular => 'jar';
|
||||
|
||||
/// en: 'jars'
|
||||
String get plural => 'jars';
|
||||
}
|
||||
|
||||
// Path: unit.sack
|
||||
class Translations$unit$sack$en {
|
||||
Translations$unit$sack$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'sack'
|
||||
String get singular => 'sack';
|
||||
|
||||
/// en: 'sacks'
|
||||
String get plural => 'sacks';
|
||||
}
|
||||
|
||||
// Path: unit.packet
|
||||
class Translations$unit$packet$en {
|
||||
Translations$unit$packet$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'packet'
|
||||
String get singular => 'packet';
|
||||
|
||||
/// en: 'packets'
|
||||
String get plural => 'packets';
|
||||
}
|
||||
|
||||
// Path: unit.cob
|
||||
class Translations$unit$cob$en {
|
||||
Translations$unit$cob$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'cob'
|
||||
String get singular => 'cob';
|
||||
|
||||
/// en: 'cobs'
|
||||
String get plural => 'cobs';
|
||||
}
|
||||
|
||||
// Path: unit.pod
|
||||
class Translations$unit$pod$en {
|
||||
Translations$unit$pod$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'pod'
|
||||
String get singular => 'pod';
|
||||
|
||||
/// en: 'pods'
|
||||
String get plural => 'pods';
|
||||
}
|
||||
|
||||
// Path: unit.ear
|
||||
class Translations$unit$ear$en {
|
||||
Translations$unit$ear$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'ear'
|
||||
String get singular => 'ear';
|
||||
|
||||
/// en: 'ears'
|
||||
String get plural => 'ears';
|
||||
}
|
||||
|
||||
// Path: unit.head
|
||||
class Translations$unit$head$en {
|
||||
Translations$unit$head$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'head'
|
||||
String get singular => 'head';
|
||||
|
||||
/// en: 'heads'
|
||||
String get plural => 'heads';
|
||||
}
|
||||
|
||||
// Path: unit.fruit
|
||||
class Translations$unit$fruit$en {
|
||||
Translations$unit$fruit$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'fruit'
|
||||
String get singular => 'fruit';
|
||||
|
||||
/// en: 'fruits'
|
||||
String get plural => 'fruits';
|
||||
}
|
||||
|
||||
// Path: unit.bulb
|
||||
class Translations$unit$bulb$en {
|
||||
Translations$unit$bulb$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'bulb'
|
||||
String get singular => 'bulb';
|
||||
|
||||
/// en: 'bulbs'
|
||||
String get plural => 'bulbs';
|
||||
}
|
||||
|
||||
// Path: unit.tuber
|
||||
class Translations$unit$tuber$en {
|
||||
Translations$unit$tuber$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'tuber'
|
||||
String get singular => 'tuber';
|
||||
|
||||
/// en: 'tubers'
|
||||
String get plural => 'tubers';
|
||||
}
|
||||
|
||||
// Path: unit.seedHead
|
||||
class Translations$unit$seedHead$en {
|
||||
Translations$unit$seedHead$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'seed head'
|
||||
String get singular => 'seed head';
|
||||
|
||||
/// en: 'seed heads'
|
||||
String get plural => 'seed heads';
|
||||
}
|
||||
|
||||
// Path: unit.bunch
|
||||
class Translations$unit$bunch$en {
|
||||
Translations$unit$bunch$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'bunch'
|
||||
String get singular => 'bunch';
|
||||
|
||||
/// en: 'bunches'
|
||||
String get plural => 'bunches';
|
||||
}
|
||||
|
||||
// Path: unit.plant
|
||||
class Translations$unit$plant$en {
|
||||
Translations$unit$plant$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'plant'
|
||||
String get singular => 'plant';
|
||||
|
||||
/// en: 'plants'
|
||||
String get plural => 'plants';
|
||||
}
|
||||
|
||||
// Path: unit.pot
|
||||
class Translations$unit$pot$en {
|
||||
Translations$unit$pot$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'pot'
|
||||
String get singular => 'pot';
|
||||
|
||||
/// en: 'pots'
|
||||
String get plural => 'pots';
|
||||
}
|
||||
|
||||
// Path: unit.tray
|
||||
class Translations$unit$tray$en {
|
||||
Translations$unit$tray$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'tray'
|
||||
String get singular => 'tray';
|
||||
|
||||
/// en: 'trays'
|
||||
String get plural => 'trays';
|
||||
}
|
||||
|
||||
// Path: unit.grams
|
||||
class Translations$unit$grams$en {
|
||||
Translations$unit$grams$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'gram'
|
||||
String get singular => 'gram';
|
||||
|
||||
/// en: 'grams'
|
||||
String get grams => 'grams';
|
||||
String get plural => 'grams';
|
||||
}
|
||||
|
||||
/// en: 'count'
|
||||
String get count => 'count';
|
||||
// Path: unit.count
|
||||
class Translations$unit$count$en {
|
||||
Translations$unit$count$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'seed'
|
||||
String get singular => 'seed';
|
||||
|
||||
/// en: 'seeds'
|
||||
String get plural => 'seeds';
|
||||
}
|
||||
|
||||
/// The flat map containing all translations for locale <en>.
|
||||
|
|
@ -322,6 +639,7 @@ extension on Translations {
|
|||
'common.cancel' => 'Cancel',
|
||||
'common.delete' => 'Delete',
|
||||
'common.edit' => 'Edit',
|
||||
'common.type' => 'Type',
|
||||
'inventory.title' => 'Inventory',
|
||||
'inventory.searchHint' => 'Search seeds',
|
||||
'inventory.empty' => 'No seeds yet. Tap + to add your first.',
|
||||
|
|
@ -357,25 +675,56 @@ extension on Translations {
|
|||
'editVariety.speciesHint' => 'Search a species…',
|
||||
'addLot.title' => 'Add lot',
|
||||
'addLot.year' => 'Harvest year',
|
||||
'addLot.quantity' => 'Quantity',
|
||||
'quantityKind.aFew' => 'a few',
|
||||
'quantityKind.some' => 'some',
|
||||
'quantityKind.plenty' => 'plenty',
|
||||
'quantityKind.handful' => 'a handful',
|
||||
'quantityKind.pinch' => 'a pinch',
|
||||
'quantityKind.jar' => 'a jar',
|
||||
'quantityKind.packet' => 'a packet',
|
||||
'quantityKind.cob' => 'a cob',
|
||||
'quantityKind.head' => 'a head',
|
||||
'quantityKind.pod' => 'a pod',
|
||||
'quantityKind.ear' => 'an ear',
|
||||
'quantityKind.fruit' => 'a fruit',
|
||||
'quantityKind.bulb' => 'a bulb',
|
||||
'quantityKind.tuber' => 'a tuber',
|
||||
'quantityKind.seedHead' => 'a seed head',
|
||||
'quantityKind.bunch' => 'a bunch',
|
||||
'quantityKind.grams' => 'grams',
|
||||
'quantityKind.count' => 'count',
|
||||
'addLot.quantity' => 'How much?',
|
||||
'addLot.amount' => 'Amount',
|
||||
'lotType.seed' => 'Seeds',
|
||||
'lotType.plant' => 'Plants',
|
||||
'unit.aFew' => 'a few',
|
||||
'unit.some' => 'some',
|
||||
'unit.plenty' => 'plenty',
|
||||
'unit.pinch' => 'a pinch',
|
||||
'unit.handful.singular' => 'handful',
|
||||
'unit.handful.plural' => 'handfuls',
|
||||
'unit.teaspoon.singular' => 'teaspoon',
|
||||
'unit.teaspoon.plural' => 'teaspoons',
|
||||
'unit.spoon.singular' => 'spoon',
|
||||
'unit.spoon.plural' => 'spoons',
|
||||
'unit.cup.singular' => 'cup',
|
||||
'unit.cup.plural' => 'cups',
|
||||
'unit.jar.singular' => 'jar',
|
||||
'unit.jar.plural' => 'jars',
|
||||
'unit.sack.singular' => 'sack',
|
||||
'unit.sack.plural' => 'sacks',
|
||||
'unit.packet.singular' => 'packet',
|
||||
'unit.packet.plural' => 'packets',
|
||||
'unit.cob.singular' => 'cob',
|
||||
'unit.cob.plural' => 'cobs',
|
||||
'unit.pod.singular' => 'pod',
|
||||
'unit.pod.plural' => 'pods',
|
||||
'unit.ear.singular' => 'ear',
|
||||
'unit.ear.plural' => 'ears',
|
||||
'unit.head.singular' => 'head',
|
||||
'unit.head.plural' => 'heads',
|
||||
'unit.fruit.singular' => 'fruit',
|
||||
'unit.fruit.plural' => 'fruits',
|
||||
'unit.bulb.singular' => 'bulb',
|
||||
'unit.bulb.plural' => 'bulbs',
|
||||
'unit.tuber.singular' => 'tuber',
|
||||
'unit.tuber.plural' => 'tubers',
|
||||
'unit.seedHead.singular' => 'seed head',
|
||||
'unit.seedHead.plural' => 'seed heads',
|
||||
'unit.bunch.singular' => 'bunch',
|
||||
'unit.bunch.plural' => 'bunches',
|
||||
'unit.plant.singular' => 'plant',
|
||||
'unit.plant.plural' => 'plants',
|
||||
'unit.pot.singular' => 'pot',
|
||||
'unit.pot.plural' => 'pots',
|
||||
'unit.tray.singular' => 'tray',
|
||||
'unit.tray.plural' => 'trays',
|
||||
'unit.grams.singular' => 'gram',
|
||||
'unit.grams.plural' => 'grams',
|
||||
'unit.count.singular' => 'seed',
|
||||
'unit.count.plural' => 'seeds',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@override late final _Translations$germination$es germination = _Translations$germination$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);
|
||||
@override late final _Translations$lotType$es lotType = _Translations$lotType$es._(_root);
|
||||
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
|
||||
}
|
||||
|
||||
// Path: app
|
||||
|
|
@ -71,6 +72,7 @@ class _Translations$common$es extends Translations$common$en {
|
|||
@override String get cancel => 'Cancelar';
|
||||
@override String get delete => 'Eliminar';
|
||||
@override String get edit => 'Editar';
|
||||
@override String get type => 'Tipo';
|
||||
}
|
||||
|
||||
// Path: inventory
|
||||
|
|
@ -160,12 +162,24 @@ class _Translations$addLot$es extends Translations$addLot$en {
|
|||
// Translations
|
||||
@override String get title => 'Añadir lote';
|
||||
@override String get year => 'Año de cosecha';
|
||||
@override String get quantity => 'Cantidad';
|
||||
@override String get quantity => '¿Cuánta?';
|
||||
@override String get amount => 'Cantidad';
|
||||
}
|
||||
|
||||
// Path: quantityKind
|
||||
class _Translations$quantityKind$es extends Translations$quantityKind$en {
|
||||
_Translations$quantityKind$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
// Path: lotType
|
||||
class _Translations$lotType$es extends Translations$lotType$en {
|
||||
_Translations$lotType$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get seed => 'Semillas';
|
||||
@override String get plant => 'Plantel';
|
||||
}
|
||||
|
||||
// Path: unit
|
||||
class _Translations$unit$es extends Translations$unit$en {
|
||||
_Translations$unit$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
|
|
@ -173,21 +187,259 @@ class _Translations$quantityKind$es extends Translations$quantityKind$en {
|
|||
@override String get aFew => 'unas pocas';
|
||||
@override String get some => 'algunas';
|
||||
@override String get plenty => 'muchas';
|
||||
@override String get handful => 'un puñado';
|
||||
@override String get pinch => 'una pizca';
|
||||
@override String get jar => 'un tarro';
|
||||
@override String get packet => 'un sobre';
|
||||
@override String get cob => 'una mazorca';
|
||||
@override String get head => 'una cabezuela';
|
||||
@override String get pod => 'una vaina';
|
||||
@override String get ear => 'una espiga';
|
||||
@override String get fruit => 'un fruto';
|
||||
@override String get bulb => 'un bulbo';
|
||||
@override String get tuber => 'un tubérculo';
|
||||
@override String get seedHead => 'una cabeza de semillas';
|
||||
@override String get bunch => 'un manojo';
|
||||
@override String get grams => 'gramos';
|
||||
@override String get count => 'unidades';
|
||||
@override late final _Translations$unit$handful$es handful = _Translations$unit$handful$es._(_root);
|
||||
@override late final _Translations$unit$teaspoon$es teaspoon = _Translations$unit$teaspoon$es._(_root);
|
||||
@override late final _Translations$unit$spoon$es spoon = _Translations$unit$spoon$es._(_root);
|
||||
@override late final _Translations$unit$cup$es cup = _Translations$unit$cup$es._(_root);
|
||||
@override late final _Translations$unit$jar$es jar = _Translations$unit$jar$es._(_root);
|
||||
@override late final _Translations$unit$sack$es sack = _Translations$unit$sack$es._(_root);
|
||||
@override late final _Translations$unit$packet$es packet = _Translations$unit$packet$es._(_root);
|
||||
@override late final _Translations$unit$cob$es cob = _Translations$unit$cob$es._(_root);
|
||||
@override late final _Translations$unit$pod$es pod = _Translations$unit$pod$es._(_root);
|
||||
@override late final _Translations$unit$ear$es ear = _Translations$unit$ear$es._(_root);
|
||||
@override late final _Translations$unit$head$es head = _Translations$unit$head$es._(_root);
|
||||
@override late final _Translations$unit$fruit$es fruit = _Translations$unit$fruit$es._(_root);
|
||||
@override late final _Translations$unit$bulb$es bulb = _Translations$unit$bulb$es._(_root);
|
||||
@override late final _Translations$unit$tuber$es tuber = _Translations$unit$tuber$es._(_root);
|
||||
@override late final _Translations$unit$seedHead$es seedHead = _Translations$unit$seedHead$es._(_root);
|
||||
@override late final _Translations$unit$bunch$es bunch = _Translations$unit$bunch$es._(_root);
|
||||
@override late final _Translations$unit$plant$es plant = _Translations$unit$plant$es._(_root);
|
||||
@override late final _Translations$unit$pot$es pot = _Translations$unit$pot$es._(_root);
|
||||
@override late final _Translations$unit$tray$es tray = _Translations$unit$tray$es._(_root);
|
||||
@override late final _Translations$unit$grams$es grams = _Translations$unit$grams$es._(_root);
|
||||
@override late final _Translations$unit$count$es count = _Translations$unit$count$es._(_root);
|
||||
}
|
||||
|
||||
// Path: unit.handful
|
||||
class _Translations$unit$handful$es extends Translations$unit$handful$en {
|
||||
_Translations$unit$handful$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'puñado';
|
||||
@override String get plural => 'puñados';
|
||||
}
|
||||
|
||||
// Path: unit.teaspoon
|
||||
class _Translations$unit$teaspoon$es extends Translations$unit$teaspoon$en {
|
||||
_Translations$unit$teaspoon$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'cucharadita';
|
||||
@override String get plural => 'cucharaditas';
|
||||
}
|
||||
|
||||
// Path: unit.spoon
|
||||
class _Translations$unit$spoon$es extends Translations$unit$spoon$en {
|
||||
_Translations$unit$spoon$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'cuchara';
|
||||
@override String get plural => 'cucharas';
|
||||
}
|
||||
|
||||
// Path: unit.cup
|
||||
class _Translations$unit$cup$es extends Translations$unit$cup$en {
|
||||
_Translations$unit$cup$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'taza';
|
||||
@override String get plural => 'tazas';
|
||||
}
|
||||
|
||||
// Path: unit.jar
|
||||
class _Translations$unit$jar$es extends Translations$unit$jar$en {
|
||||
_Translations$unit$jar$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'bote';
|
||||
@override String get plural => 'botes';
|
||||
}
|
||||
|
||||
// Path: unit.sack
|
||||
class _Translations$unit$sack$es extends Translations$unit$sack$en {
|
||||
_Translations$unit$sack$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'saco';
|
||||
@override String get plural => 'sacos';
|
||||
}
|
||||
|
||||
// Path: unit.packet
|
||||
class _Translations$unit$packet$es extends Translations$unit$packet$en {
|
||||
_Translations$unit$packet$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'sobre';
|
||||
@override String get plural => 'sobres';
|
||||
}
|
||||
|
||||
// Path: unit.cob
|
||||
class _Translations$unit$cob$es extends Translations$unit$cob$en {
|
||||
_Translations$unit$cob$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'mazorca';
|
||||
@override String get plural => 'mazorcas';
|
||||
}
|
||||
|
||||
// Path: unit.pod
|
||||
class _Translations$unit$pod$es extends Translations$unit$pod$en {
|
||||
_Translations$unit$pod$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'vaina';
|
||||
@override String get plural => 'vainas';
|
||||
}
|
||||
|
||||
// Path: unit.ear
|
||||
class _Translations$unit$ear$es extends Translations$unit$ear$en {
|
||||
_Translations$unit$ear$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'espiga';
|
||||
@override String get plural => 'espigas';
|
||||
}
|
||||
|
||||
// Path: unit.head
|
||||
class _Translations$unit$head$es extends Translations$unit$head$en {
|
||||
_Translations$unit$head$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'cabezuela';
|
||||
@override String get plural => 'cabezuelas';
|
||||
}
|
||||
|
||||
// Path: unit.fruit
|
||||
class _Translations$unit$fruit$es extends Translations$unit$fruit$en {
|
||||
_Translations$unit$fruit$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'fruto';
|
||||
@override String get plural => 'frutos';
|
||||
}
|
||||
|
||||
// Path: unit.bulb
|
||||
class _Translations$unit$bulb$es extends Translations$unit$bulb$en {
|
||||
_Translations$unit$bulb$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'bulbo';
|
||||
@override String get plural => 'bulbos';
|
||||
}
|
||||
|
||||
// Path: unit.tuber
|
||||
class _Translations$unit$tuber$es extends Translations$unit$tuber$en {
|
||||
_Translations$unit$tuber$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'tubérculo';
|
||||
@override String get plural => 'tubérculos';
|
||||
}
|
||||
|
||||
// Path: unit.seedHead
|
||||
class _Translations$unit$seedHead$es extends Translations$unit$seedHead$en {
|
||||
_Translations$unit$seedHead$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'cabeza de semillas';
|
||||
@override String get plural => 'cabezas de semillas';
|
||||
}
|
||||
|
||||
// Path: unit.bunch
|
||||
class _Translations$unit$bunch$es extends Translations$unit$bunch$en {
|
||||
_Translations$unit$bunch$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'manojo';
|
||||
@override String get plural => 'manojos';
|
||||
}
|
||||
|
||||
// Path: unit.plant
|
||||
class _Translations$unit$plant$es extends Translations$unit$plant$en {
|
||||
_Translations$unit$plant$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'planta';
|
||||
@override String get plural => 'plantas';
|
||||
}
|
||||
|
||||
// Path: unit.pot
|
||||
class _Translations$unit$pot$es extends Translations$unit$pot$en {
|
||||
_Translations$unit$pot$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'maceta';
|
||||
@override String get plural => 'macetas';
|
||||
}
|
||||
|
||||
// Path: unit.tray
|
||||
class _Translations$unit$tray$es extends Translations$unit$tray$en {
|
||||
_Translations$unit$tray$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'bandeja';
|
||||
@override String get plural => 'bandejas';
|
||||
}
|
||||
|
||||
// Path: unit.grams
|
||||
class _Translations$unit$grams$es extends Translations$unit$grams$en {
|
||||
_Translations$unit$grams$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'gramo';
|
||||
@override String get plural => 'gramos';
|
||||
}
|
||||
|
||||
// Path: unit.count
|
||||
class _Translations$unit$count$es extends Translations$unit$count$en {
|
||||
_Translations$unit$count$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get singular => 'semilla';
|
||||
@override String get plural => 'semillas';
|
||||
}
|
||||
|
||||
/// The flat map containing all translations for locale <es>.
|
||||
|
|
@ -203,6 +455,7 @@ extension on TranslationsEs {
|
|||
'common.cancel' => 'Cancelar',
|
||||
'common.delete' => 'Eliminar',
|
||||
'common.edit' => 'Editar',
|
||||
'common.type' => 'Tipo',
|
||||
'inventory.title' => 'Inventario',
|
||||
'inventory.searchHint' => 'Buscar semillas',
|
||||
'inventory.empty' => 'Aún no hay semillas. Toca + para añadir la primera.',
|
||||
|
|
@ -238,25 +491,56 @@ extension on TranslationsEs {
|
|||
'editVariety.speciesHint' => 'Buscar una especie…',
|
||||
'addLot.title' => 'Añadir lote',
|
||||
'addLot.year' => 'Año de cosecha',
|
||||
'addLot.quantity' => 'Cantidad',
|
||||
'quantityKind.aFew' => 'unas pocas',
|
||||
'quantityKind.some' => 'algunas',
|
||||
'quantityKind.plenty' => 'muchas',
|
||||
'quantityKind.handful' => 'un puñado',
|
||||
'quantityKind.pinch' => 'una pizca',
|
||||
'quantityKind.jar' => 'un tarro',
|
||||
'quantityKind.packet' => 'un sobre',
|
||||
'quantityKind.cob' => 'una mazorca',
|
||||
'quantityKind.head' => 'una cabezuela',
|
||||
'quantityKind.pod' => 'una vaina',
|
||||
'quantityKind.ear' => 'una espiga',
|
||||
'quantityKind.fruit' => 'un fruto',
|
||||
'quantityKind.bulb' => 'un bulbo',
|
||||
'quantityKind.tuber' => 'un tubérculo',
|
||||
'quantityKind.seedHead' => 'una cabeza de semillas',
|
||||
'quantityKind.bunch' => 'un manojo',
|
||||
'quantityKind.grams' => 'gramos',
|
||||
'quantityKind.count' => 'unidades',
|
||||
'addLot.quantity' => '¿Cuánta?',
|
||||
'addLot.amount' => 'Cantidad',
|
||||
'lotType.seed' => 'Semillas',
|
||||
'lotType.plant' => 'Plantel',
|
||||
'unit.aFew' => 'unas pocas',
|
||||
'unit.some' => 'algunas',
|
||||
'unit.plenty' => 'muchas',
|
||||
'unit.pinch' => 'una pizca',
|
||||
'unit.handful.singular' => 'puñado',
|
||||
'unit.handful.plural' => 'puñados',
|
||||
'unit.teaspoon.singular' => 'cucharadita',
|
||||
'unit.teaspoon.plural' => 'cucharaditas',
|
||||
'unit.spoon.singular' => 'cuchara',
|
||||
'unit.spoon.plural' => 'cucharas',
|
||||
'unit.cup.singular' => 'taza',
|
||||
'unit.cup.plural' => 'tazas',
|
||||
'unit.jar.singular' => 'bote',
|
||||
'unit.jar.plural' => 'botes',
|
||||
'unit.sack.singular' => 'saco',
|
||||
'unit.sack.plural' => 'sacos',
|
||||
'unit.packet.singular' => 'sobre',
|
||||
'unit.packet.plural' => 'sobres',
|
||||
'unit.cob.singular' => 'mazorca',
|
||||
'unit.cob.plural' => 'mazorcas',
|
||||
'unit.pod.singular' => 'vaina',
|
||||
'unit.pod.plural' => 'vainas',
|
||||
'unit.ear.singular' => 'espiga',
|
||||
'unit.ear.plural' => 'espigas',
|
||||
'unit.head.singular' => 'cabezuela',
|
||||
'unit.head.plural' => 'cabezuelas',
|
||||
'unit.fruit.singular' => 'fruto',
|
||||
'unit.fruit.plural' => 'frutos',
|
||||
'unit.bulb.singular' => 'bulbo',
|
||||
'unit.bulb.plural' => 'bulbos',
|
||||
'unit.tuber.singular' => 'tubérculo',
|
||||
'unit.tuber.plural' => 'tubérculos',
|
||||
'unit.seedHead.singular' => 'cabeza de semillas',
|
||||
'unit.seedHead.plural' => 'cabezas de semillas',
|
||||
'unit.bunch.singular' => 'manojo',
|
||||
'unit.bunch.plural' => 'manojos',
|
||||
'unit.plant.singular' => 'planta',
|
||||
'unit.plant.plural' => 'plantas',
|
||||
'unit.pot.singular' => 'maceta',
|
||||
'unit.pot.plural' => 'macetas',
|
||||
'unit.tray.singular' => 'bandeja',
|
||||
'unit.tray.plural' => 'bandejas',
|
||||
'unit.grams.singular' => 'gramo',
|
||||
'unit.grams.plural' => 'gramos',
|
||||
'unit.count.singular' => 'semilla',
|
||||
'unit.count.plural' => 'semillas',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,17 @@ import 'package:equatable/equatable.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../data/variety_repository.dart';
|
||||
import '../db/enums.dart';
|
||||
|
||||
const Object _unset = Object();
|
||||
|
||||
/// Form state for the quick-add sheet. Only [label] is required; everything
|
||||
/// else is progressive disclosure behind [expanded].
|
||||
class QuickAddState extends Equatable {
|
||||
const QuickAddState({
|
||||
this.label = '',
|
||||
this.quantityKind,
|
||||
this.quantity,
|
||||
this.lotType = LotType.seed,
|
||||
this.photoBytes,
|
||||
this.expanded = false,
|
||||
this.submitting = false,
|
||||
|
|
@ -20,7 +24,8 @@ class QuickAddState extends Equatable {
|
|||
});
|
||||
|
||||
final String label;
|
||||
final QuantityKind? quantityKind;
|
||||
final Quantity? quantity;
|
||||
final LotType lotType;
|
||||
final Uint8List? photoBytes;
|
||||
final bool expanded;
|
||||
final bool submitting;
|
||||
|
|
@ -31,7 +36,8 @@ class QuickAddState extends Equatable {
|
|||
|
||||
QuickAddState copyWith({
|
||||
String? label,
|
||||
QuantityKind? quantityKind,
|
||||
Object? quantity = _unset,
|
||||
LotType? lotType,
|
||||
Uint8List? photoBytes,
|
||||
bool? expanded,
|
||||
bool? submitting,
|
||||
|
|
@ -40,7 +46,10 @@ class QuickAddState extends Equatable {
|
|||
}) {
|
||||
return QuickAddState(
|
||||
label: label ?? this.label,
|
||||
quantityKind: quantityKind ?? this.quantityKind,
|
||||
quantity: identical(quantity, _unset)
|
||||
? this.quantity
|
||||
: quantity as Quantity?,
|
||||
lotType: lotType ?? this.lotType,
|
||||
photoBytes: photoBytes ?? this.photoBytes,
|
||||
expanded: expanded ?? this.expanded,
|
||||
submitting: submitting ?? this.submitting,
|
||||
|
|
@ -52,7 +61,8 @@ class QuickAddState extends Equatable {
|
|||
@override
|
||||
List<Object?> get props => [
|
||||
label,
|
||||
quantityKind,
|
||||
quantity,
|
||||
lotType,
|
||||
photoBytes,
|
||||
expanded,
|
||||
submitting,
|
||||
|
|
@ -70,8 +80,10 @@ class QuickAddCubit extends Cubit<QuickAddState> {
|
|||
void labelChanged(String value) =>
|
||||
emit(state.copyWith(label: value, showLabelError: false));
|
||||
|
||||
void selectQuantity(QuantityKind kind) =>
|
||||
emit(state.copyWith(quantityKind: kind));
|
||||
void setQuantity(Quantity? quantity) =>
|
||||
emit(state.copyWith(quantity: quantity));
|
||||
|
||||
void setLotType(LotType type) => emit(state.copyWith(lotType: type));
|
||||
|
||||
void photoPicked(Uint8List bytes) => emit(state.copyWith(photoBytes: bytes));
|
||||
|
||||
|
|
@ -87,9 +99,8 @@ class QuickAddCubit extends Cubit<QuickAddState> {
|
|||
emit(state.copyWith(submitting: true));
|
||||
await _repo.addQuickVariety(
|
||||
label: state.label.trim(),
|
||||
quantity: state.quantityKind == null
|
||||
? null
|
||||
: Quantity(kind: state.quantityKind!),
|
||||
lotType: state.lotType,
|
||||
quantity: state.quantity,
|
||||
photoBytes: state.photoBytes,
|
||||
);
|
||||
emit(state.copyWith(submitting: false, submitted: true));
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../data/variety_repository.dart';
|
||||
import '../db/enums.dart';
|
||||
|
||||
class VarietyDetailState extends Equatable {
|
||||
const VarietyDetailState({
|
||||
|
|
@ -50,8 +51,16 @@ class VarietyDetailCubit extends Cubit<VarietyDetailState> {
|
|||
notes: notes,
|
||||
);
|
||||
|
||||
Future<void> addLot({int? year, Quantity? quantity}) =>
|
||||
_repo.addLot(varietyId: varietyId, harvestYear: year, quantity: quantity);
|
||||
Future<void> addLot({
|
||||
LotType type = LotType.seed,
|
||||
int? year,
|
||||
Quantity? quantity,
|
||||
}) => _repo.addLot(
|
||||
varietyId: varietyId,
|
||||
type: type,
|
||||
harvestYear: year,
|
||||
quantity: quantity,
|
||||
);
|
||||
|
||||
Future<void> linkSpecies(String speciesId) =>
|
||||
_repo.linkSpecies(varietyId, speciesId);
|
||||
|
|
|
|||
|
|
@ -2,46 +2,49 @@ import 'package:commons_core/commons_core.dart';
|
|||
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
/// Localized display label for a [QuantityKind]. The enum name is the stable
|
||||
/// storage key; the label is always resolved through i18n (never hardcoded).
|
||||
String quantityKindLabel(Translations t, QuantityKind kind) {
|
||||
final q = t.quantityKind;
|
||||
switch (kind) {
|
||||
case QuantityKind.aFew:
|
||||
return q.aFew;
|
||||
case QuantityKind.some:
|
||||
return q.some;
|
||||
case QuantityKind.plenty:
|
||||
return q.plenty;
|
||||
case QuantityKind.handful:
|
||||
return q.handful;
|
||||
case QuantityKind.pinch:
|
||||
return q.pinch;
|
||||
case QuantityKind.jar:
|
||||
return q.jar;
|
||||
case QuantityKind.packet:
|
||||
return q.packet;
|
||||
case QuantityKind.cob:
|
||||
return q.cob;
|
||||
case QuantityKind.head:
|
||||
return q.head;
|
||||
case QuantityKind.pod:
|
||||
return q.pod;
|
||||
case QuantityKind.ear:
|
||||
return q.ear;
|
||||
case QuantityKind.fruit:
|
||||
return q.fruit;
|
||||
case QuantityKind.bulb:
|
||||
return q.bulb;
|
||||
case QuantityKind.tuber:
|
||||
return q.tuber;
|
||||
case QuantityKind.seedHead:
|
||||
return q.seedHead;
|
||||
case QuantityKind.bunch:
|
||||
return q.bunch;
|
||||
case QuantityKind.grams:
|
||||
return q.grams;
|
||||
case QuantityKind.count:
|
||||
return q.count;
|
||||
/// Singular + plural label for a [QuantityKind]. For uncountable *vibe* kinds
|
||||
/// (a few, some…) both are the same word.
|
||||
(String, String) _unit(Translations t, QuantityKind kind) => switch (kind) {
|
||||
QuantityKind.aFew => (t.unit.aFew, t.unit.aFew),
|
||||
QuantityKind.some => (t.unit.some, t.unit.some),
|
||||
QuantityKind.plenty => (t.unit.plenty, t.unit.plenty),
|
||||
QuantityKind.pinch => (t.unit.pinch, t.unit.pinch),
|
||||
QuantityKind.handful => (t.unit.handful.singular, t.unit.handful.plural),
|
||||
QuantityKind.teaspoon => (t.unit.teaspoon.singular, t.unit.teaspoon.plural),
|
||||
QuantityKind.spoon => (t.unit.spoon.singular, t.unit.spoon.plural),
|
||||
QuantityKind.cup => (t.unit.cup.singular, t.unit.cup.plural),
|
||||
QuantityKind.jar => (t.unit.jar.singular, t.unit.jar.plural),
|
||||
QuantityKind.sack => (t.unit.sack.singular, t.unit.sack.plural),
|
||||
QuantityKind.packet => (t.unit.packet.singular, t.unit.packet.plural),
|
||||
QuantityKind.cob => (t.unit.cob.singular, t.unit.cob.plural),
|
||||
QuantityKind.pod => (t.unit.pod.singular, t.unit.pod.plural),
|
||||
QuantityKind.ear => (t.unit.ear.singular, t.unit.ear.plural),
|
||||
QuantityKind.head => (t.unit.head.singular, t.unit.head.plural),
|
||||
QuantityKind.fruit => (t.unit.fruit.singular, t.unit.fruit.plural),
|
||||
QuantityKind.bulb => (t.unit.bulb.singular, t.unit.bulb.plural),
|
||||
QuantityKind.tuber => (t.unit.tuber.singular, t.unit.tuber.plural),
|
||||
QuantityKind.seedHead => (t.unit.seedHead.singular, t.unit.seedHead.plural),
|
||||
QuantityKind.bunch => (t.unit.bunch.singular, t.unit.bunch.plural),
|
||||
QuantityKind.plant => (t.unit.plant.singular, t.unit.plant.plural),
|
||||
QuantityKind.pot => (t.unit.pot.singular, t.unit.pot.plural),
|
||||
QuantityKind.tray => (t.unit.tray.singular, t.unit.tray.plural),
|
||||
QuantityKind.grams => (t.unit.grams.singular, t.unit.grams.plural),
|
||||
QuantityKind.count => (t.unit.count.singular, t.unit.count.plural),
|
||||
};
|
||||
|
||||
/// The unit's singular name — for chips/scale items ("cob", "packet").
|
||||
String unitLabel(Translations t, QuantityKind kind) => _unit(t, kind).$1;
|
||||
|
||||
/// A full, human quantity: "3 cobs", "2 packets", "a few".
|
||||
String quantityDisplay(Translations t, Quantity q) {
|
||||
final n = q.count;
|
||||
final (one, other) = _unit(t, q.kind);
|
||||
if (!q.kind.countable || n == null) {
|
||||
// Vibe amount, or a countable unit with no number → bare unit name.
|
||||
return (n != null && n != 1) ? other : one;
|
||||
}
|
||||
return '${_formatCount(n)} ${n == 1 ? one : other}';
|
||||
}
|
||||
|
||||
String _formatCount(num n) =>
|
||||
n == n.roundToDouble() ? n.toInt().toString() : n.toString();
|
||||
|
|
|
|||
256
apps/app_seeds/lib/ui/quantity_picker.dart
Normal file
256
apps/app_seeds/lib/ui/quantity_picker.dart
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../db/enums.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
import 'seed_glyph.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Informal seed amounts, ascending: vibes → containers → seed/fruit forms.
|
||||
const seedScaleKinds = <QuantityKind>[
|
||||
QuantityKind.aFew,
|
||||
QuantityKind.handful,
|
||||
QuantityKind.teaspoon,
|
||||
QuantityKind.spoon,
|
||||
QuantityKind.cup,
|
||||
QuantityKind.jar,
|
||||
QuantityKind.sack,
|
||||
QuantityKind.packet,
|
||||
QuantityKind.cob,
|
||||
QuantityKind.pod,
|
||||
QuantityKind.ear,
|
||||
QuantityKind.head,
|
||||
QuantityKind.fruit,
|
||||
QuantityKind.bulb,
|
||||
QuantityKind.tuber,
|
||||
QuantityKind.bunch,
|
||||
];
|
||||
|
||||
/// Units for a plant/seedling lot.
|
||||
const plantScaleKinds = <QuantityKind>[
|
||||
QuantityKind.plant,
|
||||
QuantityKind.pot,
|
||||
QuantityKind.tray,
|
||||
];
|
||||
|
||||
List<QuantityKind> kindsForType(LotType type) =>
|
||||
type == LotType.seed ? seedScaleKinds : plantScaleKinds;
|
||||
|
||||
/// Seed vs plant/seedling segmented selector.
|
||||
class LotTypeSelector extends StatelessWidget {
|
||||
const LotTypeSelector({
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final LotType value;
|
||||
final ValueChanged<LotType> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return SegmentedButton<LotType>(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: LotType.seed,
|
||||
label: Text(t.lotType.seed),
|
||||
icon: const SeedGlyph(SeedGlyphs.jars, size: 18),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: LotType.plant,
|
||||
label: Text(t.lotType.plant),
|
||||
icon: const Icon(Icons.local_florist_outlined, size: 18),
|
||||
),
|
||||
],
|
||||
selected: {value},
|
||||
showSelectedIcon: false,
|
||||
onSelectionChanged: (s) => onChanged(s.first),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal scale of large unit icons + an optional number stepper. Emits a
|
||||
/// [Quantity] (or null while nothing is picked) via [onChanged].
|
||||
class QuantityPicker extends StatefulWidget {
|
||||
const QuantityPicker({
|
||||
required this.type,
|
||||
required this.onChanged,
|
||||
this.value,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final LotType type;
|
||||
final Quantity? value;
|
||||
final ValueChanged<Quantity?> onChanged;
|
||||
|
||||
@override
|
||||
State<QuantityPicker> createState() => _QuantityPickerState();
|
||||
}
|
||||
|
||||
class _QuantityPickerState extends State<QuantityPicker> {
|
||||
QuantityKind? _kind;
|
||||
final _countController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_kind = widget.value?.kind;
|
||||
final c = widget.value?.count;
|
||||
if (c != null) _countController.text = _fmt(c);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(QuantityPicker old) {
|
||||
super.didUpdateWidget(old);
|
||||
// Switching seed/plant, or the owner clearing the value, drops a stale
|
||||
// selection. We only update local fields here (no onChanged during build);
|
||||
// the owner clears its own value alongside the type change.
|
||||
if (widget.value == null && _kind != null) {
|
||||
_kind = null;
|
||||
_countController.clear();
|
||||
} else if (widget.type != old.type &&
|
||||
_kind != null &&
|
||||
!kindsForType(widget.type).contains(_kind)) {
|
||||
_kind = null;
|
||||
_countController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_countController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _emit() {
|
||||
final kind = _kind;
|
||||
if (kind == null) {
|
||||
widget.onChanged(null);
|
||||
return;
|
||||
}
|
||||
final n = num.tryParse(_countController.text.trim().replaceAll(',', '.'));
|
||||
widget.onChanged(Quantity(kind: kind, count: kind.countable ? n : null));
|
||||
}
|
||||
|
||||
void _select(QuantityKind kind) {
|
||||
setState(() => _kind = kind);
|
||||
_emit();
|
||||
}
|
||||
|
||||
void _bump(int delta) {
|
||||
final current = int.tryParse(_countController.text.trim()) ?? 0;
|
||||
final next = (current + delta).clamp(0, 9999);
|
||||
_countController.text = next == 0 ? '' : '$next';
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
final kinds = kindsForType(widget.type);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 76,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: kinds.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(width: 4),
|
||||
itemBuilder: (context, i) {
|
||||
final kind = kinds[i];
|
||||
return _ScaleItem(
|
||||
kind: kind,
|
||||
label: unitLabel(t, kind),
|
||||
selected: _kind == kind,
|
||||
onTap: () => _select(kind),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_kind?.countable ?? false) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
onPressed: () => _bump(-1),
|
||||
icon: const Icon(Icons.remove),
|
||||
),
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: TextField(
|
||||
key: const Key('quantity.count'),
|
||||
controller: _countController,
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(isDense: true),
|
||||
onChanged: (_) => _emit(),
|
||||
),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
onPressed: () => _bump(1),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
unitLabel(t, _kind!),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _fmt(num n) =>
|
||||
n == n.roundToDouble() ? n.toInt().toString() : n.toString();
|
||||
|
||||
class _ScaleItem extends StatelessWidget {
|
||||
const _ScaleItem({
|
||||
required this.kind,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final QuantityKind kind;
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = selected ? seedGreenDark : Colors.black54;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: 68,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? seedGreen.withValues(alpha: 0.15) : null,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QuantityKindIcon(kind, size: 30, color: color),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 11, color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
|
@ -8,24 +7,12 @@ import 'package:image_picker/image_picker.dart';
|
|||
import '../data/variety_repository.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../state/quick_add_cubit.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
import 'seed_glyph.dart';
|
||||
import 'quantity_picker.dart';
|
||||
|
||||
/// Picks a photo and returns its bytes (or null if cancelled). Injected so
|
||||
/// widget tests can supply a fake instead of the camera plugin.
|
||||
typedef PhotoPicker = Future<Uint8List?> Function();
|
||||
|
||||
/// A short, high-frequency subset of quantity units shown up front to keep the
|
||||
/// add flow fast; the rest live behind "Add more…".
|
||||
const _quickKinds = <QuantityKind>[
|
||||
QuantityKind.aFew,
|
||||
QuantityKind.handful,
|
||||
QuantityKind.packet,
|
||||
QuantityKind.pod,
|
||||
QuantityKind.cob,
|
||||
QuantityKind.grams,
|
||||
];
|
||||
|
||||
Future<void> showQuickAddSheet(
|
||||
BuildContext context, {
|
||||
required VarietyRepository repository,
|
||||
|
|
@ -94,22 +81,24 @@ class QuickAddSheet extends StatelessWidget {
|
|||
onSubmitted: (_) => cubit.submit(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
LotTypeSelector(
|
||||
value: state.lotType,
|
||||
onChanged: (type) {
|
||||
cubit
|
||||
..setLotType(type)
|
||||
..setQuantity(null);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
t.quickAdd.quantity,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final kind in _quickKinds)
|
||||
ChoiceChip(
|
||||
avatar: QuantityKindIcon(kind, size: 18),
|
||||
label: Text(quantityKindLabel(t, kind)),
|
||||
selected: state.quantityKind == kind,
|
||||
onSelected: (_) => cubit.selectQuantity(kind),
|
||||
),
|
||||
],
|
||||
QuantityPicker(
|
||||
type: state.lotType,
|
||||
value: state.quantity,
|
||||
onChanged: cubit.setQuantity,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
|
|
|
|||
|
|
@ -46,9 +46,13 @@ String? seedGlyphForKind(QuantityKind kind) => switch (kind) {
|
|||
QuantityKind.aFew => SeedGlyphs.scattered,
|
||||
QuantityKind.some => SeedGlyphs.smallSpoon,
|
||||
QuantityKind.plenty => SeedGlyphs.jars,
|
||||
QuantityKind.handful => SeedGlyphs.bigSpoon,
|
||||
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.envelope,
|
||||
_ => null,
|
||||
};
|
||||
|
|
@ -59,6 +63,9 @@ IconData _materialIconForKind(QuantityKind kind) => switch (kind) {
|
|||
QuantityKind.cob || QuantityKind.ear => Icons.grass,
|
||||
QuantityKind.head || QuantityKind.seedHead => Icons.filter_vintage_outlined,
|
||||
QuantityKind.fruit => Icons.local_florist_outlined,
|
||||
QuantityKind.plant => Icons.local_florist_outlined,
|
||||
QuantityKind.pot => Icons.yard_outlined,
|
||||
QuantityKind.tray => Icons.grid_on_outlined,
|
||||
QuantityKind.bulb ||
|
||||
QuantityKind.tuber ||
|
||||
QuantityKind.bunch => Icons.spa_outlined,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
|
||||
import '../data/species_repository.dart';
|
||||
import '../data/variety_repository.dart';
|
||||
import '../db/enums.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../state/variety_detail_cubit.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
import 'quantity_picker.dart';
|
||||
import 'seed_glyph.dart';
|
||||
import 'theme.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
|
||||
/// Read + edit view of a single variety: its photo, category, notes, other
|
||||
/// names and lots. Edits go through [VarietyDetailCubit]; the view is reactive.
|
||||
|
|
@ -144,20 +146,11 @@ String _lotSubtitle(Translations t, VarietyLot lot) {
|
|||
t.detail.year(year: lot.harvestYear!)
|
||||
else
|
||||
t.detail.noYear,
|
||||
if (lot.quantity != null) _quantityLabel(t, lot.quantity!),
|
||||
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
|
||||
];
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
String _quantityLabel(Translations t, Quantity q) {
|
||||
final kind = quantityKindLabel(t, q.kind);
|
||||
if (q.precise != null) return '${_trimDouble(q.precise!)} $kind';
|
||||
return kind;
|
||||
}
|
||||
|
||||
String _trimDouble(double v) =>
|
||||
v == v.roundToDouble() ? v.toStringAsFixed(0) : v.toString();
|
||||
|
||||
String _germinationPercent(Translations t, double rate) =>
|
||||
t.germination.result(percent: (rate * 100).round());
|
||||
|
||||
|
|
@ -485,7 +478,8 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
||||
final t = context.t;
|
||||
final yearController = TextEditingController();
|
||||
QuantityKind? selectedKind;
|
||||
var selectedType = LotType.seed;
|
||||
Quantity? selectedQuantity;
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
|
|
@ -506,6 +500,14 @@ Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
|||
style: Theme.of(sheetContext).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
LotTypeSelector(
|
||||
value: selectedType,
|
||||
onChanged: (type) => setState(() {
|
||||
selectedType = type;
|
||||
selectedQuantity = null;
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('addLot.year'),
|
||||
controller: yearController,
|
||||
|
|
@ -521,17 +523,10 @@ Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
|||
style: Theme.of(sheetContext).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final kind in _addLotKinds)
|
||||
ChoiceChip(
|
||||
avatar: QuantityKindIcon(kind, size: 18),
|
||||
label: Text(quantityKindLabel(t, kind)),
|
||||
selected: selectedKind == kind,
|
||||
onSelected: (_) => setState(() => selectedKind = kind),
|
||||
),
|
||||
],
|
||||
QuantityPicker(
|
||||
type: selectedType,
|
||||
value: selectedQuantity,
|
||||
onChanged: (q) => selectedQuantity = q,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
|
|
@ -545,10 +540,9 @@ Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
|||
key: const Key('addLot.save'),
|
||||
onPressed: () {
|
||||
cubit.addLot(
|
||||
type: selectedType,
|
||||
year: int.tryParse(yearController.text.trim()),
|
||||
quantity: selectedKind == null
|
||||
? null
|
||||
: Quantity(kind: selectedKind!),
|
||||
quantity: selectedQuantity,
|
||||
);
|
||||
Navigator.of(sheetContext).pop();
|
||||
},
|
||||
|
|
@ -563,15 +557,6 @@ Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
|||
);
|
||||
}
|
||||
|
||||
const _addLotKinds = <QuantityKind>[
|
||||
QuantityKind.aFew,
|
||||
QuantityKind.handful,
|
||||
QuantityKind.packet,
|
||||
QuantityKind.pod,
|
||||
QuantityKind.cob,
|
||||
QuantityKind.grams,
|
||||
];
|
||||
|
||||
String? _nullIfBlank(String value) {
|
||||
final trimmed = value.trim();
|
||||
return trimmed.isEmpty ? null : trimmed;
|
||||
|
|
|
|||
|
|
@ -11,11 +11,20 @@ void main() {
|
|||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
test('freshly created database matches the exported schema v1', () async {
|
||||
// Guards that `createAll` stays in sync with drift_schemas/drift_schema_v1.json.
|
||||
final schema = await verifier.schemaAt(1);
|
||||
test('freshly created database matches the exported schema v2', () async {
|
||||
final schema = await verifier.schemaAt(2);
|
||||
final db = AppDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, 1);
|
||||
await verifier.migrateAndValidate(db, 2);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test(
|
||||
'upgrades v1 → v2 (adds Lot.type) and matches the fresh schema',
|
||||
() async {
|
||||
final connection = await verifier.startAt(1);
|
||||
final db = AppDatabase(connection);
|
||||
await verifier.migrateAndValidate(db, 2);
|
||||
await db.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
|
|
@ -12,10 +13,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||
switch (version) {
|
||||
case 1:
|
||||
return v1.DatabaseAtV1(db);
|
||||
case 2:
|
||||
return v2.DatabaseAtV2(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1];
|
||||
static const versions = const [1, 2];
|
||||
}
|
||||
|
|
|
|||
1390
apps/app_seeds/test/db/schema/schema_v2.dart
Normal file
1390
apps/app_seeds/test/db/schema/schema_v2.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -34,7 +34,7 @@ void main() {
|
|||
|
||||
expect(find.text('Maize'), findsOneWidget); // app bar title
|
||||
expect(find.text('Poaceae'), findsOneWidget); // category chip
|
||||
expect(find.text('Year 2024 · a cob'), findsOneWidget); // lot line
|
||||
expect(find.text('Year 2024 · cob'), findsOneWidget); // lot line
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
|
|
@ -84,11 +84,13 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(const Key('addLot.year')), '2023');
|
||||
await tester.tap(find.text('a handful'));
|
||||
await tester.tap(find.text('handful')); // pick the unit from the scale
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byKey(const Key('quantity.count')), '2');
|
||||
await tester.tap(find.byKey(const Key('addLot.save')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Year 2023 · a handful'), findsOneWidget);
|
||||
expect(find.text('Year 2023 · 2 handfuls'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,67 +1,77 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Coarse groups used by the UI to suggest relevant [QuantityKind]s for a
|
||||
/// species/family. Purely presentational — never forces a choice.
|
||||
enum QuantityGroup { informal, plantForm, precise }
|
||||
/// Groups used to lay out the quantity picker as an ascending, informal scale.
|
||||
enum QuantityGroup { vibe, size, seedForm, plant, precise }
|
||||
|
||||
/// A stable, extensible, i18n vocabulary for *rough, human* seed amounts.
|
||||
/// A stable, extensible, i18n vocabulary for *rough, human* amounts.
|
||||
///
|
||||
/// Values go deliberately beyond kitchen measures: seeds are counted in the
|
||||
/// forms the plant gives them (a cob, a pod, a flower head…). The display label
|
||||
/// is localized from [name] — so the enum name is the stable storage key and
|
||||
/// must never be renumbered or reused (see data-model §5 migration rules).
|
||||
/// Amounts are a [count] of one of these units — "3 cobs", "2 packets" — never
|
||||
/// a weight. Some units are pure *vibes* ("a few", "a handful") that carry no
|
||||
/// number. The enum name is the stable storage key: values may be appended but
|
||||
/// never renamed or repurposed (data-model §5).
|
||||
enum QuantityKind {
|
||||
// Informal / generic
|
||||
aFew(QuantityGroup.informal),
|
||||
some(QuantityGroup.informal),
|
||||
plenty(QuantityGroup.informal),
|
||||
handful(QuantityGroup.informal),
|
||||
pinch(QuantityGroup.informal),
|
||||
jar(QuantityGroup.informal),
|
||||
packet(QuantityGroup.informal),
|
||||
// Plant-form natural units
|
||||
cob(QuantityGroup.plantForm),
|
||||
head(QuantityGroup.plantForm),
|
||||
pod(QuantityGroup.plantForm),
|
||||
ear(QuantityGroup.plantForm),
|
||||
fruit(QuantityGroup.plantForm),
|
||||
bulb(QuantityGroup.plantForm),
|
||||
tuber(QuantityGroup.plantForm),
|
||||
seedHead(QuantityGroup.plantForm),
|
||||
bunch(QuantityGroup.plantForm),
|
||||
// Precise
|
||||
grams(QuantityGroup.precise),
|
||||
count(QuantityGroup.precise);
|
||||
// Vibe — rough amounts, mostly uncountable.
|
||||
aFew(QuantityGroup.vibe, countable: false),
|
||||
some(QuantityGroup.vibe, countable: false),
|
||||
plenty(QuantityGroup.vibe, countable: false),
|
||||
pinch(QuantityGroup.vibe, countable: false),
|
||||
handful(QuantityGroup.vibe, countable: true),
|
||||
// Size scale — informal containers, ascending.
|
||||
teaspoon(QuantityGroup.size, countable: true),
|
||||
spoon(QuantityGroup.size, countable: true),
|
||||
cup(QuantityGroup.size, countable: true),
|
||||
jar(QuantityGroup.size, countable: true),
|
||||
sack(QuantityGroup.size, countable: true),
|
||||
// The forms the plant gives seeds/fruit in.
|
||||
packet(QuantityGroup.seedForm, countable: true),
|
||||
cob(QuantityGroup.seedForm, countable: true),
|
||||
pod(QuantityGroup.seedForm, countable: true),
|
||||
ear(QuantityGroup.seedForm, countable: true),
|
||||
head(QuantityGroup.seedForm, countable: true),
|
||||
fruit(QuantityGroup.seedForm, countable: true),
|
||||
bulb(QuantityGroup.seedForm, countable: true),
|
||||
tuber(QuantityGroup.seedForm, countable: true),
|
||||
seedHead(QuantityGroup.seedForm, countable: true),
|
||||
bunch(QuantityGroup.seedForm, countable: true),
|
||||
// Seedlings / plants (for a plant lot).
|
||||
plant(QuantityGroup.plant, countable: true),
|
||||
pot(QuantityGroup.plant, countable: true),
|
||||
tray(QuantityGroup.plant, countable: true),
|
||||
// Precise (optional).
|
||||
grams(QuantityGroup.precise, countable: true),
|
||||
count(QuantityGroup.precise, countable: true);
|
||||
|
||||
const QuantityKind(this.group);
|
||||
const QuantityKind(this.group, {required this.countable});
|
||||
|
||||
final QuantityGroup group;
|
||||
|
||||
/// Whether a number reads naturally ("3 cobs"). Vibes like [aFew] don't.
|
||||
final bool countable;
|
||||
}
|
||||
|
||||
/// A quantity held or moved: a qualitative [kind], plus an optional [precise]
|
||||
/// amount (grams / seed count) and an optional free-text [label] for rough
|
||||
/// amounts no [kind] captures. Shared value type used by both Lot and Movement.
|
||||
/// A held or moved amount: a [count] of an informal [kind]. Shared value type
|
||||
/// used by both Lot and Movement. No weights — deliberately human.
|
||||
class Quantity extends Equatable {
|
||||
const Quantity({required this.kind, this.precise, this.label});
|
||||
const Quantity({required this.kind, this.count, this.label});
|
||||
|
||||
final QuantityKind kind;
|
||||
|
||||
/// Optional precise amount, meaningful for [QuantityGroup.precise] kinds.
|
||||
final double? precise;
|
||||
/// How many of [kind] (e.g. 3 cobs, 2 packets). Null for an uncountable vibe
|
||||
/// ("a few") or when unspecified.
|
||||
final num? count;
|
||||
|
||||
/// Optional free text ("half a jam jar") when no [kind] fits well.
|
||||
/// Free text fallback for a rough amount no [kind] captures.
|
||||
final String? label;
|
||||
|
||||
bool get isPrecise => precise != null;
|
||||
bool get hasCount => count != null;
|
||||
|
||||
Quantity copyWith({QuantityKind? kind, double? precise, String? label}) {
|
||||
return Quantity(
|
||||
kind: kind ?? this.kind,
|
||||
precise: precise ?? this.precise,
|
||||
label: label ?? this.label,
|
||||
);
|
||||
}
|
||||
Quantity copyWith({QuantityKind? kind, num? count, String? label}) =>
|
||||
Quantity(
|
||||
kind: kind ?? this.kind,
|
||||
count: count ?? this.count,
|
||||
label: label ?? this.label,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [kind, precise, label];
|
||||
List<Object?> get props => [kind, count, label];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,25 +4,22 @@ import 'package:test/test.dart';
|
|||
void main() {
|
||||
group('Quantity', () {
|
||||
test('value equality ignores object identity', () {
|
||||
const a = Quantity(kind: QuantityKind.pod, precise: 12, label: 'a bag');
|
||||
const b = Quantity(kind: QuantityKind.pod, precise: 12, label: 'a bag');
|
||||
const a = Quantity(kind: QuantityKind.pod, count: 3, label: 'a bag');
|
||||
const b = Quantity(kind: QuantityKind.pod, count: 3, label: 'a bag');
|
||||
expect(a, b);
|
||||
expect(a.hashCode, b.hashCode);
|
||||
});
|
||||
|
||||
test('isPrecise reflects the presence of a precise amount', () {
|
||||
expect(
|
||||
const Quantity(kind: QuantityKind.grams, precise: 5).isPrecise,
|
||||
isTrue,
|
||||
);
|
||||
expect(const Quantity(kind: QuantityKind.aFew).isPrecise, isFalse);
|
||||
test('hasCount reflects the presence of a number', () {
|
||||
expect(const Quantity(kind: QuantityKind.cob, count: 3).hasCount, isTrue);
|
||||
expect(const Quantity(kind: QuantityKind.aFew).hasCount, isFalse);
|
||||
});
|
||||
|
||||
test('copyWith overrides only the given fields', () {
|
||||
const q = Quantity(kind: QuantityKind.cob, precise: 2);
|
||||
const q = Quantity(kind: QuantityKind.cob, count: 2);
|
||||
final q2 = q.copyWith(kind: QuantityKind.ear);
|
||||
expect(q2.kind, QuantityKind.ear);
|
||||
expect(q2.precise, 2);
|
||||
expect(q2.count, 2);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -31,11 +28,20 @@ void main() {
|
|||
// Guards against accidental renames — these strings live in the DB.
|
||||
expect(QuantityKind.pod.name, 'pod');
|
||||
expect(QuantityKind.cob.name, 'cob');
|
||||
expect(QuantityKind.head.name, 'head');
|
||||
expect(QuantityKind.packet.name, 'packet');
|
||||
expect(QuantityKind.handful.name, 'handful');
|
||||
expect(QuantityKind.grams.name, 'grams');
|
||||
expect(QuantityKind.count.name, 'count');
|
||||
expect(QuantityKind.jar.name, 'jar');
|
||||
expect(QuantityKind.sack.name, 'sack');
|
||||
expect(QuantityKind.plant.name, 'plant');
|
||||
});
|
||||
|
||||
test('vibe amounts are not countable; forms and containers are', () {
|
||||
expect(QuantityKind.aFew.countable, isFalse);
|
||||
expect(QuantityKind.some.countable, isFalse);
|
||||
expect(QuantityKind.cob.countable, isTrue);
|
||||
expect(QuantityKind.packet.countable, isTrue);
|
||||
expect(QuantityKind.jar.countable, isTrue);
|
||||
expect(QuantityKind.plant.countable, isTrue);
|
||||
});
|
||||
|
||||
test('every kind belongs to a group', () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue