Compare commits
No commits in common. "1a826477f2aad43b2381be69dbaece3b1ce3d8c6" and "11cbdf302268161f648c36ed71cd9eba449f0fdf" have entirely different histories.
1a826477f2
...
11cbdf3022
33 changed files with 35 additions and 8080 deletions
|
|
@ -1,9 +1,7 @@
|
|||
# Release automation for git.comunes.org (Forgejo Actions).
|
||||
#
|
||||
# Password-free: pushing a tag `v*` builds a signed AAB + per-ABI APKs, uploads
|
||||
# the AAB to Google Play's internal track via fastlane, and attaches the signed
|
||||
# per-ABI APKs to the Forgejo release (the reference binaries F-Droid verifies
|
||||
# for reproducible, developer-signed publishing). All credentials come from repo
|
||||
# Password-free: pushing a tag `v*` builds a signed AAB/APK and uploads it to
|
||||
# Google Play's internal track via fastlane. All credentials come from repo
|
||||
# secrets (Settings > Actions > Secrets), never typed:
|
||||
# TANE_KEYSTORE_BASE64 base64 of the dedicated tane-upload.jks
|
||||
# TANE_KEYSTORE_PASSWORD store password
|
||||
|
|
@ -63,33 +61,11 @@ jobs:
|
|||
keyPassword=$TANE_KEY_PASSWORD
|
||||
EOF
|
||||
|
||||
- name: Build signed AAB + per-ABI APKs
|
||||
- name: Build signed AAB + APK
|
||||
working-directory: apps/app_seeds
|
||||
run: |
|
||||
flutter build appbundle --release
|
||||
# Per-ABI splits: these are the reference binaries F-Droid rebuilds and
|
||||
# verifies against (see docs/fdroid/org.comunes.tane.yml, binary:).
|
||||
flutter build apk --release --split-per-abi
|
||||
|
||||
- name: Publish signed per-ABI APKs to the Forgejo release
|
||||
working-directory: apps/app_seeds
|
||||
env:
|
||||
TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq curl jq
|
||||
api="http://forgejo:3000/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
# Create the release for this tag if it does not exist yet, then get its id.
|
||||
curl -sf -X POST "$api/releases" \
|
||||
-H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \
|
||||
-d "{\"tag_name\":\"${tag}\",\"name\":\"${tag}\"}" >/dev/null || true
|
||||
rid=$(curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/tags/${tag}" | jq -r .id)
|
||||
for abi in armeabi-v7a arm64-v8a x86_64; do
|
||||
f="build/app/outputs/flutter-apk/app-${abi}-release.apk"
|
||||
curl -sf -X POST "$api/releases/${rid}/assets?name=app-${abi}-release.apk" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-F "attachment=@${f};type=application/vnd.android.package-archive"
|
||||
done
|
||||
flutter build apk --release
|
||||
|
||||
- name: Install fastlane
|
||||
working-directory: apps/app_seeds
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -153,23 +153,6 @@ class InventoryJsonCodec {
|
|||
'notes': c.notes,
|
||||
},
|
||||
],
|
||||
'gardenOutcomes': [
|
||||
for (final o in snapshot.gardenOutcomes)
|
||||
{
|
||||
..._syncMeta(
|
||||
id: o.id,
|
||||
createdAt: o.createdAt,
|
||||
updatedAt: o.updatedAt,
|
||||
lastAuthor: o.lastAuthor,
|
||||
schemaRowVersion: o.schemaRowVersion,
|
||||
isDeleted: o.isDeleted,
|
||||
),
|
||||
'lotId': o.lotId,
|
||||
'year': o.year,
|
||||
'rating': o.rating?.name,
|
||||
'notes': o.notes,
|
||||
},
|
||||
],
|
||||
'movements': [
|
||||
for (final m in snapshot.movements)
|
||||
{
|
||||
|
|
@ -434,20 +417,6 @@ class InventoryJsonCodec {
|
|||
notes: m['notes'] as String?,
|
||||
);
|
||||
}),
|
||||
gardenOutcomes: _rows(root, 'gardenOutcomes', (m) {
|
||||
return GardenOutcome(
|
||||
id: _string(m, 'id'),
|
||||
createdAt: _int(m, 'createdAt'),
|
||||
updatedAt: _string(m, 'updatedAt'),
|
||||
lastAuthor: _string(m, 'lastAuthor'),
|
||||
isDeleted: _bool(m, 'isDeleted'),
|
||||
schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
|
||||
lotId: _string(m, 'lotId'),
|
||||
year: m['year'] as int?,
|
||||
rating: _enumOrNull(GardenOutcomeRating.values, m['rating']),
|
||||
notes: m['notes'] as String?,
|
||||
);
|
||||
}),
|
||||
movements: _rows(root, 'movements', (m) {
|
||||
final type = _enumOrNull(MovementType.values, m['type']);
|
||||
if (type == null) return null; // no safe default → drop row
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ class InventorySnapshot {
|
|||
this.externalLinks = const [],
|
||||
this.germinationTests = const [],
|
||||
this.conditionChecks = const [],
|
||||
this.gardenOutcomes = const [],
|
||||
this.movements = const [],
|
||||
this.parties = const [],
|
||||
this.attachments = const [],
|
||||
|
|
@ -42,7 +41,6 @@ class InventorySnapshot {
|
|||
final List<ExternalLink> externalLinks;
|
||||
final List<GerminationTest> germinationTests;
|
||||
final List<ConditionCheck> conditionChecks;
|
||||
final List<GardenOutcome> gardenOutcomes;
|
||||
final List<Movement> movements;
|
||||
final List<Party> parties;
|
||||
final List<Attachment> attachments;
|
||||
|
|
|
|||
|
|
@ -326,88 +326,6 @@ class ConditionEntry extends Equatable {
|
|||
];
|
||||
}
|
||||
|
||||
/// What kind of event a [LotHistoryEntry] narrates.
|
||||
enum LotHistoryKind {
|
||||
created,
|
||||
movement,
|
||||
germinationTest,
|
||||
conditionCheck,
|
||||
gardenOutcome,
|
||||
}
|
||||
|
||||
/// One line of a lot's composite history — the batch's own story, merged from
|
||||
/// what is already recorded (the lot's creation with its origin, movements,
|
||||
/// germination tests and condition checks). Read-only; built by
|
||||
/// [VarietyRepository.lotHistory].
|
||||
class LotHistoryEntry extends Equatable {
|
||||
const LotHistoryEntry({
|
||||
required this.kind,
|
||||
this.when,
|
||||
this.movementType,
|
||||
this.notes,
|
||||
this.quantity,
|
||||
this.counterpartyName,
|
||||
this.originName,
|
||||
this.originPlace,
|
||||
this.germinationRate,
|
||||
this.containerCount,
|
||||
this.desiccantState,
|
||||
this.rating,
|
||||
this.year,
|
||||
this.hasParentLink = false,
|
||||
});
|
||||
|
||||
final LotHistoryKind kind;
|
||||
|
||||
/// When it happened (ms since epoch): the event's own date when recorded,
|
||||
/// falling back to the row's `createdAt`. Null only when neither exists.
|
||||
final int? when;
|
||||
|
||||
/// Set for [LotHistoryKind.movement] entries.
|
||||
final MovementType? movementType;
|
||||
final String? notes;
|
||||
final Quantity? quantity;
|
||||
|
||||
/// Display name of the movement's counterparty, when one was recorded.
|
||||
final String? counterpartyName;
|
||||
|
||||
/// Provenance carried by the creation entry (who grew/gave it, where from).
|
||||
final String? originName;
|
||||
final String? originPlace;
|
||||
|
||||
/// Set for [LotHistoryKind.germinationTest] entries (0..1), when computable.
|
||||
final double? germinationRate;
|
||||
|
||||
/// Set for [LotHistoryKind.conditionCheck] entries.
|
||||
final int? containerCount;
|
||||
final DesiccantState? desiccantState;
|
||||
|
||||
/// Set for [LotHistoryKind.gardenOutcome] entries: how that season went.
|
||||
final GardenOutcomeRating? rating;
|
||||
final int? year;
|
||||
|
||||
/// True when the movement links back to a source batch (provenance DAG).
|
||||
final bool hasParentLink;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
kind,
|
||||
when,
|
||||
movementType,
|
||||
notes,
|
||||
quantity,
|
||||
counterpartyName,
|
||||
originName,
|
||||
originPlace,
|
||||
germinationRate,
|
||||
containerCount,
|
||||
desiccantState,
|
||||
rating,
|
||||
year,
|
||||
hasParentLink,
|
||||
];
|
||||
}
|
||||
|
||||
/// One held batch of a variety, for the detail view. [germinationTests] and
|
||||
/// [conditionChecks] are ordered most-recent first, so `.first` is the latest.
|
||||
class VarietyLot extends Equatable {
|
||||
|
|
@ -1952,184 +1870,6 @@ class VarietyRepository {
|
|||
return id;
|
||||
}
|
||||
|
||||
/// Records a one-tap cultivation event on a lot — the grower sowed from it or
|
||||
/// harvested into it. Only [MovementType.sown] and [MovementType.harvested]
|
||||
/// are cultivation events; exchanges go through [recordHandover]. Returns the
|
||||
/// new movement id. [occurredOn] defaults to now.
|
||||
Future<String> recordCultivation({
|
||||
required String lotId,
|
||||
required MovementType type,
|
||||
int? occurredOn,
|
||||
String? notes,
|
||||
}) async {
|
||||
if (type != MovementType.sown && type != MovementType.harvested) {
|
||||
throw ArgumentError.value(
|
||||
type,
|
||||
'type',
|
||||
'only sown/harvested are cultivation events',
|
||||
);
|
||||
}
|
||||
final (created, _) = await _stamp();
|
||||
final id = idGen.newId();
|
||||
await _db
|
||||
.into(_db.movements)
|
||||
.insert(
|
||||
MovementsCompanion.insert(
|
||||
id: id,
|
||||
createdAt: created,
|
||||
lastAuthor: nodeId,
|
||||
lotId: lotId,
|
||||
type: type,
|
||||
occurredOn: Value(occurredOn ?? created),
|
||||
notes: Value(_hasText(notes) ? notes!.trim() : null),
|
||||
),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
/// The id of the non-deleted variety whose label matches [label]
|
||||
/// (case-insensitive, trimmed), or null. This is how a scanned envelope
|
||||
/// label finds its record: the label IS the identity a keeper printed.
|
||||
Future<String?> findVarietyIdByLabel(String label) async {
|
||||
final row =
|
||||
await (_db.select(_db.varieties)
|
||||
..where(
|
||||
(v) =>
|
||||
v.isDeleted.equals(false) &
|
||||
v.label.lower().equals(label.trim().toLowerCase()),
|
||||
)
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row?.id;
|
||||
}
|
||||
|
||||
/// Records how a season went for a lot — the optional, skippable answer to
|
||||
/// "how did it do?" asked when a harvest is noted. Returns the new row id.
|
||||
Future<String> addGardenOutcome({
|
||||
required String lotId,
|
||||
int? year,
|
||||
GardenOutcomeRating? rating,
|
||||
String? notes,
|
||||
}) async {
|
||||
final (created, updated) = await _stamp();
|
||||
final id = idGen.newId();
|
||||
await _db
|
||||
.into(_db.gardenOutcomes)
|
||||
.insert(
|
||||
GardenOutcomesCompanion.insert(
|
||||
id: id,
|
||||
lotId: lotId,
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
lastAuthor: nodeId,
|
||||
year: Value(year),
|
||||
rating: Value(rating),
|
||||
notes: Value(_hasText(notes) ? notes!.trim() : null),
|
||||
),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
/// The lot's composite history, most-recent first: every movement,
|
||||
/// germination test and condition check already recorded, closed by the
|
||||
/// lot's own creation entry (carrying its origin, so "received from…" shows
|
||||
/// even when no movement was ever logged). One-shot Future — a transient
|
||||
/// sheet must never hold a live subscription.
|
||||
Future<List<LotHistoryEntry>> lotHistory(String lotId) async {
|
||||
final lot = await (_db.select(
|
||||
_db.lots,
|
||||
)..where((l) => l.id.equals(lotId))).getSingleOrNull();
|
||||
if (lot == null) return const [];
|
||||
|
||||
final movements = await (_db.select(
|
||||
_db.movements,
|
||||
)..where((m) => m.lotId.equals(lotId))).get();
|
||||
final tests = await (_db.select(
|
||||
_db.germinationTests,
|
||||
)..where((g) => g.lotId.equals(lotId) & g.isDeleted.equals(false))).get();
|
||||
final checks = await (_db.select(
|
||||
_db.conditionChecks,
|
||||
)..where((c) => c.lotId.equals(lotId) & c.isDeleted.equals(false))).get();
|
||||
final outcomes = await (_db.select(
|
||||
_db.gardenOutcomes,
|
||||
)..where((o) => o.lotId.equals(lotId) & o.isDeleted.equals(false))).get();
|
||||
|
||||
// Resolve counterparty display names in one go (usually zero or one).
|
||||
final partyIds = movements
|
||||
.map((m) => m.counterpartyId)
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
final partyNames = <String, String>{};
|
||||
if (partyIds.isNotEmpty) {
|
||||
final parties = await (_db.select(
|
||||
_db.parties,
|
||||
)..where((p) => p.id.isIn(partyIds))).get();
|
||||
for (final p in parties) {
|
||||
partyNames[p.id] = p.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
final entries = <LotHistoryEntry>[
|
||||
for (final m in movements)
|
||||
LotHistoryEntry(
|
||||
kind: LotHistoryKind.movement,
|
||||
when: m.occurredOn ?? m.createdAt,
|
||||
movementType: m.type,
|
||||
notes: m.notes,
|
||||
quantity: m.quantityKind == null && m.quantityLabel == null
|
||||
? null
|
||||
: Quantity(
|
||||
kind: _parseKind(m.quantityKind),
|
||||
count: m.quantityPrecise,
|
||||
label: m.quantityLabel,
|
||||
),
|
||||
counterpartyName: m.counterpartyId == null
|
||||
? null
|
||||
: partyNames[m.counterpartyId],
|
||||
hasParentLink: m.parentMovementId != null,
|
||||
),
|
||||
for (final g in tests)
|
||||
LotHistoryEntry(
|
||||
kind: LotHistoryKind.germinationTest,
|
||||
when: g.testedOn ?? g.createdAt,
|
||||
notes: g.notes,
|
||||
germinationRate: GerminationEntry(
|
||||
id: g.id,
|
||||
sampleSize: g.sampleSize,
|
||||
germinatedCount: g.germinatedCount,
|
||||
).rate,
|
||||
),
|
||||
for (final c in checks)
|
||||
LotHistoryEntry(
|
||||
kind: LotHistoryKind.conditionCheck,
|
||||
when: c.checkedOn ?? c.createdAt,
|
||||
notes: c.notes,
|
||||
containerCount: c.containerCount,
|
||||
desiccantState: c.desiccantState,
|
||||
),
|
||||
for (final o in outcomes)
|
||||
LotHistoryEntry(
|
||||
kind: LotHistoryKind.gardenOutcome,
|
||||
when: o.createdAt,
|
||||
notes: o.notes,
|
||||
rating: o.rating,
|
||||
year: o.year,
|
||||
),
|
||||
]..sort((a, b) => (b.when ?? 0).compareTo(a.when ?? 0));
|
||||
|
||||
return [
|
||||
...entries,
|
||||
// Creation closes the story (oldest), whatever its stamp: it reads as
|
||||
// "this batch entered your collection", origin attached.
|
||||
LotHistoryEntry(
|
||||
kind: LotHistoryKind.created,
|
||||
when: lot.createdAt,
|
||||
originName: lot.originName,
|
||||
originPlace: lot.originPlace,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// --- Plantares: reproduction commitments (data-model §2.7) ----------------
|
||||
|
||||
/// Records a Plantare — a promise to reproduce seed and return some (or a
|
||||
|
|
@ -2519,9 +2259,6 @@ class VarietyRepository {
|
|||
conditionChecks: await (_db.select(
|
||||
_db.conditionChecks,
|
||||
)..where((c) => c.isDeleted.equals(false))).get(),
|
||||
gardenOutcomes: await (_db.select(
|
||||
_db.gardenOutcomes,
|
||||
)..where((o) => o.isDeleted.equals(false))).get(),
|
||||
movements: await _db.select(_db.movements).get(),
|
||||
parties: await (_db.select(
|
||||
_db.parties,
|
||||
|
|
@ -2555,7 +2292,6 @@ class VarietyRepository {
|
|||
externalLinks: await _db.select(_db.externalLinks).get(),
|
||||
germinationTests: await _db.select(_db.germinationTests).get(),
|
||||
conditionChecks: await _db.select(_db.conditionChecks).get(),
|
||||
gardenOutcomes: await _db.select(_db.gardenOutcomes).get(),
|
||||
movements: await _db.select(_db.movements).get(),
|
||||
parties: await _db.select(_db.parties).get(),
|
||||
plantares: await _db.select(_db.plantares).get(),
|
||||
|
|
@ -2636,14 +2372,6 @@ class VarietyRepository {
|
|||
updatedAtOf: (r) => r.updatedAt,
|
||||
reconciler: reconciler,
|
||||
);
|
||||
summary += await _importMutableRows(
|
||||
table: _db.gardenOutcomes,
|
||||
idColumn: _db.gardenOutcomes.id,
|
||||
rows: snapshot.gardenOutcomes,
|
||||
idOf: (r) => r.id,
|
||||
updatedAtOf: (r) => r.updatedAt,
|
||||
reconciler: reconciler,
|
||||
);
|
||||
summary += await _importMutableRows(
|
||||
table: _db.parties,
|
||||
idColumn: _db.parties.id,
|
||||
|
|
@ -2686,7 +2414,6 @@ class VarietyRepository {
|
|||
for (final e in snapshot.externalLinks) e.updatedAt,
|
||||
for (final g in snapshot.germinationTests) g.updatedAt,
|
||||
for (final c in snapshot.conditionChecks) c.updatedAt,
|
||||
for (final o in snapshot.gardenOutcomes) o.updatedAt,
|
||||
for (final p in snapshot.parties) p.updatedAt,
|
||||
for (final a in snapshot.attachments) a.updatedAt,
|
||||
for (final pl in snapshot.plantares) pl.updatedAt,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ part 'database.g.dart';
|
|||
Lots,
|
||||
GerminationTests,
|
||||
ConditionChecks,
|
||||
GardenOutcomes,
|
||||
Movements,
|
||||
Parties,
|
||||
Attachments,
|
||||
|
|
@ -32,7 +31,7 @@ class AppDatabase extends _$AppDatabase {
|
|||
|
||||
/// Current schema version; also stamped into interchange exports so an
|
||||
/// importer knows which app generation wrote the file (data-model §7).
|
||||
static const int currentSchemaVersion = 13;
|
||||
static const int currentSchemaVersion = 12;
|
||||
|
||||
@override
|
||||
int get schemaVersion => currentSchemaVersion;
|
||||
|
|
@ -188,14 +187,6 @@ class AppDatabase extends _$AppDatabase {
|
|||
await addIfMissing('return_kind', plantares.returnKind);
|
||||
await addIfMissing('work_hours', plantares.workHours);
|
||||
}
|
||||
// v13: GardenOutcomes — the per-season "how did it do in my garden"
|
||||
// answer (rating + note) captured when a harvest is recorded. Guarded
|
||||
// (see the v7 note above).
|
||||
if (from < 13) {
|
||||
if (!await _hasTable('garden_outcomes')) {
|
||||
await m.createTable(gardenOutcomes);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -5765,624 +5765,6 @@ class ConditionChecksCompanion extends UpdateCompanion<ConditionCheck> {
|
|||
}
|
||||
}
|
||||
|
||||
class $GardenOutcomesTable extends GardenOutcomes
|
||||
with TableInfo<$GardenOutcomesTable, GardenOutcome> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$GardenOutcomesTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||
'createdAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<int> createdAt = GeneratedColumn<int>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _updatedAtMeta = const VerificationMeta(
|
||||
'updatedAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> updatedAt = GeneratedColumn<String>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _lastAuthorMeta = const VerificationMeta(
|
||||
'lastAuthor',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> lastAuthor = GeneratedColumn<String>(
|
||||
'last_author',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _isDeletedMeta = const VerificationMeta(
|
||||
'isDeleted',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<bool> isDeleted = GeneratedColumn<bool>(
|
||||
'is_deleted',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_deleted" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const Constant(false),
|
||||
);
|
||||
static const VerificationMeta _schemaRowVersionMeta = const VerificationMeta(
|
||||
'schemaRowVersion',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<int> schemaRowVersion = GeneratedColumn<int>(
|
||||
'schema_row_version',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant(1),
|
||||
);
|
||||
static const VerificationMeta _lotIdMeta = const VerificationMeta('lotId');
|
||||
@override
|
||||
late final GeneratedColumn<String> lotId = GeneratedColumn<String>(
|
||||
'lot_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _yearMeta = const VerificationMeta('year');
|
||||
@override
|
||||
late final GeneratedColumn<int> year = GeneratedColumn<int>(
|
||||
'year',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<GardenOutcomeRating?, String>
|
||||
rating = GeneratedColumn<String>(
|
||||
'rating',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
).withConverter<GardenOutcomeRating?>($GardenOutcomesTable.$converterratingn);
|
||||
static const VerificationMeta _notesMeta = const VerificationMeta('notes');
|
||||
@override
|
||||
late final GeneratedColumn<String> notes = GeneratedColumn<String>(
|
||||
'notes',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
lastAuthor,
|
||||
isDeleted,
|
||||
schemaRowVersion,
|
||||
lotId,
|
||||
year,
|
||||
rating,
|
||||
notes,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'garden_outcomes';
|
||||
@override
|
||||
VerificationContext validateIntegrity(
|
||||
Insertable<GardenOutcome> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(
|
||||
_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_createdAtMeta);
|
||||
}
|
||||
if (data.containsKey('updated_at')) {
|
||||
context.handle(
|
||||
_updatedAtMeta,
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_updatedAtMeta);
|
||||
}
|
||||
if (data.containsKey('last_author')) {
|
||||
context.handle(
|
||||
_lastAuthorMeta,
|
||||
lastAuthor.isAcceptableOrUnknown(data['last_author']!, _lastAuthorMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_lastAuthorMeta);
|
||||
}
|
||||
if (data.containsKey('is_deleted')) {
|
||||
context.handle(
|
||||
_isDeletedMeta,
|
||||
isDeleted.isAcceptableOrUnknown(data['is_deleted']!, _isDeletedMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('schema_row_version')) {
|
||||
context.handle(
|
||||
_schemaRowVersionMeta,
|
||||
schemaRowVersion.isAcceptableOrUnknown(
|
||||
data['schema_row_version']!,
|
||||
_schemaRowVersionMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('lot_id')) {
|
||||
context.handle(
|
||||
_lotIdMeta,
|
||||
lotId.isAcceptableOrUnknown(data['lot_id']!, _lotIdMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_lotIdMeta);
|
||||
}
|
||||
if (data.containsKey('year')) {
|
||||
context.handle(
|
||||
_yearMeta,
|
||||
year.isAcceptableOrUnknown(data['year']!, _yearMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('notes')) {
|
||||
context.handle(
|
||||
_notesMeta,
|
||||
notes.isAcceptableOrUnknown(data['notes']!, _notesMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
GardenOutcome map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return GardenOutcome(
|
||||
id: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}id'],
|
||||
)!,
|
||||
createdAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}created_at'],
|
||||
)!,
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}updated_at'],
|
||||
)!,
|
||||
lastAuthor: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}last_author'],
|
||||
)!,
|
||||
isDeleted: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.bool,
|
||||
data['${effectivePrefix}is_deleted'],
|
||||
)!,
|
||||
schemaRowVersion: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}schema_row_version'],
|
||||
)!,
|
||||
lotId: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}lot_id'],
|
||||
)!,
|
||||
year: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}year'],
|
||||
),
|
||||
rating: $GardenOutcomesTable.$converterratingn.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}rating'],
|
||||
),
|
||||
),
|
||||
notes: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}notes'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$GardenOutcomesTable createAlias(String alias) {
|
||||
return $GardenOutcomesTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static JsonTypeConverter2<GardenOutcomeRating, String, String>
|
||||
$converterrating = const EnumNameConverter<GardenOutcomeRating>(
|
||||
GardenOutcomeRating.values,
|
||||
);
|
||||
static JsonTypeConverter2<GardenOutcomeRating?, String?, String?>
|
||||
$converterratingn = JsonTypeConverter2.asNullable($converterrating);
|
||||
}
|
||||
|
||||
class GardenOutcome extends DataClass implements Insertable<GardenOutcome> {
|
||||
final String id;
|
||||
final int createdAt;
|
||||
final String updatedAt;
|
||||
final String lastAuthor;
|
||||
final bool isDeleted;
|
||||
final int schemaRowVersion;
|
||||
final String lotId;
|
||||
final int? year;
|
||||
final GardenOutcomeRating? rating;
|
||||
final String? notes;
|
||||
const GardenOutcome({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.lastAuthor,
|
||||
required this.isDeleted,
|
||||
required this.schemaRowVersion,
|
||||
required this.lotId,
|
||||
this.year,
|
||||
this.rating,
|
||||
this.notes,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<String>(id);
|
||||
map['created_at'] = Variable<int>(createdAt);
|
||||
map['updated_at'] = Variable<String>(updatedAt);
|
||||
map['last_author'] = Variable<String>(lastAuthor);
|
||||
map['is_deleted'] = Variable<bool>(isDeleted);
|
||||
map['schema_row_version'] = Variable<int>(schemaRowVersion);
|
||||
map['lot_id'] = Variable<String>(lotId);
|
||||
if (!nullToAbsent || year != null) {
|
||||
map['year'] = Variable<int>(year);
|
||||
}
|
||||
if (!nullToAbsent || rating != null) {
|
||||
map['rating'] = Variable<String>(
|
||||
$GardenOutcomesTable.$converterratingn.toSql(rating),
|
||||
);
|
||||
}
|
||||
if (!nullToAbsent || notes != null) {
|
||||
map['notes'] = Variable<String>(notes);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
GardenOutcomesCompanion toCompanion(bool nullToAbsent) {
|
||||
return GardenOutcomesCompanion(
|
||||
id: Value(id),
|
||||
createdAt: Value(createdAt),
|
||||
updatedAt: Value(updatedAt),
|
||||
lastAuthor: Value(lastAuthor),
|
||||
isDeleted: Value(isDeleted),
|
||||
schemaRowVersion: Value(schemaRowVersion),
|
||||
lotId: Value(lotId),
|
||||
year: year == null && nullToAbsent ? const Value.absent() : Value(year),
|
||||
rating: rating == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(rating),
|
||||
notes: notes == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(notes),
|
||||
);
|
||||
}
|
||||
|
||||
factory GardenOutcome.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return GardenOutcome(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
createdAt: serializer.fromJson<int>(json['createdAt']),
|
||||
updatedAt: serializer.fromJson<String>(json['updatedAt']),
|
||||
lastAuthor: serializer.fromJson<String>(json['lastAuthor']),
|
||||
isDeleted: serializer.fromJson<bool>(json['isDeleted']),
|
||||
schemaRowVersion: serializer.fromJson<int>(json['schemaRowVersion']),
|
||||
lotId: serializer.fromJson<String>(json['lotId']),
|
||||
year: serializer.fromJson<int?>(json['year']),
|
||||
rating: $GardenOutcomesTable.$converterratingn.fromJson(
|
||||
serializer.fromJson<String?>(json['rating']),
|
||||
),
|
||||
notes: serializer.fromJson<String?>(json['notes']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'createdAt': serializer.toJson<int>(createdAt),
|
||||
'updatedAt': serializer.toJson<String>(updatedAt),
|
||||
'lastAuthor': serializer.toJson<String>(lastAuthor),
|
||||
'isDeleted': serializer.toJson<bool>(isDeleted),
|
||||
'schemaRowVersion': serializer.toJson<int>(schemaRowVersion),
|
||||
'lotId': serializer.toJson<String>(lotId),
|
||||
'year': serializer.toJson<int?>(year),
|
||||
'rating': serializer.toJson<String?>(
|
||||
$GardenOutcomesTable.$converterratingn.toJson(rating),
|
||||
),
|
||||
'notes': serializer.toJson<String?>(notes),
|
||||
};
|
||||
}
|
||||
|
||||
GardenOutcome copyWith({
|
||||
String? id,
|
||||
int? createdAt,
|
||||
String? updatedAt,
|
||||
String? lastAuthor,
|
||||
bool? isDeleted,
|
||||
int? schemaRowVersion,
|
||||
String? lotId,
|
||||
Value<int?> year = const Value.absent(),
|
||||
Value<GardenOutcomeRating?> rating = const Value.absent(),
|
||||
Value<String?> notes = const Value.absent(),
|
||||
}) => GardenOutcome(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
lastAuthor: lastAuthor ?? this.lastAuthor,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
schemaRowVersion: schemaRowVersion ?? this.schemaRowVersion,
|
||||
lotId: lotId ?? this.lotId,
|
||||
year: year.present ? year.value : this.year,
|
||||
rating: rating.present ? rating.value : this.rating,
|
||||
notes: notes.present ? notes.value : this.notes,
|
||||
);
|
||||
GardenOutcome copyWithCompanion(GardenOutcomesCompanion data) {
|
||||
return GardenOutcome(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
lastAuthor: data.lastAuthor.present
|
||||
? data.lastAuthor.value
|
||||
: this.lastAuthor,
|
||||
isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted,
|
||||
schemaRowVersion: data.schemaRowVersion.present
|
||||
? data.schemaRowVersion.value
|
||||
: this.schemaRowVersion,
|
||||
lotId: data.lotId.present ? data.lotId.value : this.lotId,
|
||||
year: data.year.present ? data.year.value : this.year,
|
||||
rating: data.rating.present ? data.rating.value : this.rating,
|
||||
notes: data.notes.present ? data.notes.value : this.notes,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('GardenOutcome(')
|
||||
..write('id: $id, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('lastAuthor: $lastAuthor, ')
|
||||
..write('isDeleted: $isDeleted, ')
|
||||
..write('schemaRowVersion: $schemaRowVersion, ')
|
||||
..write('lotId: $lotId, ')
|
||||
..write('year: $year, ')
|
||||
..write('rating: $rating, ')
|
||||
..write('notes: $notes')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
lastAuthor,
|
||||
isDeleted,
|
||||
schemaRowVersion,
|
||||
lotId,
|
||||
year,
|
||||
rating,
|
||||
notes,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is GardenOutcome &&
|
||||
other.id == this.id &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.updatedAt == this.updatedAt &&
|
||||
other.lastAuthor == this.lastAuthor &&
|
||||
other.isDeleted == this.isDeleted &&
|
||||
other.schemaRowVersion == this.schemaRowVersion &&
|
||||
other.lotId == this.lotId &&
|
||||
other.year == this.year &&
|
||||
other.rating == this.rating &&
|
||||
other.notes == this.notes);
|
||||
}
|
||||
|
||||
class GardenOutcomesCompanion extends UpdateCompanion<GardenOutcome> {
|
||||
final Value<String> id;
|
||||
final Value<int> createdAt;
|
||||
final Value<String> updatedAt;
|
||||
final Value<String> lastAuthor;
|
||||
final Value<bool> isDeleted;
|
||||
final Value<int> schemaRowVersion;
|
||||
final Value<String> lotId;
|
||||
final Value<int?> year;
|
||||
final Value<GardenOutcomeRating?> rating;
|
||||
final Value<String?> notes;
|
||||
final Value<int> rowid;
|
||||
const GardenOutcomesCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.updatedAt = const Value.absent(),
|
||||
this.lastAuthor = const Value.absent(),
|
||||
this.isDeleted = const Value.absent(),
|
||||
this.schemaRowVersion = const Value.absent(),
|
||||
this.lotId = const Value.absent(),
|
||||
this.year = const Value.absent(),
|
||||
this.rating = const Value.absent(),
|
||||
this.notes = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
GardenOutcomesCompanion.insert({
|
||||
required String id,
|
||||
required int createdAt,
|
||||
required String updatedAt,
|
||||
required String lastAuthor,
|
||||
this.isDeleted = const Value.absent(),
|
||||
this.schemaRowVersion = const Value.absent(),
|
||||
required String lotId,
|
||||
this.year = const Value.absent(),
|
||||
this.rating = const Value.absent(),
|
||||
this.notes = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
createdAt = Value(createdAt),
|
||||
updatedAt = Value(updatedAt),
|
||||
lastAuthor = Value(lastAuthor),
|
||||
lotId = Value(lotId);
|
||||
static Insertable<GardenOutcome> custom({
|
||||
Expression<String>? id,
|
||||
Expression<int>? createdAt,
|
||||
Expression<String>? updatedAt,
|
||||
Expression<String>? lastAuthor,
|
||||
Expression<bool>? isDeleted,
|
||||
Expression<int>? schemaRowVersion,
|
||||
Expression<String>? lotId,
|
||||
Expression<int>? year,
|
||||
Expression<String>? rating,
|
||||
Expression<String>? notes,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
if (lastAuthor != null) 'last_author': lastAuthor,
|
||||
if (isDeleted != null) 'is_deleted': isDeleted,
|
||||
if (schemaRowVersion != null) 'schema_row_version': schemaRowVersion,
|
||||
if (lotId != null) 'lot_id': lotId,
|
||||
if (year != null) 'year': year,
|
||||
if (rating != null) 'rating': rating,
|
||||
if (notes != null) 'notes': notes,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
GardenOutcomesCompanion copyWith({
|
||||
Value<String>? id,
|
||||
Value<int>? createdAt,
|
||||
Value<String>? updatedAt,
|
||||
Value<String>? lastAuthor,
|
||||
Value<bool>? isDeleted,
|
||||
Value<int>? schemaRowVersion,
|
||||
Value<String>? lotId,
|
||||
Value<int?>? year,
|
||||
Value<GardenOutcomeRating?>? rating,
|
||||
Value<String?>? notes,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return GardenOutcomesCompanion(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
lastAuthor: lastAuthor ?? this.lastAuthor,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
schemaRowVersion: schemaRowVersion ?? this.schemaRowVersion,
|
||||
lotId: lotId ?? this.lotId,
|
||||
year: year ?? this.year,
|
||||
rating: rating ?? this.rating,
|
||||
notes: notes ?? this.notes,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<String>(id.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<int>(createdAt.value);
|
||||
}
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = Variable<String>(updatedAt.value);
|
||||
}
|
||||
if (lastAuthor.present) {
|
||||
map['last_author'] = Variable<String>(lastAuthor.value);
|
||||
}
|
||||
if (isDeleted.present) {
|
||||
map['is_deleted'] = Variable<bool>(isDeleted.value);
|
||||
}
|
||||
if (schemaRowVersion.present) {
|
||||
map['schema_row_version'] = Variable<int>(schemaRowVersion.value);
|
||||
}
|
||||
if (lotId.present) {
|
||||
map['lot_id'] = Variable<String>(lotId.value);
|
||||
}
|
||||
if (year.present) {
|
||||
map['year'] = Variable<int>(year.value);
|
||||
}
|
||||
if (rating.present) {
|
||||
map['rating'] = Variable<String>(
|
||||
$GardenOutcomesTable.$converterratingn.toSql(rating.value),
|
||||
);
|
||||
}
|
||||
if (notes.present) {
|
||||
map['notes'] = Variable<String>(notes.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('GardenOutcomesCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('lastAuthor: $lastAuthor, ')
|
||||
..write('isDeleted: $isDeleted, ')
|
||||
..write('schemaRowVersion: $schemaRowVersion, ')
|
||||
..write('lotId: $lotId, ')
|
||||
..write('year: $year, ')
|
||||
..write('rating: $rating, ')
|
||||
..write('notes: $notes, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class $MovementsTable extends Movements
|
||||
with TableInfo<$MovementsTable, Movement> {
|
||||
@override
|
||||
|
|
@ -11428,7 +10810,6 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||
late final $ConditionChecksTable conditionChecks = $ConditionChecksTable(
|
||||
this,
|
||||
);
|
||||
late final $GardenOutcomesTable gardenOutcomes = $GardenOutcomesTable(this);
|
||||
late final $MovementsTable movements = $MovementsTable(this);
|
||||
late final $PartiesTable parties = $PartiesTable(this);
|
||||
late final $AttachmentsTable attachments = $AttachmentsTable(this);
|
||||
|
|
@ -11447,7 +10828,6 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||
lots,
|
||||
germinationTests,
|
||||
conditionChecks,
|
||||
gardenOutcomes,
|
||||
movements,
|
||||
parties,
|
||||
attachments,
|
||||
|
|
@ -14119,312 +13499,6 @@ typedef $$ConditionChecksTableProcessedTableManager =
|
|||
ConditionCheck,
|
||||
PrefetchHooks Function()
|
||||
>;
|
||||
typedef $$GardenOutcomesTableCreateCompanionBuilder =
|
||||
GardenOutcomesCompanion Function({
|
||||
required String id,
|
||||
required int createdAt,
|
||||
required String updatedAt,
|
||||
required String lastAuthor,
|
||||
Value<bool> isDeleted,
|
||||
Value<int> schemaRowVersion,
|
||||
required String lotId,
|
||||
Value<int?> year,
|
||||
Value<GardenOutcomeRating?> rating,
|
||||
Value<String?> notes,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$GardenOutcomesTableUpdateCompanionBuilder =
|
||||
GardenOutcomesCompanion Function({
|
||||
Value<String> id,
|
||||
Value<int> createdAt,
|
||||
Value<String> updatedAt,
|
||||
Value<String> lastAuthor,
|
||||
Value<bool> isDeleted,
|
||||
Value<int> schemaRowVersion,
|
||||
Value<String> lotId,
|
||||
Value<int?> year,
|
||||
Value<GardenOutcomeRating?> rating,
|
||||
Value<String?> notes,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
class $$GardenOutcomesTableFilterComposer
|
||||
extends Composer<_$AppDatabase, $GardenOutcomesTable> {
|
||||
$$GardenOutcomesTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<int> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get lastAuthor => $composableBuilder(
|
||||
column: $table.lastAuthor,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<bool> get isDeleted => $composableBuilder(
|
||||
column: $table.isDeleted,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<int> get schemaRowVersion => $composableBuilder(
|
||||
column: $table.schemaRowVersion,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get lotId => $composableBuilder(
|
||||
column: $table.lotId,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<int> get year => $composableBuilder(
|
||||
column: $table.year,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnWithTypeConverterFilters<
|
||||
GardenOutcomeRating?,
|
||||
GardenOutcomeRating,
|
||||
String
|
||||
>
|
||||
get rating => $composableBuilder(
|
||||
column: $table.rating,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get notes => $composableBuilder(
|
||||
column: $table.notes,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$GardenOutcomesTableOrderingComposer
|
||||
extends Composer<_$AppDatabase, $GardenOutcomesTable> {
|
||||
$$GardenOutcomesTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<int> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get lastAuthor => $composableBuilder(
|
||||
column: $table.lastAuthor,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<bool> get isDeleted => $composableBuilder(
|
||||
column: $table.isDeleted,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<int> get schemaRowVersion => $composableBuilder(
|
||||
column: $table.schemaRowVersion,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get lotId => $composableBuilder(
|
||||
column: $table.lotId,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<int> get year => $composableBuilder(
|
||||
column: $table.year,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get rating => $composableBuilder(
|
||||
column: $table.rating,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get notes => $composableBuilder(
|
||||
column: $table.notes,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$GardenOutcomesTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $GardenOutcomesTable> {
|
||||
$$GardenOutcomesTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<String> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get updatedAt =>
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get lastAuthor => $composableBuilder(
|
||||
column: $table.lastAuthor,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<bool> get isDeleted =>
|
||||
$composableBuilder(column: $table.isDeleted, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get schemaRowVersion => $composableBuilder(
|
||||
column: $table.schemaRowVersion,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<String> get lotId =>
|
||||
$composableBuilder(column: $table.lotId, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get year =>
|
||||
$composableBuilder(column: $table.year, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<GardenOutcomeRating?, String> get rating =>
|
||||
$composableBuilder(column: $table.rating, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get notes =>
|
||||
$composableBuilder(column: $table.notes, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$GardenOutcomesTableTableManager
|
||||
extends
|
||||
RootTableManager<
|
||||
_$AppDatabase,
|
||||
$GardenOutcomesTable,
|
||||
GardenOutcome,
|
||||
$$GardenOutcomesTableFilterComposer,
|
||||
$$GardenOutcomesTableOrderingComposer,
|
||||
$$GardenOutcomesTableAnnotationComposer,
|
||||
$$GardenOutcomesTableCreateCompanionBuilder,
|
||||
$$GardenOutcomesTableUpdateCompanionBuilder,
|
||||
(
|
||||
GardenOutcome,
|
||||
BaseReferences<_$AppDatabase, $GardenOutcomesTable, GardenOutcome>,
|
||||
),
|
||||
GardenOutcome,
|
||||
PrefetchHooks Function()
|
||||
> {
|
||||
$$GardenOutcomesTableTableManager(
|
||||
_$AppDatabase db,
|
||||
$GardenOutcomesTable table,
|
||||
) : super(
|
||||
TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
$$GardenOutcomesTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
$$GardenOutcomesTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
$$GardenOutcomesTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
Value<String> id = const Value.absent(),
|
||||
Value<int> createdAt = const Value.absent(),
|
||||
Value<String> updatedAt = const Value.absent(),
|
||||
Value<String> lastAuthor = const Value.absent(),
|
||||
Value<bool> isDeleted = const Value.absent(),
|
||||
Value<int> schemaRowVersion = const Value.absent(),
|
||||
Value<String> lotId = const Value.absent(),
|
||||
Value<int?> year = const Value.absent(),
|
||||
Value<GardenOutcomeRating?> rating = const Value.absent(),
|
||||
Value<String?> notes = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => GardenOutcomesCompanion(
|
||||
id: id,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
lastAuthor: lastAuthor,
|
||||
isDeleted: isDeleted,
|
||||
schemaRowVersion: schemaRowVersion,
|
||||
lotId: lotId,
|
||||
year: year,
|
||||
rating: rating,
|
||||
notes: notes,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String id,
|
||||
required int createdAt,
|
||||
required String updatedAt,
|
||||
required String lastAuthor,
|
||||
Value<bool> isDeleted = const Value.absent(),
|
||||
Value<int> schemaRowVersion = const Value.absent(),
|
||||
required String lotId,
|
||||
Value<int?> year = const Value.absent(),
|
||||
Value<GardenOutcomeRating?> rating = const Value.absent(),
|
||||
Value<String?> notes = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => GardenOutcomesCompanion.insert(
|
||||
id: id,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
lastAuthor: lastAuthor,
|
||||
isDeleted: isDeleted,
|
||||
schemaRowVersion: schemaRowVersion,
|
||||
lotId: lotId,
|
||||
year: year,
|
||||
rating: rating,
|
||||
notes: notes,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$GardenOutcomesTableProcessedTableManager =
|
||||
ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$GardenOutcomesTable,
|
||||
GardenOutcome,
|
||||
$$GardenOutcomesTableFilterComposer,
|
||||
$$GardenOutcomesTableOrderingComposer,
|
||||
$$GardenOutcomesTableAnnotationComposer,
|
||||
$$GardenOutcomesTableCreateCompanionBuilder,
|
||||
$$GardenOutcomesTableUpdateCompanionBuilder,
|
||||
(
|
||||
GardenOutcome,
|
||||
BaseReferences<_$AppDatabase, $GardenOutcomesTable, GardenOutcome>,
|
||||
),
|
||||
GardenOutcome,
|
||||
PrefetchHooks Function()
|
||||
>;
|
||||
typedef $$MovementsTableCreateCompanionBuilder =
|
||||
MovementsCompanion Function({
|
||||
required String id,
|
||||
|
|
@ -16726,8 +15800,6 @@ class $AppDatabaseManager {
|
|||
$$GerminationTestsTableTableManager(_db, _db.germinationTests);
|
||||
$$ConditionChecksTableTableManager get conditionChecks =>
|
||||
$$ConditionChecksTableTableManager(_db, _db.conditionChecks);
|
||||
$$GardenOutcomesTableTableManager get gardenOutcomes =>
|
||||
$$GardenOutcomesTableTableManager(_db, _db.gardenOutcomes);
|
||||
$$MovementsTableTableManager get movements =>
|
||||
$$MovementsTableTableManager(_db, _db.movements);
|
||||
$$PartiesTableTableManager get parties =>
|
||||
|
|
|
|||
|
|
@ -67,12 +67,6 @@ enum PreservationFormat {
|
|||
/// - `fresh` — just renewed (lila/violet).
|
||||
enum DesiccantState { none, add, replace, dry, fresh }
|
||||
|
||||
/// How a sowing from this batch turned out in the grower's own garden — the
|
||||
/// note people most wish they had two seasons later. Deliberately coarse
|
||||
/// (three faces, not a breeder's scorecard): the free-text note carries any
|
||||
/// nuance.
|
||||
enum GardenOutcomeRating { good, mixed, poor }
|
||||
|
||||
/// Append-only event kinds on a Lot (data-model §2.4).
|
||||
enum MovementType {
|
||||
received,
|
||||
|
|
|
|||
|
|
@ -142,18 +142,6 @@ class ConditionChecks extends Table with SyncColumns {
|
|||
TextColumn get notes => text().nullable()();
|
||||
}
|
||||
|
||||
/// "How did it do in MY garden" — one optional, skippable answer per season
|
||||
/// (v13). Captured at the natural moment (recording a harvest), shown as one
|
||||
/// more line of the lot's story. Deliberately minimal — a coarse rating plus a
|
||||
/// free note; the note carries any nuance. Mirrors [GerminationTests]: a dated
|
||||
/// log under a Lot.
|
||||
class GardenOutcomes extends Table with SyncColumns {
|
||||
TextColumn get lotId => text()();
|
||||
IntColumn get year => integer().nullable()(); // season, e.g. 2026
|
||||
TextColumn get rating => textEnum<GardenOutcomeRating>().nullable()();
|
||||
TextColumn get notes => text().nullable()();
|
||||
}
|
||||
|
||||
/// The append-only event log on a Lot — history + provenance DAG.
|
||||
class Movements extends Table with AppendOnlyColumns {
|
||||
TextColumn get lotId => text()();
|
||||
|
|
|
|||
|
|
@ -368,15 +368,6 @@
|
|||
"saved": "Labels saved",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"scan": {
|
||||
"action": "Scan a seed label",
|
||||
"title": "Scan a label",
|
||||
"notALabel": "That code is not a seed label",
|
||||
"addTitle": "Not in your collection",
|
||||
"addBody": "Add “{label}” to your seeds?",
|
||||
"add": "Add it",
|
||||
"added": "Added to your collection"
|
||||
},
|
||||
"cropCalendar": {
|
||||
"add": "Crop calendar",
|
||||
"title": "Crop calendar",
|
||||
|
|
@ -709,37 +700,6 @@
|
|||
"promiseGave": "They'll return me seed",
|
||||
"promiseReceived": "I'll return seed"
|
||||
},
|
||||
"history": {
|
||||
"title": "Story of this batch",
|
||||
"tooltip": "Story",
|
||||
"sowToday": "Sown today",
|
||||
"harvestToday": "Harvested today",
|
||||
"sownRecorded": "Sowing noted",
|
||||
"harvestRecorded": "Harvest noted",
|
||||
"movementReceived": "Received",
|
||||
"movementGiven": "Given away",
|
||||
"movementSown": "Sown",
|
||||
"movementHarvested": "Harvested",
|
||||
"movementGerminationTest": "Germination test",
|
||||
"movementSplit": "Split into batches",
|
||||
"movementDiscarded": "Discarded",
|
||||
"created": "Added to your collection",
|
||||
"from": "From {origin}",
|
||||
"germinationResult": "Germination test — {percent}%",
|
||||
"linkedEarlier": "Comes from an earlier batch",
|
||||
"outcomeQuestion": "How did it do?",
|
||||
"outcomeGood": "Well",
|
||||
"outcomeMixed": "So-so",
|
||||
"outcomePoor": "Poorly",
|
||||
"outcomeNoteHint": "A note for your future self (optional)",
|
||||
"outcomeSaved": "Noted",
|
||||
"ratedGood": "It did well",
|
||||
"ratedMixed": "It did so-so",
|
||||
"ratedPoor": "It did poorly",
|
||||
"outcomeTitle": "Season note",
|
||||
"outcomeYear": "Season {year}",
|
||||
"isolationHint": "This species crosses with its neighbours — grow it about {meters} m away from others to keep it true"
|
||||
},
|
||||
"sale": {
|
||||
"title": "Sales",
|
||||
"help": "Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.",
|
||||
|
|
|
|||
|
|
@ -367,15 +367,6 @@
|
|||
"saved": "Etiquetas guardadas",
|
||||
"cancelled": "Cancelado"
|
||||
},
|
||||
"scan": {
|
||||
"action": "Escanear una etiqueta",
|
||||
"title": "Escanear etiqueta",
|
||||
"notALabel": "Ese código no es una etiqueta de semillas",
|
||||
"addTitle": "No está en tu colección",
|
||||
"addBody": "¿Añadir «{label}» a tus semillas?",
|
||||
"add": "Añadirla",
|
||||
"added": "Añadida a tu colección"
|
||||
},
|
||||
"cropCalendar": {
|
||||
"add": "Calendario de cultivo",
|
||||
"title": "Calendario de cultivo",
|
||||
|
|
@ -708,37 +699,6 @@
|
|||
"promiseGave": "Me devolverán semilla",
|
||||
"promiseReceived": "Devolveré semilla"
|
||||
},
|
||||
"history": {
|
||||
"title": "Historia de este lote",
|
||||
"tooltip": "Historia",
|
||||
"sowToday": "Sembrado hoy",
|
||||
"harvestToday": "Cosechado hoy",
|
||||
"sownRecorded": "Siembra anotada",
|
||||
"harvestRecorded": "Cosecha anotada",
|
||||
"movementReceived": "Recibido",
|
||||
"movementGiven": "Regalado",
|
||||
"movementSown": "Sembrado",
|
||||
"movementHarvested": "Cosechado",
|
||||
"movementGerminationTest": "Prueba de germinación",
|
||||
"movementSplit": "Repartido en lotes",
|
||||
"movementDiscarded": "Descartado",
|
||||
"created": "Entró en tu colección",
|
||||
"from": "De {origin}",
|
||||
"germinationResult": "Prueba de germinación — {percent}%",
|
||||
"linkedEarlier": "Viene de un lote anterior",
|
||||
"outcomeQuestion": "¿Qué tal se dio?",
|
||||
"outcomeGood": "Bien",
|
||||
"outcomeMixed": "Regular",
|
||||
"outcomePoor": "Mal",
|
||||
"outcomeNoteHint": "Una nota para tu yo del futuro (opcional)",
|
||||
"outcomeSaved": "Anotado",
|
||||
"ratedGood": "Se dio bien",
|
||||
"ratedMixed": "Se dio regular",
|
||||
"ratedPoor": "Se dio mal",
|
||||
"outcomeTitle": "Nota de temporada",
|
||||
"outcomeYear": "Temporada {year}",
|
||||
"isolationHint": "Esta especie se cruza con sus vecinas — cultívala a unos {meters} m de otras para que se mantenga fiel"
|
||||
},
|
||||
"sale": {
|
||||
"title": "Ventas",
|
||||
"help": "Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 7
|
||||
/// Strings: 3566 (509 per locale)
|
||||
/// Strings: 3494 (499 per locale)
|
||||
///
|
||||
/// Built on 2026-07-18 at 10:28 UTC
|
||||
/// Built on 2026-07-17 at 11:10 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||
late final Translations$abundance$en abundance = Translations$abundance$en.internal(_root);
|
||||
late final Translations$share$en share = Translations$share$en.internal(_root);
|
||||
late final Translations$printLabels$en printLabels = Translations$printLabels$en.internal(_root);
|
||||
late final Translations$scan$en scan = Translations$scan$en.internal(_root);
|
||||
late final Translations$cropCalendar$en cropCalendar = Translations$cropCalendar$en.internal(_root);
|
||||
late final Translations$needsReproduction$en needsReproduction = Translations$needsReproduction$en.internal(_root);
|
||||
late final Translations$preservation$en preservation = Translations$preservation$en.internal(_root);
|
||||
|
|
@ -87,7 +86,6 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||
late final Translations$notifications$en notifications = Translations$notifications$en.internal(_root);
|
||||
late final Translations$plantare$en plantare = Translations$plantare$en.internal(_root);
|
||||
late final Translations$handover$en handover = Translations$handover$en.internal(_root);
|
||||
late final Translations$history$en history = Translations$history$en.internal(_root);
|
||||
late final Translations$sale$en sale = Translations$sale$en.internal(_root);
|
||||
late final Translations$legal$en legal = Translations$legal$en.internal(_root);
|
||||
late final Translations$marketGate$en marketGate = Translations$marketGate$en.internal(_root);
|
||||
|
|
@ -1201,36 +1199,6 @@ class Translations$printLabels$en {
|
|||
String get cancelled => 'Cancelled';
|
||||
}
|
||||
|
||||
// Path: scan
|
||||
class Translations$scan$en {
|
||||
Translations$scan$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Scan a seed label'
|
||||
String get action => 'Scan a seed label';
|
||||
|
||||
/// en: 'Scan a label'
|
||||
String get title => 'Scan a label';
|
||||
|
||||
/// en: 'That code is not a seed label'
|
||||
String get notALabel => 'That code is not a seed label';
|
||||
|
||||
/// en: 'Not in your collection'
|
||||
String get addTitle => 'Not in your collection';
|
||||
|
||||
/// en: 'Add “{label}” to your seeds?'
|
||||
String addBody({required Object label}) => 'Add “${label}” to your seeds?';
|
||||
|
||||
/// en: 'Add it'
|
||||
String get add => 'Add it';
|
||||
|
||||
/// en: 'Added to your collection'
|
||||
String get added => 'Added to your collection';
|
||||
}
|
||||
|
||||
// Path: cropCalendar
|
||||
class Translations$cropCalendar$en {
|
||||
Translations$cropCalendar$en.internal(this._root);
|
||||
|
|
@ -2012,102 +1980,6 @@ class Translations$handover$en {
|
|||
String get promiseReceived => 'I\'ll return seed';
|
||||
}
|
||||
|
||||
// Path: history
|
||||
class Translations$history$en {
|
||||
Translations$history$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Story of this batch'
|
||||
String get title => 'Story of this batch';
|
||||
|
||||
/// en: 'Story'
|
||||
String get tooltip => 'Story';
|
||||
|
||||
/// en: 'Sown today'
|
||||
String get sowToday => 'Sown today';
|
||||
|
||||
/// en: 'Harvested today'
|
||||
String get harvestToday => 'Harvested today';
|
||||
|
||||
/// en: 'Sowing noted'
|
||||
String get sownRecorded => 'Sowing noted';
|
||||
|
||||
/// en: 'Harvest noted'
|
||||
String get harvestRecorded => 'Harvest noted';
|
||||
|
||||
/// en: 'Received'
|
||||
String get movementReceived => 'Received';
|
||||
|
||||
/// en: 'Given away'
|
||||
String get movementGiven => 'Given away';
|
||||
|
||||
/// en: 'Sown'
|
||||
String get movementSown => 'Sown';
|
||||
|
||||
/// en: 'Harvested'
|
||||
String get movementHarvested => 'Harvested';
|
||||
|
||||
/// en: 'Germination test'
|
||||
String get movementGerminationTest => 'Germination test';
|
||||
|
||||
/// en: 'Split into batches'
|
||||
String get movementSplit => 'Split into batches';
|
||||
|
||||
/// en: 'Discarded'
|
||||
String get movementDiscarded => 'Discarded';
|
||||
|
||||
/// en: 'Added to your collection'
|
||||
String get created => 'Added to your collection';
|
||||
|
||||
/// en: 'From {origin}'
|
||||
String from({required Object origin}) => 'From ${origin}';
|
||||
|
||||
/// en: 'Germination test — {percent}%'
|
||||
String germinationResult({required Object percent}) => 'Germination test — ${percent}%';
|
||||
|
||||
/// en: 'Comes from an earlier batch'
|
||||
String get linkedEarlier => 'Comes from an earlier batch';
|
||||
|
||||
/// en: 'How did it do?'
|
||||
String get outcomeQuestion => 'How did it do?';
|
||||
|
||||
/// en: 'Well'
|
||||
String get outcomeGood => 'Well';
|
||||
|
||||
/// en: 'So-so'
|
||||
String get outcomeMixed => 'So-so';
|
||||
|
||||
/// en: 'Poorly'
|
||||
String get outcomePoor => 'Poorly';
|
||||
|
||||
/// en: 'A note for your future self (optional)'
|
||||
String get outcomeNoteHint => 'A note for your future self (optional)';
|
||||
|
||||
/// en: 'Noted'
|
||||
String get outcomeSaved => 'Noted';
|
||||
|
||||
/// en: 'It did well'
|
||||
String get ratedGood => 'It did well';
|
||||
|
||||
/// en: 'It did so-so'
|
||||
String get ratedMixed => 'It did so-so';
|
||||
|
||||
/// en: 'It did poorly'
|
||||
String get ratedPoor => 'It did poorly';
|
||||
|
||||
/// en: 'Season note'
|
||||
String get outcomeTitle => 'Season note';
|
||||
|
||||
/// en: 'Season {year}'
|
||||
String outcomeYear({required Object year}) => 'Season ${year}';
|
||||
|
||||
/// en: 'This species crosses with its neighbours — grow it about {meters} m away from others to keep it true'
|
||||
String isolationHint({required Object meters}) => 'This species crosses with its neighbours — grow it about ${meters} m away from others to keep it true';
|
||||
}
|
||||
|
||||
// Path: sale
|
||||
class Translations$sale$en {
|
||||
Translations$sale$en.internal(this._root);
|
||||
|
|
@ -3073,13 +2945,6 @@ extension on Translations {
|
|||
'printLabels.save' => 'Save labels',
|
||||
'printLabels.saved' => 'Labels saved',
|
||||
'printLabels.cancelled' => 'Cancelled',
|
||||
'scan.action' => 'Scan a seed label',
|
||||
'scan.title' => 'Scan a label',
|
||||
'scan.notALabel' => 'That code is not a seed label',
|
||||
'scan.addTitle' => 'Not in your collection',
|
||||
'scan.addBody' => ({required Object label}) => 'Add “${label}” to your seeds?',
|
||||
'scan.add' => 'Add it',
|
||||
'scan.added' => 'Added to your collection',
|
||||
'cropCalendar.add' => 'Crop calendar',
|
||||
'cropCalendar.title' => 'Crop calendar',
|
||||
'cropCalendar.sow' => 'Sow',
|
||||
|
|
@ -3290,8 +3155,6 @@ extension on Translations {
|
|||
'plantare.statusForgiven' => 'Settled',
|
||||
'plantare.openSection' => 'Open',
|
||||
'plantare.settledSection' => 'Done',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'plantare.removeConfirm' => 'Remove this commitment?',
|
||||
'plantare.returnBy' => ({required Object date}) => 'Return by ${date}',
|
||||
'plantare.overdue' => 'overdue',
|
||||
|
|
@ -3299,6 +3162,8 @@ extension on Translations {
|
|||
'plantare.dueByHint' => 'A gentle reminder, never enforced',
|
||||
'plantare.pickDate' => 'Pick a date',
|
||||
'plantare.clearDate' => 'Clear date',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'plantare.sectionTitle' => 'Commitments',
|
||||
'plantare.propose' => 'Propose a signed Plantaré',
|
||||
'plantare.proposeHelp' => 'Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.',
|
||||
|
|
@ -3334,35 +3199,6 @@ extension on Translations {
|
|||
'handover.paymentChip' => 'Money changed hands',
|
||||
'handover.promiseGave' => 'They\'ll return me seed',
|
||||
'handover.promiseReceived' => 'I\'ll return seed',
|
||||
'history.title' => 'Story of this batch',
|
||||
'history.tooltip' => 'Story',
|
||||
'history.sowToday' => 'Sown today',
|
||||
'history.harvestToday' => 'Harvested today',
|
||||
'history.sownRecorded' => 'Sowing noted',
|
||||
'history.harvestRecorded' => 'Harvest noted',
|
||||
'history.movementReceived' => 'Received',
|
||||
'history.movementGiven' => 'Given away',
|
||||
'history.movementSown' => 'Sown',
|
||||
'history.movementHarvested' => 'Harvested',
|
||||
'history.movementGerminationTest' => 'Germination test',
|
||||
'history.movementSplit' => 'Split into batches',
|
||||
'history.movementDiscarded' => 'Discarded',
|
||||
'history.created' => 'Added to your collection',
|
||||
'history.from' => ({required Object origin}) => 'From ${origin}',
|
||||
'history.germinationResult' => ({required Object percent}) => 'Germination test — ${percent}%',
|
||||
'history.linkedEarlier' => 'Comes from an earlier batch',
|
||||
'history.outcomeQuestion' => 'How did it do?',
|
||||
'history.outcomeGood' => 'Well',
|
||||
'history.outcomeMixed' => 'So-so',
|
||||
'history.outcomePoor' => 'Poorly',
|
||||
'history.outcomeNoteHint' => 'A note for your future self (optional)',
|
||||
'history.outcomeSaved' => 'Noted',
|
||||
'history.ratedGood' => 'It did well',
|
||||
'history.ratedMixed' => 'It did so-so',
|
||||
'history.ratedPoor' => 'It did poorly',
|
||||
'history.outcomeTitle' => 'Season note',
|
||||
'history.outcomeYear' => ({required Object year}) => 'Season ${year}',
|
||||
'history.isolationHint' => ({required Object meters}) => 'This species crosses with its neighbours — grow it about ${meters} m away from others to keep it true',
|
||||
'sale.title' => 'Sales',
|
||||
'sale.help' => 'Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.',
|
||||
'sale.add' => 'Record a sale',
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@override late final _Translations$abundance$es abundance = _Translations$abundance$es._(_root);
|
||||
@override late final _Translations$share$es share = _Translations$share$es._(_root);
|
||||
@override late final _Translations$printLabels$es printLabels = _Translations$printLabels$es._(_root);
|
||||
@override late final _Translations$scan$es scan = _Translations$scan$es._(_root);
|
||||
@override late final _Translations$cropCalendar$es cropCalendar = _Translations$cropCalendar$es._(_root);
|
||||
@override late final _Translations$needsReproduction$es needsReproduction = _Translations$needsReproduction$es._(_root);
|
||||
@override late final _Translations$preservation$es preservation = _Translations$preservation$es._(_root);
|
||||
|
|
@ -86,7 +85,6 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@override late final _Translations$notifications$es notifications = _Translations$notifications$es._(_root);
|
||||
@override late final _Translations$plantare$es plantare = _Translations$plantare$es._(_root);
|
||||
@override late final _Translations$handover$es handover = _Translations$handover$es._(_root);
|
||||
@override late final _Translations$history$es history = _Translations$history$es._(_root);
|
||||
@override late final _Translations$sale$es sale = _Translations$sale$es._(_root);
|
||||
@override late final _Translations$legal$es legal = _Translations$legal$es._(_root);
|
||||
@override late final _Translations$marketGate$es marketGate = _Translations$marketGate$es._(_root);
|
||||
|
|
@ -651,22 +649,6 @@ class _Translations$printLabels$es extends Translations$printLabels$en {
|
|||
@override String get cancelled => 'Cancelado';
|
||||
}
|
||||
|
||||
// Path: scan
|
||||
class _Translations$scan$es extends Translations$scan$en {
|
||||
_Translations$scan$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get action => 'Escanear una etiqueta';
|
||||
@override String get title => 'Escanear etiqueta';
|
||||
@override String get notALabel => 'Ese código no es una etiqueta de semillas';
|
||||
@override String get addTitle => 'No está en tu colección';
|
||||
@override String addBody({required Object label}) => '¿Añadir «${label}» a tus semillas?';
|
||||
@override String get add => 'Añadirla';
|
||||
@override String get added => 'Añadida a tu colección';
|
||||
}
|
||||
|
||||
// Path: cropCalendar
|
||||
class _Translations$cropCalendar$es extends Translations$cropCalendar$en {
|
||||
_Translations$cropCalendar$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
|
@ -1039,44 +1021,6 @@ class _Translations$handover$es extends Translations$handover$en {
|
|||
@override String get promiseReceived => 'Devolveré semilla';
|
||||
}
|
||||
|
||||
// Path: history
|
||||
class _Translations$history$es extends Translations$history$en {
|
||||
_Translations$history$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get title => 'Historia de este lote';
|
||||
@override String get tooltip => 'Historia';
|
||||
@override String get sowToday => 'Sembrado hoy';
|
||||
@override String get harvestToday => 'Cosechado hoy';
|
||||
@override String get sownRecorded => 'Siembra anotada';
|
||||
@override String get harvestRecorded => 'Cosecha anotada';
|
||||
@override String get movementReceived => 'Recibido';
|
||||
@override String get movementGiven => 'Regalado';
|
||||
@override String get movementSown => 'Sembrado';
|
||||
@override String get movementHarvested => 'Cosechado';
|
||||
@override String get movementGerminationTest => 'Prueba de germinación';
|
||||
@override String get movementSplit => 'Repartido en lotes';
|
||||
@override String get movementDiscarded => 'Descartado';
|
||||
@override String get created => 'Entró en tu colección';
|
||||
@override String from({required Object origin}) => 'De ${origin}';
|
||||
@override String germinationResult({required Object percent}) => 'Prueba de germinación — ${percent}%';
|
||||
@override String get linkedEarlier => 'Viene de un lote anterior';
|
||||
@override String get outcomeQuestion => '¿Qué tal se dio?';
|
||||
@override String get outcomeGood => 'Bien';
|
||||
@override String get outcomeMixed => 'Regular';
|
||||
@override String get outcomePoor => 'Mal';
|
||||
@override String get outcomeNoteHint => 'Una nota para tu yo del futuro (opcional)';
|
||||
@override String get outcomeSaved => 'Anotado';
|
||||
@override String get ratedGood => 'Se dio bien';
|
||||
@override String get ratedMixed => 'Se dio regular';
|
||||
@override String get ratedPoor => 'Se dio mal';
|
||||
@override String get outcomeTitle => 'Nota de temporada';
|
||||
@override String outcomeYear({required Object year}) => 'Temporada ${year}';
|
||||
@override String isolationHint({required Object meters}) => 'Esta especie se cruza con sus vecinas — cultívala a unos ${meters} m de otras para que se mantenga fiel';
|
||||
}
|
||||
|
||||
// Path: sale
|
||||
class _Translations$sale$es extends Translations$sale$en {
|
||||
_Translations$sale$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
|
@ -1813,13 +1757,6 @@ extension on TranslationsEs {
|
|||
'printLabels.save' => 'Guardar etiquetas',
|
||||
'printLabels.saved' => 'Etiquetas guardadas',
|
||||
'printLabels.cancelled' => 'Cancelado',
|
||||
'scan.action' => 'Escanear una etiqueta',
|
||||
'scan.title' => 'Escanear etiqueta',
|
||||
'scan.notALabel' => 'Ese código no es una etiqueta de semillas',
|
||||
'scan.addTitle' => 'No está en tu colección',
|
||||
'scan.addBody' => ({required Object label}) => '¿Añadir «${label}» a tus semillas?',
|
||||
'scan.add' => 'Añadirla',
|
||||
'scan.added' => 'Añadida a tu colección',
|
||||
'cropCalendar.add' => 'Calendario de cultivo',
|
||||
'cropCalendar.title' => 'Calendario de cultivo',
|
||||
'cropCalendar.sow' => 'Siembra',
|
||||
|
|
@ -2031,8 +1968,6 @@ extension on TranslationsEs {
|
|||
'plantare.openSection' => 'Pendientes',
|
||||
'plantare.settledSection' => 'Hechos',
|
||||
'plantare.removeConfirm' => '¿Quitar este compromiso?',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'plantare.returnBy' => ({required Object date}) => 'Devolver antes del ${date}',
|
||||
'plantare.overdue' => 'vencido',
|
||||
'plantare.dueByLabel' => 'Devolver antes de (opcional)',
|
||||
|
|
@ -2040,6 +1975,8 @@ extension on TranslationsEs {
|
|||
'plantare.pickDate' => 'Elegir fecha',
|
||||
'plantare.clearDate' => 'Quitar fecha',
|
||||
'plantare.sectionTitle' => 'Compromisos',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'plantare.propose' => 'Proponer un Plantaré firmado',
|
||||
'plantare.proposeHelp' => 'Los dos guardáis la misma promesa, firmada por ambos: prueba de que esta semilla cambió de manos y se cultivará y devolverá.',
|
||||
'plantare.proposeTo' => ({required Object name}) => 'Con ${name}',
|
||||
|
|
@ -2074,35 +2011,6 @@ extension on TranslationsEs {
|
|||
'handover.paymentChip' => 'Hubo dinero por medio',
|
||||
'handover.promiseGave' => 'Me devolverán semilla',
|
||||
'handover.promiseReceived' => 'Devolveré semilla',
|
||||
'history.title' => 'Historia de este lote',
|
||||
'history.tooltip' => 'Historia',
|
||||
'history.sowToday' => 'Sembrado hoy',
|
||||
'history.harvestToday' => 'Cosechado hoy',
|
||||
'history.sownRecorded' => 'Siembra anotada',
|
||||
'history.harvestRecorded' => 'Cosecha anotada',
|
||||
'history.movementReceived' => 'Recibido',
|
||||
'history.movementGiven' => 'Regalado',
|
||||
'history.movementSown' => 'Sembrado',
|
||||
'history.movementHarvested' => 'Cosechado',
|
||||
'history.movementGerminationTest' => 'Prueba de germinación',
|
||||
'history.movementSplit' => 'Repartido en lotes',
|
||||
'history.movementDiscarded' => 'Descartado',
|
||||
'history.created' => 'Entró en tu colección',
|
||||
'history.from' => ({required Object origin}) => 'De ${origin}',
|
||||
'history.germinationResult' => ({required Object percent}) => 'Prueba de germinación — ${percent}%',
|
||||
'history.linkedEarlier' => 'Viene de un lote anterior',
|
||||
'history.outcomeQuestion' => '¿Qué tal se dio?',
|
||||
'history.outcomeGood' => 'Bien',
|
||||
'history.outcomeMixed' => 'Regular',
|
||||
'history.outcomePoor' => 'Mal',
|
||||
'history.outcomeNoteHint' => 'Una nota para tu yo del futuro (opcional)',
|
||||
'history.outcomeSaved' => 'Anotado',
|
||||
'history.ratedGood' => 'Se dio bien',
|
||||
'history.ratedMixed' => 'Se dio regular',
|
||||
'history.ratedPoor' => 'Se dio mal',
|
||||
'history.outcomeTitle' => 'Nota de temporada',
|
||||
'history.outcomeYear' => ({required Object year}) => 'Temporada ${year}',
|
||||
'history.isolationHint' => ({required Object meters}) => 'Esta especie se cruza con sus vecinas — cultívala a unos ${meters} m de otras para que se mantenga fiel',
|
||||
'sale.title' => 'Ventas',
|
||||
'sale.help' => 'Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.',
|
||||
'sale.add' => 'Registrar venta',
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../data/export_import/seed_label_codec.dart';
|
||||
import '../data/species_repository.dart';
|
||||
import '../data/variety_repository.dart';
|
||||
|
||||
/// What scanning a QR yielded: not a seed label at all, a variety already in
|
||||
/// the collection ([varietyId] set), or a seed not held yet ([data] set — the
|
||||
/// UI asks before adding anything).
|
||||
class SeedScanResult extends Equatable {
|
||||
const SeedScanResult.notALabel() : varietyId = null, data = null;
|
||||
const SeedScanResult.match(String this.varietyId) : data = null;
|
||||
const SeedScanResult.unknown(SeedLabelData this.data) : varietyId = null;
|
||||
|
||||
final String? varietyId;
|
||||
final SeedLabelData? data;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [varietyId, data];
|
||||
}
|
||||
|
||||
/// Resolves a scanned QR payload against the collection: decodes the
|
||||
/// `tane://seed` label and looks the variety up by its label (the identity a
|
||||
/// keeper printed on the envelope). This is how a physical packet finds its
|
||||
/// way back to its record — even a re-saved one whose digital thread broke.
|
||||
Future<SeedScanResult> resolveScannedLabel(
|
||||
VarietyRepository repository,
|
||||
String raw,
|
||||
) async {
|
||||
final data = SeedLabelCodec.decode(raw);
|
||||
if (data == null) return const SeedScanResult.notALabel();
|
||||
final varietyId = await repository.findVarietyIdByLabel(data.varietyLabel);
|
||||
return varietyId == null
|
||||
? SeedScanResult.unknown(data)
|
||||
: SeedScanResult.match(varietyId);
|
||||
}
|
||||
|
||||
/// Adds the scanned seed to the collection — a variety named as the label
|
||||
/// says, linked to the bundled species when the scientific name matches
|
||||
/// exactly, with one lot carrying the label's harvest year and origin.
|
||||
/// Called only after the person confirmed the add prompt.
|
||||
Future<String> importScannedLabel({
|
||||
required VarietyRepository repository,
|
||||
required SpeciesRepository species,
|
||||
required SeedLabelData data,
|
||||
}) async {
|
||||
final varietyId = await repository.addQuickVariety(label: data.varietyLabel);
|
||||
|
||||
final scientific = data.scientificName?.trim();
|
||||
if (scientific != null && scientific.isNotEmpty) {
|
||||
final matches = await species.search(scientific, limit: 4);
|
||||
for (final m in matches) {
|
||||
if (m.scientificName.toLowerCase() == scientific.toLowerCase()) {
|
||||
await repository.linkSpecies(varietyId, m.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await repository.addLot(
|
||||
varietyId: varietyId,
|
||||
harvestYear: data.year,
|
||||
originName: data.origin,
|
||||
);
|
||||
return varietyId;
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../data/species_repository.dart';
|
||||
import '../data/variety_repository.dart';
|
||||
import '../db/enums.dart';
|
||||
import '../di/injector.dart';
|
||||
|
|
@ -15,7 +14,6 @@ import 'draft_triage.dart';
|
|||
import 'edge_fade.dart';
|
||||
import 'filter_chips.dart';
|
||||
import 'label_print_sheet.dart';
|
||||
import 'qr_scan.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
import 'quantity_picker.dart';
|
||||
import 'quick_add_sheet.dart';
|
||||
|
|
@ -135,15 +133,6 @@ class InventoryListScreen extends StatelessWidget {
|
|||
tooltip: t.printLabels.action,
|
||||
onPressed: context.read<InventoryCubit>().startSelection,
|
||||
),
|
||||
// Scan a printed seed label back into its record — the physical
|
||||
// envelope re-finds its digital thread. Mobile-only (camera).
|
||||
if (qrScanSupported)
|
||||
IconButton(
|
||||
key: const Key('inventory.scanLabel'),
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
tooltip: t.scan.action,
|
||||
onPressed: () => _scanLabel(context),
|
||||
),
|
||||
IconButton(
|
||||
key: const Key('inventory.captureBurst'),
|
||||
icon: const Icon(Icons.add_a_photo_outlined),
|
||||
|
|
@ -244,20 +233,6 @@ class InventoryListScreen extends StatelessWidget {
|
|||
return parts.isEmpty ? null : parts.join(' · ');
|
||||
}
|
||||
|
||||
Future<void> _scanLabel(BuildContext context) async {
|
||||
final repository = context.read<VarietyRepository>();
|
||||
final species = context.read<SpeciesRepository>();
|
||||
final payload = await scanSeedLabelQr(context);
|
||||
if (payload == null || !context.mounted) return;
|
||||
await handleScannedPayload(
|
||||
context,
|
||||
repository: repository,
|
||||
species: species,
|
||||
payload: payload,
|
||||
openVariety: (id) => context.push('/variety/$id'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _captureBurst(BuildContext context) async {
|
||||
final t = context.t;
|
||||
final repository = context.read<VarietyRepository>();
|
||||
|
|
|
|||
|
|
@ -1,401 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../data/variety_repository.dart';
|
||||
import '../db/enums.dart';
|
||||
import '../domain/seed_saving.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
|
||||
/// Opens the batch's story — a read-only timeline of everything already
|
||||
/// recorded about it (movements, germination tests, storage checks, and how it
|
||||
/// entered the collection), plus two one-tap chips to note "sown today" /
|
||||
/// "harvested today". The happy path is literally one tap; there is no form.
|
||||
Future<void> showLotHistorySheet(
|
||||
BuildContext context, {
|
||||
required VarietyRepository repository,
|
||||
required VarietyLot lot,
|
||||
SeedSavingGuide? guide,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) =>
|
||||
_LotHistorySheet(repository: repository, lot: lot, guide: guide),
|
||||
);
|
||||
}
|
||||
|
||||
class _LotHistorySheet extends StatefulWidget {
|
||||
const _LotHistorySheet({
|
||||
required this.repository,
|
||||
required this.lot,
|
||||
this.guide,
|
||||
});
|
||||
|
||||
final VarietyRepository repository;
|
||||
final VarietyLot lot;
|
||||
|
||||
/// Bundled species guidance, or null. Only read to drop the one-line
|
||||
/// isolation hint when a cross-pollinating crop is sown — selfers get
|
||||
/// nothing (the fields people record religiously where they don't matter).
|
||||
final SeedSavingGuide? guide;
|
||||
|
||||
@override
|
||||
State<_LotHistorySheet> createState() => _LotHistorySheetState();
|
||||
}
|
||||
|
||||
class _LotHistorySheetState extends State<_LotHistorySheet> {
|
||||
// One-shot future (data-model rule: a transient sheet must never hold a
|
||||
// live subscription). Re-assigned after a quick record to refresh the list.
|
||||
late Future<List<LotHistoryEntry>> _history;
|
||||
bool _saving = false;
|
||||
|
||||
// The optional, skippable "how did it do?" asked right after a harvest is
|
||||
// noted — the only moment it appears. Dismissed, it never nags again.
|
||||
bool _askOutcome = false;
|
||||
final _outcomeNote = TextEditingController();
|
||||
|
||||
// One passive line after sowing a crosser ("this species crosses — isolate
|
||||
// ~400 m"). Never shown for selfers, never an input.
|
||||
bool _showSowingHint = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_history = widget.repository.lotHistory(widget.lot.id);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_outcomeNote.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _record(MovementType type) async {
|
||||
if (_saving) return;
|
||||
setState(() => _saving = true);
|
||||
final t = context.t;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await widget.repository.recordCultivation(
|
||||
lotId: widget.lot.id,
|
||||
type: type,
|
||||
);
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
type == MovementType.sown
|
||||
? t.history.sownRecorded
|
||||
: t.history.harvestRecorded,
|
||||
),
|
||||
),
|
||||
);
|
||||
final guide = widget.guide;
|
||||
setState(() {
|
||||
_saving = false;
|
||||
_askOutcome = type == MovementType.harvested;
|
||||
_showSowingHint =
|
||||
type == MovementType.sown &&
|
||||
guide?.pollination == Pollination.cross &&
|
||||
guide?.isolationMinM != null;
|
||||
_history = widget.repository.lotHistory(widget.lot.id);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveOutcome(GardenOutcomeRating rating) async {
|
||||
if (_saving) return;
|
||||
setState(() => _saving = true);
|
||||
final t = context.t;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final note = _outcomeNote.text.trim();
|
||||
await widget.repository.addGardenOutcome(
|
||||
lotId: widget.lot.id,
|
||||
year: DateTime.now().year,
|
||||
rating: rating,
|
||||
notes: note.isEmpty ? null : note,
|
||||
);
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(SnackBar(content: Text(t.history.outcomeSaved)));
|
||||
_outcomeNote.clear();
|
||||
setState(() {
|
||||
_saving = false;
|
||||
_askOutcome = false;
|
||||
_history = widget.repository.lotHistory(widget.lot.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
// Bottom inset keeps the optional note field above the keyboard.
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
24 + MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.history.title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (widget.lot.type == LotType.seed)
|
||||
ActionChip(
|
||||
key: const Key('history.sowToday'),
|
||||
avatar: const Icon(Icons.grass_outlined, size: 18),
|
||||
label: Text(t.history.sowToday),
|
||||
onPressed: _saving
|
||||
? null
|
||||
: () => _record(MovementType.sown),
|
||||
),
|
||||
ActionChip(
|
||||
key: const Key('history.harvestToday'),
|
||||
avatar: const Icon(Icons.agriculture_outlined, size: 18),
|
||||
label: Text(t.history.harvestToday),
|
||||
onPressed: _saving
|
||||
? null
|
||||
: () => _record(MovementType.harvested),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_showSowingHint)
|
||||
Padding(
|
||||
key: const Key('history.isolationHint'),
|
||||
padding: const EdgeInsetsDirectional.only(top: 8, start: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.lightbulb_outline, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.t.history.isolationHint(
|
||||
meters: widget.guide!.isolationMinM!,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_askOutcome) ...[
|
||||
const SizedBox(height: 8),
|
||||
_OutcomeQuestion(
|
||||
note: _outcomeNote,
|
||||
saving: _saving,
|
||||
onRate: _saveOutcome,
|
||||
onDismiss: () => setState(() => _askOutcome = false),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Flexible(
|
||||
child: FutureBuilder<List<LotHistoryEntry>>(
|
||||
future: _history,
|
||||
builder: (context, snapshot) {
|
||||
final entries = snapshot.data;
|
||||
if (entries == null) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: entries.length,
|
||||
itemBuilder: (context, i) =>
|
||||
_HistoryTile(entry: entries[i]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The one skippable question of the whole flow: three faces and an optional
|
||||
/// note. Tapping a face saves; the X dismisses without a trace.
|
||||
class _OutcomeQuestion extends StatelessWidget {
|
||||
const _OutcomeQuestion({
|
||||
required this.note,
|
||||
required this.saving,
|
||||
required this.onRate,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
final TextEditingController note;
|
||||
final bool saving;
|
||||
final ValueChanged<GardenOutcomeRating> onRate;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return Card(
|
||||
key: const Key('history.outcomeQuestion'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 4, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
t.history.outcomeQuestion,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: const Key('history.outcomeDismiss'),
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: onDismiss,
|
||||
),
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ActionChip(
|
||||
key: const Key('history.outcomeGood'),
|
||||
avatar: const Icon(Icons.sentiment_satisfied_alt, size: 18),
|
||||
label: Text(t.history.outcomeGood),
|
||||
onPressed: saving
|
||||
? null
|
||||
: () => onRate(GardenOutcomeRating.good),
|
||||
),
|
||||
ActionChip(
|
||||
key: const Key('history.outcomeMixed'),
|
||||
avatar: const Icon(Icons.sentiment_neutral, size: 18),
|
||||
label: Text(t.history.outcomeMixed),
|
||||
onPressed: saving
|
||||
? null
|
||||
: () => onRate(GardenOutcomeRating.mixed),
|
||||
),
|
||||
ActionChip(
|
||||
key: const Key('history.outcomePoor'),
|
||||
avatar: const Icon(Icons.sentiment_dissatisfied, size: 18),
|
||||
label: Text(t.history.outcomePoor),
|
||||
onPressed: saving
|
||||
? null
|
||||
: () => onRate(GardenOutcomeRating.poor),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextField(
|
||||
key: const Key('history.outcomeNote'),
|
||||
controller: note,
|
||||
decoration: InputDecoration(
|
||||
hintText: t.history.outcomeNoteHint,
|
||||
isDense: true,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryTile extends StatelessWidget {
|
||||
const _HistoryTile({required this.entry});
|
||||
|
||||
final LotHistoryEntry entry;
|
||||
|
||||
IconData get _icon => switch (entry.kind) {
|
||||
LotHistoryKind.created => Icons.spa_outlined,
|
||||
LotHistoryKind.germinationTest => Icons.science_outlined,
|
||||
LotHistoryKind.conditionCheck => Icons.inventory_2_outlined,
|
||||
LotHistoryKind.gardenOutcome => switch (entry.rating) {
|
||||
GardenOutcomeRating.good => Icons.sentiment_satisfied_alt,
|
||||
GardenOutcomeRating.mixed => Icons.sentiment_neutral,
|
||||
GardenOutcomeRating.poor => Icons.sentiment_dissatisfied,
|
||||
null => Icons.edit_note,
|
||||
},
|
||||
LotHistoryKind.movement => switch (entry.movementType!) {
|
||||
MovementType.received => Icons.call_received,
|
||||
MovementType.given => Icons.redeem_outlined,
|
||||
MovementType.sown => Icons.grass_outlined,
|
||||
MovementType.harvested => Icons.agriculture_outlined,
|
||||
MovementType.germinationTest => Icons.science_outlined,
|
||||
MovementType.split => Icons.call_split,
|
||||
MovementType.discarded => Icons.delete_outline,
|
||||
},
|
||||
};
|
||||
|
||||
String _title(Translations t) => switch (entry.kind) {
|
||||
LotHistoryKind.created => t.history.created,
|
||||
LotHistoryKind.conditionCheck => t.conditionCheck.title,
|
||||
LotHistoryKind.gardenOutcome => switch (entry.rating) {
|
||||
GardenOutcomeRating.good => t.history.ratedGood,
|
||||
GardenOutcomeRating.mixed => t.history.ratedMixed,
|
||||
GardenOutcomeRating.poor => t.history.ratedPoor,
|
||||
null => t.history.outcomeTitle,
|
||||
},
|
||||
LotHistoryKind.germinationTest => entry.germinationRate == null
|
||||
? t.germination.title
|
||||
: t.history.germinationResult(
|
||||
percent: (entry.germinationRate! * 100).round(),
|
||||
),
|
||||
LotHistoryKind.movement => switch (entry.movementType!) {
|
||||
MovementType.received => t.history.movementReceived,
|
||||
MovementType.given => t.history.movementGiven,
|
||||
MovementType.sown => t.history.movementSown,
|
||||
MovementType.harvested => t.history.movementHarvested,
|
||||
MovementType.germinationTest => t.history.movementGerminationTest,
|
||||
MovementType.split => t.history.movementSplit,
|
||||
MovementType.discarded => t.history.movementDiscarded,
|
||||
},
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
final theme = Theme.of(context);
|
||||
final when = entry.when;
|
||||
final date = when == null
|
||||
? null
|
||||
// Format via the Localizations locale, not the raw app locale (see
|
||||
// backup_section.dart: `ast` has no intl symbols, mapped to `es`).
|
||||
: DateFormat.yMMMd(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
).format(DateTime.fromMillisecondsSinceEpoch(when));
|
||||
|
||||
final origin = [
|
||||
entry.originName,
|
||||
entry.originPlace,
|
||||
].whereType<String>().where((s) => s.trim().isNotEmpty).join(' · ');
|
||||
|
||||
final details = [
|
||||
if (entry.kind == LotHistoryKind.created && origin.isNotEmpty)
|
||||
t.history.from(origin: origin),
|
||||
if (entry.year case final year?) t.history.outcomeYear(year: year),
|
||||
if (entry.quantity != null) quantityDisplay(t, entry.quantity!),
|
||||
?entry.counterpartyName,
|
||||
if (entry.hasParentLink) t.history.linkedEarlier,
|
||||
if (entry.notes case final n? when n.trim().isNotEmpty) n.trim(),
|
||||
].join('\n');
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(_icon, size: 20, color: theme.colorScheme.primary),
|
||||
title: Text(_title(t)),
|
||||
subtitle: details.isEmpty ? null : Text(details),
|
||||
trailing: date == null
|
||||
? null
|
||||
: Text(date, style: theme.textTheme.bodySmall),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:zxing_barcode_scanner/zxing_barcode_scanner.dart';
|
||||
|
||||
import '../data/species_repository.dart';
|
||||
import '../data/variety_repository.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/seed_label_scan.dart';
|
||||
|
||||
/// Whether this platform can open the camera scanner. Mobile-only for now
|
||||
/// (pure-ZXing platform views; no Google Play Services) — elsewhere the scan
|
||||
/// button simply isn't offered, same pattern as the OCR capture.
|
||||
bool get qrScanSupported => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||
|
||||
/// Opens the full-screen camera scanner and returns the first decoded QR
|
||||
/// payload, or null if the person backed out. (Ğ1nkgo's scanner pattern on
|
||||
/// the same pure-ZXing stack.)
|
||||
Future<String?> scanSeedLabelQr(BuildContext context) {
|
||||
return Navigator.of(context).push<String>(
|
||||
MaterialPageRoute<String>(builder: (_) => const _ScannerScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Acts on a scanned payload: a known seed opens its record (via
|
||||
/// [openVariety]); an unknown seed label asks before adding anything; anything
|
||||
/// else says so and moves on. Navigation is injected so the flow is
|
||||
/// widget-testable without a camera or a router.
|
||||
Future<void> handleScannedPayload(
|
||||
BuildContext context, {
|
||||
required VarietyRepository repository,
|
||||
required SpeciesRepository species,
|
||||
required String payload,
|
||||
required void Function(String varietyId) openVariety,
|
||||
}) async {
|
||||
final t = context.t;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final result = await resolveScannedLabel(repository, payload);
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (result.varietyId case final id?) {
|
||||
openVariety(id);
|
||||
return;
|
||||
}
|
||||
final data = result.data;
|
||||
if (data == null) {
|
||||
messenger.showSnackBar(SnackBar(content: Text(t.scan.notALabel)));
|
||||
return;
|
||||
}
|
||||
|
||||
final add = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
key: const Key('scan.addPrompt'),
|
||||
title: Text(t.scan.addTitle),
|
||||
content: Text(t.scan.addBody(label: data.varietyLabel)),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: const Key('scan.cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
key: const Key('scan.add'),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(t.scan.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (add != true) return;
|
||||
|
||||
final varietyId = await importScannedLabel(
|
||||
repository: repository,
|
||||
species: species,
|
||||
data: data,
|
||||
);
|
||||
messenger.showSnackBar(SnackBar(content: Text(t.scan.added)));
|
||||
openVariety(varietyId);
|
||||
}
|
||||
|
||||
class _ScannerScreen extends StatefulWidget {
|
||||
const _ScannerScreen();
|
||||
|
||||
@override
|
||||
State<_ScannerScreen> createState() => _ScannerScreenState();
|
||||
}
|
||||
|
||||
class _ScannerScreenState extends State<_ScannerScreen> {
|
||||
bool _done = false;
|
||||
|
||||
void _onScan(List<BarcodeResult> results) {
|
||||
if (_done || !mounted || results.isEmpty) return;
|
||||
final text = results.first.text;
|
||||
if (text == null || text.isEmpty) return;
|
||||
_done = true;
|
||||
// A beat so the camera view settles before popping (Ğ1nkgo does the same).
|
||||
Future<void>.delayed(const Duration(milliseconds: 150), () {
|
||||
if (mounted) Navigator.of(context).pop(text);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(t.scan.title)),
|
||||
body: ZxingBarcodeScanner(
|
||||
onScan: _onScan,
|
||||
onError: (error) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(error.message ?? t.common.offline),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ import '../i18n/strings.g.dart';
|
|||
import '../state/variety_detail_cubit.dart';
|
||||
import 'currency_quick_picks.dart';
|
||||
import 'handover_sheet.dart';
|
||||
import 'lot_history_sheet.dart';
|
||||
import 'harvest_date_picker.dart';
|
||||
import 'photo_pick.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
|
|
@ -237,10 +236,6 @@ class _DetailView extends StatelessWidget {
|
|||
cubit: cubit,
|
||||
lot: lot,
|
||||
viabilityYears: detail.viabilityYears,
|
||||
guide: SeedSavingCatalog.guideFor(
|
||||
scientificName: detail.scientificName,
|
||||
family: detail.family ?? detail.category,
|
||||
),
|
||||
),
|
||||
_PlantaresSection(varietyId: detail.id),
|
||||
],
|
||||
|
|
@ -379,7 +374,6 @@ class _LotTile extends StatelessWidget {
|
|||
required this.cubit,
|
||||
required this.lot,
|
||||
required this.viabilityYears,
|
||||
this.guide,
|
||||
});
|
||||
|
||||
final VarietyDetailCubit cubit;
|
||||
|
|
@ -388,10 +382,6 @@ class _LotTile extends StatelessWidget {
|
|||
/// Typical seed longevity of the variety's species (years), or null.
|
||||
final int? viabilityYears;
|
||||
|
||||
/// Bundled seed-saving guidance for the species, or null — lets the history
|
||||
/// sheet drop a one-line isolation hint when a crosser is sown.
|
||||
final SeedSavingGuide? guide;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
|
|
@ -444,17 +434,6 @@ class _LotTile extends StatelessWidget {
|
|||
children: [
|
||||
if (lot.type == LotType.seed && lot.latestGerminationRate != null)
|
||||
_GerminationBadge(rate: lot.latestGerminationRate!),
|
||||
IconButton(
|
||||
key: Key('lot.history.${lot.id}'),
|
||||
icon: const Icon(Icons.history),
|
||||
tooltip: t.history.tooltip,
|
||||
onPressed: () => showLotHistorySheet(
|
||||
context,
|
||||
repository: context.read<VarietyRepository>(),
|
||||
lot: lot,
|
||||
guide: guide,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: Key('lot.handover.${lot.id}'),
|
||||
icon: const Icon(Icons.handshake_outlined),
|
||||
|
|
|
|||
|
|
@ -60,10 +60,6 @@ dependencies:
|
|||
# Pure-Dart barcode/QR generation (Apache-2.0; already used transitively by
|
||||
# pdf). Painted on screen for the profile's shareable identity QR.
|
||||
barcode: ^2.2.9
|
||||
# Camera QR scanning via pure ZXing (MIT) — no ML Kit, no Google Play
|
||||
# Services (F-Droid-safe; same stack Ğ1nkgo ships). Mobile-only; other
|
||||
# platforms simply don't offer the scan button.
|
||||
zxing_barcode_scanner: ^1.0.3
|
||||
path: ^1.9.0
|
||||
path_provider: ^2.1.5
|
||||
|
||||
|
|
|
|||
|
|
@ -68,12 +68,6 @@ void main() {
|
|||
containerCount: 2,
|
||||
desiccantState: DesiccantState.dry,
|
||||
);
|
||||
await source.addGardenOutcome(
|
||||
lotId: lotId,
|
||||
year: 2024,
|
||||
rating: GardenOutcomeRating.good,
|
||||
notes: 'kept it',
|
||||
);
|
||||
await source.addVernacularName(
|
||||
varietyId,
|
||||
'tomate de la abuela',
|
||||
|
|
@ -132,7 +126,6 @@ void main() {
|
|||
expect(targetRows.externalLinks, sourceRows.externalLinks);
|
||||
expect(targetRows.germinationTests, sourceRows.germinationTests);
|
||||
expect(targetRows.conditionChecks, sourceRows.conditionChecks);
|
||||
expect(targetRows.gardenOutcomes, sourceRows.gardenOutcomes);
|
||||
expect(targetRows.movements, sourceRows.movements);
|
||||
expect(targetRows.attachments, sourceRows.attachments);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ void main() {
|
|||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
test('freshly created database matches the exported schema v13', () async {
|
||||
final schema = await verifier.schemaAt(13);
|
||||
test('freshly created database matches the exported schema v12', () async {
|
||||
final schema = await verifier.schemaAt(12);
|
||||
final db = AppDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, 13);
|
||||
await verifier.migrateAndValidate(db, 12);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
// Every historical version upgrades cleanly to the current schema (v13).
|
||||
for (var from = 1; from <= 12; from++) {
|
||||
test('upgrades v$from → v13 and matches the fresh schema', () async {
|
||||
// Every historical version upgrades cleanly to the current schema (v12).
|
||||
for (var from = 1; from <= 11; from++) {
|
||||
test('upgrades v$from → v12 and matches the fresh schema', () async {
|
||||
final connection = await verifier.startAt(from);
|
||||
final db = AppDatabase(connection);
|
||||
await verifier.migrateAndValidate(db, 13);
|
||||
await verifier.migrateAndValidate(db, 12);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import 'schema_v9.dart' as v9;
|
|||
import 'schema_v10.dart' as v10;
|
||||
import 'schema_v11.dart' as v11;
|
||||
import 'schema_v12.dart' as v12;
|
||||
import 'schema_v13.dart' as v13;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
|
|
@ -46,12 +45,10 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||
return v11.DatabaseAtV11(db);
|
||||
case 12:
|
||||
return v12.DatabaseAtV12(db);
|
||||
case 13:
|
||||
return v13.DatabaseAtV13(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,170 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/data/variety_repository.dart';
|
||||
import 'package:tane/db/database.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
late VarietyRepository repo;
|
||||
|
||||
setUp(() {
|
||||
db = newTestDatabase();
|
||||
repo = newTestRepository(db);
|
||||
});
|
||||
|
||||
tearDown(() async => db.close());
|
||||
|
||||
Future<String> newLot({String? originName, String? originPlace}) async {
|
||||
final varietyId = await repo.addQuickVariety(label: 'Tomate rosa');
|
||||
return repo.addLot(
|
||||
varietyId: varietyId,
|
||||
harvestYear: 2025,
|
||||
originName: originName,
|
||||
originPlace: originPlace,
|
||||
);
|
||||
}
|
||||
|
||||
group('recordCultivation', () {
|
||||
test('records a sown movement with defaults', () async {
|
||||
final lotId = await newLot();
|
||||
final id = await repo.recordCultivation(
|
||||
lotId: lotId,
|
||||
type: MovementType.sown,
|
||||
);
|
||||
expect(id, isNotEmpty);
|
||||
final history = await repo.lotHistory(lotId);
|
||||
final sown = history.where(
|
||||
(e) => e.movementType == MovementType.sown,
|
||||
);
|
||||
expect(sown, hasLength(1));
|
||||
expect(sown.first.kind, LotHistoryKind.movement);
|
||||
expect(sown.first.when, isNotNull);
|
||||
});
|
||||
|
||||
test('records a harvested movement with a note and date', () async {
|
||||
final lotId = await newLot();
|
||||
await repo.recordCultivation(
|
||||
lotId: lotId,
|
||||
type: MovementType.harvested,
|
||||
occurredOn: DateTime(2026, 7, 1).millisecondsSinceEpoch,
|
||||
notes: 'great year',
|
||||
);
|
||||
final history = await repo.lotHistory(lotId);
|
||||
final harvested = history.singleWhere(
|
||||
(e) => e.movementType == MovementType.harvested,
|
||||
);
|
||||
expect(harvested.notes, 'great year');
|
||||
expect(
|
||||
harvested.when,
|
||||
DateTime(2026, 7, 1).millisecondsSinceEpoch,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects non-cultivation movement types', () async {
|
||||
final lotId = await newLot();
|
||||
expect(
|
||||
() => repo.recordCultivation(lotId: lotId, type: MovementType.given),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('lotHistory', () {
|
||||
test('starts with the lot creation entry carrying its origin', () async {
|
||||
final lotId = await newLot(originName: 'María', originPlace: 'Aiako');
|
||||
final history = await repo.lotHistory(lotId);
|
||||
expect(history, isNotEmpty);
|
||||
// Most-recent first: creation is the last entry.
|
||||
final created = history.last;
|
||||
expect(created.kind, LotHistoryKind.created);
|
||||
expect(created.originName, 'María');
|
||||
expect(created.originPlace, 'Aiako');
|
||||
});
|
||||
|
||||
test('merges movements, germination tests and condition checks', () async {
|
||||
final lotId = await newLot();
|
||||
await repo.addGerminationTest(
|
||||
lotId: lotId,
|
||||
testedOn: DateTime(2026, 2, 1).millisecondsSinceEpoch,
|
||||
sampleSize: 10,
|
||||
germinatedCount: 8,
|
||||
);
|
||||
await repo.addConditionCheck(
|
||||
lotId: lotId,
|
||||
checkedOn: DateTime(2026, 3, 1).millisecondsSinceEpoch,
|
||||
containerCount: 2,
|
||||
desiccantState: DesiccantState.dry,
|
||||
);
|
||||
await repo.recordCultivation(
|
||||
lotId: lotId,
|
||||
type: MovementType.sown,
|
||||
occurredOn: DateTime(2026, 4, 1).millisecondsSinceEpoch,
|
||||
);
|
||||
await repo.recordHandover(
|
||||
lotId: lotId,
|
||||
direction: HandoverDirection.iGave,
|
||||
counterparty: 'Pepe',
|
||||
);
|
||||
|
||||
final history = await repo.lotHistory(lotId);
|
||||
expect(history.map((e) => e.kind).toSet(), {
|
||||
LotHistoryKind.created,
|
||||
LotHistoryKind.movement,
|
||||
LotHistoryKind.germinationTest,
|
||||
LotHistoryKind.conditionCheck,
|
||||
});
|
||||
|
||||
final germination = history.singleWhere(
|
||||
(e) => e.kind == LotHistoryKind.germinationTest,
|
||||
);
|
||||
expect(germination.germinationRate, closeTo(0.8, 1e-9));
|
||||
|
||||
final check = history.singleWhere(
|
||||
(e) => e.kind == LotHistoryKind.conditionCheck,
|
||||
);
|
||||
expect(check.containerCount, 2);
|
||||
|
||||
// Most-recent first: the handover (today) leads, creation closes.
|
||||
expect(history.first.movementType, MovementType.given);
|
||||
expect(history.last.kind, LotHistoryKind.created);
|
||||
|
||||
// The middle entries are ordered by their explicit dates, descending.
|
||||
final dated = history
|
||||
.where((e) => e.kind != LotHistoryKind.created)
|
||||
.toList();
|
||||
for (var i = 0; i + 1 < dated.length; i++) {
|
||||
expect(dated[i].when ?? 0, greaterThanOrEqualTo(dated[i + 1].when ?? 0));
|
||||
}
|
||||
});
|
||||
|
||||
test('includes garden outcomes as story lines', () async {
|
||||
final lotId = await newLot();
|
||||
await repo.addGardenOutcome(
|
||||
lotId: lotId,
|
||||
year: 2026,
|
||||
rating: GardenOutcomeRating.good,
|
||||
notes: 'true to type, keeping it',
|
||||
);
|
||||
final history = await repo.lotHistory(lotId);
|
||||
final outcome = history.singleWhere(
|
||||
(e) => e.kind == LotHistoryKind.gardenOutcome,
|
||||
);
|
||||
expect(outcome.rating, GardenOutcomeRating.good);
|
||||
expect(outcome.year, 2026);
|
||||
expect(outcome.notes, 'true to type, keeping it');
|
||||
});
|
||||
|
||||
test('ignores other lots', () async {
|
||||
final lotId = await newLot();
|
||||
final otherLot = await newLot();
|
||||
await repo.recordCultivation(lotId: otherLot, type: MovementType.sown);
|
||||
final history = await repo.lotHistory(lotId);
|
||||
expect(
|
||||
history.where((e) => e.kind == LotHistoryKind.movement),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/data/export_import/seed_label_codec.dart';
|
||||
import 'package:tane/data/species_repository.dart';
|
||||
import 'package:tane/db/database.dart';
|
||||
import 'package:tane/services/seed_label_scan.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
late var repo = newTestRepository(db);
|
||||
late SpeciesRepository species;
|
||||
|
||||
setUp(() {
|
||||
db = newTestDatabase();
|
||||
repo = newTestRepository(db);
|
||||
species = newTestSpeciesRepository(db);
|
||||
});
|
||||
|
||||
tearDown(() async => db.close());
|
||||
|
||||
group('resolveScannedLabel', () {
|
||||
test('rejects anything that is not a seed label', () async {
|
||||
expect(
|
||||
await resolveScannedLabel(repo, 'https://example.org'),
|
||||
const SeedScanResult.notALabel(),
|
||||
);
|
||||
expect(
|
||||
await resolveScannedLabel(repo, 'gibberish'),
|
||||
const SeedScanResult.notALabel(),
|
||||
);
|
||||
});
|
||||
|
||||
test('finds the existing variety by its label, case-insensitively',
|
||||
() async {
|
||||
final id = await repo.addQuickVariety(label: 'Tomate Rosa');
|
||||
final raw = SeedLabelCodec.encode(
|
||||
const SeedLabelData(varietyLabel: 'tomate rosa'),
|
||||
);
|
||||
expect(
|
||||
await resolveScannedLabel(repo, raw),
|
||||
SeedScanResult.match(id),
|
||||
);
|
||||
});
|
||||
|
||||
test('an unknown label comes back with its data for the add prompt',
|
||||
() async {
|
||||
final raw = SeedLabelCodec.encode(
|
||||
const SeedLabelData(
|
||||
varietyLabel: 'Acelga de Perales',
|
||||
scientificName: 'Beta vulgaris',
|
||||
year: 2024,
|
||||
origin: 'María · Aiako',
|
||||
),
|
||||
);
|
||||
final result = await resolveScannedLabel(repo, raw);
|
||||
expect(result.data?.varietyLabel, 'Acelga de Perales');
|
||||
expect(result.data?.year, 2024);
|
||||
});
|
||||
});
|
||||
|
||||
group('importScannedLabel', () {
|
||||
test('creates the variety with a lot carrying year and origin', () async {
|
||||
const data = SeedLabelData(
|
||||
varietyLabel: 'Acelga de Perales',
|
||||
year: 2024,
|
||||
origin: 'María · Aiako',
|
||||
);
|
||||
final varietyId = await importScannedLabel(
|
||||
repository: repo,
|
||||
species: species,
|
||||
data: data,
|
||||
);
|
||||
final varieties = await db.select(db.varieties).get();
|
||||
expect(varieties.single.id, varietyId);
|
||||
expect(varieties.single.label, 'Acelga de Perales');
|
||||
final lots = await db.select(db.lots).get();
|
||||
expect(lots.single.varietyId, varietyId);
|
||||
expect(lots.single.harvestYear, 2024);
|
||||
expect(lots.single.originName, 'María · Aiako');
|
||||
});
|
||||
|
||||
test('links the species by exact scientific name when bundled', () async {
|
||||
await species.seedBundled(const [
|
||||
SpeciesSeed(
|
||||
scientificName: 'Beta vulgaris',
|
||||
family: 'Amaranthaceae',
|
||||
commonNames: {
|
||||
'es': ['Acelga'],
|
||||
},
|
||||
),
|
||||
]);
|
||||
const data = SeedLabelData(
|
||||
varietyLabel: 'Una acelga cualquiera',
|
||||
scientificName: 'beta vulgaris',
|
||||
);
|
||||
final varietyId = await importScannedLabel(
|
||||
repository: repo,
|
||||
species: species,
|
||||
data: data,
|
||||
);
|
||||
final variety = await (db.select(
|
||||
db.varieties,
|
||||
)..where((v) => v.id.equals(varietyId))).getSingle();
|
||||
final bundled = await db.select(db.species).get();
|
||||
expect(variety.speciesId, bundled.single.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/data/seed_saving_catalog.dart';
|
||||
import 'package:tane/db/database.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
import 'package:tane/domain/seed_saving.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
|
||||
setUp(() => db = newTestDatabase());
|
||||
tearDown(() {
|
||||
SeedSavingCatalog.data = null;
|
||||
return db.close();
|
||||
});
|
||||
|
||||
testWidgets('the lot tile opens the batch story, closed by its origin', (
|
||||
tester,
|
||||
) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
final lotId = await repo.addLot(
|
||||
varietyId: id,
|
||||
originName: 'María',
|
||||
originPlace: 'Aiako',
|
||||
);
|
||||
await repo.addGerminationTest(
|
||||
lotId: lotId,
|
||||
sampleSize: 10,
|
||||
germinatedCount: 9,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(Key('lot.history.$lotId')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The story: germination test (90%) first, creation-with-origin last.
|
||||
expect(find.text('Germination test — 90%'), findsOneWidget);
|
||||
expect(find.text('Added to your collection'), findsOneWidget);
|
||||
expect(find.text('From María · Aiako'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('one tap records sown today and it shows up in the story', (
|
||||
tester,
|
||||
) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
final lotId = await repo.addLot(varietyId: id);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(Key('lot.history.$lotId')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('history.sowToday')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// One-shot .get() (NOT watch().first — hangs under the fake-async clock).
|
||||
final movements = await db.select(db.movements).get();
|
||||
expect(movements.single.type, MovementType.sown);
|
||||
expect(movements.single.lotId, lotId);
|
||||
expect(movements.single.occurredOn, isNotNull);
|
||||
|
||||
// And the story refreshed to show it.
|
||||
expect(find.text('Sown'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('harvesting asks "how did it do?" once, a face tap saves it', (
|
||||
tester,
|
||||
) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
final lotId = await repo.addLot(varietyId: id);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(Key('lot.history.$lotId')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Sowing does NOT ask; harvesting does.
|
||||
await tester.tap(find.byKey(const Key('history.sowToday')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('history.outcomeQuestion')), findsNothing);
|
||||
|
||||
await tester.tap(find.byKey(const Key('history.harvestToday')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('history.outcomeQuestion')), findsOneWidget);
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('history.outcomeNote')),
|
||||
'true to type',
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('history.outcomeGood')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Question gone, answer saved, story shows it.
|
||||
expect(find.byKey(const Key('history.outcomeQuestion')), findsNothing);
|
||||
final outcomes = await db.select(db.gardenOutcomes).get();
|
||||
expect(outcomes.single.rating, GardenOutcomeRating.good);
|
||||
expect(outcomes.single.notes, 'true to type');
|
||||
expect(outcomes.single.year, DateTime.now().year);
|
||||
expect(find.text('It did well'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('the harvest question can be dismissed without a trace', (
|
||||
tester,
|
||||
) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
final lotId = await repo.addLot(varietyId: id);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(Key('lot.history.$lotId')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('history.harvestToday')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('history.outcomeDismiss')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('history.outcomeQuestion')), findsNothing);
|
||||
expect(await db.select(db.gardenOutcomes).get(), isEmpty);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('sowing a crosser drops the isolation hint; a selfer gets '
|
||||
'nothing', (tester) async {
|
||||
SeedSavingCatalog.data = SeedSavingData(
|
||||
families: {
|
||||
'Cucurbitaceae': const SeedSavingGuide(
|
||||
pollination: Pollination.cross,
|
||||
isolationMinM: 400,
|
||||
),
|
||||
'Solanaceae': const SeedSavingGuide(
|
||||
pollination: Pollination.self,
|
||||
isolationMinM: 3,
|
||||
),
|
||||
},
|
||||
taxa: const {},
|
||||
);
|
||||
final repo = newTestRepository(db);
|
||||
|
||||
Future<void> sowAndCheck(String category, {required bool hinted}) async {
|
||||
final id = await repo.addQuickVariety(label: 'x-$category');
|
||||
await repo.updateVariety(id: id, label: 'x-$category', category: category);
|
||||
final lotId = await repo.addLot(varietyId: id);
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(Key('lot.history.$lotId')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('history.sowToday')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byKey(const Key('history.isolationHint')),
|
||||
hinted ? findsOneWidget : findsNothing,
|
||||
);
|
||||
await disposeTree(tester);
|
||||
}
|
||||
|
||||
// The family drives it: squash (crosses, 400 m) hints; tomato does not.
|
||||
await sowAndCheck('Cucurbitaceae', hinted: true);
|
||||
expect(find.textContaining('400'), findsNothing); // tree disposed
|
||||
await sowAndCheck('Solanaceae', hinted: false);
|
||||
});
|
||||
|
||||
testWidgets('a plant lot offers harvest but not sowing', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Rosemary');
|
||||
final lotId = await repo.addLot(varietyId: id, type: LotType.plant);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(Key('lot.history.$lotId')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('history.sowToday')), findsNothing);
|
||||
expect(find.byKey(const Key('history.harvestToday')), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/data/export_import/seed_label_codec.dart';
|
||||
import 'package:tane/data/species_repository.dart';
|
||||
import 'package:tane/data/variety_repository.dart';
|
||||
import 'package:tane/db/database.dart';
|
||||
import 'package:tane/ui/qr_scan.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
/// Hosts a button that feeds [payload] to [handleScannedPayload], standing in
|
||||
/// for the camera — the handler is what carries the flow's logic.
|
||||
class _ScanHost extends StatelessWidget {
|
||||
const _ScanHost({
|
||||
required this.repository,
|
||||
required this.species,
|
||||
required this.payload,
|
||||
required this.opened,
|
||||
});
|
||||
|
||||
final VarietyRepository repository;
|
||||
final SpeciesRepository species;
|
||||
final String payload;
|
||||
final List<String> opened;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
key: const Key('scanHost.fire'),
|
||||
onPressed: () => handleScannedPayload(
|
||||
context,
|
||||
repository: repository,
|
||||
species: species,
|
||||
payload: payload,
|
||||
openVariety: opened.add,
|
||||
),
|
||||
child: const Text('scan'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
late VarietyRepository repo;
|
||||
late SpeciesRepository species;
|
||||
|
||||
setUp(() {
|
||||
db = newTestDatabase();
|
||||
repo = newTestRepository(db);
|
||||
species = newTestSpeciesRepository(db);
|
||||
});
|
||||
|
||||
tearDown(() => db.close());
|
||||
|
||||
Future<List<String>> pumpAndFire(WidgetTester tester, String payload) async {
|
||||
final opened = <String>[];
|
||||
await tester.pumpWidget(
|
||||
wrapScreen(
|
||||
repository: repo,
|
||||
child: _ScanHost(
|
||||
repository: repo,
|
||||
species: species,
|
||||
payload: payload,
|
||||
opened: opened,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('scanHost.fire')));
|
||||
await tester.pumpAndSettle();
|
||||
return opened;
|
||||
}
|
||||
|
||||
testWidgets('a known label opens its record straight away', (tester) async {
|
||||
final id = await repo.addQuickVariety(label: 'Tomate Rosa');
|
||||
final payload = SeedLabelCodec.encode(
|
||||
const SeedLabelData(varietyLabel: 'tomate rosa'),
|
||||
);
|
||||
final opened = await pumpAndFire(tester, payload);
|
||||
expect(opened, [id]);
|
||||
expect(find.byKey(const Key('scan.addPrompt')), findsNothing);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('an unknown label asks first, then adds and opens it', (
|
||||
tester,
|
||||
) async {
|
||||
final payload = SeedLabelCodec.encode(
|
||||
const SeedLabelData(
|
||||
varietyLabel: 'Acelga de Perales',
|
||||
year: 2024,
|
||||
origin: 'María',
|
||||
),
|
||||
);
|
||||
final opened = await pumpAndFire(tester, payload);
|
||||
expect(find.byKey(const Key('scan.addPrompt')), findsOneWidget);
|
||||
expect(opened, isEmpty); // nothing created before the person says so
|
||||
expect(await db.select(db.varieties).get(), isEmpty);
|
||||
|
||||
await tester.tap(find.byKey(const Key('scan.add')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final varieties = await db.select(db.varieties).get();
|
||||
expect(varieties.single.label, 'Acelga de Perales');
|
||||
final lots = await db.select(db.lots).get();
|
||||
expect(lots.single.harvestYear, 2024);
|
||||
expect(lots.single.originName, 'María');
|
||||
expect(opened, [varieties.single.id]);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('declining the prompt adds nothing', (tester) async {
|
||||
final payload = SeedLabelCodec.encode(
|
||||
const SeedLabelData(varietyLabel: 'Acelga de Perales'),
|
||||
);
|
||||
final opened = await pumpAndFire(tester, payload);
|
||||
await tester.tap(find.byKey(const Key('scan.cancel')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(await db.select(db.varieties).get(), isEmpty);
|
||||
expect(opened, isEmpty);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('a non-label QR just says so', (tester) async {
|
||||
final opened = await pumpAndFire(tester, 'https://example.org/whatever');
|
||||
expect(find.text('That code is not a seed label'), findsOneWidget);
|
||||
expect(opened, isEmpty);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
}
|
||||
|
|
@ -15,30 +15,17 @@ AutoName: Tane
|
|||
RepoType: git
|
||||
Repo: https://git.comunes.org/comunes/tane.git
|
||||
|
||||
# Reproducible, developer-signed builds: F-Droid rebuilds from source and, if the
|
||||
# output matches our own signed APK on git.comunes.org byte-for-byte, publishes
|
||||
# OUR APK. Same signature as the Google Play build, so users move between stores
|
||||
# without reinstalling. The fingerprint below is the SHA-256 of the tane-upload
|
||||
# signing certificate (public value); the keystore/passwords never leave CI.
|
||||
AllowedAPKSigningKeys: ddfae432091b8248a8a4a1b353487fa626301f4357ef835e94ec312f69418e38
|
||||
|
||||
# One build entry per ABI: `flutter build --split-per-abi` yields smaller APKs and
|
||||
# each split carries a distinct versionCode (gradle: versionCode*10 + {arm-v7a:1,
|
||||
# arm64:2, x86_64:3}), so v0.1.2 (+4) -> 41/42/43. The `binary:` is the reference
|
||||
# APK F-Droid verifies against, uploaded to the Forgejo release by
|
||||
# .forgejo/workflows/release.yml on the `v*` tag.
|
||||
Builds:
|
||||
- versionName: 0.1.2
|
||||
versionCode: 41
|
||||
commit: v0.1.2
|
||||
- versionName: 0.1.0
|
||||
versionCode: 2
|
||||
commit: v0.1.0
|
||||
subdir: apps/app_seeds
|
||||
sudo:
|
||||
- apt-get update
|
||||
- apt-get install -y git unzip xz-utils
|
||||
- git clone --depth 1 -b 3.41.9 https://github.com/flutter/flutter.git /opt/flutter
|
||||
- chown -R vagrant:vagrant /opt/flutter
|
||||
output: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
||||
binary: https://git.comunes.org/comunes/tane/releases/download/v%v/app-armeabi-v7a-release.apk
|
||||
output: build/app/outputs/flutter-apk/app-release.apk
|
||||
prebuild:
|
||||
- export PATH="/opt/flutter/bin:$PATH"
|
||||
- git config --global --add safe.directory /opt/flutter
|
||||
|
|
@ -48,19 +35,18 @@ Builds:
|
|||
- dart run build_runner build --delete-conflicting-outputs
|
||||
build:
|
||||
- export PATH="/opt/flutter/bin:$PATH"
|
||||
- flutter build apk --release --split-per-abi --target-platform=android-arm
|
||||
- flutter build apk --release
|
||||
|
||||
- versionName: 0.1.2
|
||||
versionCode: 42
|
||||
commit: v0.1.2
|
||||
- versionName: 0.1.1
|
||||
versionCode: 3
|
||||
commit: v0.1.1
|
||||
subdir: apps/app_seeds
|
||||
sudo:
|
||||
- apt-get update
|
||||
- apt-get install -y git unzip xz-utils
|
||||
- git clone --depth 1 -b 3.41.9 https://github.com/flutter/flutter.git /opt/flutter
|
||||
- chown -R vagrant:vagrant /opt/flutter
|
||||
output: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
||||
binary: https://git.comunes.org/comunes/tane/releases/download/v%v/app-arm64-v8a-release.apk
|
||||
output: build/app/outputs/flutter-apk/app-release.apk
|
||||
prebuild:
|
||||
- export PATH="/opt/flutter/bin:$PATH"
|
||||
- git config --global --add safe.directory /opt/flutter
|
||||
|
|
@ -70,32 +56,10 @@ Builds:
|
|||
- dart run build_runner build --delete-conflicting-outputs
|
||||
build:
|
||||
- export PATH="/opt/flutter/bin:$PATH"
|
||||
- flutter build apk --release --split-per-abi --target-platform=android-arm64
|
||||
|
||||
- versionName: 0.1.2
|
||||
versionCode: 43
|
||||
commit: v0.1.2
|
||||
subdir: apps/app_seeds
|
||||
sudo:
|
||||
- apt-get update
|
||||
- apt-get install -y git unzip xz-utils
|
||||
- git clone --depth 1 -b 3.41.9 https://github.com/flutter/flutter.git /opt/flutter
|
||||
- chown -R vagrant:vagrant /opt/flutter
|
||||
output: build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||
binary: https://git.comunes.org/comunes/tane/releases/download/v%v/app-x86_64-release.apk
|
||||
prebuild:
|
||||
- export PATH="/opt/flutter/bin:$PATH"
|
||||
- git config --global --add safe.directory /opt/flutter
|
||||
- flutter config --no-analytics
|
||||
- flutter pub get
|
||||
- dart run slang
|
||||
- dart run build_runner build --delete-conflicting-outputs
|
||||
build:
|
||||
- export PATH="/opt/flutter/bin:$PATH"
|
||||
- flutter build apk --release --split-per-abi --target-platform=android-x64
|
||||
- flutter build apk --release
|
||||
|
||||
AutoUpdateMode: Version
|
||||
UpdateCheckMode: Tags v[\d.]+
|
||||
UpdateCheckData: apps/app_seeds/pubspec.yaml|version:\s*[\d.]+\+(\d+)|apps/app_seeds/pubspec.yaml|version:\s*([\d.]+)\+
|
||||
CurrentVersion: 0.1.2
|
||||
CurrentVersionCode: 43
|
||||
CurrentVersion: 0.1.1
|
||||
CurrentVersionCode: 3
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
# Ecorred: Encuentro Estatal (Piloña) y financiación RCF
|
||||
|
||||
> Nota interna (sin trackear). Preparada el 18-07-2026. **El formulario cierra el lunes 21 de julio.**
|
||||
|
||||
## 1. El encuentro
|
||||
|
||||
**Encuentro Estatal de Comunidades Regenerativas** — [ecorred.es/encuentro-comunidades-regenerativas](https://www.ecorred.es/encuentro-comunidades-regenerativas/)
|
||||
|
||||
- **Cuándo**: 24–27 de septiembre de 2026.
|
||||
- **Dónde**: La Benéfica de Piloña, L'Infiestu (Asturias).
|
||||
- **Qué incluye**: alojamiento compartido (3 noches), comidas de los 3 días, ayuda de viaje de hasta 100 €/persona.
|
||||
- **Programa**: jueves llegada y "pasillo de la fama" (espacio expositivo de iniciativas); viernes sesión estratégica entre redes; sábado jornada pública del Día Europeo de las Comunidades Sostenibles, con diálogo institucional; domingo visitas opcionales a proyectos locales.
|
||||
- **Reuniones preparatorias online**: 1 y 8 de septiembre.
|
||||
- **Plazos**: formulario de interés hasta el **21 de julio**; selección comunicada **antes del 31 de julio**. Plazas limitadas; seleccionan buscando diversidad de territorios, enfoques y perfiles.
|
||||
- **Formulario**: https://forms.gle/bcizx2eXHPFigZZp7 (lo gestiona Altekio S.Coop.Mad.)
|
||||
|
||||
**Por qué encaja Tane**: priorizan iniciativas de alimentación y agroecología, y el formulario pregunta literalmente *"¿Qué traéis al encuentro? Un aprendizaje, una herramienta, una experiencia concreta que otras iniciativas podrían necesitar escuchar"*. Tane es exactamente eso: una herramienta libre y ya disponible. Además, el encuentro reúne al público ideal (redes de semillas, grupos agroecológicos, bancos comunitarios) para pilotos y difusión.
|
||||
|
||||
## 2. Financiación en Ecorred más allá del encuentro
|
||||
|
||||
Ecorred es el nodo español del **Regenerative Communities Fund (RCF)** de ECOLISE (2025–2027), financiado por la UE dentro de "Funding Fairer Futures" (programa DEAR). Nodos hermanos en Croacia, Finlandia, Francia y Portugal. Dos líneas:
|
||||
|
||||
### Pathfinder / Comunidades Emergentes — la oportunidad real
|
||||
|
||||
- **4.000 €** por iniciativa + formación, acompañamiento, visitas a iniciativas demostrativas, apoyo en captación de fondos y comunicación. Presupuesto total del proyecto entre 1.000 y 10.000 € (cofinanciación no obligatoria).
|
||||
- La **3ª convocatoria cerró el 26-01-2026** (42 solicitudes en España; resolución hacia finales de septiembre de 2026, ~10 seleccionadas por país).
|
||||
- **Habrá una 4ª convocatoria a mediados de 2026** para otras 50 comunidades. **Esta es la ventana para Tane** — y el encuentro de Piloña, el sitio donde enterarse de las fechas y conocer al equipo de Ecorredes (que forma parte del comité de selección).
|
||||
|
||||
**Elegibilidad** (según las [bases de la 3ª convocatoria](https://ecolise.eu/wp-content/uploads/2025/11/CfP-Regenerative-Communities-Fund-%E2%80%93-Cycle-III-2025-Pathfinder-Communities.pdf), previsiblemente similares en la 4ª):
|
||||
|
||||
- Puede solicitar una **entidad sin ánimo de lucro registrada** (CSO) **o un grupo no registrado** con entidad fiscal de acogida. La asociación serviría como vehículo legal, sin protagonismo.
|
||||
- Establecida y trabajando en España ✓; no haber recibido fondos DEAR ✓.
|
||||
- **Punto de fricción**: piden iniciativas *"pequeñas, arraigadas localmente"* que *"trabajen con comunidades locales de un territorio concreto"* (escala barrio/pueblo). Una app, por sí sola, encaja regular.
|
||||
- **Encaje recomendado**: presentar un **proyecto local y concreto** — un piloto con una red de intercambio de semillas o banco comunitario de un territorio (talleres, inventariado colectivo del banco local, trueques con la app, formación intergeneracional) — donde Tane es la herramienta, no el fin. Ahí el encaje es bueno: soberanía alimentaria, empoderamiento comunitario, participación de mayores y jóvenes (criterio de relevancia, 30 % de la nota, prima colectivos infrarrepresentados).
|
||||
- Solicitud en inglés (la propuesta narrativa puede ir en castellano + copia en inglés). Informes narrativos, sin informes financieros.
|
||||
- Obligaciones si seleccionan: ejecutar el proyecto (13 meses), participar en formaciones, organizar un **diálogo local con la administración** durante el Día Europeo de las Comunidades Sostenibles, y compartir la historia en los canales de ECOLISE.
|
||||
- Contacto: rcf@ecolise.eu (indicar SPAIN en el asunto).
|
||||
|
||||
**Conclusión**: buscar antes del verano/otoño una **comunidad local aliada** dispuesta a co-presentar el piloto. El encuentro de Piloña es el mejor sitio para encontrarla.
|
||||
|
||||
### Demonstrator / Iniciativas Demostrativas
|
||||
|
||||
Para iniciativas consolidadas que sirven de modelo a otras (15 ya seleccionadas en las dos primeras convocatorias). Requisitos y cuantías no publicados en la web de Ecorred. **Preguntar en el encuentro** si habrá nuevas convocatorias y si una herramienta con comunidad de uso podría entrar más adelante.
|
||||
|
||||
### Fuentes
|
||||
|
||||
- [Fondo en ecorred.es](https://www.ecorred.es/fondo-de-comunidades-regenerativas/)
|
||||
- [Bases 3ª convocatoria Pathfinder (PDF, ECOLISE)](https://ecolise.eu/wp-content/uploads/2025/11/CfP-Regenerative-Communities-Fund-%E2%80%93-Cycle-III-2025-Pathfinder-Communities.pdf)
|
||||
- [Anuncio de la convocatoria (ECOLISE)](https://ecolise.eu/regenerative-communities-fund-apply-call-for-pathfinder-communities/)
|
||||
- [Nota en economiasolidaria.org](https://www.economiasolidaria.org/noticias/convocatoria-de-comunidades-emergentes-pathfinder-del-fondo-para-comunidades-regenerativas/) · [Red de Transición](https://www.reddetransicion.org/abierta-la-convocatoria-de-comunidades-emergentes-pathfinder-del-fondo-para-comunidades-regenerativas/)
|
||||
|
||||
## 3. Borrador de respuestas al formulario de interés
|
||||
|
||||
Listo para copiar y pegar. Los campos `[RELLENAR]` son personales. Límites de palabras respetados con margen.
|
||||
|
||||
### Datos personales
|
||||
|
||||
| Campo | Respuesta |
|
||||
|---|---|
|
||||
| Nombre y apellidos | `[RELLENAR]` |
|
||||
| Correo electrónico | `[RELLENAR]` (sugerencia: vjrj@comunes.org) |
|
||||
| Teléfono de contacto | `[RELLENAR]` |
|
||||
| ¿Cómo has conocido este encuentro? | `[RELLENAR]` (opciones: RCF y Ecorredes / Red o entidad en la que participo / Redes sociales / Otro) |
|
||||
|
||||
### Red, organización o iniciativa
|
||||
|
||||
| Campo | Respuesta |
|
||||
|---|---|
|
||||
| Nombre | **Tane** |
|
||||
| Territorio donde opera | `[CONFIRMAR]` (p. ej. tu municipio/comunidad como base, aclarando que la herramienta se usa en todo el estado) |
|
||||
| Año de inicio | `[CONFIRMAR: 2025 o 2026]` |
|
||||
| Web o redes sociales | https://tane.comunes.org |
|
||||
| Ámbito principal de trabajo | **alimentación y agroecología** |
|
||||
|
||||
**Describe brevemente qué hacéis, cómo y para quién** *(máx. 200 palabras; el borrador tiene ~170)*:
|
||||
|
||||
> Tane es una aplicación libre y gratuita para guardar, organizar e intercambiar semillas tradicionales. Cada persona lleva su banco de semillas en el bolsillo: qué variedades tiene, de dónde vienen, cuándo se sembraron, si siguen germinando bien y a quién se han dado, para que las variedades locales no se pierdan.
|
||||
>
|
||||
> Funciona sin internet, sin cuentas y sin servidores centrales: los datos se quedan cifrados en el dispositivo de cada persona. Cuando hay conexión, se puede publicar lo que se ofrece y conversar con otras personas para intercambiar. Nadie hace negocio con los datos ni con las semillas.
|
||||
>
|
||||
> Está pensada para hortelanas y hortelanos de cualquier edad, redes de intercambio y bancos comunitarios de semillas: es muy simple de entrada (basta apuntar el nombre de una variedad) y ofrece profundidad a quien la busca: pruebas de germinación, calendario de siembra, consejos para producir semilla de cada cultivo, avisos cuando una semilla envejece.
|
||||
>
|
||||
> Es software libre, multilingüe (cualquiera puede ayudar a traducirla) y está disponible gratis en Google Play y F-Droid.
|
||||
|
||||
### Impacto y representatividad
|
||||
|
||||
**¿Qué representa vuestra iniciativa en el ecosistema regenerativo?** *(máx. 150 palabras; ~130)*:
|
||||
|
||||
> Tane es una herramienta común de reciente creación: infraestructura digital al servicio de las redes de semillas, los bancos comunitarios y los grupos agroecológicos, construida como bien colectivo y no como plataforma comercial.
|
||||
>
|
||||
> Es descentralizada por diseño, y eso se nota en algo poco habitual: no sabemos quién la usa ni cuántas personas somos, porque no existe ningún registro central ni recopilamos dato alguno. Igual que las semillas pasan de mano en mano, la aplicación funciona directamente entre las personas, sin pasar por ningún servidor nuestro. Esa renuncia deliberada a controlar es nuestra manera de entender la tecnología para el procomún.
|
||||
>
|
||||
> Venimos del mundo del software libre y estamos en contacto con comunidades que ya se intercambian semillas de mano en mano. Queremos hacer de puente entre quienes guardan semillas y quienes construyen tecnología comunitaria, y tejer en este encuentro las alianzas que un proyecto joven necesita.
|
||||
|
||||
**¿Qué traéis al encuentro?** *(máx. 150 palabras; ~100)*:
|
||||
|
||||
> Una herramienta concreta, libre y gratuita que cualquier iniciativa puede empezar a usar hoy mismo: sirve para inventariar un banco comunitario de semillas, organizar trueques y no perder la memoria de cada variedad (de dónde viene, quién la ha cuidado, cómo germina).
|
||||
>
|
||||
> Y una experiencia que creemos útil compartir: cómo construir tecnología al servicio de las comunidades y no al revés — sin recopilar datos, sin publicidad, funcionando incluso sin internet, traducida por su propia comunidad. Podemos enseñarla en directo, escuchar qué le falta y adaptarla a lo que las redes de semillas necesiten de verdad.
|
||||
|
||||
**¿Qué buscáis?** *(máx. 150 palabras; ~90)*:
|
||||
|
||||
> Alianzas con redes de intercambio de semillas, bancos comunitarios y grupos agroecológicos que quieran probar la herramienta en su día a día y decirnos qué mejorar: pilotos reales con personas reales, especialmente con quienes llevan décadas guardando semillas y con gente joven que empieza.
|
||||
>
|
||||
> También queremos entender cómo sostener un proyecto así sin traicionarlo (sin vender datos ni cobrar por lo básico), y conocer de cerca el Fondo de Comunidades Regenerativas y la próxima convocatoria de comunidades emergentes, idealmente de la mano de una comunidad local con la que presentar un proyecto conjunto.
|
||||
|
||||
**¿Qué conversación queréis tener con las administraciones?** *(máx. 150 palabras; ~105)*:
|
||||
|
||||
> Con humildad: somos un proyecto joven y no venimos a pedir nada para nosotros, sino a escuchar y a acompañar lo que las redes de semillas llevan años planteando — que conservar e intercambiar variedades tradicionales siga siendo posible para personas y colectivos, no solo para empresas, también en la revisión europea de la normativa de semillas que está en curso.
|
||||
>
|
||||
> Y una idea sencilla que sí es nuestra: cuando las administraciones apoyen la digitalización del mundo rural y comunitario, que cuenten con herramientas libres y respetuosas con los datos, que funcionen incluso con mala conexión, en lugar de atar a las comunidades a plataformas comerciales.
|
||||
|
||||
### Diversidad e inclusión
|
||||
|
||||
| Campo | Respuesta |
|
||||
|---|---|
|
||||
| Género | `[RELLENAR]` |
|
||||
| Edad | `[RELLENAR]` |
|
||||
| ¿Trabajáis prioritariamente con jóvenes, migrantes o colectivos vulnerables? | Sugerencia: **Parcialmente** (la app está pensada para cualquier edad, 10–80 años, con especial cuidado de mayores y gente sin soltura digital) — `[DECIDIR]` |
|
||||
| ¿Participáis en el Consejo Estratégico de Ecorredes o en el RCF? | **No** |
|
||||
|
||||
### Logística
|
||||
|
||||
| Campo | Respuesta |
|
||||
|---|---|
|
||||
| ¿Puedes asistir a las reuniones preparatorias online? (1 y/o 8 sept) | `[RELLENAR]` (opciones: Ambas / Solo la primera / Solo la segunda / Ninguna) |
|
||||
| ¿Necesitas apoyo económico para el desplazamiento? | `[RELLENAR]` (Sí / Sería de ayuda, pero podría cubrirlo / No es necesario) |
|
||||
| ¿Participarías igualmente costeando viaje y alojamiento? | `[RELLENAR]` (Sí, probablemente / No me sería posible) |
|
||||
| ¿Necesidad especial para no compartir habitación? | `[RELLENAR]` |
|
||||
| ¿Restricción alimentaria o accesibilidad? | `[RELLENAR]` |
|
||||
| ¿Comentario adicional? | Sugerencia: ofrecer una demostración en vivo de Tane en el "pasillo de la fama" del jueves. |
|
||||
|
||||
## 4. Lista de tareas
|
||||
|
||||
- [ ] **Antes del lunes 21-07**: rellenar y enviar el formulario (respuestas de arriba + campos personales).
|
||||
- [ ] Antes del 31-07: llegará la respuesta de selección.
|
||||
- [ ] Si seleccionan: apuntar reuniones preparatorias del **1 y 8 de septiembre**; preparar material para el pasillo de la fama (cartel con QR de descarga, capturas localizadas ya existentes de los golden tests, móvil/tablet con la app, sobres de semillas de muestra).
|
||||
- [ ] **Vigilar la 4ª convocatoria Pathfinder** (mediados de 2026, 4.000 € + acompañamiento): suscribirse a la [lista de FFF](https://ecolise.eu/) o preguntar en el encuentro; buscar comunidad local aliada para co-presentar un piloto.
|
||||
- [ ] Preguntar en el encuentro por la línea Demonstrator (requisitos, próximas convocatorias).
|
||||
|
|
@ -1,292 +0,0 @@
|
|||
# Tane — NGI Fediversity (NLnet) application draft
|
||||
|
||||
> **Working document, NOT tracked in git.** Ready-to-submit draft for the NLnet
|
||||
> NGI Fediversity call (nlnet.nl/propose). Deadline **1 Aug 2026, 12:00 CEST** (confirmed on the site).
|
||||
> Derived from `VISION.md`, `PLAN.md`, `docs/design/*` and `docs/notes/modelos-financiacion.md`.
|
||||
> Written in English (the proposal must be in English; VISION.md is the Spanish source).
|
||||
> **Updated 2026-07-17** to reflect the shipped state: app live on Google Play (v0.1.1), site at
|
||||
> tane.comunes.org, most of the social layer already built. The ask now funds the *remaining*
|
||||
> federated-infrastructure work, not a from-scratch build.
|
||||
|
||||
---
|
||||
|
||||
## Part A — Strategy & framing (for us, NOT for the form)
|
||||
|
||||
### The new, stronger story: we shipped, now we complete the federated infrastructure
|
||||
Since the first draft, the project advanced a lot. This is now a **track-record application**, not a
|
||||
promise:
|
||||
- **Live on Google Play** (v0.1.1, `org.comunes.tane`), **GMS-free build** ready for F-Droid.
|
||||
- **Public site** at **tane.comunes.org**, translated via Weblate (translate.comunes.org).
|
||||
- **Block 1 (inventory) shipped**, and **most of Block 2 (the social layer) already shipped**: offers
|
||||
(NIP-99), private metadata-hiding messaging (NIP-17), a Duniter-style egocentric web of trust
|
||||
(kind 30777), ratings (kind 30778), profiles + multi-account identity, Plantaré pledges, saved-search
|
||||
alerts. All transports live in the reusable `commons_core` engine, with ~131 test files and CI.
|
||||
|
||||
NLnet weighs **technical feasibility (30%)** and **value for money (30%)**: a shipped, tested,
|
||||
in-store app de-risks both. Lead with that.
|
||||
|
||||
### Fit with Fediversity — now genuine, not a stretch
|
||||
The Fediversity fund leans to the *hosting stack / self-hosting* side. The **work that remains is
|
||||
exactly that side**, which is why the fit is now real:
|
||||
1. **Self-hostable community relay** — `relay.comunes.org` is **already deployed and running**. The
|
||||
remaining Fediversity-shaped work is turning it into a **reproducible, documented recipe any seed
|
||||
network can self-host**, plus moderation tooling (NIP-56 reports, key-blocking) and operational
|
||||
hardening — so the network is owned by communities, not a single instance.
|
||||
2. **Multi-device sync** — the encrypted `SyncTransport` landed (NIP-78, relay sees only ciphertext);
|
||||
the app-level `SyncService` that replicates your inventory across your own devices is **pending**.
|
||||
This is data portability + self-custody, a Fediversity theme.
|
||||
3. **Interoperability & portability** — open, versioned export/import already exists; add opt-in
|
||||
**Darwin Core / GBIF** export so agrobiodiversity data can flow to open biodiversity atlases.
|
||||
4. **`commons_core` as reusable federated infrastructure** — Tane is a seed app wrapped around a generic
|
||||
Nostr + CRDT + web-of-trust + messaging engine; seeds are the first app, others can follow.
|
||||
|
||||
Framing in one line: **bringing federation beyond social media, into the physical commons — and we
|
||||
already have a working app to prove it.**
|
||||
|
||||
### Plan B — don't bet everything on this call
|
||||
Honest odds: the fit is now decent (the remaining work IS hosting/portability-shaped) but Fediversity
|
||||
reviewers may still read "mobile app on Nostr" as adjacent to their ActivityPub/NixOS hosting focus.
|
||||
Mitigation, at near-zero extra cost:
|
||||
- **Submit to Fediversity anyway** (short form, strong track record, real infra milestones) — worst case
|
||||
is a rejection with feedback.
|
||||
- **Reuse this same dossier for NGI Zero Commons / Core when regular calls reopen after summer 2026** —
|
||||
Tane as commons infrastructure is arguably a *better* fit there (`commons_core`, open protocols,
|
||||
community-run relays). Keep this file as the master; only the fund-specific framing paragraph changes.
|
||||
- Meanwhile, keep the **Goteo matchfunding** (Red de Semillas) track as the community-round complement.
|
||||
|
||||
### NLnet requirements checklist
|
||||
- [x] **FOSS licence** — code AGPL-3.0; docs/assets CC-BY-SA.
|
||||
- [x] **Open standards** — Nostr NIPs (99/17/44/78/56), Duniter-style WoT, open data formats, Darwin Core.
|
||||
- [x] **European dimension** — EC-LLD (23 European seed networks), Red de Semillas, EU seed-law (PRM) context.
|
||||
- [x] **Milestone-based, verifiable deliverables** — see Part C.
|
||||
- [x] **Sustainability after funding** — local-first, open data/code, cheap community relays; never per-transaction fees.
|
||||
- [ ] **Generative-AI disclosure** — declared honestly and minimally (see §13 + the log template below).
|
||||
- [ ] **Privacy statement** — tick the acknowledgement of NLnet's privacy statement on the form.
|
||||
|
||||
### The AI concern (read before submitting)
|
||||
NLnet has a formal **Generative AI policy** (nlnet.nl/foundation/policies/generativeAI/). It does **not**
|
||||
forbid AI, but imposes real obligations, and reviewers value **substance over marketing**. Three things,
|
||||
don't conflate them:
|
||||
1. **Prompt provenance log (required):** if GenAI is used in the application process, you must keep a
|
||||
log of prompts/outputs. → the Appendix table below.
|
||||
2. **Public disclosure (required for substantive use):** any GenAI use that materially affects the
|
||||
output must be disclosed so evaluators understand how the proposal was produced. → §13.
|
||||
3. **Purely-AI outcomes are NOT payable, and must be FLOSS-clean:** "Outcomes purely generated by AI are
|
||||
not allowed to be submitted as work eligible for payment." → this hits the **milestones (M1–M5)**, not
|
||||
just the application: the delivered code/docs must be **human-authored** (AI-assisted is fine; you must
|
||||
also verify outputs don't reproduce copyrighted/licence-incompatible material). **Non-compliance can
|
||||
mean rejection of the proposal or termination of a running grant.**
|
||||
|
||||
Plus the softer risk: an application that *reads* as AI slop scores worse. Fix for all of it = make the
|
||||
text genuinely yours.
|
||||
|
||||
**Action for vjrj — rewrite §3, §4, §5 in your own voice** (that's what a reviewer reads first; the rest
|
||||
is technical scaffolding). Add first-person specifics only you can write: Kokopelli, the 2009 paper
|
||||
Plantare you originated, why the whole seed sector refuses per-transaction fees, your Ğ1nkgo/Duniter
|
||||
background, and the fact the app is already in people's hands.
|
||||
|
||||
**De-"slop" pass:** cut the tells — em-dashes everywhere, rule-of-three lists, heavy bold,
|
||||
"not X but Y" constructions, brochure cadence. Rougher, more technical, more personal = better here.
|
||||
|
||||
### Process notes
|
||||
- Submit several days early — "deadlines are hard". Draft offline (this file), paste at the end.
|
||||
- Plain text is preferred; keep formatting light. Attachments ≤ 50 MB total.
|
||||
- You may submit multiple independent proposals in one round; do not double-submit via FundingBox + NLnet.
|
||||
|
||||
---
|
||||
|
||||
## Part B — Form answers (the actual draft, English)
|
||||
|
||||
### Current status (facts to weave in — all verifiable)
|
||||
- **Live:** Google Play v0.1.1 (`org.comunes.tane`); site **tane.comunes.org**; community Nostr relay
|
||||
**relay.comunes.org running**; source public (AGPL-3.0).
|
||||
- **F-Droid:** GMS-free build + recipe ready (awaiting registry upload).
|
||||
- **Languages:** 7 (en, es, pt, fr, ast, de, ja); RTL + CJK fonts bundled; translated on Weblate.
|
||||
- **Engine:** `commons_core` (pure Dart) — identity derivation, HLC, CRDT, geohash, all Nostr transports.
|
||||
- **Tests/CI:** ~131 test files; Forgejo Actions runs analyze + test on both packages.
|
||||
|
||||
### 1. Contact / Organisation / Country
|
||||
- **Applicant organisation:** Comunes (registered non-profit association), Spain / EU.
|
||||
- **Project lead:** Vicente J. Ruiz Jurado (vjrj) — technical lead.
|
||||
- **Package id / namespace:** `org.comunes.tane`.
|
||||
- **Contact:** vjrj@comunes.org.
|
||||
- **Project page:** https://tane.comunes.org · source repository public (AGPL-3.0) · live on Google Play.
|
||||
|
||||
### 2. Project name
|
||||
**Tane — self-hostable, local-first infrastructure for seed-sharing communities.** (種, "seed"; from
|
||||
*tanemaki*, "to sow".)
|
||||
|
||||
### 3. Abstract (a few sentences)
|
||||
> ✍️ **vjrj: rewrite in your own voice.** Scaffolding below — keep the meaning, drop the polished cadence.
|
||||
|
||||
Tane lets communities run their own seed-sharing network end to end: a local-first mobile app, a
|
||||
self-hostable relay, and identity and data the user fully owns and can take anywhere — no account, no
|
||||
central server, no company in the middle. The app is already live on Google Play in seven languages;
|
||||
offers, private messaging, reputation and encrypted device sync ride on open federated protocols
|
||||
(Nostr NIPs) and a Duniter-style web of trust, implemented in a reusable engine (`commons_core`) where
|
||||
seeds are only the first application. What remains — and what this grant funds — is the hosting and
|
||||
portability side: a **reproducible relay recipe any collective can self-host** (our reference instance
|
||||
already runs), **completion of encrypted multi-device sync**, **open-standards data portability**
|
||||
(Darwin Core / GBIF export), and production hardening — so a seed network can operate the whole stack
|
||||
without depending on us, or on anyone.
|
||||
|
||||
### 4. Can you explain the whole project and its expected outcome?
|
||||
> ✍️ **vjrj: rewrite in your own voice.** Add first-person specifics (Kokopelli, the 2009 paper Plantare,
|
||||
> Ğ1nkgo). Scaffolding below.
|
||||
|
||||
A handful of corporations control most of the world's seed supply, and traditional varieties — and the
|
||||
knowledge to grow them — are being lost. Sharing seeds has even become legally suspect (e.g. Kokopelli
|
||||
fined in France). Seed networks resist with notebooks and spreadsheets; there was no friendly, free,
|
||||
decentralised tool for them.
|
||||
|
||||
Tane is built in **layers, each useful alone**, and most are **already shipped**:
|
||||
1. **Inventory (local-first) — shipped.** Encrypted seed bank on your phone (SQLite + SQLCipher), fully
|
||||
offline, no account; backup + printed recovery; 7 languages.
|
||||
2. **Offer status — shipped.** Per item: private / gift / exchange / sale.
|
||||
3. **Local sharing (federated) — shipped.** Offers as signed Nostr events (NIP-99), discovered by
|
||||
**coarse geohash** (~2.4 km, never your address); private DMs (NIP-17); ratings; Plantaré pledges;
|
||||
saved-search alerts. No central operator can censor, sell data, or charge a fee.
|
||||
4. **Trust — shipped.** Egocentric Duniter-style web of trust (you vouch for people you've met); spam
|
||||
from unknown keys is filtered with no central moderator.
|
||||
|
||||
A de-risking spike proved the whole social happy-path on vetted pure-Dart libraries, and it is now
|
||||
**production code in people's hands**. **What this grant funds is the hosting and portability layer that
|
||||
makes the network genuinely community-owned** (see Part C): turning our running relay into a
|
||||
**reproducible, self-hostable recipe with moderation tooling**; completing **encrypted multi-device
|
||||
sync** (the transport already landed; the relay only ever sees ciphertext); **open-standards data
|
||||
portability** (opt-in Darwin Core / GBIF export toward public biodiversity atlases); plus trust
|
||||
cold-start for real seed fairs, optional Ğ1 price tags, and messaging/offers hardening.
|
||||
|
||||
**Expected outcome:** any seed collective in Europe can **self-host the entire stack from a documented
|
||||
recipe** — relay, moderation, app, data — with identity and data fully portable and **zero dependency on
|
||||
Comunes or any single operator**. And because the hard parts live in the generic `commons_core` engine,
|
||||
the same infrastructure is reusable for other physical-commons networks (tool libraries are the next
|
||||
candidate).
|
||||
|
||||
### 5. Compare your own project with existing or historical efforts
|
||||
> ✍️ **vjrj: rewrite in your own voice.** You know this sector first-hand — say why it refuses
|
||||
> per-transaction fees, and tell the Plantare story as its originator. Scaffolding below.
|
||||
|
||||
- **Seed Savers Exchange, Graines de Troc, Arche Noah, Red de Semillas** — valuable, but each is a
|
||||
**centralised web platform**: single operator, an account, a server that can go dark, no local-first
|
||||
offline app, not federated, not self-hostable.
|
||||
- **Garden/inventory apps** — mostly **proprietary and extractive**; none do decentralised sharing or trust.
|
||||
- **Marketplaces (Wallapop-style)** — a central intermediary that takes a commission and can censor.
|
||||
Tellingly, **no seed-exchange platform anywhere charges per-transaction fees** — the whole sector uses
|
||||
gift/exchange/membership. We follow that, by design.
|
||||
- **Historical precedent — the Plantare (2009).** A paper "seed promissory note" already solved
|
||||
decentralised trust and reciprocity *socially* (a bearer instrument, both parties hold a copy, the
|
||||
ledger distributed across drawers). We digitise it without a central register.
|
||||
|
||||
**Why it's needed / why existing solutions don't answer it:** none combines local-first, federation over
|
||||
open standards, self-hostability, identity/data portability, and a spam-resistant web of trust — while
|
||||
staying free and non-extractive. Tane already does, in production; the engine is reusable for other
|
||||
physical-commons networks.
|
||||
|
||||
### 6. Significant technical challenges (the remaining work)
|
||||
- **Self-hostable relay + moderation** — `relay.comunes.org` already runs; the challenge is making it a
|
||||
reproducible recipe others can run, with NIP-56 report handling and key-blocking, so moderation is
|
||||
"with a response" but not centralised.
|
||||
- **Multi-device sync at the app level** — wire `SyncService` onto the landed encrypted `SyncTransport`:
|
||||
serialise inventory, push on mutation, idempotent LWW import on receipt, stable per-install device id,
|
||||
rare-conflict UI. Relay only ever sees ciphertext.
|
||||
- **Interoperability / portability** — opt-in Darwin Core / GBIF export of agrobiodiversity (blurred
|
||||
location, collective-bank level, one-way) — exactly the data those atlases lack.
|
||||
- **Messaging/offers hardening** — NIP-44 interop-exact test vectors, offline DM delivery/retry, offer
|
||||
expiry & revocation on already-replicated relays.
|
||||
- **Trust cold-start** — bootstrap the web of trust at real fairs / Ğ1 seed groups (QR, no network).
|
||||
- **Portable derived identity** — one root seed, one-way HKDF secp256k1 subkey for Nostr, one printable
|
||||
QR backs up everything (already implemented; polish + docs).
|
||||
|
||||
### 7. Project ecosystem & stakeholder engagement
|
||||
- **Seed networks:** Red de Semillas (Spain), **EC-LLD / Let's Liberate Diversity** (23 European seed
|
||||
networks) for European reach; Arche Noah as a policy ally.
|
||||
- **Free-money communities (Ğ1/Duniter):** first pilots — existing identity, existing web of trust,
|
||||
aligned values.
|
||||
- **Seed fairs and markets:** where trust bootstraps face to face.
|
||||
- **Translators & contributors:** already active via Weblate; code and docs public.
|
||||
|
||||
### 8. Relevant prior experience & track record
|
||||
- **Shipped, in production:** Tane is **live on Google Play** (v0.1.1), GMS-free build ready for F-Droid,
|
||||
site at tane.comunes.org, 7 languages via Weblate, RTL+CJK support, a full legal package, automated
|
||||
store publishing (Fastlane) and CI (Forgejo Actions, ~131 test files). Block 1 + most of Block 2 done.
|
||||
- **vjrj** authored **Ğ1nkgo**, a Duniter/Ğ1 wallet in Flutter/Dart; Tane reuses its cryptographic
|
||||
primitives (secp256k1, HKDF, Duniter identity) — no reinvention.
|
||||
- **vjrj originated the Plantare concept** (BAH-Semillero, 2009, CC-BY-SA) and wrote *"Las semillas del
|
||||
conocimiento libre"* (2005) — a decade of domain authority in seed commons.
|
||||
- **Comunes** — registered association, stable legal counterpart for milestone accountability.
|
||||
|
||||
### 9. Requested amount
|
||||
**€32,000.**
|
||||
|
||||
### 10. Budget usage / task breakdown
|
||||
See Part C. Milestone-based; each milestone is an independent, verifiable deliverable with a CI-gated
|
||||
test suite where applicable. NLnet pays on verification of each milestone.
|
||||
|
||||
### 11. Other funding sources (past and present)
|
||||
- Built so far with **volunteer work**; **no prior grant funding**; no overlapping funding for this scope.
|
||||
- Complementary community funding (e.g. Goteo matchfunding with Red de Semillas) is a *future*
|
||||
sustainability avenue, not a duplicate of this request.
|
||||
|
||||
### 12. Sustainability after the grant
|
||||
- **Local-first:** useful even with no servers and no maintainer — utility does not expire.
|
||||
- **Open code & data (AGPL-3.0, open export):** anyone can continue, fork, or **self-host the relay**.
|
||||
- **Cheap, community-run relays** (collectives / Comunes), plus opportunistic app-as-relay and
|
||||
physical-proximity exchange at fairs.
|
||||
- **Non-extractive revenue only, never on the seed:** voluntary association membership (Seed Savers
|
||||
model), optional managed services for institutional seed banks. No per-transaction commission — legally
|
||||
prudent (EU PRM in trilogue) and coherent with the commons.
|
||||
|
||||
### 13. Compliance & disclosure
|
||||
- **Licence:** AGPL-3.0 (code); CC-BY-SA (docs/assets). Compatible with the reused Duniter/Ğ1nkgo stack.
|
||||
- **Privacy statement:** acknowledged.
|
||||
- **Generative-AI disclosure (honest & minimal — paste into the form):**
|
||||
> An AI assistant was used only as a drafting aid for this application text: to translate and structure
|
||||
> an initial English outline from our pre-existing Spanish design documents (VISION.md, PLAN.md,
|
||||
> docs/design/*), which predate and are independent of this application. All technical claims,
|
||||
> architecture, milestones and budget are the applicant's own; the project lead rewrote and verified
|
||||
> the text, and no AI output was submitted unedited. A prompt provenance log is available on request.
|
||||
> The funded deliverables (M1–M5) will be human-authored (AI-assisted at most), released under
|
||||
> AGPL-3.0, with no purely-AI-generated outcomes submitted for payment.
|
||||
|
||||
*Adjust to match what you actually did before sending. If you rewrite §3–§5 yourself (recommended),
|
||||
this statement stays true and strong.*
|
||||
|
||||
---
|
||||
|
||||
## Part C — Milestones & budget (€32,000)
|
||||
|
||||
Milestones reflect the **real remaining work** (Block 1 + most of Block 2 are already shipped). They are
|
||||
ordered by theme: **M1–M3 are the federated-hosting core** (self-hosting, sync, portability); **M4–M5
|
||||
harden and bootstrap it**. Each is an independent, verifiable deliverable; NLnet pays on verification.
|
||||
|
||||
| # | Milestone | Verifiable deliverable | € |
|
||||
|---|-----------|------------------------|---|
|
||||
| **M1** | **Self-hostable relay recipe + moderation** | `relay.comunes.org` already runs; deliver a **reproducible, documented recipe** any seed network can self-host, plus NIP-56 report handling + key-blocking moderation tooling and an operator manual | 6,000 |
|
||||
| **M2** | **Multi-device sync (app `SyncService`)** | Inventory replication across a user's own devices over the landed encrypted `SyncTransport` (NIP-78): serialise + push on mutation, idempotent LWW import, stable per-install device id, conflict UI; tests (relay sees only ciphertext) | 8,000 |
|
||||
| **M3** | **Interoperability & data portability** | Opt-in Darwin Core / GBIF export of agrobiodiversity (blurred location, collective-bank level, one-way); polished open export/import + identity portability docs | 5,000 |
|
||||
| **M4** | **Messaging & offers hardening** | NIP-44 interop-exact test vectors, offline DM delivery/retry queue, offer expiry + revocation on replicated relays, spam-filter polish; CI-gated | 7,000 |
|
||||
| **M5** | **Trust cold-start + Ğ1 price integration + docs/release** | WoT cold-start flows for fairs / Ğ1 groups (QR, offline); Ğ1 price tag + deep-link to wallets (no in-app payment); contributor architecture guide + multilingual user guide; F-Droid release | 6,000 |
|
||||
| | **Total** | | **32,000** |
|
||||
|
||||
---
|
||||
|
||||
## Submission checklist (before nlnet.nl/propose)
|
||||
1. Cross-check every field above against the live form + Guide for Applicants.
|
||||
2. Fit test: does the abstract + §4 opening read "federation / portability / self-hosting" *before* "seeds"?
|
||||
3. Budget: milestones independent & verifiable, effort realistic, sum = €32,000.
|
||||
4. Compliance: FOSS licence stated, privacy statement acknowledged, **AI-disclosure + provenance log ready**.
|
||||
5. Confirm the 1 Aug 2026 deadline on the site; submit with several days' margin.
|
||||
6. Language pass: rewrite §3–§5 in vjrj's voice; clear, technical English throughout.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — AI-use log (keep, attach or provide on request)
|
||||
Fill as you go so the disclosure is accurate and verifiable.
|
||||
|
||||
| Date | Tool / model | Prompt (verbatim) | What the output was used for | Kept unedited? |
|
||||
|------|--------------|-------------------|------------------------------|----------------|
|
||||
| 2026-07-10 | Claude Code (Opus 4.8) | "Me ayudas a presentar tane a esta convocatoria? qué pedirías…" | Initial English draft/outline from repo docs | No — to be rewritten & verified by lead |
|
||||
| 2026-07-17 | Claude Code (Opus 4.8) | "He avanzado mucho en el desarrollo, puedes actualizar la application?…" | Updated draft to reflect shipped status + new milestones | No — to be rewritten & verified by lead |
|
||||
|
||||
> Keep the raw session transcript alongside this table. If §3–§5 are rewritten by hand, note that here too.
|
||||
|
|
@ -125,31 +125,11 @@ listing (titles, descriptions, changelogs, screenshots) is read straight from
|
|||
|
||||
## Publish to F-Droid (official repo)
|
||||
|
||||
F-Droid builds from source after a merge request to `fdroiddata`. The build recipe
|
||||
is kept in-repo at [`fdroid/org.comunes.tane.yml`](fdroid/org.comunes.tane.yml).
|
||||
F-Droid builds and signs from source after a merge request to `fdroiddata`. The
|
||||
build recipe is kept in-repo at [`fdroid/org.comunes.tane.yml`](fdroid/org.comunes.tane.yml).
|
||||
Copy it to `metadata/org.comunes.tane.yml` in a fork of fdroiddata, validate with
|
||||
`fdroid lint` / `fdroid build -l org.comunes.tane`, then open the MR
|
||||
([43144](https://gitlab.com/fdroid/fdroiddata/-/merge_requests/43144)).
|
||||
|
||||
**Reproducible, developer-signed.** The recipe pins `AllowedAPKSigningKeys` (the
|
||||
SHA-256 of the tane-upload certificate) and a `binary:` URL per ABI. F-Droid
|
||||
rebuilds each split, and if its output matches our signed reference APK
|
||||
byte-for-byte it publishes **our** APK — so F-Droid and Play share the same
|
||||
signature and users can move between stores without reinstalling. The reference
|
||||
APKs are the per-ABI splits the release workflow attaches to the Forgejo release
|
||||
on `git.comunes.org` (`.forgejo/workflows/release.yml`).
|
||||
|
||||
To (re)compute the fingerprint from the keystore:
|
||||
|
||||
```bash
|
||||
keytool -list -v -keystore .../tane-upload.jks -alias tane-upload \
|
||||
| sed -n 's/.*SHA256: //p' | tr -d ':' | tr 'A-Z' 'a-z'
|
||||
```
|
||||
|
||||
Before pushing the MR, confirm reproducibility in the fdroiddata fork:
|
||||
`fdroid build -l org.comunes.tane` then `fdroid verify org.comunes.tane` must be
|
||||
green against the Forgejo `binary:`. If a split is not reproducible, fall back to
|
||||
F-Droid-signing that version (drop `AllowedAPKSigningKeys`/`binary:`) until fixed.
|
||||
`fdroid lint` / `fdroid build -l org.comunes.tane`, then open the MR. F-Droid signs
|
||||
with its own key, so the F-Droid and Play builds have different signatures.
|
||||
|
||||
## Store metadata
|
||||
|
||||
|
|
|
|||
24
pubspec.lock
24
pubspec.lock
|
|
@ -201,14 +201,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.11.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1004,14 +996,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
pigeon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pigeon
|
||||
sha256: "04cfefc8add8b47ddf9ccac8b92bb4edeb67c87f185c623ba0db118ac99334ad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "26.3.4"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1497,14 +1481,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
zxing_barcode_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: zxing_barcode_scanner
|
||||
sha256: "10f77da15624b1222d1e4b792991bf485b0b7c08d170b99e9a658c69a5fc0a3a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
sdks:
|
||||
dart: ">=3.11.5 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue