diff --git a/apps/app_seeds/.metadata b/apps/app_seeds/.metadata index 93f64e1..c513e7b 100644 --- a/apps/app_seeds/.metadata +++ b/apps/app_seeds/.metadata @@ -24,6 +24,12 @@ migration: - platform: linux create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + - platform: web + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + - platform: windows + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 # User provided section diff --git a/apps/app_seeds/assets/catalog/species.json b/apps/app_seeds/assets/catalog/species.json index 2463e65..cc251f5 100644 --- a/apps/app_seeds/assets/catalog/species.json +++ b/apps/app_seeds/assets/catalog/species.json @@ -1,76 +1,90 @@ { - "version": 1, - "note": "Curated starter set of Iberian horticultural species. Family and common names are stable; wikidata_qid/gbif_key are intentionally omitted here and enriched later from the varilla (Wikidata CC0 + GBIF) work.", + "version": 2, + "note": "Curated starter set of common horticultural species — a seed, not a region-locked list. `common` is a locale-keyed map extensible to ANY language (add es/en/fr/de/ar/zh/…); the app resolves names by the user's locale and falls back gracefully, so this must never assume es/en only. Names are enriched later per-locale from the varilla (Wikidata CC0 + GBIF) work, which carries vernacular names in every language. viability_years is typical seed longevity under normal home storage (public-domain agricultural-extension figures); a conservative single value, used to warn about aging lots.", "species": [ { "scientific_name": "Solanum lycopersicum", "family": "Solanaceae", - "common": { "es": ["Tomate"], "en": ["Tomato"] } + "common": { "es": ["Tomate"], "en": ["Tomato"] }, + "viability_years": 5 }, { "scientific_name": "Solanum melongena", "family": "Solanaceae", - "common": { "es": ["Berenjena"], "en": ["Aubergine", "Eggplant"] } + "common": { "es": ["Berenjena"], "en": ["Aubergine", "Eggplant"] }, + "viability_years": 5 }, { "scientific_name": "Capsicum annuum", "family": "Solanaceae", - "common": { "es": ["Pimiento", "Guindilla"], "en": ["Pepper", "Chilli"] } + "common": { "es": ["Pimiento", "Guindilla"], "en": ["Pepper", "Chilli"] }, + "viability_years": 3 }, { "scientific_name": "Phaseolus vulgaris", "family": "Fabaceae", - "common": { "es": ["Judía", "Alubia", "Habichuela"], "en": ["Common bean"] } + "common": { "es": ["Judía", "Alubia", "Habichuela"], "en": ["Common bean"] }, + "viability_years": 3 }, { "scientific_name": "Vicia faba", "family": "Fabaceae", - "common": { "es": ["Haba"], "en": ["Broad bean", "Fava bean"] } + "common": { "es": ["Haba"], "en": ["Broad bean", "Fava bean"] }, + "viability_years": 4 }, { "scientific_name": "Pisum sativum", "family": "Fabaceae", - "common": { "es": ["Guisante"], "en": ["Pea"] } + "common": { "es": ["Guisante"], "en": ["Pea"] }, + "viability_years": 3 }, { "scientific_name": "Zea mays", "family": "Poaceae", - "common": { "es": ["Maíz"], "en": ["Maize", "Corn"] } + "common": { "es": ["Maíz"], "en": ["Maize", "Corn"] }, + "viability_years": 2 }, { "scientific_name": "Cucurbita pepo", "family": "Cucurbitaceae", - "common": { "es": ["Calabacín", "Calabaza"], "en": ["Courgette", "Zucchini", "Squash"] } + "common": { "es": ["Calabacín", "Calabaza"], "en": ["Courgette", "Zucchini", "Squash"] }, + "viability_years": 5 }, { "scientific_name": "Cucumis sativus", "family": "Cucurbitaceae", - "common": { "es": ["Pepino"], "en": ["Cucumber"] } + "common": { "es": ["Pepino"], "en": ["Cucumber"] }, + "viability_years": 5 }, { "scientific_name": "Lactuca sativa", "family": "Asteraceae", - "common": { "es": ["Lechuga"], "en": ["Lettuce"] } + "common": { "es": ["Lechuga"], "en": ["Lettuce"] }, + "viability_years": 3 }, { "scientific_name": "Beta vulgaris", "family": "Amaranthaceae", - "common": { "es": ["Acelga", "Remolacha"], "en": ["Chard", "Beetroot"] } + "common": { "es": ["Acelga", "Remolacha"], "en": ["Chard", "Beetroot"] }, + "viability_years": 4 }, { "scientific_name": "Allium cepa", "family": "Amaryllidaceae", - "common": { "es": ["Cebolla"], "en": ["Onion"] } + "common": { "es": ["Cebolla"], "en": ["Onion"] }, + "viability_years": 1 }, { "scientific_name": "Allium sativum", "family": "Amaryllidaceae", - "common": { "es": ["Ajo"], "en": ["Garlic"] } + "common": { "es": ["Ajo"], "en": ["Garlic"] }, + "viability_years": 1 }, { "scientific_name": "Daucus carota", "family": "Apiaceae", - "common": { "es": ["Zanahoria"], "en": ["Carrot"] } + "common": { "es": ["Zanahoria"], "en": ["Carrot"] }, + "viability_years": 3 } ] } diff --git a/apps/app_seeds/assets/ocr_fixtures/README.md b/apps/app_seeds/assets/ocr_fixtures/README.md new file mode 100644 index 0000000..035f846 --- /dev/null +++ b/apps/app_seeds/assets/ocr_fixtures/README.md @@ -0,0 +1,18 @@ +# OCR validation fixtures + +Real packet photos used by `integration_test/ocr_label_test.dart` to check that +on-device Tesseract reads the variety name correctly. + +Drop your photos here with these names: + +- `aubergine.jpg` — the AUBERGINE BLACK BEAUTY packet +- `cucumber.jpg` — the CUCUMBER SUPERMARKETER packet + +Then run on a connected Android device/emulator (native OCR does not run on the +host test VM): + + flutter test integration_test/ocr_label_test.dart -d + +Each test prints `OCR[] => ""` so you can see exactly what +Tesseract read. These fixtures are only for validation — remove the folder from +`pubspec.yaml` assets before shipping if you don't want them in the bundle. diff --git a/apps/app_seeds/assets/ocr_fixtures/aubergine.jpg b/apps/app_seeds/assets/ocr_fixtures/aubergine.jpg new file mode 100644 index 0000000..7b0fd99 Binary files /dev/null and b/apps/app_seeds/assets/ocr_fixtures/aubergine.jpg differ diff --git a/apps/app_seeds/assets/ocr_fixtures/cucumber.jpg b/apps/app_seeds/assets/ocr_fixtures/cucumber.jpg new file mode 100644 index 0000000..7fc3986 Binary files /dev/null and b/apps/app_seeds/assets/ocr_fixtures/cucumber.jpg differ diff --git a/apps/app_seeds/assets/tessdata/eng.traineddata b/apps/app_seeds/assets/tessdata/eng.traineddata new file mode 100644 index 0000000..bbef467 Binary files /dev/null and b/apps/app_seeds/assets/tessdata/eng.traineddata differ diff --git a/apps/app_seeds/assets/tessdata/spa.traineddata b/apps/app_seeds/assets/tessdata/spa.traineddata new file mode 100644 index 0000000..72e901f Binary files /dev/null and b/apps/app_seeds/assets/tessdata/spa.traineddata differ diff --git a/apps/app_seeds/assets/tessdata_config.json b/apps/app_seeds/assets/tessdata_config.json new file mode 100644 index 0000000..5e2e988 --- /dev/null +++ b/apps/app_seeds/assets/tessdata_config.json @@ -0,0 +1,6 @@ +{ + "files": [ + "eng.traineddata", + "spa.traineddata" + ] +} diff --git a/apps/app_seeds/drift_schemas/drift_schema_v6.json b/apps/app_seeds/drift_schemas/drift_schema_v6.json new file mode 100644 index 0000000..7eac8ce --- /dev/null +++ b/apps/app_seeds/drift_schemas/drift_schema_v6.json @@ -0,0 +1,1519 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "varieties", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "label", + "getter_name": "label", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "species_id", + "getter_name": "speciesId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "cultivar_name", + "getter_name": "cultivarName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "category", + "getter_name": "category", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_draft", + "getter_name": "isDraft", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_draft\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_draft\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "variety_vernacular_names", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "variety_id", + "getter_name": "varietyId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language", + "getter_name": "language", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "region", + "getter_name": "region", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "species", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "scientific_name", + "getter_name": "scientificName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "wikidata_qid", + "getter_name": "wikidataQid", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "gbif_key", + "getter_name": "gbifKey", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "family", + "getter_name": "family", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_bundled", + "getter_name": "isBundled", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_bundled\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_bundled\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "species_common_names", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "species_id", + "getter_name": "speciesId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language", + "getter_name": "language", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "lots", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "variety_id", + "getter_name": "varietyId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'seed\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(LotType.values)", + "dart_type_name": "LotType" + } + }, + { + "name": "harvest_year", + "getter_name": "harvestYear", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "harvest_month", + "getter_name": "harvestMonth", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_kind", + "getter_name": "quantityKind", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_precise", + "getter_name": "quantityPrecise", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_label", + "getter_name": "quantityLabel", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "presentation", + "getter_name": "presentation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(Presentation.values)", + "dart_type_name": "Presentation" + } + }, + { + "name": "storage_location", + "getter_name": "storageLocation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "offer_status", + "getter_name": "offerStatus", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'private\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(OfferStatus.values)", + "dart_type_name": "OfferStatus" + } + }, + { + "name": "seedbank_id", + "getter_name": "seedbankId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "germination_tests", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "tested_on", + "getter_name": "testedOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sample_size", + "getter_name": "sampleSize", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "germinated_count", + "getter_name": "germinatedCount", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "movements", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MovementType.values)", + "dart_type_name": "MovementType" + } + }, + { + "name": "occurred_on", + "getter_name": "occurredOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "counterparty_id", + "getter_name": "counterpartyId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_kind", + "getter_name": "quantityKind", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_precise", + "getter_name": "quantityPrecise", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_label", + "getter_name": "quantityLabel", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_movement_id", + "getter_name": "parentMovementId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "plantare_id", + "getter_name": "plantareId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 7, + "references": [], + "type": "table", + "data": { + "name": "parties", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "display_name", + "getter_name": "displayName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_key", + "getter_name": "publicKey", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "kind", + "getter_name": "kind", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'person\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(PartyKind.values)", + "dart_type_name": "PartyKind" + } + }, + { + "name": "note", + "getter_name": "note", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "attachments", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_type", + "getter_name": "parentType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ParentType.values)", + "dart_type_name": "ParentType" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "kind", + "getter_name": "kind", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(AttachmentKind.values)", + "dart_type_name": "AttachmentKind" + } + }, + { + "name": "uri", + "getter_name": "uri", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bytes", + "getter_name": "bytes", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mime_type", + "getter_name": "mimeType", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sort_order", + "getter_name": "sortOrder", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "external_links", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_type", + "getter_name": "parentType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ParentType.values)", + "dart_type_name": "ParentType" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + } + ], + "fixed_sql": [ + { + "name": "varieties", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"varieties\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"label\" TEXT NOT NULL, \"species_id\" TEXT NULL, \"cultivar_name\" TEXT NULL, \"category\" TEXT NULL, \"notes\" TEXT NULL, \"is_draft\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_draft\" IN (0, 1)), PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "variety_vernacular_names", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"variety_vernacular_names\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"variety_id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"language\" TEXT NULL, \"region\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "species", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"species\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"scientific_name\" TEXT NOT NULL, \"wikidata_qid\" TEXT NULL, \"gbif_key\" INTEGER NULL, \"family\" TEXT NULL, \"is_bundled\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_bundled\" IN (0, 1)), PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "species_common_names", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"species_common_names\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"species_id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"language\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "lots", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"lots\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"variety_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL DEFAULT 'seed', \"harvest_year\" INTEGER NULL, \"harvest_month\" INTEGER NULL, \"quantity_kind\" TEXT NULL, \"quantity_precise\" REAL NULL, \"quantity_label\" TEXT NULL, \"presentation\" TEXT NULL, \"storage_location\" TEXT NULL, \"offer_status\" TEXT NOT NULL DEFAULT 'private', \"seedbank_id\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "germination_tests", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"germination_tests\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"tested_on\" INTEGER NULL, \"sample_size\" INTEGER NULL, \"germinated_count\" INTEGER NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "movements", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"movements\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"last_author\" TEXT NOT NULL, \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"occurred_on\" INTEGER NULL, \"counterparty_id\" TEXT NULL, \"quantity_kind\" TEXT NULL, \"quantity_precise\" REAL NULL, \"quantity_label\" TEXT NULL, \"parent_movement_id\" TEXT NULL, \"plantare_id\" TEXT NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "parties", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"parties\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"display_name\" TEXT NOT NULL, \"public_key\" TEXT NULL, \"kind\" TEXT NOT NULL DEFAULT 'person', \"note\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "attachments", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"attachments\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"parent_type\" TEXT NOT NULL, \"parent_id\" TEXT NOT NULL, \"kind\" TEXT NOT NULL, \"uri\" TEXT NULL, \"bytes\" BLOB NULL, \"mime_type\" TEXT NULL, \"sort_order\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "external_links", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"external_links\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"parent_type\" TEXT NOT NULL, \"parent_id\" TEXT NOT NULL, \"url\" TEXT NOT NULL, \"title\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/app_seeds/drift_schemas/drift_schema_v7.json b/apps/app_seeds/drift_schemas/drift_schema_v7.json new file mode 100644 index 0000000..947744e --- /dev/null +++ b/apps/app_seeds/drift_schemas/drift_schema_v7.json @@ -0,0 +1,1543 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "varieties", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "label", + "getter_name": "label", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "species_id", + "getter_name": "speciesId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "cultivar_name", + "getter_name": "cultivarName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "category", + "getter_name": "category", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_draft", + "getter_name": "isDraft", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_draft\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_draft\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_organic", + "getter_name": "isOrganic", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_organic\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_organic\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "variety_vernacular_names", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "variety_id", + "getter_name": "varietyId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language", + "getter_name": "language", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "region", + "getter_name": "region", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "species", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "scientific_name", + "getter_name": "scientificName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "wikidata_qid", + "getter_name": "wikidataQid", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "gbif_key", + "getter_name": "gbifKey", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "family", + "getter_name": "family", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_bundled", + "getter_name": "isBundled", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_bundled\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_bundled\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "viability_years", + "getter_name": "viabilityYears", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "species_common_names", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "species_id", + "getter_name": "speciesId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language", + "getter_name": "language", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "lots", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "variety_id", + "getter_name": "varietyId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'seed\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(LotType.values)", + "dart_type_name": "LotType" + } + }, + { + "name": "harvest_year", + "getter_name": "harvestYear", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "harvest_month", + "getter_name": "harvestMonth", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_kind", + "getter_name": "quantityKind", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_precise", + "getter_name": "quantityPrecise", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_label", + "getter_name": "quantityLabel", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "presentation", + "getter_name": "presentation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(Presentation.values)", + "dart_type_name": "Presentation" + } + }, + { + "name": "storage_location", + "getter_name": "storageLocation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "offer_status", + "getter_name": "offerStatus", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'private\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(OfferStatus.values)", + "dart_type_name": "OfferStatus" + } + }, + { + "name": "seedbank_id", + "getter_name": "seedbankId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "germination_tests", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "tested_on", + "getter_name": "testedOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sample_size", + "getter_name": "sampleSize", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "germinated_count", + "getter_name": "germinatedCount", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "movements", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MovementType.values)", + "dart_type_name": "MovementType" + } + }, + { + "name": "occurred_on", + "getter_name": "occurredOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "counterparty_id", + "getter_name": "counterpartyId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_kind", + "getter_name": "quantityKind", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_precise", + "getter_name": "quantityPrecise", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_label", + "getter_name": "quantityLabel", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_movement_id", + "getter_name": "parentMovementId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "plantare_id", + "getter_name": "plantareId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 7, + "references": [], + "type": "table", + "data": { + "name": "parties", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "display_name", + "getter_name": "displayName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_key", + "getter_name": "publicKey", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "kind", + "getter_name": "kind", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'person\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(PartyKind.values)", + "dart_type_name": "PartyKind" + } + }, + { + "name": "note", + "getter_name": "note", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "attachments", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_type", + "getter_name": "parentType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ParentType.values)", + "dart_type_name": "ParentType" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "kind", + "getter_name": "kind", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(AttachmentKind.values)", + "dart_type_name": "AttachmentKind" + } + }, + { + "name": "uri", + "getter_name": "uri", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bytes", + "getter_name": "bytes", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mime_type", + "getter_name": "mimeType", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sort_order", + "getter_name": "sortOrder", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "external_links", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_type", + "getter_name": "parentType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ParentType.values)", + "dart_type_name": "ParentType" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + } + ], + "fixed_sql": [ + { + "name": "varieties", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"varieties\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"label\" TEXT NOT NULL, \"species_id\" TEXT NULL, \"cultivar_name\" TEXT NULL, \"category\" TEXT NULL, \"notes\" TEXT NULL, \"is_draft\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_draft\" IN (0, 1)), \"is_organic\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_organic\" IN (0, 1)), PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "variety_vernacular_names", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"variety_vernacular_names\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"variety_id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"language\" TEXT NULL, \"region\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "species", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"species\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"scientific_name\" TEXT NOT NULL, \"wikidata_qid\" TEXT NULL, \"gbif_key\" INTEGER NULL, \"family\" TEXT NULL, \"is_bundled\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_bundled\" IN (0, 1)), \"viability_years\" INTEGER NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "species_common_names", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"species_common_names\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"species_id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"language\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "lots", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"lots\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"variety_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL DEFAULT 'seed', \"harvest_year\" INTEGER NULL, \"harvest_month\" INTEGER NULL, \"quantity_kind\" TEXT NULL, \"quantity_precise\" REAL NULL, \"quantity_label\" TEXT NULL, \"presentation\" TEXT NULL, \"storage_location\" TEXT NULL, \"offer_status\" TEXT NOT NULL DEFAULT 'private', \"seedbank_id\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "germination_tests", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"germination_tests\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"tested_on\" INTEGER NULL, \"sample_size\" INTEGER NULL, \"germinated_count\" INTEGER NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "movements", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"movements\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"last_author\" TEXT NOT NULL, \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"occurred_on\" INTEGER NULL, \"counterparty_id\" TEXT NULL, \"quantity_kind\" TEXT NULL, \"quantity_precise\" REAL NULL, \"quantity_label\" TEXT NULL, \"parent_movement_id\" TEXT NULL, \"plantare_id\" TEXT NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "parties", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"parties\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"display_name\" TEXT NOT NULL, \"public_key\" TEXT NULL, \"kind\" TEXT NOT NULL DEFAULT 'person', \"note\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "attachments", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"attachments\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"parent_type\" TEXT NOT NULL, \"parent_id\" TEXT NOT NULL, \"kind\" TEXT NOT NULL, \"uri\" TEXT NULL, \"bytes\" BLOB NULL, \"mime_type\" TEXT NULL, \"sort_order\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "external_links", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"external_links\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"parent_type\" TEXT NOT NULL, \"parent_id\" TEXT NOT NULL, \"url\" TEXT NOT NULL, \"title\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/app_seeds/drift_schemas/drift_schema_v8.json b/apps/app_seeds/drift_schemas/drift_schema_v8.json new file mode 100644 index 0000000..c2df026 --- /dev/null +++ b/apps/app_seeds/drift_schemas/drift_schema_v8.json @@ -0,0 +1,1799 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "varieties", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "label", + "getter_name": "label", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "species_id", + "getter_name": "speciesId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "cultivar_name", + "getter_name": "cultivarName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "category", + "getter_name": "category", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_draft", + "getter_name": "isDraft", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_draft\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_draft\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_organic", + "getter_name": "isOrganic", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_organic\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_organic\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "needs_reproduction", + "getter_name": "needsReproduction", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"needs_reproduction\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"needs_reproduction\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sow_months", + "getter_name": "sowMonths", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "transplant_months", + "getter_name": "transplantMonths", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "flowering_months", + "getter_name": "floweringMonths", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "fruiting_months", + "getter_name": "fruitingMonths", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "seed_harvest_months", + "getter_name": "seedHarvestMonths", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "variety_vernacular_names", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "variety_id", + "getter_name": "varietyId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language", + "getter_name": "language", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "region", + "getter_name": "region", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "species", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "scientific_name", + "getter_name": "scientificName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "wikidata_qid", + "getter_name": "wikidataQid", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "gbif_key", + "getter_name": "gbifKey", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "family", + "getter_name": "family", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_bundled", + "getter_name": "isBundled", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_bundled\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_bundled\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "viability_years", + "getter_name": "viabilityYears", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "species_common_names", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "species_id", + "getter_name": "speciesId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language", + "getter_name": "language", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "lots", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "variety_id", + "getter_name": "varietyId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'seed\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(LotType.values)", + "dart_type_name": "LotType" + } + }, + { + "name": "harvest_year", + "getter_name": "harvestYear", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "harvest_month", + "getter_name": "harvestMonth", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_kind", + "getter_name": "quantityKind", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_precise", + "getter_name": "quantityPrecise", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_label", + "getter_name": "quantityLabel", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "presentation", + "getter_name": "presentation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(Presentation.values)", + "dart_type_name": "Presentation" + } + }, + { + "name": "storage_location", + "getter_name": "storageLocation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "offer_status", + "getter_name": "offerStatus", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'private\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(OfferStatus.values)", + "dart_type_name": "OfferStatus" + } + }, + { + "name": "seedbank_id", + "getter_name": "seedbankId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "origin_name", + "getter_name": "originName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "origin_place", + "getter_name": "originPlace", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "abundance", + "getter_name": "abundance", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(Abundance.values)", + "dart_type_name": "Abundance" + } + }, + { + "name": "preservation_format", + "getter_name": "preservationFormat", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(PreservationFormat.values)", + "dart_type_name": "PreservationFormat" + } + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [], + "type": "table", + "data": { + "name": "germination_tests", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "tested_on", + "getter_name": "testedOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sample_size", + "getter_name": "sampleSize", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "germinated_count", + "getter_name": "germinatedCount", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 6, + "references": [], + "type": "table", + "data": { + "name": "condition_checks", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checked_on", + "getter_name": "checkedOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "container_count", + "getter_name": "containerCount", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "desiccant_state", + "getter_name": "desiccantState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(DesiccantState.values)", + "dart_type_name": "DesiccantState" + } + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 7, + "references": [], + "type": "table", + "data": { + "name": "movements", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lot_id", + "getter_name": "lotId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MovementType.values)", + "dart_type_name": "MovementType" + } + }, + { + "name": "occurred_on", + "getter_name": "occurredOn", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "counterparty_id", + "getter_name": "counterpartyId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_kind", + "getter_name": "quantityKind", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_precise", + "getter_name": "quantityPrecise", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quantity_label", + "getter_name": "quantityLabel", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_movement_id", + "getter_name": "parentMovementId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "plantare_id", + "getter_name": "plantareId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "notes", + "getter_name": "notes", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "parties", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "display_name", + "getter_name": "displayName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_key", + "getter_name": "publicKey", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "kind", + "getter_name": "kind", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'person\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(PartyKind.values)", + "dart_type_name": "PartyKind" + } + }, + { + "name": "note", + "getter_name": "note", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "attachments", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_type", + "getter_name": "parentType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ParentType.values)", + "dart_type_name": "ParentType" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "kind", + "getter_name": "kind", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(AttachmentKind.values)", + "dart_type_name": "AttachmentKind" + } + }, + { + "name": "uri", + "getter_name": "uri", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bytes", + "getter_name": "bytes", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mime_type", + "getter_name": "mimeType", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sort_order", + "getter_name": "sortOrder", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 10, + "references": [], + "type": "table", + "data": { + "name": "external_links", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_author", + "getter_name": "lastAuthor", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted", + "getter_name": "isDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "schema_row_version", + "getter_name": "schemaRowVersion", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "parent_type", + "getter_name": "parentType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ParentType.values)", + "dart_type_name": "ParentType" + } + }, + { + "name": "parent_id", + "getter_name": "parentId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + } + ], + "fixed_sql": [ + { + "name": "varieties", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"varieties\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"label\" TEXT NOT NULL, \"species_id\" TEXT NULL, \"cultivar_name\" TEXT NULL, \"category\" TEXT NULL, \"notes\" TEXT NULL, \"is_draft\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_draft\" IN (0, 1)), \"is_organic\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_organic\" IN (0, 1)), \"needs_reproduction\" INTEGER NOT NULL DEFAULT 0 CHECK (\"needs_reproduction\" IN (0, 1)), \"sow_months\" INTEGER NULL, \"transplant_months\" INTEGER NULL, \"flowering_months\" INTEGER NULL, \"fruiting_months\" INTEGER NULL, \"seed_harvest_months\" INTEGER NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "variety_vernacular_names", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"variety_vernacular_names\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"variety_id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"language\" TEXT NULL, \"region\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "species", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"species\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"scientific_name\" TEXT NOT NULL, \"wikidata_qid\" TEXT NULL, \"gbif_key\" INTEGER NULL, \"family\" TEXT NULL, \"is_bundled\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_bundled\" IN (0, 1)), \"viability_years\" INTEGER NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "species_common_names", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"species_common_names\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"species_id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"language\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "lots", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"lots\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"variety_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL DEFAULT 'seed', \"harvest_year\" INTEGER NULL, \"harvest_month\" INTEGER NULL, \"quantity_kind\" TEXT NULL, \"quantity_precise\" REAL NULL, \"quantity_label\" TEXT NULL, \"presentation\" TEXT NULL, \"storage_location\" TEXT NULL, \"offer_status\" TEXT NOT NULL DEFAULT 'private', \"seedbank_id\" TEXT NULL, \"origin_name\" TEXT NULL, \"origin_place\" TEXT NULL, \"abundance\" TEXT NULL, \"preservation_format\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "germination_tests", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"germination_tests\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"tested_on\" INTEGER NULL, \"sample_size\" INTEGER NULL, \"germinated_count\" INTEGER NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "condition_checks", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"condition_checks\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"checked_on\" INTEGER NULL, \"container_count\" INTEGER NULL, \"desiccant_state\" TEXT NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "movements", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"movements\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"last_author\" TEXT NOT NULL, \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"lot_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"occurred_on\" INTEGER NULL, \"counterparty_id\" TEXT NULL, \"quantity_kind\" TEXT NULL, \"quantity_precise\" REAL NULL, \"quantity_label\" TEXT NULL, \"parent_movement_id\" TEXT NULL, \"plantare_id\" TEXT NULL, \"notes\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "parties", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"parties\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"display_name\" TEXT NOT NULL, \"public_key\" TEXT NULL, \"kind\" TEXT NOT NULL DEFAULT 'person', \"note\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "attachments", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"attachments\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"parent_type\" TEXT NOT NULL, \"parent_id\" TEXT NOT NULL, \"kind\" TEXT NOT NULL, \"uri\" TEXT NULL, \"bytes\" BLOB NULL, \"mime_type\" TEXT NULL, \"sort_order\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\"));" + } + ] + }, + { + "name": "external_links", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"external_links\" (\"id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL, \"updated_at\" TEXT NOT NULL, \"last_author\" TEXT NOT NULL, \"is_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted\" IN (0, 1)), \"schema_row_version\" INTEGER NOT NULL DEFAULT 1, \"parent_type\" TEXT NOT NULL, \"parent_id\" TEXT NOT NULL, \"url\" TEXT NOT NULL, \"title\" TEXT NULL, PRIMARY KEY (\"id\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/app_seeds/integration_test/ocr_label_test.dart b/apps/app_seeds/integration_test/ocr_label_test.dart new file mode 100644 index 0000000..2466c6b --- /dev/null +++ b/apps/app_seeds/integration_test/ocr_label_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:tane/services/ocr/tesseract_label_extractor.dart'; + +/// Real, on-device OCR validation against actual packet photos — the "does +/// Tesseract really read my packets" gate from the plan. Runs the native engine, +/// so it must run on a device/emulator, NOT under `flutter test` on the host: +/// +/// flutter test integration_test/ocr_label_test.dart -d +/// +/// Drop the photos to check into `assets/ocr_fixtures/` (declared in pubspec) +/// with the file names below. Each test prints what Tesseract actually read so +/// you can see and tune it, then asserts the expected variety word is present. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + const extractor = TesseractLabelExtractor(); + + Future reportRead(String asset) async { + final data = await rootBundle.load('assets/ocr_fixtures/$asset'); + final result = await extractor.suggestLabel(data.buffer.asUint8List()); + // ignore: avoid_print + print('OCR[$asset] => "$result"'); + // Best-effort: a usable, editable suggestion should appear. Exact spelling + // is not asserted — real OCR may misread a letter (e.g. "Sucumber"), which + // is still useful as a prefill the user corrects. + expect(result, isNotNull, reason: 'Tesseract produced no suggestion for ' + '$asset (see the printed value above)'); + } + + testWidgets('suggests a name from the AUBERGINE packet photo', (_) async { + await reportRead('aubergine.jpg'); + }); + + testWidgets('suggests a name from the CUCUMBER packet photo', (_) async { + await reportRead('cucumber.jpg'); + }); +} diff --git a/apps/app_seeds/lib/app.dart b/apps/app_seeds/lib/app.dart index f0770df..b80561a 100644 --- a/apps/app_seeds/lib/app.dart +++ b/apps/app_seeds/lib/app.dart @@ -7,10 +7,12 @@ import 'package:go_router/go_router.dart'; import 'data/species_repository.dart'; import 'data/variety_repository.dart'; import 'i18n/strings.g.dart'; +import 'services/onboarding_store.dart'; import 'state/inventory_cubit.dart'; import 'state/variety_detail_cubit.dart'; import 'ui/about_screen.dart'; import 'ui/home_screen.dart'; +import 'ui/intro_screen.dart'; import 'ui/inventory_list_screen.dart'; import 'ui/settings_screen.dart'; import 'ui/theme.dart'; @@ -18,18 +20,40 @@ import 'ui/variety_detail_screen.dart'; /// Root widget. Provides the repositories to the tree and wires go_router: /// `/` is the home menu, `/inventory` the list, `/variety/:id` the item detail. +/// When [showIntro] is set (first launch), the app opens on `/intro`. class TaneApp extends StatelessWidget { - TaneApp({required this.repository, required this.species, super.key}) - : _router = _buildRouter(repository); + TaneApp({ + required this.repository, + required this.species, + required this.onboarding, + this.showIntro = false, + super.key, + }) : _router = _buildRouter(repository, onboarding, showIntro); final VarietyRepository repository; final SpeciesRepository species; + final OnboardingStore onboarding; + final bool showIntro; final GoRouter _router; - static GoRouter _buildRouter(VarietyRepository repository) { + static GoRouter _buildRouter( + VarietyRepository repository, + OnboardingStore onboarding, + bool showIntro, + ) { return GoRouter( + initialLocation: showIntro ? '/intro' : '/', routes: [ GoRoute(path: '/', builder: (context, state) => const HomeScreen()), + GoRoute( + path: '/intro', + builder: (context, state) => IntroScreen( + onDone: () async { + await onboarding.markIntroSeen(); + if (context.mounted) context.go('/'); + }, + ), + ), GoRoute( path: '/settings', builder: (context, state) => const SettingsScreen(), diff --git a/apps/app_seeds/lib/data/export_import/inventory_csv_codec.dart b/apps/app_seeds/lib/data/export_import/inventory_csv_codec.dart index 6dc937d..90f2dae 100644 --- a/apps/app_seeds/lib/data/export_import/inventory_csv_codec.dart +++ b/apps/app_seeds/lib/data/export_import/inventory_csv_codec.dart @@ -2,6 +2,7 @@ import 'package:commons_core/commons_core.dart' show QuantityKind; import '../../db/database.dart'; import '../../db/enums.dart'; +import '../../domain/crop_calendar.dart'; import 'inventory_snapshot.dart'; /// Flattens an [InventorySnapshot] into a spreadsheet-friendly CSV — one row @@ -20,6 +21,12 @@ class InventoryCsvCodec { 'variety_notes', 'vernacular_names', 'external_links', + 'needs_reproduction', + 'sow_months', + 'transplant_months', + 'flowering_months', + 'fruiting_months', + 'seed_harvest_months', 'lot_id', 'lot_type', 'harvest_year', @@ -30,7 +37,13 @@ class InventoryCsvCodec { 'presentation', 'storage_location', 'offer_status', + 'origin_name', + 'origin_place', + 'abundance', + 'preservation_format', 'latest_germination_rate', + 'container_count', + 'desiccant_state', 'created_at', 'updated_at', ]; @@ -54,6 +67,7 @@ class InventoryCsvCodec { .add(e.title == null ? e.url : '${e.title}|${e.url}'); } final latestRateByLot = _latestGerminationRates(snapshot.germinationTests); + final latestCheckByLot = _latestConditionChecks(snapshot.conditionChecks); final lines = [_header.map(_escape).join(',')]; for (final v in snapshot.varieties) { @@ -66,13 +80,19 @@ class InventoryCsvCodec { v.notes ?? '', (namesByVariety[v.id] ?? const []).join('; '), (linksByVariety[v.id] ?? const []).join('; '), + v.needsReproduction ? 'true' : '', + _monthsCell(v.sowMonths), + _monthsCell(v.transplantMonths), + _monthsCell(v.floweringMonths), + _monthsCell(v.fruitingMonths), + _monthsCell(v.seedHarvestMonths), ]; final lots = lotsByVariety[v.id] ?? const []; if (lots.isEmpty) { lines.add( [ ...varietyCells, - ...List.filled(11, ''), + ...List.filled(17, ''), _isoDate(v.createdAt), _hlcMillisIso(v.updatedAt), ].map(_escape).join(','), @@ -80,6 +100,7 @@ class InventoryCsvCodec { continue; } for (final l in lots) { + final check = latestCheckByLot[l.id]; lines.add( [ ...varietyCells, @@ -93,7 +114,13 @@ class InventoryCsvCodec { l.presentation?.name ?? '', l.storageLocation ?? '', l.offerStatus.name, + l.originName ?? '', + l.originPlace ?? '', + l.abundance?.name ?? '', + l.preservationFormat?.name ?? '', latestRateByLot[l.id]?.toStringAsFixed(2) ?? '', + check?.containerCount?.toString() ?? '', + check?.desiccantState?.name ?? '', _isoDate(l.createdAt), _hlcMillisIso(l.updatedAt), ].map(_escape).join(','), @@ -103,6 +130,25 @@ class InventoryCsvCodec { return '${lines.join('\r\n')}\r\n'; } + /// Renders a crop-calendar month mask as a space-separated list of month + /// numbers (e.g. `3 4 9`), or empty when unset — readable in a spreadsheet. + String _monthsCell(int? mask) => maskToMonths(mask).join(' '); + + /// lotId → most-recent condition check (by checkedOn), so the CSV can show a + /// lot's latest jar count and drying-agent state as flat columns. + Map _latestConditionChecks( + List checks, + ) { + final latest = {}; + for (final c in checks) { + final current = latest[c.lotId]; + if (current == null || (c.checkedOn ?? 0) > (current.checkedOn ?? 0)) { + latest[c.lotId] = c; + } + } + return latest; + } + /// lotId → latest germination rate (0..1), from the most recent test that /// has a computable rate. Map _latestGerminationRates(List tests) { @@ -200,6 +246,12 @@ class InventoryCsvCodec { notes: cell(row, 'variety_notes'), vernacularNames: _parseVernacular(cell(row, 'vernacular_names')), links: _parseLinks(cell(row, 'external_links')), + needsReproduction: _parseBool(cell(row, 'needs_reproduction')), + sowMonths: _parseMonths(cell(row, 'sow_months')), + transplantMonths: _parseMonths(cell(row, 'transplant_months')), + floweringMonths: _parseMonths(cell(row, 'flowering_months')), + fruitingMonths: _parseMonths(cell(row, 'fruiting_months')), + seedHarvestMonths: _parseMonths(cell(row, 'seed_harvest_months')), ); builders.add(builder); if (groupId != null) byGroup[groupId] = builder; @@ -225,6 +277,10 @@ class InventoryCsvCodec { final presentation = cell(row, 'presentation'); final storage = cell(row, 'storage_location'); final offer = cell(row, 'offer_status'); + final originName = cell(row, 'origin_name'); + final originPlace = cell(row, 'origin_place'); + final abundance = cell(row, 'abundance'); + final preservationFormat = cell(row, 'preservation_format'); final hasLot = [ typeCell, @@ -236,6 +292,10 @@ class InventoryCsvCodec { presentation, storage, offer, + originName, + originPlace, + abundance, + preservationFormat, ].any((c) => c != null); if (!hasLot) return null; @@ -249,9 +309,35 @@ class InventoryCsvCodec { presentation: _enumOrNull(Presentation.values, presentation), storageLocation: storage, offerStatus: _enumOr(OfferStatus.values, offer, OfferStatus.private), + originName: originName, + originPlace: originPlace, + abundance: _enumOrNull(Abundance.values, abundance), + preservationFormat: _enumOrNull( + PreservationFormat.values, + preservationFormat, + ), ); } + /// Parses a boolean cell: `true`/`yes`/`1` (case-insensitive) → true, + /// anything else (including null) → false. + bool _parseBool(String? cell) { + final v = cell?.trim().toLowerCase(); + return v == 'true' || v == 'yes' || v == '1'; + } + + /// Parses a crop-calendar months cell (month numbers separated by spaces, + /// commas or semicolons, e.g. `3 4 9`) into a 12-bit mask, or null when empty. + int? _parseMonths(String? cell) { + if (cell == null) return null; + final months = []; + for (final part in cell.split(RegExp(r'[\s,;]+'))) { + final n = int.tryParse(part.trim()); + if (n != null) months.add(n); + } + return monthsToMask(months); + } + /// `name (lang); name2` → vernacular names; the trailing `(xx)` is the ISO /// language, mirroring the export join in [encode]. List _parseVernacular(String? cell) { @@ -393,6 +479,12 @@ class CsvImportVariety { this.vernacularNames = const [], this.links = const [], this.lots = const [], + this.needsReproduction = false, + this.sowMonths, + this.transplantMonths, + this.floweringMonths, + this.fruitingMonths, + this.seedHarvestMonths, }); final String label; @@ -403,6 +495,12 @@ class CsvImportVariety { final List vernacularNames; final List links; final List lots; + final bool needsReproduction; + final int? sowMonths; + final int? transplantMonths; + final int? floweringMonths; + final int? fruitingMonths; + final int? seedHarvestMonths; } /// One lot parsed from a CSV row. @@ -417,6 +515,10 @@ class CsvImportLot { this.presentation, this.storageLocation, this.offerStatus = OfferStatus.private, + this.originName, + this.originPlace, + this.abundance, + this.preservationFormat, }); final LotType type; @@ -428,6 +530,10 @@ class CsvImportLot { final Presentation? presentation; final String? storageLocation; final OfferStatus offerStatus; + final String? originName; + final String? originPlace; + final Abundance? abundance; + final PreservationFormat? preservationFormat; } /// One vernacular name parsed from CSV. @@ -456,6 +562,12 @@ class _CsvVarietyBuilder { this.notes, this.vernacularNames = const [], this.links = const [], + this.needsReproduction = false, + this.sowMonths, + this.transplantMonths, + this.floweringMonths, + this.fruitingMonths, + this.seedHarvestMonths, }); final String label; @@ -465,6 +577,12 @@ class _CsvVarietyBuilder { final String? notes; final List vernacularNames; final List links; + final bool needsReproduction; + final int? sowMonths; + final int? transplantMonths; + final int? floweringMonths; + final int? fruitingMonths; + final int? seedHarvestMonths; final List lots = []; CsvImportVariety build() => CsvImportVariety( @@ -475,6 +593,12 @@ class _CsvVarietyBuilder { notes: notes, vernacularNames: vernacularNames, links: links, + needsReproduction: needsReproduction, + sowMonths: sowMonths, + transplantMonths: transplantMonths, + floweringMonths: floweringMonths, + fruitingMonths: fruitingMonths, + seedHarvestMonths: seedHarvestMonths, lots: List.unmodifiable(lots), ); } diff --git a/apps/app_seeds/lib/data/export_import/inventory_json_codec.dart b/apps/app_seeds/lib/data/export_import/inventory_json_codec.dart index 4272f6f..9d8c3ee 100644 --- a/apps/app_seeds/lib/data/export_import/inventory_json_codec.dart +++ b/apps/app_seeds/lib/data/export_import/inventory_json_codec.dart @@ -42,6 +42,14 @@ class InventoryJsonCodec { 'cultivarName': v.cultivarName, 'category': v.category, 'notes': v.notes, + 'isDraft': v.isDraft, + 'isOrganic': v.isOrganic, + 'needsReproduction': v.needsReproduction, + 'sowMonths': v.sowMonths, + 'transplantMonths': v.transplantMonths, + 'floweringMonths': v.floweringMonths, + 'fruitingMonths': v.fruitingMonths, + 'seedHarvestMonths': v.seedHarvestMonths, }, ], 'lots': [ @@ -65,6 +73,10 @@ class InventoryJsonCodec { 'storageLocation': l.storageLocation, 'offerStatus': l.offerStatus.name, 'seedbankId': l.seedbankId, + 'originName': l.originName, + 'originPlace': l.originPlace, + 'abundance': l.abundance?.name, + 'preservationFormat': l.preservationFormat?.name, }, ], 'vernacularNames': [ @@ -116,6 +128,23 @@ class InventoryJsonCodec { 'notes': g.notes, }, ], + 'conditionChecks': [ + for (final c in snapshot.conditionChecks) + { + ..._syncMeta( + id: c.id, + createdAt: c.createdAt, + updatedAt: c.updatedAt, + lastAuthor: c.lastAuthor, + schemaRowVersion: c.schemaRowVersion, + ), + 'lotId': c.lotId, + 'checkedOn': c.checkedOn, + 'containerCount': c.containerCount, + 'desiccantState': c.desiccantState?.name, + 'notes': c.notes, + }, + ], 'movements': [ for (final m in snapshot.movements) { @@ -214,6 +243,14 @@ class InventoryJsonCodec { cultivarName: m['cultivarName'] as String?, category: m['category'] as String?, notes: m['notes'] as String?, + isDraft: m['isDraft'] == true, + isOrganic: m['isOrganic'] == true, + needsReproduction: m['needsReproduction'] == true, + sowMonths: m['sowMonths'] as int?, + transplantMonths: m['transplantMonths'] as int?, + floweringMonths: m['floweringMonths'] as int?, + fruitingMonths: m['fruitingMonths'] as int?, + seedHarvestMonths: m['seedHarvestMonths'] as int?, ); }); @@ -244,6 +281,13 @@ class InventoryJsonCodec { OfferStatus.private, ), seedbankId: m['seedbankId'] as String?, + originName: m['originName'] as String?, + originPlace: m['originPlace'] as String?, + abundance: _enumOrNull(Abundance.values, m['abundance']), + preservationFormat: _enumOrNull( + PreservationFormat.values, + m['preservationFormat'], + ), ); }), vernacularNames: _rows(root, 'vernacularNames', (m) { @@ -291,6 +335,24 @@ class InventoryJsonCodec { notes: m['notes'] as String?, ); }), + conditionChecks: _rows(root, 'conditionChecks', (m) { + return ConditionCheck( + id: _string(m, 'id'), + createdAt: _int(m, 'createdAt'), + updatedAt: _string(m, 'updatedAt'), + lastAuthor: _string(m, 'lastAuthor'), + isDeleted: false, + schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1), + lotId: _string(m, 'lotId'), + checkedOn: m['checkedOn'] as int?, + containerCount: m['containerCount'] as int?, + desiccantState: _enumOrNull( + DesiccantState.values, + m['desiccantState'], + ), + 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 diff --git a/apps/app_seeds/lib/data/export_import/inventory_snapshot.dart b/apps/app_seeds/lib/data/export_import/inventory_snapshot.dart index 27cee28..bdf3314 100644 --- a/apps/app_seeds/lib/data/export_import/inventory_snapshot.dart +++ b/apps/app_seeds/lib/data/export_import/inventory_snapshot.dart @@ -26,6 +26,7 @@ class InventorySnapshot { this.vernacularNames = const [], this.externalLinks = const [], this.germinationTests = const [], + this.conditionChecks = const [], this.movements = const [], this.parties = const [], this.attachments = const [], @@ -37,6 +38,7 @@ class InventorySnapshot { final List vernacularNames; final List externalLinks; final List germinationTests; + final List conditionChecks; final List movements; final List parties; final List attachments; diff --git a/apps/app_seeds/lib/data/species_catalog.dart b/apps/app_seeds/lib/data/species_catalog.dart index 1969c21..3a082de 100644 --- a/apps/app_seeds/lib/data/species_catalog.dart +++ b/apps/app_seeds/lib/data/species_catalog.dart @@ -19,6 +19,7 @@ List parseSpeciesCatalog(String jsonString) { scientificName: e['scientific_name'] as String, family: e['family'] as String?, commonNames: common, + viabilityYears: (e['viability_years'] as num?)?.toInt(), ); }).toList(); } diff --git a/apps/app_seeds/lib/data/species_repository.dart b/apps/app_seeds/lib/data/species_repository.dart index af485e8..9abd1e8 100644 --- a/apps/app_seeds/lib/data/species_repository.dart +++ b/apps/app_seeds/lib/data/species_repository.dart @@ -9,6 +9,7 @@ class SpeciesSeed { required this.scientificName, this.family, this.commonNames = const {}, + this.viabilityYears, }); final String scientificName; @@ -16,6 +17,9 @@ class SpeciesSeed { /// language code → list of common names. final Map> commonNames; + + /// Typical seed longevity in years (bundled reference data); null if unknown. + final int? viabilityYears; } /// A catalog match surfaced to the UI (with a best common name for the locale). @@ -47,7 +51,9 @@ class SpeciesRepository { final String nodeId; /// Idempotently inserts bundled species (keyed by scientific name). Safe to - /// call on every startup; existing entries are left untouched. + /// call on every startup. Existing rows keep their identity, but a bundled + /// row still missing [SpeciesSeed.viabilityYears] is backfilled — so the + /// reference data reaches installs seeded before the field was bundled. Future seedBundled(List seeds) async { final stamp = Hlc.zero(nodeId).pack(); await _db.transaction(() async { @@ -56,7 +62,19 @@ class SpeciesRepository { await (_db.select(_db.species) ..where((s) => s.scientificName.equals(seed.scientificName))) .getSingleOrNull(); - if (existing != null) continue; + if (existing != null) { + // Backfill bundled reference data added after the row was seeded. + if (existing.viabilityYears == null && seed.viabilityYears != null) { + await (_db.update(_db.species) + ..where((s) => s.id.equals(existing.id))) + .write( + SpeciesCompanion( + viabilityYears: Value(seed.viabilityYears), + ), + ); + } + continue; + } final speciesId = idGen.newId(); await _db @@ -70,6 +88,7 @@ class SpeciesRepository { scientificName: seed.scientificName, family: Value(seed.family), isBundled: const Value(true), + viabilityYears: Value(seed.viabilityYears), ), ); diff --git a/apps/app_seeds/lib/data/variety_repository.dart b/apps/app_seeds/lib/data/variety_repository.dart index 603e6a3..2377043 100644 --- a/apps/app_seeds/lib/data/variety_repository.dart +++ b/apps/app_seeds/lib/data/variety_repository.dart @@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart'; import '../db/database.dart'; import '../db/enums.dart'; +import '../domain/seed_viability.dart'; import 'export_import/import_reconciler.dart'; import 'export_import/inventory_csv_codec.dart'; import 'export_import/inventory_snapshot.dart'; @@ -18,6 +19,10 @@ class VarietyListItem extends Equatable { this.scientificName, this.photo, this.lotTypes = const {}, + this.isDraft = false, + this.isOrganic = false, + this.needsReproduction = false, + this.viability = SeedViability.unknown, }); final String id; @@ -34,6 +39,23 @@ class VarietyListItem extends Equatable { /// used to filter the list by form. Empty when it has no lots yet. final Set lotTypes; + /// True for a photo-first capture still awaiting a name (in the "to + /// catalogue" tray, hidden from the main inventory list). + final bool isDraft; + + /// Grower-declared organic ("eco") provenance — badged and filterable. + final bool isOrganic; + + /// Grower-flagged "regrow this season" stewardship intent — badged and + /// filterable, alongside the automatic viability warning. + final bool needsReproduction; + + /// The most-urgent viability status across this variety's seed lots + /// (expired ranks above expiringSoon), so the list can flag what to sow or + /// reproduce before it lapses. [SeedViability.unknown] when nothing is aging + /// or there is no reference figure. + final SeedViability viability; + @override List get props => [ id, @@ -42,6 +64,10 @@ class VarietyListItem extends Equatable { scientificName, photo, lotTypes, + isDraft, + isOrganic, + needsReproduction, + viability, ]; } @@ -73,8 +99,35 @@ class GerminationEntry extends Equatable { List get props => [id, testedOn, sampleSize, germinatedCount, notes]; } -/// One held batch of a variety, for the detail view. [germinationTests] are -/// ordered most-recent first, so `germinationTests.first` is the latest. +/// One storage-condition check on a lot: how many containers held it and the +/// state of the drying agent at a point in time. +class ConditionEntry extends Equatable { + const ConditionEntry({ + required this.id, + this.checkedOn, + this.containerCount, + this.desiccantState, + this.notes, + }); + + final String id; + final int? checkedOn; // ms since epoch + final int? containerCount; + final DesiccantState? desiccantState; + final String? notes; + + @override + List get props => [ + id, + checkedOn, + containerCount, + desiccantState, + notes, + ]; +} + +/// 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 { const VarietyLot({ required this.id, @@ -84,7 +137,12 @@ class VarietyLot extends Equatable { this.quantity, this.presentation, this.storageLocation, + this.originName, + this.originPlace, + this.abundance, + this.preservationFormat, this.germinationTests = const [], + this.conditionChecks = const [], }); final String id; @@ -98,8 +156,23 @@ class VarietyLot extends Equatable { /// How living material is packaged (pot, tray, bare-root…); null for seeds. final Presentation? presentation; final String? storageLocation; + + /// Provenance of this batch: who grew/gave it ([originName]) and where it + /// comes from ([originPlace], with region/province). Optional free text. + final String? originName; + final String? originPlace; + + /// Optional coarse "how much I have" fusing amount with shareability. + final Abundance? abundance; + + /// How the (seed) lot is physically conserved; distinct from storage location. + final PreservationFormat? preservationFormat; + final List germinationTests; + /// Storage-condition checks, most-recent first (`conditionChecks.first` = last). + final List conditionChecks; + /// The most recent germination rate (0..1), or null if there are no tests. double? get latestGerminationRate => germinationTests.isEmpty ? null : germinationTests.first.rate; @@ -113,7 +186,12 @@ class VarietyLot extends Equatable { quantity, presentation, storageLocation, + originName, + originPlace, + abundance, + preservationFormat, germinationTests, + conditionChecks, ]; } @@ -171,6 +249,14 @@ class VarietyDetail extends Equatable { this.notes, this.speciesId, this.scientificName, + this.viabilityYears, + this.isOrganic = false, + this.needsReproduction = false, + this.sowMonths, + this.transplantMonths, + this.floweringMonths, + this.fruitingMonths, + this.seedHarvestMonths, this.lots = const [], this.vernacularNames = const [], this.photos = const [], @@ -183,6 +269,35 @@ class VarietyDetail extends Equatable { final String? notes; final String? speciesId; final String? scientificName; + + /// Grower-declared organic ("eco") provenance. + final bool isOrganic; + + /// Grower-flagged "regrow this season" stewardship intent. + final bool needsReproduction; + + /// Advisory crop-calendar months, packed as 12-bit masks (a phase can span + /// several months); null where unrecorded. See [Varieties.sowMonths] et al. + /// and `domain/crop_calendar.dart` for packing. + final int? sowMonths; + final int? transplantMonths; + final int? floweringMonths; + final int? fruitingMonths; + final int? seedHarvestMonths; + + /// True when any crop-calendar phase is recorded — so the UI shows the + /// calendar section only when there is something in it. + bool get hasCropCalendar => + sowMonths != null || + transplantMonths != null || + floweringMonths != null || + fruitingMonths != null || + seedHarvestMonths != null; + + /// Typical seed longevity (years) of the linked species, from the bundled + /// catalog; null when no species is linked or the figure is unknown. Drives + /// the per-lot viability warning together with each lot's harvest year. + final int? viabilityYears; final List lots; final List vernacularNames; final List photos; @@ -196,6 +311,14 @@ class VarietyDetail extends Equatable { notes, speciesId, scientificName, + viabilityYears, + isOrganic, + needsReproduction, + sowMonths, + transplantMonths, + floweringMonths, + fruitingMonths, + seedHarvestMonths, lots, vernacularNames, photos, @@ -245,18 +368,51 @@ class VarietyRepository { Future> _loadInventory() async { final rows = await (_db.select(_db.varieties) - ..where((v) => v.isDeleted.equals(false)) + // Drafts live in the "to catalogue" tray, not the main list. + ..where((v) => v.isDeleted.equals(false) & v.isDraft.equals(false)) ..orderBy([ (v) => OrderingTerm(expression: v.category), (v) => OrderingTerm(expression: v.label), ])) .get(); + return _toListItems(rows); + } + + /// Reactively watches the photo-first drafts awaiting a name, newest first — + /// the "to catalogue" tray. Re-emits when a draft is added, named or its + /// photo changes. + Stream> watchDrafts() { + final triggers = StreamGroup.merge([ + (_db.select(_db.varieties)..where((v) => v.isDraft.equals(true))) + .watch() + .map((_) {}), + _db.select(_db.attachments).watch().map((_) {}), + ]); + return triggers.asyncMap((_) => _loadDrafts()); + } + + Future> _loadDrafts() async { + final rows = + await (_db.select(_db.varieties) + ..where( + (v) => v.isDeleted.equals(false) & v.isDraft.equals(true), + ) + // Packed HLC sorts monotonically, so newest capture is first even + // when two are stamped within the same wall-clock millisecond. + ..orderBy([(v) => OrderingTerm.desc(v.updatedAt)])) + .get(); + return _toListItems(rows); + } + + Future> _toListItems(List rows) async { final varietyIds = rows.map((v) => v.id).toList(); + final speciesIds = rows.map((v) => v.speciesId).whereType().toSet(); final photos = await _firstPhotosFor(varietyIds); - final sciNames = await _scientificNamesFor( - rows.map((v) => v.speciesId).whereType().toSet(), - ); + final sciNames = await _scientificNamesFor(speciesIds); + final speciesViability = await _speciesViabilityFor(speciesIds); final lotTypes = await _lotTypesFor(varietyIds); + final seedYears = await _seedHarvestYearsFor(varietyIds); + final currentYear = DateTime.fromMillisecondsSinceEpoch(_now()).year; return rows .map( (v) => VarietyListItem( @@ -266,11 +422,82 @@ class VarietyRepository { scientificName: v.speciesId == null ? null : sciNames[v.speciesId], photo: photos[v.id], lotTypes: lotTypes[v.id] ?? const {}, + isDraft: v.isDraft, + isOrganic: v.isOrganic, + needsReproduction: v.needsReproduction, + viability: _worstViability( + harvestYears: seedYears[v.id] ?? const [], + viabilityYears: v.speciesId == null + ? null + : speciesViability[v.speciesId], + currentYear: currentYear, + ), ), ) .toList(); } + /// Maps each of [speciesIds] to its bundled viability figure (years); species + /// without a figure are simply absent from the map. + Future> _speciesViabilityFor(Set speciesIds) async { + if (speciesIds.isEmpty) return const {}; + final rows = await (_db.select( + _db.species, + )..where((s) => s.id.isIn(speciesIds))).get(); + return { + for (final s in rows) + if (s.viabilityYears != null) s.id: s.viabilityYears!, + }; + } + + /// Harvest years of each variety's non-deleted seed lots (living lots don't + /// age like dry seed). Lots without a year are omitted — they can't be judged. + Future>> _seedHarvestYearsFor( + List varietyIds, + ) async { + if (varietyIds.isEmpty) return const {}; + final rows = + await (_db.select(_db.lots)..where( + (l) => + l.varietyId.isIn(varietyIds) & + l.isDeleted.equals(false) & + l.type.equalsValue(LotType.seed), + )) + .get(); + final byVariety = >{}; + for (final row in rows) { + final year = row.harvestYear; + if (year != null) (byVariety[row.varietyId] ??= []).add(year); + } + return byVariety; + } + + /// The most-urgent viability across a variety's seed lots (expired outranks + /// expiring-soon), so the list surfaces the worst case at a glance. + SeedViability _worstViability({ + required List harvestYears, + required int? viabilityYears, + required int currentYear, + }) { + var worst = SeedViability.unknown; + for (final year in harvestYears) { + final status = seedViability( + harvestYear: year, + viabilityYears: viabilityYears, + currentYear: currentYear, + ); + if (_viabilityRank(status) > _viabilityRank(worst)) worst = status; + } + return worst; + } + + int _viabilityRank(SeedViability status) => switch (status) { + SeedViability.unknown => 0, + SeedViability.fresh => 1, + SeedViability.expiringSoon => 2, + SeedViability.expired => 3, + }; + /// Maps each variety to the distinct lot forms it currently holds (one /// query). Varieties without lots are simply absent from the map. Future>> _lotTypesFor( @@ -394,6 +621,59 @@ class VarietyRepository { return varietyId; } + /// Captures a photo-first draft: a Variety with no name yet ([label] empty), + /// flagged [isDraft], plus its photo. It lands in the "to catalogue" tray + /// ([watchDrafts]) until the user names it with [nameDraft]. Returns the id. + Future addDraftVariety(Uint8List photoBytes) async { + final varietyId = idGen.newId(); + await _db.transaction(() async { + final (created, updated) = _stamp(); + await _db + .into(_db.varieties) + .insert( + VarietiesCompanion.insert( + id: varietyId, + label: '', + createdAt: created, + updatedAt: updated, + lastAuthor: nodeId, + isDraft: const Value(true), + ), + ); + final (createdA, updatedA) = _stamp(); + await _db + .into(_db.attachments) + .insert( + AttachmentsCompanion.insert( + id: idGen.newId(), + createdAt: createdA, + updatedAt: updatedA, + lastAuthor: nodeId, + parentType: ParentType.variety, + parentId: varietyId, + kind: AttachmentKind.photo, + bytes: Value(photoBytes), + mimeType: const Value('image/jpeg'), + ), + ); + }); + return varietyId; + } + + /// Names a draft and promotes it out of the "to catalogue" tray: sets its + /// [label] and clears [isDraft] in one LWW write. + Future nameDraft(String id, String label) async { + final (_, updated) = _stamp(); + await (_db.update(_db.varieties)..where((v) => v.id.equals(id))).write( + VarietiesCompanion( + label: Value(label), + isDraft: const Value(false), + updatedAt: Value(updated), + lastAuthor: Value(nodeId), + ), + ); + } + /// Reactively watches one variety with its lots, vernacular names and first /// photo. Emits `null` if the variety does not exist or is soft-deleted. /// @@ -419,6 +699,8 @@ class VarietyRepository { )..where((e) => e.parentId.equals(id))).watch().map((_) {}), // Coarse: any germination-test change re-emits (catalog of tests is tiny). _db.select(_db.germinationTests).watch().map((_) {}), + // Same for condition checks (jars + drying-agent state). + _db.select(_db.conditionChecks).watch().map((_) {}), ]); return triggers.asyncMap((_) => _loadVariety(id)); } @@ -431,11 +713,13 @@ class VarietyRepository { if (v == null) return null; String? scientificName; + int? viabilityYears; if (v.speciesId != null) { final species = await (_db.select( _db.species, )..where((s) => s.id.equals(v.speciesId!))).getSingleOrNull(); scientificName = species?.scientificName; + viabilityYears = species?.viabilityYears; } final lots = @@ -446,6 +730,7 @@ class VarietyRepository { final lotIds = lots.map((l) => l.id).toList(); final testsByLot = >{}; + final checksByLot = >{}; if (lotIds.isNotEmpty) { final tests = await (_db.select(_db.germinationTests) @@ -465,6 +750,24 @@ class VarietyRepository { ), ); } + final checks = + await (_db.select(_db.conditionChecks) + ..where((c) => c.lotId.isIn(lotIds) & c.isDeleted.equals(false)) + ..orderBy([(c) => OrderingTerm.desc(c.checkedOn)])) + .get(); + for (final c in checks) { + checksByLot + .putIfAbsent(c.lotId, () => []) + .add( + ConditionEntry( + id: c.id, + checkedOn: c.checkedOn, + containerCount: c.containerCount, + desiccantState: c.desiccantState, + notes: c.notes, + ), + ); + } } final names = await (_db.select( @@ -503,7 +806,23 @@ class VarietyRepository { notes: v.notes, speciesId: v.speciesId, scientificName: scientificName, - lots: lots.map((l) => _toLot(l, testsByLot[l.id] ?? const [])).toList(), + viabilityYears: viabilityYears, + isOrganic: v.isOrganic, + needsReproduction: v.needsReproduction, + sowMonths: v.sowMonths, + transplantMonths: v.transplantMonths, + floweringMonths: v.floweringMonths, + fruitingMonths: v.fruitingMonths, + seedHarvestMonths: v.seedHarvestMonths, + lots: lots + .map( + (l) => _toLot( + l, + testsByLot[l.id] ?? const [], + checksByLot[l.id] ?? const [], + ), + ) + .toList(), vernacularNames: names .map( (n) => VernacularName( @@ -552,12 +871,21 @@ class VarietyRepository { } /// Updates a variety's scalar fields (LWW). Passing null clears [category] - /// and [notes]; a null [label] leaves the label unchanged. + /// and [notes]; a null [label] leaves the label unchanged. Boolean flags and + /// the crop-calendar months are only touched when a wrapping [Value] is + /// passed, so callers can update one field without disturbing the rest. Future updateVariety({ required String id, String? label, String? category, String? notes, + bool? isOrganic, + bool? needsReproduction, + Value sowMonths = const Value.absent(), + Value transplantMonths = const Value.absent(), + Value floweringMonths = const Value.absent(), + Value fruitingMonths = const Value.absent(), + Value seedHarvestMonths = const Value.absent(), }) async { final (_, updated) = _stamp(); await (_db.update(_db.varieties)..where((v) => v.id.equals(id))).write( @@ -565,6 +893,15 @@ class VarietyRepository { label: label == null ? const Value.absent() : Value(label), category: Value(category), notes: Value(notes), + isOrganic: isOrganic == null ? const Value.absent() : Value(isOrganic), + needsReproduction: needsReproduction == null + ? const Value.absent() + : Value(needsReproduction), + sowMonths: sowMonths, + transplantMonths: transplantMonths, + floweringMonths: floweringMonths, + fruitingMonths: fruitingMonths, + seedHarvestMonths: seedHarvestMonths, updatedAt: Value(updated), lastAuthor: Value(nodeId), ), @@ -580,6 +917,10 @@ class VarietyRepository { Quantity? quantity, Presentation? presentation, String? storageLocation, + String? originName, + String? originPlace, + Abundance? abundance, + PreservationFormat? preservationFormat, }) async { final (created, updated) = _stamp(); final id = idGen.newId(); @@ -600,13 +941,17 @@ class VarietyRepository { quantityLabel: Value(quantity?.label), presentation: Value(presentation), storageLocation: Value(storageLocation), + originName: Value(originName), + originPlace: Value(originPlace), + abundance: Value(abundance), + preservationFormat: Value(preservationFormat), ), ); return id; } - /// Updates an existing lot's fields (LWW). The [quantity], [harvestYear] and - /// [harvestMonth] are set as given (null clears them). + /// Updates an existing lot's fields (LWW). Every field is set as given (null + /// clears it), matching the "save the whole lot form" call from the UI. Future updateLot({ required String lotId, required LotType type, @@ -615,6 +960,10 @@ class VarietyRepository { Quantity? quantity, Presentation? presentation, String? storageLocation, + String? originName, + String? originPlace, + Abundance? abundance, + PreservationFormat? preservationFormat, }) async { final (_, updated) = _stamp(); await (_db.update(_db.lots)..where((l) => l.id.equals(lotId))).write( @@ -627,6 +976,10 @@ class VarietyRepository { quantityLabel: Value(quantity?.label), presentation: Value(presentation), storageLocation: Value(storageLocation), + originName: Value(originName), + originPlace: Value(originPlace), + abundance: Value(abundance), + preservationFormat: Value(preservationFormat), updatedAt: Value(updated), lastAuthor: Value(nodeId), ), @@ -829,6 +1182,35 @@ class VarietyRepository { return id; } + /// Records a storage-condition check for a lot (container count + drying-agent + /// state). Returns the new check id. + Future addConditionCheck({ + required String lotId, + int? checkedOn, + int? containerCount, + DesiccantState? desiccantState, + String? notes, + }) async { + final (created, updated) = _stamp(); + final id = idGen.newId(); + await _db + .into(_db.conditionChecks) + .insert( + ConditionChecksCompanion.insert( + id: id, + lotId: lotId, + createdAt: created, + updatedAt: updated, + lastAuthor: nodeId, + checkedOn: Value(checkedOn), + containerCount: Value(containerCount), + desiccantState: Value(desiccantState), + notes: Value(notes), + ), + ); + return id; + } + /// Snapshots the live inventory (tombstones excluded) for the interchange /// export — data-model §7. Includes photo bytes; the JSON codec embeds them /// as base64 and the CSV codec ignores them. @@ -854,6 +1236,9 @@ class VarietyRepository { germinationTests: await (_db.select( _db.germinationTests, )..where((g) => g.isDeleted.equals(false))).get(), + conditionChecks: await (_db.select( + _db.conditionChecks, + )..where((c) => c.isDeleted.equals(false))).get(), movements: await _db.select(_db.movements).get(), parties: await (_db.select( _db.parties, @@ -928,6 +1313,14 @@ class VarietyRepository { updatedAtOf: (r) => r.updatedAt, reconciler: reconciler, ); + summary += await _importMutableRows( + table: _db.conditionChecks, + idColumn: _db.conditionChecks.id, + rows: snapshot.conditionChecks, + idOf: (r) => r.id, + updatedAtOf: (r) => r.updatedAt, + reconciler: reconciler, + ); summary += await _importMutableRows( table: _db.parties, idColumn: _db.parties.id, @@ -953,6 +1346,7 @@ class VarietyRepository { for (final n in snapshot.vernacularNames) n.updatedAt, 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 p in snapshot.parties) p.updatedAt, for (final a in snapshot.attachments) a.updatedAt, ]); @@ -990,6 +1384,12 @@ class VarietyRepository { cultivarName: Value(v.cultivarName), notes: Value(v.notes), speciesId: Value(speciesId), + needsReproduction: Value(v.needsReproduction), + sowMonths: Value(v.sowMonths), + transplantMonths: Value(v.transplantMonths), + floweringMonths: Value(v.floweringMonths), + fruitingMonths: Value(v.fruitingMonths), + seedHarvestMonths: Value(v.seedHarvestMonths), ), ); inserted++; @@ -1014,6 +1414,10 @@ class VarietyRepository { presentation: Value(lot.presentation), storageLocation: Value(lot.storageLocation), offerStatus: Value(lot.offerStatus), + originName: Value(lot.originName), + originPlace: Value(lot.originPlace), + abundance: Value(lot.abundance), + preservationFormat: Value(lot.preservationFormat), ), ); } @@ -1153,7 +1557,11 @@ class VarietyRepository { return ImportSummary(inserted: inserted, skipped: skipped); } - VarietyLot _toLot(Lot l, List germinationTests) { + VarietyLot _toLot( + Lot l, + List germinationTests, + List conditionChecks, + ) { final hasQuantity = l.quantityKind != null || l.quantityPrecise != null || @@ -1165,7 +1573,12 @@ class VarietyRepository { harvestMonth: l.harvestMonth, presentation: l.presentation, storageLocation: l.storageLocation, + originName: l.originName, + originPlace: l.originPlace, + abundance: l.abundance, + preservationFormat: l.preservationFormat, germinationTests: germinationTests, + conditionChecks: conditionChecks, quantity: hasQuantity ? Quantity( kind: _parseKind(l.quantityKind), diff --git a/apps/app_seeds/lib/db/database.dart b/apps/app_seeds/lib/db/database.dart index bc61f17..41c3cf7 100644 --- a/apps/app_seeds/lib/db/database.dart +++ b/apps/app_seeds/lib/db/database.dart @@ -17,6 +17,7 @@ part 'database.g.dart'; SpeciesCommonNames, Lots, GerminationTests, + ConditionChecks, Movements, Parties, Attachments, @@ -28,7 +29,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 = 5; + static const int currentSchemaVersion = 8; @override int get schemaVersion => currentSchemaVersion; @@ -53,6 +54,110 @@ class AppDatabase extends _$AppDatabase { if (from < 5) { await m.addColumn(attachments, attachments.sortOrder); } + // v6: photo-first draft varieties awaiting a name ("to catalogue" tray). + if (from < 6) { + await m.addColumn(varieties, varieties.isDraft); + } + // v7: organic ("eco") self-declaration on varieties; per-species seed + // viability (years) reference data for expiry warnings. Guarded by a + // column-existence check so a dev database left half-migrated (column + // added but user_version not bumped) re-runs cleanly instead of failing + // on "duplicate column". + if (from < 7) { + if (!await _hasColumn('varieties', 'is_organic')) { + await m.addColumn(varieties, varieties.isOrganic); + } + if (!await _hasColumn('species', 'viability_years')) { + await m.addColumn(species, species.viabilityYears); + } + } + // v8: absorbs fields from a real seed-bank inventory. Varieties gain a + // "needs reproducing" intent and an advisory crop calendar; Lots gain + // provenance (origin name/place), a qualitative abundance level and a + // preservation format; a new ConditionChecks table logs container count + + // drying-agent state. Every step is guarded so a half-migrated dev + // database re-runs cleanly (see the v7 note above). + if (from < 8) { + Future addIfMissing( + String table, + String column, + GeneratedColumn definition, + TableInfo tableInfo, + ) async { + if (!await _hasColumn(table, column)) { + await m.addColumn(tableInfo, definition); + } + } + + await addIfMissing( + 'varieties', + 'needs_reproduction', + varieties.needsReproduction, + varieties, + ); + await addIfMissing( + 'varieties', + 'sow_months', + varieties.sowMonths, + varieties, + ); + await addIfMissing( + 'varieties', + 'transplant_months', + varieties.transplantMonths, + varieties, + ); + await addIfMissing( + 'varieties', + 'flowering_months', + varieties.floweringMonths, + varieties, + ); + await addIfMissing( + 'varieties', + 'fruiting_months', + varieties.fruitingMonths, + varieties, + ); + await addIfMissing( + 'varieties', + 'seed_harvest_months', + varieties.seedHarvestMonths, + varieties, + ); + await addIfMissing('lots', 'origin_name', lots.originName, lots); + await addIfMissing('lots', 'origin_place', lots.originPlace, lots); + await addIfMissing('lots', 'abundance', lots.abundance, lots); + await addIfMissing( + 'lots', + 'preservation_format', + lots.preservationFormat, + lots, + ); + if (!await _hasTable('condition_checks')) { + await m.createTable(conditionChecks); + } + } }, ); + + /// Whether a table named [table] already exists. Keeps the additive v8 + /// table creation idempotent against partially-migrated databases. + Future _hasTable(String table) async { + final rows = await customSelect( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", + variables: [Variable.withString(table)], + ).get(); + return rows.isNotEmpty; + } + + /// Whether [table] already has a column named [column]. Used to keep additive + /// migrations idempotent against partially-migrated databases. + Future _hasColumn(String table, String column) async { + final rows = await customSelect( + 'SELECT 1 FROM pragma_table_info(?) WHERE name = ?', + variables: [Variable.withString(table), Variable.withString(column)], + ).get(); + return rows.isNotEmpty; + } } diff --git a/apps/app_seeds/lib/db/database.g.dart b/apps/app_seeds/lib/db/database.g.dart index fb2d458..3b14863 100644 --- a/apps/app_seeds/lib/db/database.g.dart +++ b/apps/app_seeds/lib/db/database.g.dart @@ -129,6 +129,106 @@ class $VarietiesTable extends Varieties type: DriftSqlType.string, requiredDuringInsert: false, ); + static const VerificationMeta _isDraftMeta = const VerificationMeta( + 'isDraft', + ); + @override + late final GeneratedColumn isDraft = GeneratedColumn( + 'is_draft', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_draft" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _isOrganicMeta = const VerificationMeta( + 'isOrganic', + ); + @override + late final GeneratedColumn isOrganic = GeneratedColumn( + 'is_organic', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_organic" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _needsReproductionMeta = const VerificationMeta( + 'needsReproduction', + ); + @override + late final GeneratedColumn needsReproduction = GeneratedColumn( + 'needs_reproduction', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("needs_reproduction" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _sowMonthsMeta = const VerificationMeta( + 'sowMonths', + ); + @override + late final GeneratedColumn sowMonths = GeneratedColumn( + 'sow_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _transplantMonthsMeta = const VerificationMeta( + 'transplantMonths', + ); + @override + late final GeneratedColumn transplantMonths = GeneratedColumn( + 'transplant_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _floweringMonthsMeta = const VerificationMeta( + 'floweringMonths', + ); + @override + late final GeneratedColumn floweringMonths = GeneratedColumn( + 'flowering_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _fruitingMonthsMeta = const VerificationMeta( + 'fruitingMonths', + ); + @override + late final GeneratedColumn fruitingMonths = GeneratedColumn( + 'fruiting_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _seedHarvestMonthsMeta = const VerificationMeta( + 'seedHarvestMonths', + ); + @override + late final GeneratedColumn seedHarvestMonths = GeneratedColumn( + 'seed_harvest_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); @override List get $columns => [ id, @@ -142,6 +242,14 @@ class $VarietiesTable extends Varieties cultivarName, category, notes, + isDraft, + isOrganic, + needsReproduction, + sowMonths, + transplantMonths, + floweringMonths, + fruitingMonths, + seedHarvestMonths, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -234,6 +342,69 @@ class $VarietiesTable extends Varieties notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), ); } + if (data.containsKey('is_draft')) { + context.handle( + _isDraftMeta, + isDraft.isAcceptableOrUnknown(data['is_draft']!, _isDraftMeta), + ); + } + if (data.containsKey('is_organic')) { + context.handle( + _isOrganicMeta, + isOrganic.isAcceptableOrUnknown(data['is_organic']!, _isOrganicMeta), + ); + } + if (data.containsKey('needs_reproduction')) { + context.handle( + _needsReproductionMeta, + needsReproduction.isAcceptableOrUnknown( + data['needs_reproduction']!, + _needsReproductionMeta, + ), + ); + } + if (data.containsKey('sow_months')) { + context.handle( + _sowMonthsMeta, + sowMonths.isAcceptableOrUnknown(data['sow_months']!, _sowMonthsMeta), + ); + } + if (data.containsKey('transplant_months')) { + context.handle( + _transplantMonthsMeta, + transplantMonths.isAcceptableOrUnknown( + data['transplant_months']!, + _transplantMonthsMeta, + ), + ); + } + if (data.containsKey('flowering_months')) { + context.handle( + _floweringMonthsMeta, + floweringMonths.isAcceptableOrUnknown( + data['flowering_months']!, + _floweringMonthsMeta, + ), + ); + } + if (data.containsKey('fruiting_months')) { + context.handle( + _fruitingMonthsMeta, + fruitingMonths.isAcceptableOrUnknown( + data['fruiting_months']!, + _fruitingMonthsMeta, + ), + ); + } + if (data.containsKey('seed_harvest_months')) { + context.handle( + _seedHarvestMonthsMeta, + seedHarvestMonths.isAcceptableOrUnknown( + data['seed_harvest_months']!, + _seedHarvestMonthsMeta, + ), + ); + } return context; } @@ -287,6 +458,38 @@ class $VarietiesTable extends Varieties DriftSqlType.string, data['${effectivePrefix}notes'], ), + isDraft: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_draft'], + )!, + isOrganic: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_organic'], + )!, + needsReproduction: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}needs_reproduction'], + )!, + sowMonths: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sow_months'], + ), + transplantMonths: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}transplant_months'], + ), + floweringMonths: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}flowering_months'], + ), + fruitingMonths: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}fruiting_months'], + ), + seedHarvestMonths: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}seed_harvest_months'], + ), ); } @@ -308,6 +511,34 @@ class Variety extends DataClass implements Insertable { final String? cultivarName; final String? category; final String? notes; + + /// A draft captured photo-first ("capture now, catalogue later"): it holds a + /// photo but not yet a real name, and lives in the "to catalogue" tray until + /// the user labels it. A plain LWW scalar, so it merges like any other field. + final bool isDraft; + + /// Grower-declared organic ("eco") provenance. A self-declaration, not a + /// third-party certification (that would be a separate flag). Surfaced as a + /// badge and an inventory filter. A plain LWW scalar. + final bool isOrganic; + + /// A stewardship intent set by the grower: "regrow this variety this season" + /// before its stock or vitality runs out. Complements the automatic viability + /// warning (which is age-derived) with an explicit human decision. Surfaced + /// as a badge and an inventory filter. A plain LWW scalar. + final bool needsReproduction; + + /// Advisory crop-calendar months, typical for this variety — when to sow, + /// transplant, expect flowers/fruit, and harvest seed. Each phase usually + /// spans several months (e.g. sow in spring *and* autumn), so each is a set + /// of months packed as a 12-bit mask (see `domain/crop_calendar.dart`), an + /// optional LWW scalar; null means "not recorded". Guidance, not a per-year + /// actuals log (that path stays available via Movements). + final int? sowMonths; + final int? transplantMonths; + final int? floweringMonths; + final int? fruitingMonths; + final int? seedHarvestMonths; const Variety({ required this.id, required this.createdAt, @@ -320,6 +551,14 @@ class Variety extends DataClass implements Insertable { this.cultivarName, this.category, this.notes, + required this.isDraft, + required this.isOrganic, + required this.needsReproduction, + this.sowMonths, + this.transplantMonths, + this.floweringMonths, + this.fruitingMonths, + this.seedHarvestMonths, }); @override Map toColumns(bool nullToAbsent) { @@ -343,6 +582,24 @@ class Variety extends DataClass implements Insertable { if (!nullToAbsent || notes != null) { map['notes'] = Variable(notes); } + map['is_draft'] = Variable(isDraft); + map['is_organic'] = Variable(isOrganic); + map['needs_reproduction'] = Variable(needsReproduction); + if (!nullToAbsent || sowMonths != null) { + map['sow_months'] = Variable(sowMonths); + } + if (!nullToAbsent || transplantMonths != null) { + map['transplant_months'] = Variable(transplantMonths); + } + if (!nullToAbsent || floweringMonths != null) { + map['flowering_months'] = Variable(floweringMonths); + } + if (!nullToAbsent || fruitingMonths != null) { + map['fruiting_months'] = Variable(fruitingMonths); + } + if (!nullToAbsent || seedHarvestMonths != null) { + map['seed_harvest_months'] = Variable(seedHarvestMonths); + } return map; } @@ -367,6 +624,24 @@ class Variety extends DataClass implements Insertable { notes: notes == null && nullToAbsent ? const Value.absent() : Value(notes), + isDraft: Value(isDraft), + isOrganic: Value(isOrganic), + needsReproduction: Value(needsReproduction), + sowMonths: sowMonths == null && nullToAbsent + ? const Value.absent() + : Value(sowMonths), + transplantMonths: transplantMonths == null && nullToAbsent + ? const Value.absent() + : Value(transplantMonths), + floweringMonths: floweringMonths == null && nullToAbsent + ? const Value.absent() + : Value(floweringMonths), + fruitingMonths: fruitingMonths == null && nullToAbsent + ? const Value.absent() + : Value(fruitingMonths), + seedHarvestMonths: seedHarvestMonths == null && nullToAbsent + ? const Value.absent() + : Value(seedHarvestMonths), ); } @@ -387,6 +662,14 @@ class Variety extends DataClass implements Insertable { cultivarName: serializer.fromJson(json['cultivarName']), category: serializer.fromJson(json['category']), notes: serializer.fromJson(json['notes']), + isDraft: serializer.fromJson(json['isDraft']), + isOrganic: serializer.fromJson(json['isOrganic']), + needsReproduction: serializer.fromJson(json['needsReproduction']), + sowMonths: serializer.fromJson(json['sowMonths']), + transplantMonths: serializer.fromJson(json['transplantMonths']), + floweringMonths: serializer.fromJson(json['floweringMonths']), + fruitingMonths: serializer.fromJson(json['fruitingMonths']), + seedHarvestMonths: serializer.fromJson(json['seedHarvestMonths']), ); } @override @@ -404,6 +687,14 @@ class Variety extends DataClass implements Insertable { 'cultivarName': serializer.toJson(cultivarName), 'category': serializer.toJson(category), 'notes': serializer.toJson(notes), + 'isDraft': serializer.toJson(isDraft), + 'isOrganic': serializer.toJson(isOrganic), + 'needsReproduction': serializer.toJson(needsReproduction), + 'sowMonths': serializer.toJson(sowMonths), + 'transplantMonths': serializer.toJson(transplantMonths), + 'floweringMonths': serializer.toJson(floweringMonths), + 'fruitingMonths': serializer.toJson(fruitingMonths), + 'seedHarvestMonths': serializer.toJson(seedHarvestMonths), }; } @@ -419,6 +710,14 @@ class Variety extends DataClass implements Insertable { Value cultivarName = const Value.absent(), Value category = const Value.absent(), Value notes = const Value.absent(), + bool? isDraft, + bool? isOrganic, + bool? needsReproduction, + Value sowMonths = const Value.absent(), + Value transplantMonths = const Value.absent(), + Value floweringMonths = const Value.absent(), + Value fruitingMonths = const Value.absent(), + Value seedHarvestMonths = const Value.absent(), }) => Variety( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, @@ -431,6 +730,22 @@ class Variety extends DataClass implements Insertable { cultivarName: cultivarName.present ? cultivarName.value : this.cultivarName, category: category.present ? category.value : this.category, notes: notes.present ? notes.value : this.notes, + isDraft: isDraft ?? this.isDraft, + isOrganic: isOrganic ?? this.isOrganic, + needsReproduction: needsReproduction ?? this.needsReproduction, + sowMonths: sowMonths.present ? sowMonths.value : this.sowMonths, + transplantMonths: transplantMonths.present + ? transplantMonths.value + : this.transplantMonths, + floweringMonths: floweringMonths.present + ? floweringMonths.value + : this.floweringMonths, + fruitingMonths: fruitingMonths.present + ? fruitingMonths.value + : this.fruitingMonths, + seedHarvestMonths: seedHarvestMonths.present + ? seedHarvestMonths.value + : this.seedHarvestMonths, ); Variety copyWithCompanion(VarietiesCompanion data) { return Variety( @@ -451,6 +766,24 @@ class Variety extends DataClass implements Insertable { : this.cultivarName, category: data.category.present ? data.category.value : this.category, notes: data.notes.present ? data.notes.value : this.notes, + isDraft: data.isDraft.present ? data.isDraft.value : this.isDraft, + isOrganic: data.isOrganic.present ? data.isOrganic.value : this.isOrganic, + needsReproduction: data.needsReproduction.present + ? data.needsReproduction.value + : this.needsReproduction, + sowMonths: data.sowMonths.present ? data.sowMonths.value : this.sowMonths, + transplantMonths: data.transplantMonths.present + ? data.transplantMonths.value + : this.transplantMonths, + floweringMonths: data.floweringMonths.present + ? data.floweringMonths.value + : this.floweringMonths, + fruitingMonths: data.fruitingMonths.present + ? data.fruitingMonths.value + : this.fruitingMonths, + seedHarvestMonths: data.seedHarvestMonths.present + ? data.seedHarvestMonths.value + : this.seedHarvestMonths, ); } @@ -467,7 +800,15 @@ class Variety extends DataClass implements Insertable { ..write('speciesId: $speciesId, ') ..write('cultivarName: $cultivarName, ') ..write('category: $category, ') - ..write('notes: $notes') + ..write('notes: $notes, ') + ..write('isDraft: $isDraft, ') + ..write('isOrganic: $isOrganic, ') + ..write('needsReproduction: $needsReproduction, ') + ..write('sowMonths: $sowMonths, ') + ..write('transplantMonths: $transplantMonths, ') + ..write('floweringMonths: $floweringMonths, ') + ..write('fruitingMonths: $fruitingMonths, ') + ..write('seedHarvestMonths: $seedHarvestMonths') ..write(')')) .toString(); } @@ -485,6 +826,14 @@ class Variety extends DataClass implements Insertable { cultivarName, category, notes, + isDraft, + isOrganic, + needsReproduction, + sowMonths, + transplantMonths, + floweringMonths, + fruitingMonths, + seedHarvestMonths, ); @override bool operator ==(Object other) => @@ -500,7 +849,15 @@ class Variety extends DataClass implements Insertable { other.speciesId == this.speciesId && other.cultivarName == this.cultivarName && other.category == this.category && - other.notes == this.notes); + other.notes == this.notes && + other.isDraft == this.isDraft && + other.isOrganic == this.isOrganic && + other.needsReproduction == this.needsReproduction && + other.sowMonths == this.sowMonths && + other.transplantMonths == this.transplantMonths && + other.floweringMonths == this.floweringMonths && + other.fruitingMonths == this.fruitingMonths && + other.seedHarvestMonths == this.seedHarvestMonths); } class VarietiesCompanion extends UpdateCompanion { @@ -515,6 +872,14 @@ class VarietiesCompanion extends UpdateCompanion { final Value cultivarName; final Value category; final Value notes; + final Value isDraft; + final Value isOrganic; + final Value needsReproduction; + final Value sowMonths; + final Value transplantMonths; + final Value floweringMonths; + final Value fruitingMonths; + final Value seedHarvestMonths; final Value rowid; const VarietiesCompanion({ this.id = const Value.absent(), @@ -528,6 +893,14 @@ class VarietiesCompanion extends UpdateCompanion { this.cultivarName = const Value.absent(), this.category = const Value.absent(), this.notes = const Value.absent(), + this.isDraft = const Value.absent(), + this.isOrganic = const Value.absent(), + this.needsReproduction = const Value.absent(), + this.sowMonths = const Value.absent(), + this.transplantMonths = const Value.absent(), + this.floweringMonths = const Value.absent(), + this.fruitingMonths = const Value.absent(), + this.seedHarvestMonths = const Value.absent(), this.rowid = const Value.absent(), }); VarietiesCompanion.insert({ @@ -542,6 +915,14 @@ class VarietiesCompanion extends UpdateCompanion { this.cultivarName = const Value.absent(), this.category = const Value.absent(), this.notes = const Value.absent(), + this.isDraft = const Value.absent(), + this.isOrganic = const Value.absent(), + this.needsReproduction = const Value.absent(), + this.sowMonths = const Value.absent(), + this.transplantMonths = const Value.absent(), + this.floweringMonths = const Value.absent(), + this.fruitingMonths = const Value.absent(), + this.seedHarvestMonths = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), createdAt = Value(createdAt), @@ -560,6 +941,14 @@ class VarietiesCompanion extends UpdateCompanion { Expression? cultivarName, Expression? category, Expression? notes, + Expression? isDraft, + Expression? isOrganic, + Expression? needsReproduction, + Expression? sowMonths, + Expression? transplantMonths, + Expression? floweringMonths, + Expression? fruitingMonths, + Expression? seedHarvestMonths, Expression? rowid, }) { return RawValuesInsertable({ @@ -574,6 +963,14 @@ class VarietiesCompanion extends UpdateCompanion { if (cultivarName != null) 'cultivar_name': cultivarName, if (category != null) 'category': category, if (notes != null) 'notes': notes, + if (isDraft != null) 'is_draft': isDraft, + if (isOrganic != null) 'is_organic': isOrganic, + if (needsReproduction != null) 'needs_reproduction': needsReproduction, + if (sowMonths != null) 'sow_months': sowMonths, + if (transplantMonths != null) 'transplant_months': transplantMonths, + if (floweringMonths != null) 'flowering_months': floweringMonths, + if (fruitingMonths != null) 'fruiting_months': fruitingMonths, + if (seedHarvestMonths != null) 'seed_harvest_months': seedHarvestMonths, if (rowid != null) 'rowid': rowid, }); } @@ -590,6 +987,14 @@ class VarietiesCompanion extends UpdateCompanion { Value? cultivarName, Value? category, Value? notes, + Value? isDraft, + Value? isOrganic, + Value? needsReproduction, + Value? sowMonths, + Value? transplantMonths, + Value? floweringMonths, + Value? fruitingMonths, + Value? seedHarvestMonths, Value? rowid, }) { return VarietiesCompanion( @@ -604,6 +1009,14 @@ class VarietiesCompanion extends UpdateCompanion { cultivarName: cultivarName ?? this.cultivarName, category: category ?? this.category, notes: notes ?? this.notes, + isDraft: isDraft ?? this.isDraft, + isOrganic: isOrganic ?? this.isOrganic, + needsReproduction: needsReproduction ?? this.needsReproduction, + sowMonths: sowMonths ?? this.sowMonths, + transplantMonths: transplantMonths ?? this.transplantMonths, + floweringMonths: floweringMonths ?? this.floweringMonths, + fruitingMonths: fruitingMonths ?? this.fruitingMonths, + seedHarvestMonths: seedHarvestMonths ?? this.seedHarvestMonths, rowid: rowid ?? this.rowid, ); } @@ -644,6 +1057,30 @@ class VarietiesCompanion extends UpdateCompanion { if (notes.present) { map['notes'] = Variable(notes.value); } + if (isDraft.present) { + map['is_draft'] = Variable(isDraft.value); + } + if (isOrganic.present) { + map['is_organic'] = Variable(isOrganic.value); + } + if (needsReproduction.present) { + map['needs_reproduction'] = Variable(needsReproduction.value); + } + if (sowMonths.present) { + map['sow_months'] = Variable(sowMonths.value); + } + if (transplantMonths.present) { + map['transplant_months'] = Variable(transplantMonths.value); + } + if (floweringMonths.present) { + map['flowering_months'] = Variable(floweringMonths.value); + } + if (fruitingMonths.present) { + map['fruiting_months'] = Variable(fruitingMonths.value); + } + if (seedHarvestMonths.present) { + map['seed_harvest_months'] = Variable(seedHarvestMonths.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -664,6 +1101,14 @@ class VarietiesCompanion extends UpdateCompanion { ..write('cultivarName: $cultivarName, ') ..write('category: $category, ') ..write('notes: $notes, ') + ..write('isDraft: $isDraft, ') + ..write('isOrganic: $isOrganic, ') + ..write('needsReproduction: $needsReproduction, ') + ..write('sowMonths: $sowMonths, ') + ..write('transplantMonths: $transplantMonths, ') + ..write('floweringMonths: $floweringMonths, ') + ..write('fruitingMonths: $fruitingMonths, ') + ..write('seedHarvestMonths: $seedHarvestMonths, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -1417,6 +1862,17 @@ class $SpeciesTable extends Species with TableInfo<$SpeciesTable, Specy> { ), defaultValue: const Constant(false), ); + static const VerificationMeta _viabilityYearsMeta = const VerificationMeta( + 'viabilityYears', + ); + @override + late final GeneratedColumn viabilityYears = GeneratedColumn( + 'viability_years', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); @override List get $columns => [ id, @@ -1430,6 +1886,7 @@ class $SpeciesTable extends Species with TableInfo<$SpeciesTable, Specy> { gbifKey, family, isBundled, + viabilityYears, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -1525,6 +1982,15 @@ class $SpeciesTable extends Species with TableInfo<$SpeciesTable, Specy> { isBundled.isAcceptableOrUnknown(data['is_bundled']!, _isBundledMeta), ); } + if (data.containsKey('viability_years')) { + context.handle( + _viabilityYearsMeta, + viabilityYears.isAcceptableOrUnknown( + data['viability_years']!, + _viabilityYearsMeta, + ), + ); + } return context; } @@ -1578,6 +2044,10 @@ class $SpeciesTable extends Species with TableInfo<$SpeciesTable, Specy> { DriftSqlType.bool, data['${effectivePrefix}is_bundled'], )!, + viabilityYears: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}viability_years'], + ), ); } @@ -1599,6 +2069,13 @@ class Specy extends DataClass implements Insertable { final int? gbifKey; final String? family; final bool isBundled; + + /// Typical seed longevity in years under normal home storage — public-domain + /// reference data bundled with the catalog (agricultural-extension viability + /// tables). Drives the "expiring / past viability" warning on aging lots by + /// comparing against a lot's [Lots.harvestYear]. Nullable: unknown for + /// species without a bundled figure. + final int? viabilityYears; const Specy({ required this.id, required this.createdAt, @@ -1611,6 +2088,7 @@ class Specy extends DataClass implements Insertable { this.gbifKey, this.family, required this.isBundled, + this.viabilityYears, }); @override Map toColumns(bool nullToAbsent) { @@ -1632,6 +2110,9 @@ class Specy extends DataClass implements Insertable { map['family'] = Variable(family); } map['is_bundled'] = Variable(isBundled); + if (!nullToAbsent || viabilityYears != null) { + map['viability_years'] = Variable(viabilityYears); + } return map; } @@ -1654,6 +2135,9 @@ class Specy extends DataClass implements Insertable { ? const Value.absent() : Value(family), isBundled: Value(isBundled), + viabilityYears: viabilityYears == null && nullToAbsent + ? const Value.absent() + : Value(viabilityYears), ); } @@ -1674,6 +2158,7 @@ class Specy extends DataClass implements Insertable { gbifKey: serializer.fromJson(json['gbifKey']), family: serializer.fromJson(json['family']), isBundled: serializer.fromJson(json['isBundled']), + viabilityYears: serializer.fromJson(json['viabilityYears']), ); } @override @@ -1691,6 +2176,7 @@ class Specy extends DataClass implements Insertable { 'gbifKey': serializer.toJson(gbifKey), 'family': serializer.toJson(family), 'isBundled': serializer.toJson(isBundled), + 'viabilityYears': serializer.toJson(viabilityYears), }; } @@ -1706,6 +2192,7 @@ class Specy extends DataClass implements Insertable { Value gbifKey = const Value.absent(), Value family = const Value.absent(), bool? isBundled, + Value viabilityYears = const Value.absent(), }) => Specy( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, @@ -1718,6 +2205,9 @@ class Specy extends DataClass implements Insertable { gbifKey: gbifKey.present ? gbifKey.value : this.gbifKey, family: family.present ? family.value : this.family, isBundled: isBundled ?? this.isBundled, + viabilityYears: viabilityYears.present + ? viabilityYears.value + : this.viabilityYears, ); Specy copyWithCompanion(SpeciesCompanion data) { return Specy( @@ -1740,6 +2230,9 @@ class Specy extends DataClass implements Insertable { gbifKey: data.gbifKey.present ? data.gbifKey.value : this.gbifKey, family: data.family.present ? data.family.value : this.family, isBundled: data.isBundled.present ? data.isBundled.value : this.isBundled, + viabilityYears: data.viabilityYears.present + ? data.viabilityYears.value + : this.viabilityYears, ); } @@ -1756,7 +2249,8 @@ class Specy extends DataClass implements Insertable { ..write('wikidataQid: $wikidataQid, ') ..write('gbifKey: $gbifKey, ') ..write('family: $family, ') - ..write('isBundled: $isBundled') + ..write('isBundled: $isBundled, ') + ..write('viabilityYears: $viabilityYears') ..write(')')) .toString(); } @@ -1774,6 +2268,7 @@ class Specy extends DataClass implements Insertable { gbifKey, family, isBundled, + viabilityYears, ); @override bool operator ==(Object other) => @@ -1789,7 +2284,8 @@ class Specy extends DataClass implements Insertable { other.wikidataQid == this.wikidataQid && other.gbifKey == this.gbifKey && other.family == this.family && - other.isBundled == this.isBundled); + other.isBundled == this.isBundled && + other.viabilityYears == this.viabilityYears); } class SpeciesCompanion extends UpdateCompanion { @@ -1804,6 +2300,7 @@ class SpeciesCompanion extends UpdateCompanion { final Value gbifKey; final Value family; final Value isBundled; + final Value viabilityYears; final Value rowid; const SpeciesCompanion({ this.id = const Value.absent(), @@ -1817,6 +2314,7 @@ class SpeciesCompanion extends UpdateCompanion { this.gbifKey = const Value.absent(), this.family = const Value.absent(), this.isBundled = const Value.absent(), + this.viabilityYears = const Value.absent(), this.rowid = const Value.absent(), }); SpeciesCompanion.insert({ @@ -1831,6 +2329,7 @@ class SpeciesCompanion extends UpdateCompanion { this.gbifKey = const Value.absent(), this.family = const Value.absent(), this.isBundled = const Value.absent(), + this.viabilityYears = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), createdAt = Value(createdAt), @@ -1849,6 +2348,7 @@ class SpeciesCompanion extends UpdateCompanion { Expression? gbifKey, Expression? family, Expression? isBundled, + Expression? viabilityYears, Expression? rowid, }) { return RawValuesInsertable({ @@ -1863,6 +2363,7 @@ class SpeciesCompanion extends UpdateCompanion { if (gbifKey != null) 'gbif_key': gbifKey, if (family != null) 'family': family, if (isBundled != null) 'is_bundled': isBundled, + if (viabilityYears != null) 'viability_years': viabilityYears, if (rowid != null) 'rowid': rowid, }); } @@ -1879,6 +2380,7 @@ class SpeciesCompanion extends UpdateCompanion { Value? gbifKey, Value? family, Value? isBundled, + Value? viabilityYears, Value? rowid, }) { return SpeciesCompanion( @@ -1893,6 +2395,7 @@ class SpeciesCompanion extends UpdateCompanion { gbifKey: gbifKey ?? this.gbifKey, family: family ?? this.family, isBundled: isBundled ?? this.isBundled, + viabilityYears: viabilityYears ?? this.viabilityYears, rowid: rowid ?? this.rowid, ); } @@ -1933,6 +2436,9 @@ class SpeciesCompanion extends UpdateCompanion { if (isBundled.present) { map['is_bundled'] = Variable(isBundled.value); } + if (viabilityYears.present) { + map['viability_years'] = Variable(viabilityYears.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -1953,6 +2459,7 @@ class SpeciesCompanion extends UpdateCompanion { ..write('gbifKey: $gbifKey, ') ..write('family: $family, ') ..write('isBundled: $isBundled, ') + ..write('viabilityYears: $viabilityYears, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -2716,6 +3223,49 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { type: DriftSqlType.string, requiredDuringInsert: false, ); + static const VerificationMeta _originNameMeta = const VerificationMeta( + 'originName', + ); + @override + late final GeneratedColumn originName = GeneratedColumn( + 'origin_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _originPlaceMeta = const VerificationMeta( + 'originPlace', + ); + @override + late final GeneratedColumn originPlace = GeneratedColumn( + 'origin_place', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter abundance = + GeneratedColumn( + 'abundance', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter($LotsTable.$converterabundancen); + @override + late final GeneratedColumnWithTypeConverter + preservationFormat = + GeneratedColumn( + 'preservation_format', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter( + $LotsTable.$converterpreservationFormatn, + ); @override List get $columns => [ id, @@ -2735,6 +3285,10 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { storageLocation, offerStatus, seedbankId, + originName, + originPlace, + abundance, + preservationFormat, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -2860,6 +3414,21 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { seedbankId.isAcceptableOrUnknown(data['seedbank_id']!, _seedbankIdMeta), ); } + if (data.containsKey('origin_name')) { + context.handle( + _originNameMeta, + originName.isAcceptableOrUnknown(data['origin_name']!, _originNameMeta), + ); + } + if (data.containsKey('origin_place')) { + context.handle( + _originPlaceMeta, + originPlace.isAcceptableOrUnknown( + data['origin_place']!, + _originPlaceMeta, + ), + ); + } return context; } @@ -2943,6 +3512,26 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { DriftSqlType.string, data['${effectivePrefix}seedbank_id'], ), + originName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}origin_name'], + ), + originPlace: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}origin_place'], + ), + abundance: $LotsTable.$converterabundancen.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}abundance'], + ), + ), + preservationFormat: $LotsTable.$converterpreservationFormatn.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}preservation_format'], + ), + ), ); } @@ -2963,6 +3552,18 @@ class $LotsTable extends Lots with TableInfo<$LotsTable, Lot> { ); static JsonTypeConverter2 $converterofferStatus = const EnumNameConverter(OfferStatus.values); + static JsonTypeConverter2 $converterabundance = + const EnumNameConverter(Abundance.values); + static JsonTypeConverter2 $converterabundancen = + JsonTypeConverter2.asNullable($converterabundance); + static JsonTypeConverter2 + $converterpreservationFormat = const EnumNameConverter( + PreservationFormat.values, + ); + static JsonTypeConverter2 + $converterpreservationFormatn = JsonTypeConverter2.asNullable( + $converterpreservationFormat, + ); } class Lot extends DataClass implements Insertable { @@ -2983,6 +3584,22 @@ class Lot extends DataClass implements Insertable { final String? storageLocation; final OfferStatus offerStatus; final String? seedbankId; + + /// Provenance of this batch, kept as lightweight free text so it needs no + /// Party/Movement to record (the rigorous exchange path stays via Movements). + /// [originName] = who grew or gave the seeds; [originPlace] = where they come + /// from (with region/province). Both optional LWW scalars. + final String? originName; + final String? originPlace; + + /// Optional coarse "how much I have" for this lot — see [Abundance]. An + /// offline alternative to the precise Quantity columns; either, both, or + /// neither may be set. + final Abundance? abundance; + + /// How the (seed) lot is physically conserved — see [PreservationFormat]. + /// Distinct from [storageLocation]. Optional. + final PreservationFormat? preservationFormat; const Lot({ required this.id, required this.createdAt, @@ -3001,6 +3618,10 @@ class Lot extends DataClass implements Insertable { this.storageLocation, required this.offerStatus, this.seedbankId, + this.originName, + this.originPlace, + this.abundance, + this.preservationFormat, }); @override Map toColumns(bool nullToAbsent) { @@ -3046,6 +3667,22 @@ class Lot extends DataClass implements Insertable { if (!nullToAbsent || seedbankId != null) { map['seedbank_id'] = Variable(seedbankId); } + if (!nullToAbsent || originName != null) { + map['origin_name'] = Variable(originName); + } + if (!nullToAbsent || originPlace != null) { + map['origin_place'] = Variable(originPlace); + } + if (!nullToAbsent || abundance != null) { + map['abundance'] = Variable( + $LotsTable.$converterabundancen.toSql(abundance), + ); + } + if (!nullToAbsent || preservationFormat != null) { + map['preservation_format'] = Variable( + $LotsTable.$converterpreservationFormatn.toSql(preservationFormat), + ); + } return map; } @@ -3084,6 +3721,18 @@ class Lot extends DataClass implements Insertable { seedbankId: seedbankId == null && nullToAbsent ? const Value.absent() : Value(seedbankId), + originName: originName == null && nullToAbsent + ? const Value.absent() + : Value(originName), + originPlace: originPlace == null && nullToAbsent + ? const Value.absent() + : Value(originPlace), + abundance: abundance == null && nullToAbsent + ? const Value.absent() + : Value(abundance), + preservationFormat: preservationFormat == null && nullToAbsent + ? const Value.absent() + : Value(preservationFormat), ); } @@ -3116,6 +3765,14 @@ class Lot extends DataClass implements Insertable { serializer.fromJson(json['offerStatus']), ), seedbankId: serializer.fromJson(json['seedbankId']), + originName: serializer.fromJson(json['originName']), + originPlace: serializer.fromJson(json['originPlace']), + abundance: $LotsTable.$converterabundancen.fromJson( + serializer.fromJson(json['abundance']), + ), + preservationFormat: $LotsTable.$converterpreservationFormatn.fromJson( + serializer.fromJson(json['preservationFormat']), + ), ); } @override @@ -3143,6 +3800,14 @@ class Lot extends DataClass implements Insertable { $LotsTable.$converterofferStatus.toJson(offerStatus), ), 'seedbankId': serializer.toJson(seedbankId), + 'originName': serializer.toJson(originName), + 'originPlace': serializer.toJson(originPlace), + 'abundance': serializer.toJson( + $LotsTable.$converterabundancen.toJson(abundance), + ), + 'preservationFormat': serializer.toJson( + $LotsTable.$converterpreservationFormatn.toJson(preservationFormat), + ), }; } @@ -3164,6 +3829,10 @@ class Lot extends DataClass implements Insertable { Value storageLocation = const Value.absent(), OfferStatus? offerStatus, Value seedbankId = const Value.absent(), + Value originName = const Value.absent(), + Value originPlace = const Value.absent(), + Value abundance = const Value.absent(), + Value preservationFormat = const Value.absent(), }) => Lot( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, @@ -3188,6 +3857,12 @@ class Lot extends DataClass implements Insertable { : this.storageLocation, offerStatus: offerStatus ?? this.offerStatus, seedbankId: seedbankId.present ? seedbankId.value : this.seedbankId, + originName: originName.present ? originName.value : this.originName, + originPlace: originPlace.present ? originPlace.value : this.originPlace, + abundance: abundance.present ? abundance.value : this.abundance, + preservationFormat: preservationFormat.present + ? preservationFormat.value + : this.preservationFormat, ); Lot copyWithCompanion(LotsCompanion data) { return Lot( @@ -3230,6 +3905,16 @@ class Lot extends DataClass implements Insertable { seedbankId: data.seedbankId.present ? data.seedbankId.value : this.seedbankId, + originName: data.originName.present + ? data.originName.value + : this.originName, + originPlace: data.originPlace.present + ? data.originPlace.value + : this.originPlace, + abundance: data.abundance.present ? data.abundance.value : this.abundance, + preservationFormat: data.preservationFormat.present + ? data.preservationFormat.value + : this.preservationFormat, ); } @@ -3252,13 +3937,17 @@ class Lot extends DataClass implements Insertable { ..write('presentation: $presentation, ') ..write('storageLocation: $storageLocation, ') ..write('offerStatus: $offerStatus, ') - ..write('seedbankId: $seedbankId') + ..write('seedbankId: $seedbankId, ') + ..write('originName: $originName, ') + ..write('originPlace: $originPlace, ') + ..write('abundance: $abundance, ') + ..write('preservationFormat: $preservationFormat') ..write(')')) .toString(); } @override - int get hashCode => Object.hash( + int get hashCode => Object.hashAll([ id, createdAt, updatedAt, @@ -3276,7 +3965,11 @@ class Lot extends DataClass implements Insertable { storageLocation, offerStatus, seedbankId, - ); + originName, + originPlace, + abundance, + preservationFormat, + ]); @override bool operator ==(Object other) => identical(this, other) || @@ -3297,7 +3990,11 @@ class Lot extends DataClass implements Insertable { other.presentation == this.presentation && other.storageLocation == this.storageLocation && other.offerStatus == this.offerStatus && - other.seedbankId == this.seedbankId); + other.seedbankId == this.seedbankId && + other.originName == this.originName && + other.originPlace == this.originPlace && + other.abundance == this.abundance && + other.preservationFormat == this.preservationFormat); } class LotsCompanion extends UpdateCompanion { @@ -3318,6 +4015,10 @@ class LotsCompanion extends UpdateCompanion { final Value storageLocation; final Value offerStatus; final Value seedbankId; + final Value originName; + final Value originPlace; + final Value abundance; + final Value preservationFormat; final Value rowid; const LotsCompanion({ this.id = const Value.absent(), @@ -3337,6 +4038,10 @@ class LotsCompanion extends UpdateCompanion { this.storageLocation = const Value.absent(), this.offerStatus = const Value.absent(), this.seedbankId = const Value.absent(), + this.originName = const Value.absent(), + this.originPlace = const Value.absent(), + this.abundance = const Value.absent(), + this.preservationFormat = const Value.absent(), this.rowid = const Value.absent(), }); LotsCompanion.insert({ @@ -3357,6 +4062,10 @@ class LotsCompanion extends UpdateCompanion { this.storageLocation = const Value.absent(), this.offerStatus = const Value.absent(), this.seedbankId = const Value.absent(), + this.originName = const Value.absent(), + this.originPlace = const Value.absent(), + this.abundance = const Value.absent(), + this.preservationFormat = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), createdAt = Value(createdAt), @@ -3381,6 +4090,10 @@ class LotsCompanion extends UpdateCompanion { Expression? storageLocation, Expression? offerStatus, Expression? seedbankId, + Expression? originName, + Expression? originPlace, + Expression? abundance, + Expression? preservationFormat, Expression? rowid, }) { return RawValuesInsertable({ @@ -3401,6 +4114,10 @@ class LotsCompanion extends UpdateCompanion { if (storageLocation != null) 'storage_location': storageLocation, if (offerStatus != null) 'offer_status': offerStatus, if (seedbankId != null) 'seedbank_id': seedbankId, + if (originName != null) 'origin_name': originName, + if (originPlace != null) 'origin_place': originPlace, + if (abundance != null) 'abundance': abundance, + if (preservationFormat != null) 'preservation_format': preservationFormat, if (rowid != null) 'rowid': rowid, }); } @@ -3423,6 +4140,10 @@ class LotsCompanion extends UpdateCompanion { Value? storageLocation, Value? offerStatus, Value? seedbankId, + Value? originName, + Value? originPlace, + Value? abundance, + Value? preservationFormat, Value? rowid, }) { return LotsCompanion( @@ -3443,6 +4164,10 @@ class LotsCompanion extends UpdateCompanion { storageLocation: storageLocation ?? this.storageLocation, offerStatus: offerStatus ?? this.offerStatus, seedbankId: seedbankId ?? this.seedbankId, + originName: originName ?? this.originName, + originPlace: originPlace ?? this.originPlace, + abundance: abundance ?? this.abundance, + preservationFormat: preservationFormat ?? this.preservationFormat, rowid: rowid ?? this.rowid, ); } @@ -3507,6 +4232,24 @@ class LotsCompanion extends UpdateCompanion { if (seedbankId.present) { map['seedbank_id'] = Variable(seedbankId.value); } + if (originName.present) { + map['origin_name'] = Variable(originName.value); + } + if (originPlace.present) { + map['origin_place'] = Variable(originPlace.value); + } + if (abundance.present) { + map['abundance'] = Variable( + $LotsTable.$converterabundancen.toSql(abundance.value), + ); + } + if (preservationFormat.present) { + map['preservation_format'] = Variable( + $LotsTable.$converterpreservationFormatn.toSql( + preservationFormat.value, + ), + ); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -3533,6 +4276,10 @@ class LotsCompanion extends UpdateCompanion { ..write('storageLocation: $storageLocation, ') ..write('offerStatus: $offerStatus, ') ..write('seedbankId: $seedbankId, ') + ..write('originName: $originName, ') + ..write('originPlace: $originPlace, ') + ..write('abundance: $abundance, ') + ..write('preservationFormat: $preservationFormat, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -4210,6 +4957,695 @@ class GerminationTestsCompanion extends UpdateCompanion { } } +class $ConditionChecksTable extends ConditionChecks + with TableInfo<$ConditionChecksTable, ConditionCheck> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ConditionChecksTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _lastAuthorMeta = const VerificationMeta( + 'lastAuthor', + ); + @override + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _isDeletedMeta = const VerificationMeta( + 'isDeleted', + ); + @override + late final GeneratedColumn isDeleted = GeneratedColumn( + '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 schemaRowVersion = GeneratedColumn( + '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 lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _checkedOnMeta = const VerificationMeta( + 'checkedOn', + ); + @override + late final GeneratedColumn checkedOn = GeneratedColumn( + 'checked_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _containerCountMeta = const VerificationMeta( + 'containerCount', + ); + @override + late final GeneratedColumn containerCount = GeneratedColumn( + 'container_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter + desiccantState = + GeneratedColumn( + 'desiccant_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter( + $ConditionChecksTable.$converterdesiccantStaten, + ); + static const VerificationMeta _notesMeta = const VerificationMeta('notes'); + @override + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + lotId, + checkedOn, + containerCount, + desiccantState, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'condition_checks'; + @override + VerificationContext validateIntegrity( + Insertable 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('checked_on')) { + context.handle( + _checkedOnMeta, + checkedOn.isAcceptableOrUnknown(data['checked_on']!, _checkedOnMeta), + ); + } + if (data.containsKey('container_count')) { + context.handle( + _containerCountMeta, + containerCount.isAcceptableOrUnknown( + data['container_count']!, + _containerCountMeta, + ), + ); + } + if (data.containsKey('notes')) { + context.handle( + _notesMeta, + notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ConditionCheck map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ConditionCheck( + 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'], + )!, + checkedOn: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}checked_on'], + ), + containerCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}container_count'], + ), + desiccantState: $ConditionChecksTable.$converterdesiccantStaten.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}desiccant_state'], + ), + ), + notes: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}notes'], + ), + ); + } + + @override + $ConditionChecksTable createAlias(String alias) { + return $ConditionChecksTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $converterdesiccantState = const EnumNameConverter( + DesiccantState.values, + ); + static JsonTypeConverter2 + $converterdesiccantStaten = JsonTypeConverter2.asNullable( + $converterdesiccantState, + ); +} + +class ConditionCheck extends DataClass implements Insertable { + final String id; + final int createdAt; + final String updatedAt; + final String lastAuthor; + final bool isDeleted; + final int schemaRowVersion; + final String lotId; + final int? checkedOn; + final int? containerCount; + final DesiccantState? desiccantState; + final String? notes; + const ConditionCheck({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.lastAuthor, + required this.isDeleted, + required this.schemaRowVersion, + required this.lotId, + this.checkedOn, + this.containerCount, + this.desiccantState, + this.notes, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['last_author'] = Variable(lastAuthor); + map['is_deleted'] = Variable(isDeleted); + map['schema_row_version'] = Variable(schemaRowVersion); + map['lot_id'] = Variable(lotId); + if (!nullToAbsent || checkedOn != null) { + map['checked_on'] = Variable(checkedOn); + } + if (!nullToAbsent || containerCount != null) { + map['container_count'] = Variable(containerCount); + } + if (!nullToAbsent || desiccantState != null) { + map['desiccant_state'] = Variable( + $ConditionChecksTable.$converterdesiccantStaten.toSql(desiccantState), + ); + } + if (!nullToAbsent || notes != null) { + map['notes'] = Variable(notes); + } + return map; + } + + ConditionChecksCompanion toCompanion(bool nullToAbsent) { + return ConditionChecksCompanion( + id: Value(id), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + lastAuthor: Value(lastAuthor), + isDeleted: Value(isDeleted), + schemaRowVersion: Value(schemaRowVersion), + lotId: Value(lotId), + checkedOn: checkedOn == null && nullToAbsent + ? const Value.absent() + : Value(checkedOn), + containerCount: containerCount == null && nullToAbsent + ? const Value.absent() + : Value(containerCount), + desiccantState: desiccantState == null && nullToAbsent + ? const Value.absent() + : Value(desiccantState), + notes: notes == null && nullToAbsent + ? const Value.absent() + : Value(notes), + ); + } + + factory ConditionCheck.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ConditionCheck( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + lastAuthor: serializer.fromJson(json['lastAuthor']), + isDeleted: serializer.fromJson(json['isDeleted']), + schemaRowVersion: serializer.fromJson(json['schemaRowVersion']), + lotId: serializer.fromJson(json['lotId']), + checkedOn: serializer.fromJson(json['checkedOn']), + containerCount: serializer.fromJson(json['containerCount']), + desiccantState: $ConditionChecksTable.$converterdesiccantStaten.fromJson( + serializer.fromJson(json['desiccantState']), + ), + notes: serializer.fromJson(json['notes']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'lastAuthor': serializer.toJson(lastAuthor), + 'isDeleted': serializer.toJson(isDeleted), + 'schemaRowVersion': serializer.toJson(schemaRowVersion), + 'lotId': serializer.toJson(lotId), + 'checkedOn': serializer.toJson(checkedOn), + 'containerCount': serializer.toJson(containerCount), + 'desiccantState': serializer.toJson( + $ConditionChecksTable.$converterdesiccantStaten.toJson(desiccantState), + ), + 'notes': serializer.toJson(notes), + }; + } + + ConditionCheck copyWith({ + String? id, + int? createdAt, + String? updatedAt, + String? lastAuthor, + bool? isDeleted, + int? schemaRowVersion, + String? lotId, + Value checkedOn = const Value.absent(), + Value containerCount = const Value.absent(), + Value desiccantState = const Value.absent(), + Value notes = const Value.absent(), + }) => ConditionCheck( + 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, + checkedOn: checkedOn.present ? checkedOn.value : this.checkedOn, + containerCount: containerCount.present + ? containerCount.value + : this.containerCount, + desiccantState: desiccantState.present + ? desiccantState.value + : this.desiccantState, + notes: notes.present ? notes.value : this.notes, + ); + ConditionCheck copyWithCompanion(ConditionChecksCompanion data) { + return ConditionCheck( + 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, + checkedOn: data.checkedOn.present ? data.checkedOn.value : this.checkedOn, + containerCount: data.containerCount.present + ? data.containerCount.value + : this.containerCount, + desiccantState: data.desiccantState.present + ? data.desiccantState.value + : this.desiccantState, + notes: data.notes.present ? data.notes.value : this.notes, + ); + } + + @override + String toString() { + return (StringBuffer('ConditionCheck(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('lastAuthor: $lastAuthor, ') + ..write('isDeleted: $isDeleted, ') + ..write('schemaRowVersion: $schemaRowVersion, ') + ..write('lotId: $lotId, ') + ..write('checkedOn: $checkedOn, ') + ..write('containerCount: $containerCount, ') + ..write('desiccantState: $desiccantState, ') + ..write('notes: $notes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + lotId, + checkedOn, + containerCount, + desiccantState, + notes, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ConditionCheck && + 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.checkedOn == this.checkedOn && + other.containerCount == this.containerCount && + other.desiccantState == this.desiccantState && + other.notes == this.notes); +} + +class ConditionChecksCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value lastAuthor; + final Value isDeleted; + final Value schemaRowVersion; + final Value lotId; + final Value checkedOn; + final Value containerCount; + final Value desiccantState; + final Value notes; + final Value rowid; + const ConditionChecksCompanion({ + 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.checkedOn = const Value.absent(), + this.containerCount = const Value.absent(), + this.desiccantState = const Value.absent(), + this.notes = const Value.absent(), + this.rowid = const Value.absent(), + }); + ConditionChecksCompanion.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.checkedOn = const Value.absent(), + this.containerCount = const Value.absent(), + this.desiccantState = 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 custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? lastAuthor, + Expression? isDeleted, + Expression? schemaRowVersion, + Expression? lotId, + Expression? checkedOn, + Expression? containerCount, + Expression? desiccantState, + Expression? notes, + Expression? 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 (checkedOn != null) 'checked_on': checkedOn, + if (containerCount != null) 'container_count': containerCount, + if (desiccantState != null) 'desiccant_state': desiccantState, + if (notes != null) 'notes': notes, + if (rowid != null) 'rowid': rowid, + }); + } + + ConditionChecksCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? lastAuthor, + Value? isDeleted, + Value? schemaRowVersion, + Value? lotId, + Value? checkedOn, + Value? containerCount, + Value? desiccantState, + Value? notes, + Value? rowid, + }) { + return ConditionChecksCompanion( + 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, + checkedOn: checkedOn ?? this.checkedOn, + containerCount: containerCount ?? this.containerCount, + desiccantState: desiccantState ?? this.desiccantState, + notes: notes ?? this.notes, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (lastAuthor.present) { + map['last_author'] = Variable(lastAuthor.value); + } + if (isDeleted.present) { + map['is_deleted'] = Variable(isDeleted.value); + } + if (schemaRowVersion.present) { + map['schema_row_version'] = Variable(schemaRowVersion.value); + } + if (lotId.present) { + map['lot_id'] = Variable(lotId.value); + } + if (checkedOn.present) { + map['checked_on'] = Variable(checkedOn.value); + } + if (containerCount.present) { + map['container_count'] = Variable(containerCount.value); + } + if (desiccantState.present) { + map['desiccant_state'] = Variable( + $ConditionChecksTable.$converterdesiccantStaten.toSql( + desiccantState.value, + ), + ); + } + if (notes.present) { + map['notes'] = Variable(notes.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ConditionChecksCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('lastAuthor: $lastAuthor, ') + ..write('isDeleted: $isDeleted, ') + ..write('schemaRowVersion: $schemaRowVersion, ') + ..write('lotId: $lotId, ') + ..write('checkedOn: $checkedOn, ') + ..write('containerCount: $containerCount, ') + ..write('desiccantState: $desiccantState, ') + ..write('notes: $notes, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $MovementsTable extends Movements with TableInfo<$MovementsTable, Movement> { @override @@ -7080,6 +8516,9 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $GerminationTestsTable germinationTests = $GerminationTestsTable( this, ); + late final $ConditionChecksTable conditionChecks = $ConditionChecksTable( + this, + ); late final $MovementsTable movements = $MovementsTable(this); late final $PartiesTable parties = $PartiesTable(this); late final $AttachmentsTable attachments = $AttachmentsTable(this); @@ -7095,6 +8534,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { speciesCommonNames, lots, germinationTests, + conditionChecks, movements, parties, attachments, @@ -7115,6 +8555,14 @@ typedef $$VarietiesTableCreateCompanionBuilder = Value cultivarName, Value category, Value notes, + Value isDraft, + Value isOrganic, + Value needsReproduction, + Value sowMonths, + Value transplantMonths, + Value floweringMonths, + Value fruitingMonths, + Value seedHarvestMonths, Value rowid, }); typedef $$VarietiesTableUpdateCompanionBuilder = @@ -7130,6 +8578,14 @@ typedef $$VarietiesTableUpdateCompanionBuilder = Value cultivarName, Value category, Value notes, + Value isDraft, + Value isOrganic, + Value needsReproduction, + Value sowMonths, + Value transplantMonths, + Value floweringMonths, + Value fruitingMonths, + Value seedHarvestMonths, Value rowid, }); @@ -7196,6 +8652,46 @@ class $$VarietiesTableFilterComposer column: $table.notes, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get isDraft => $composableBuilder( + column: $table.isDraft, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isOrganic => $composableBuilder( + column: $table.isOrganic, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get needsReproduction => $composableBuilder( + column: $table.needsReproduction, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sowMonths => $composableBuilder( + column: $table.sowMonths, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get transplantMonths => $composableBuilder( + column: $table.transplantMonths, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get floweringMonths => $composableBuilder( + column: $table.floweringMonths, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get fruitingMonths => $composableBuilder( + column: $table.fruitingMonths, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get seedHarvestMonths => $composableBuilder( + column: $table.seedHarvestMonths, + builder: (column) => ColumnFilters(column), + ); } class $$VarietiesTableOrderingComposer @@ -7261,6 +8757,46 @@ class $$VarietiesTableOrderingComposer column: $table.notes, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get isDraft => $composableBuilder( + column: $table.isDraft, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isOrganic => $composableBuilder( + column: $table.isOrganic, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get needsReproduction => $composableBuilder( + column: $table.needsReproduction, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sowMonths => $composableBuilder( + column: $table.sowMonths, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get transplantMonths => $composableBuilder( + column: $table.transplantMonths, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get floweringMonths => $composableBuilder( + column: $table.floweringMonths, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get fruitingMonths => $composableBuilder( + column: $table.fruitingMonths, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get seedHarvestMonths => $composableBuilder( + column: $table.seedHarvestMonths, + builder: (column) => ColumnOrderings(column), + ); } class $$VarietiesTableAnnotationComposer @@ -7310,6 +8846,40 @@ class $$VarietiesTableAnnotationComposer GeneratedColumn get notes => $composableBuilder(column: $table.notes, builder: (column) => column); + + GeneratedColumn get isDraft => + $composableBuilder(column: $table.isDraft, builder: (column) => column); + + GeneratedColumn get isOrganic => + $composableBuilder(column: $table.isOrganic, builder: (column) => column); + + GeneratedColumn get needsReproduction => $composableBuilder( + column: $table.needsReproduction, + builder: (column) => column, + ); + + GeneratedColumn get sowMonths => + $composableBuilder(column: $table.sowMonths, builder: (column) => column); + + GeneratedColumn get transplantMonths => $composableBuilder( + column: $table.transplantMonths, + builder: (column) => column, + ); + + GeneratedColumn get floweringMonths => $composableBuilder( + column: $table.floweringMonths, + builder: (column) => column, + ); + + GeneratedColumn get fruitingMonths => $composableBuilder( + column: $table.fruitingMonths, + builder: (column) => column, + ); + + GeneratedColumn get seedHarvestMonths => $composableBuilder( + column: $table.seedHarvestMonths, + builder: (column) => column, + ); } class $$VarietiesTableTableManager @@ -7351,6 +8921,14 @@ class $$VarietiesTableTableManager Value cultivarName = const Value.absent(), Value category = const Value.absent(), Value notes = const Value.absent(), + Value isDraft = const Value.absent(), + Value isOrganic = const Value.absent(), + Value needsReproduction = const Value.absent(), + Value sowMonths = const Value.absent(), + Value transplantMonths = const Value.absent(), + Value floweringMonths = const Value.absent(), + Value fruitingMonths = const Value.absent(), + Value seedHarvestMonths = const Value.absent(), Value rowid = const Value.absent(), }) => VarietiesCompanion( id: id, @@ -7364,6 +8942,14 @@ class $$VarietiesTableTableManager cultivarName: cultivarName, category: category, notes: notes, + isDraft: isDraft, + isOrganic: isOrganic, + needsReproduction: needsReproduction, + sowMonths: sowMonths, + transplantMonths: transplantMonths, + floweringMonths: floweringMonths, + fruitingMonths: fruitingMonths, + seedHarvestMonths: seedHarvestMonths, rowid: rowid, ), createCompanionCallback: @@ -7379,6 +8965,14 @@ class $$VarietiesTableTableManager Value cultivarName = const Value.absent(), Value category = const Value.absent(), Value notes = const Value.absent(), + Value isDraft = const Value.absent(), + Value isOrganic = const Value.absent(), + Value needsReproduction = const Value.absent(), + Value sowMonths = const Value.absent(), + Value transplantMonths = const Value.absent(), + Value floweringMonths = const Value.absent(), + Value fruitingMonths = const Value.absent(), + Value seedHarvestMonths = const Value.absent(), Value rowid = const Value.absent(), }) => VarietiesCompanion.insert( id: id, @@ -7392,6 +8986,14 @@ class $$VarietiesTableTableManager cultivarName: cultivarName, category: category, notes: notes, + isDraft: isDraft, + isOrganic: isOrganic, + needsReproduction: needsReproduction, + sowMonths: sowMonths, + transplantMonths: transplantMonths, + floweringMonths: floweringMonths, + fruitingMonths: fruitingMonths, + seedHarvestMonths: seedHarvestMonths, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -7747,6 +9349,7 @@ typedef $$SpeciesTableCreateCompanionBuilder = Value gbifKey, Value family, Value isBundled, + Value viabilityYears, Value rowid, }); typedef $$SpeciesTableUpdateCompanionBuilder = @@ -7762,6 +9365,7 @@ typedef $$SpeciesTableUpdateCompanionBuilder = Value gbifKey, Value family, Value isBundled, + Value viabilityYears, Value rowid, }); @@ -7828,6 +9432,11 @@ class $$SpeciesTableFilterComposer column: $table.isBundled, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get viabilityYears => $composableBuilder( + column: $table.viabilityYears, + builder: (column) => ColumnFilters(column), + ); } class $$SpeciesTableOrderingComposer @@ -7893,6 +9502,11 @@ class $$SpeciesTableOrderingComposer column: $table.isBundled, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get viabilityYears => $composableBuilder( + column: $table.viabilityYears, + builder: (column) => ColumnOrderings(column), + ); } class $$SpeciesTableAnnotationComposer @@ -7944,6 +9558,11 @@ class $$SpeciesTableAnnotationComposer GeneratedColumn get isBundled => $composableBuilder(column: $table.isBundled, builder: (column) => column); + + GeneratedColumn get viabilityYears => $composableBuilder( + column: $table.viabilityYears, + builder: (column) => column, + ); } class $$SpeciesTableTableManager @@ -7985,6 +9604,7 @@ class $$SpeciesTableTableManager Value gbifKey = const Value.absent(), Value family = const Value.absent(), Value isBundled = const Value.absent(), + Value viabilityYears = const Value.absent(), Value rowid = const Value.absent(), }) => SpeciesCompanion( id: id, @@ -7998,6 +9618,7 @@ class $$SpeciesTableTableManager gbifKey: gbifKey, family: family, isBundled: isBundled, + viabilityYears: viabilityYears, rowid: rowid, ), createCompanionCallback: @@ -8013,6 +9634,7 @@ class $$SpeciesTableTableManager Value gbifKey = const Value.absent(), Value family = const Value.absent(), Value isBundled = const Value.absent(), + Value viabilityYears = const Value.absent(), Value rowid = const Value.absent(), }) => SpeciesCompanion.insert( id: id, @@ -8026,6 +9648,7 @@ class $$SpeciesTableTableManager gbifKey: gbifKey, family: family, isBundled: isBundled, + viabilityYears: viabilityYears, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -8362,6 +9985,10 @@ typedef $$LotsTableCreateCompanionBuilder = Value storageLocation, Value offerStatus, Value seedbankId, + Value originName, + Value originPlace, + Value abundance, + Value preservationFormat, Value rowid, }); typedef $$LotsTableUpdateCompanionBuilder = @@ -8383,6 +10010,10 @@ typedef $$LotsTableUpdateCompanionBuilder = Value storageLocation, Value offerStatus, Value seedbankId, + Value originName, + Value originPlace, + Value abundance, + Value preservationFormat, Value rowid, }); @@ -8481,6 +10112,32 @@ class $$LotsTableFilterComposer extends Composer<_$AppDatabase, $LotsTable> { column: $table.seedbankId, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get originName => $composableBuilder( + column: $table.originName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get originPlace => $composableBuilder( + column: $table.originPlace, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters get abundance => + $composableBuilder( + column: $table.abundance, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnWithTypeConverterFilters< + PreservationFormat?, + PreservationFormat, + String + > + get preservationFormat => $composableBuilder( + column: $table.preservationFormat, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); } class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> { @@ -8575,6 +10232,26 @@ class $$LotsTableOrderingComposer extends Composer<_$AppDatabase, $LotsTable> { column: $table.seedbankId, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get originName => $composableBuilder( + column: $table.originName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get originPlace => $composableBuilder( + column: $table.originPlace, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get abundance => $composableBuilder( + column: $table.abundance, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get preservationFormat => $composableBuilder( + column: $table.preservationFormat, + builder: (column) => ColumnOrderings(column), + ); } class $$LotsTableAnnotationComposer @@ -8660,6 +10337,25 @@ class $$LotsTableAnnotationComposer column: $table.seedbankId, builder: (column) => column, ); + + GeneratedColumn get originName => $composableBuilder( + column: $table.originName, + builder: (column) => column, + ); + + GeneratedColumn get originPlace => $composableBuilder( + column: $table.originPlace, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter get abundance => + $composableBuilder(column: $table.abundance, builder: (column) => column); + + GeneratedColumnWithTypeConverter + get preservationFormat => $composableBuilder( + column: $table.preservationFormat, + builder: (column) => column, + ); } class $$LotsTableTableManager @@ -8707,6 +10403,11 @@ class $$LotsTableTableManager Value storageLocation = const Value.absent(), Value offerStatus = const Value.absent(), Value seedbankId = const Value.absent(), + Value originName = const Value.absent(), + Value originPlace = const Value.absent(), + Value abundance = const Value.absent(), + Value preservationFormat = + const Value.absent(), Value rowid = const Value.absent(), }) => LotsCompanion( id: id, @@ -8726,6 +10427,10 @@ class $$LotsTableTableManager storageLocation: storageLocation, offerStatus: offerStatus, seedbankId: seedbankId, + originName: originName, + originPlace: originPlace, + abundance: abundance, + preservationFormat: preservationFormat, rowid: rowid, ), createCompanionCallback: @@ -8747,6 +10452,11 @@ class $$LotsTableTableManager Value storageLocation = const Value.absent(), Value offerStatus = const Value.absent(), Value seedbankId = const Value.absent(), + Value originName = const Value.absent(), + Value originPlace = const Value.absent(), + Value abundance = const Value.absent(), + Value preservationFormat = + const Value.absent(), Value rowid = const Value.absent(), }) => LotsCompanion.insert( id: id, @@ -8766,6 +10476,10 @@ class $$LotsTableTableManager storageLocation: storageLocation, offerStatus: offerStatus, seedbankId: seedbankId, + originName: originName, + originPlace: originPlace, + abundance: abundance, + preservationFormat: preservationFormat, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -9118,6 +10832,336 @@ typedef $$GerminationTestsTableProcessedTableManager = GerminationTest, PrefetchHooks Function() >; +typedef $$ConditionChecksTableCreateCompanionBuilder = + ConditionChecksCompanion Function({ + required String id, + required int createdAt, + required String updatedAt, + required String lastAuthor, + Value isDeleted, + Value schemaRowVersion, + required String lotId, + Value checkedOn, + Value containerCount, + Value desiccantState, + Value notes, + Value rowid, + }); +typedef $$ConditionChecksTableUpdateCompanionBuilder = + ConditionChecksCompanion Function({ + Value id, + Value createdAt, + Value updatedAt, + Value lastAuthor, + Value isDeleted, + Value schemaRowVersion, + Value lotId, + Value checkedOn, + Value containerCount, + Value desiccantState, + Value notes, + Value rowid, + }); + +class $$ConditionChecksTableFilterComposer + extends Composer<_$AppDatabase, $ConditionChecksTable> { + $$ConditionChecksTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastAuthor => $composableBuilder( + column: $table.lastAuthor, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isDeleted => $composableBuilder( + column: $table.isDeleted, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get schemaRowVersion => $composableBuilder( + column: $table.schemaRowVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lotId => $composableBuilder( + column: $table.lotId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get checkedOn => $composableBuilder( + column: $table.checkedOn, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get containerCount => $composableBuilder( + column: $table.containerCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters + get desiccantState => $composableBuilder( + column: $table.desiccantState, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ConditionChecksTableOrderingComposer + extends Composer<_$AppDatabase, $ConditionChecksTable> { + $$ConditionChecksTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastAuthor => $composableBuilder( + column: $table.lastAuthor, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isDeleted => $composableBuilder( + column: $table.isDeleted, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get schemaRowVersion => $composableBuilder( + column: $table.schemaRowVersion, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lotId => $composableBuilder( + column: $table.lotId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get checkedOn => $composableBuilder( + column: $table.checkedOn, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get containerCount => $composableBuilder( + column: $table.containerCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get desiccantState => $composableBuilder( + column: $table.desiccantState, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get notes => $composableBuilder( + column: $table.notes, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ConditionChecksTableAnnotationComposer + extends Composer<_$AppDatabase, $ConditionChecksTable> { + $$ConditionChecksTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get lastAuthor => $composableBuilder( + column: $table.lastAuthor, + builder: (column) => column, + ); + + GeneratedColumn get isDeleted => + $composableBuilder(column: $table.isDeleted, builder: (column) => column); + + GeneratedColumn get schemaRowVersion => $composableBuilder( + column: $table.schemaRowVersion, + builder: (column) => column, + ); + + GeneratedColumn get lotId => + $composableBuilder(column: $table.lotId, builder: (column) => column); + + GeneratedColumn get checkedOn => + $composableBuilder(column: $table.checkedOn, builder: (column) => column); + + GeneratedColumn get containerCount => $composableBuilder( + column: $table.containerCount, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter + get desiccantState => $composableBuilder( + column: $table.desiccantState, + builder: (column) => column, + ); + + GeneratedColumn get notes => + $composableBuilder(column: $table.notes, builder: (column) => column); +} + +class $$ConditionChecksTableTableManager + extends + RootTableManager< + _$AppDatabase, + $ConditionChecksTable, + ConditionCheck, + $$ConditionChecksTableFilterComposer, + $$ConditionChecksTableOrderingComposer, + $$ConditionChecksTableAnnotationComposer, + $$ConditionChecksTableCreateCompanionBuilder, + $$ConditionChecksTableUpdateCompanionBuilder, + ( + ConditionCheck, + BaseReferences< + _$AppDatabase, + $ConditionChecksTable, + ConditionCheck + >, + ), + ConditionCheck, + PrefetchHooks Function() + > { + $$ConditionChecksTableTableManager( + _$AppDatabase db, + $ConditionChecksTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ConditionChecksTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ConditionChecksTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ConditionChecksTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value lastAuthor = const Value.absent(), + Value isDeleted = const Value.absent(), + Value schemaRowVersion = const Value.absent(), + Value lotId = const Value.absent(), + Value checkedOn = const Value.absent(), + Value containerCount = const Value.absent(), + Value desiccantState = const Value.absent(), + Value notes = const Value.absent(), + Value rowid = const Value.absent(), + }) => ConditionChecksCompanion( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + lastAuthor: lastAuthor, + isDeleted: isDeleted, + schemaRowVersion: schemaRowVersion, + lotId: lotId, + checkedOn: checkedOn, + containerCount: containerCount, + desiccantState: desiccantState, + notes: notes, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required int createdAt, + required String updatedAt, + required String lastAuthor, + Value isDeleted = const Value.absent(), + Value schemaRowVersion = const Value.absent(), + required String lotId, + Value checkedOn = const Value.absent(), + Value containerCount = const Value.absent(), + Value desiccantState = const Value.absent(), + Value notes = const Value.absent(), + Value rowid = const Value.absent(), + }) => ConditionChecksCompanion.insert( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + lastAuthor: lastAuthor, + isDeleted: isDeleted, + schemaRowVersion: schemaRowVersion, + lotId: lotId, + checkedOn: checkedOn, + containerCount: containerCount, + desiccantState: desiccantState, + notes: notes, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ConditionChecksTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $ConditionChecksTable, + ConditionCheck, + $$ConditionChecksTableFilterComposer, + $$ConditionChecksTableOrderingComposer, + $$ConditionChecksTableAnnotationComposer, + $$ConditionChecksTableCreateCompanionBuilder, + $$ConditionChecksTableUpdateCompanionBuilder, + ( + ConditionCheck, + BaseReferences<_$AppDatabase, $ConditionChecksTable, ConditionCheck>, + ), + ConditionCheck, + PrefetchHooks Function() + >; typedef $$MovementsTableCreateCompanionBuilder = MovementsCompanion Function({ required String id, @@ -10480,6 +12524,8 @@ class $AppDatabaseManager { $$LotsTableTableManager get lots => $$LotsTableTableManager(_db, _db.lots); $$GerminationTestsTableTableManager get germinationTests => $$GerminationTestsTableTableManager(_db, _db.germinationTests); + $$ConditionChecksTableTableManager get conditionChecks => + $$ConditionChecksTableTableManager(_db, _db.conditionChecks); $$MovementsTableTableManager get movements => $$MovementsTableTableManager(_db, _db.movements); $$PartiesTableTableManager get parties => diff --git a/apps/app_seeds/lib/db/enums.dart b/apps/app_seeds/lib/db/enums.dart index 965917a..1c5128a 100644 --- a/apps/app_seeds/lib/db/enums.dart +++ b/apps/app_seeds/lib/db/enums.dart @@ -27,6 +27,46 @@ enum Presentation { pot, tray, plug, bareRoot, rootBall } /// Per-lot visibility (data-model §2.3). Used by the future sharing layer. enum OfferStatus { private, shared, exchange, sell } +/// A coarse, offline-friendly "how much do I have" that fuses amount with +/// shareability — the way a seed saver actually thinks, without weighing or +/// counting. Modelled on the qualitative scale a real seed bank used +/// (mucha/bastante/suficiente/poca), where each level is defined by what you +/// can do with it. Optional alternative to a precise Quantity. Append-only by +/// name. +/// +/// - `plentyToShare` — surplus, free to give away (mucha). +/// - `enoughToShare` — can share a little, sparingly (bastante). +/// - `enoughForMe` — enough for my own use, not to share (suficiente). +/// - `runningLow` — only enough to keep the variety alive (poca). +enum Abundance { plentyToShare, enoughToShare, enoughForMe, runningLow } + +/// How dry seed is physically conserved — affects longevity, kept separate from +/// *where* it is stored. Optional attribute of a seed [Lot]. Append-only by name. +/// +/// - `jarWithDesiccant` — sealed jar with a drying agent (bote con sílice). +/// - `glassJar` — plain glass jar (bote de cristal). +/// - `paperEnvelope` — paper envelope (sobre de papel). +/// - `paperBag` — paper bag (bolsa de papel). +/// - `plasticBag` — plastic bag (bolsa de plástico). +enum PreservationFormat { + jarWithDesiccant, + glassJar, + paperEnvelope, + paperBag, + plasticBag, +} + +/// State of the drying agent (silica gel) in a stored seed container, recorded +/// at a periodic condition check. The colour is a proxy for moisture: a +/// saturated agent means the seed is at risk. Append-only by name. +/// +/// - `none` — no drying agent present (no tiene). +/// - `add` — should add some (se pone). +/// - `replace` — saturated, replace it (se cambia). +/// - `dry` — indicating dry (azul/blue). +/// - `fresh` — just renewed (lila/violet). +enum DesiccantState { none, add, replace, dry, fresh } + /// Append-only event kinds on a Lot (data-model §2.4). enum MovementType { received, diff --git a/apps/app_seeds/lib/db/tables.dart b/apps/app_seeds/lib/db/tables.dart index ca0eeae..e496c04 100644 --- a/apps/app_seeds/lib/db/tables.dart +++ b/apps/app_seeds/lib/db/tables.dart @@ -12,6 +12,35 @@ class Varieties extends Table with SyncColumns { TextColumn get category => text().nullable()(); // free text, prefilled from family TextColumn get notes => text().nullable()(); + + /// A draft captured photo-first ("capture now, catalogue later"): it holds a + /// photo but not yet a real name, and lives in the "to catalogue" tray until + /// the user labels it. A plain LWW scalar, so it merges like any other field. + BoolColumn get isDraft => boolean().withDefault(const Constant(false))(); + + /// Grower-declared organic ("eco") provenance. A self-declaration, not a + /// third-party certification (that would be a separate flag). Surfaced as a + /// badge and an inventory filter. A plain LWW scalar. + BoolColumn get isOrganic => boolean().withDefault(const Constant(false))(); + + /// A stewardship intent set by the grower: "regrow this variety this season" + /// before its stock or vitality runs out. Complements the automatic viability + /// warning (which is age-derived) with an explicit human decision. Surfaced + /// as a badge and an inventory filter. A plain LWW scalar. + BoolColumn get needsReproduction => + boolean().withDefault(const Constant(false))(); + + /// Advisory crop-calendar months, typical for this variety — when to sow, + /// transplant, expect flowers/fruit, and harvest seed. Each phase usually + /// spans several months (e.g. sow in spring *and* autumn), so each is a set + /// of months packed as a 12-bit mask (see `domain/crop_calendar.dart`), an + /// optional LWW scalar; null means "not recorded". Guidance, not a per-year + /// actuals log (that path stays available via Movements). + IntColumn get sowMonths => integer().nullable()(); + IntColumn get transplantMonths => integer().nullable()(); + IntColumn get floweringMonths => integer().nullable()(); + IntColumn get fruitingMonths => integer().nullable()(); + IntColumn get seedHarvestMonths => integer().nullable()(); } /// The multiple common names of a Variety (separate table so concurrent adds @@ -30,6 +59,13 @@ class Species extends Table with SyncColumns { IntColumn get gbifKey => integer().nullable()(); TextColumn get family => text().nullable()(); BoolColumn get isBundled => boolean().withDefault(const Constant(false))(); + + /// Typical seed longevity in years under normal home storage — public-domain + /// reference data bundled with the catalog (agricultural-extension viability + /// tables). Drives the "expiring / past viability" warning on aging lots by + /// comparing against a lot's [Lots.harvestYear]. Nullable: unknown for + /// species without a bundled figure. + IntColumn get viabilityYears => integer().nullable()(); } /// Localized common names for the catalog (bundled). @@ -56,6 +92,23 @@ class Lots extends Table with SyncColumns { TextColumn get offerStatus => textEnum().withDefault(const Constant('private'))(); TextColumn get seedbankId => text().nullable()(); + + /// Provenance of this batch, kept as lightweight free text so it needs no + /// Party/Movement to record (the rigorous exchange path stays via Movements). + /// [originName] = who grew or gave the seeds; [originPlace] = where they come + /// from (with region/province). Both optional LWW scalars. + TextColumn get originName => text().nullable()(); + TextColumn get originPlace => text().nullable()(); + + /// Optional coarse "how much I have" for this lot — see [Abundance]. An + /// offline alternative to the precise Quantity columns; either, both, or + /// neither may be set. + TextColumn get abundance => textEnum().nullable()(); + + /// How the (seed) lot is physically conserved — see [PreservationFormat]. + /// Distinct from [storageLocation]. Optional. + TextColumn get preservationFormat => + textEnum().nullable()(); } /// Optional germination history for a Lot; percent is derived in code. @@ -67,6 +120,17 @@ class GerminationTests extends Table with SyncColumns { TextColumn get notes => text().nullable()(); } +/// Optional storage-condition history for a seed Lot: periodic physical checks +/// of how many containers hold it and the state of the drying agent. Mirrors +/// [GerminationTests] — a dated log under a Lot, newest shown first. +class ConditionChecks extends Table with SyncColumns { + TextColumn get lotId => text()(); + IntColumn get checkedOn => integer().nullable()(); // date, ms since epoch + IntColumn get containerCount => integer().nullable()(); // "botes" + TextColumn get desiccantState => textEnum().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()(); diff --git a/apps/app_seeds/lib/di/injector.dart b/apps/app_seeds/lib/di/injector.dart index dc35ab6..96e67f5 100644 --- a/apps/app_seeds/lib/di/injector.dart +++ b/apps/app_seeds/lib/di/injector.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:commons_core/commons_core.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; @@ -10,11 +12,16 @@ import '../data/species_repository.dart'; import '../data/variety_repository.dart'; import '../db/database.dart'; import '../db/encrypted_executor.dart'; +import '../i18n/strings.g.dart'; import '../security/secret_store.dart'; import '../security/secure_key_store.dart'; import '../services/export_import_service.dart'; import '../services/file_picker_file_service.dart'; import '../services/file_service.dart'; +import '../services/ocr/label_text_extractor.dart'; +import '../services/ocr/ocr_language.dart'; +import '../services/ocr/tesseract_label_extractor.dart'; +import '../services/onboarding_store.dart'; /// The app's service locator. Kept to the composition root — widgets get their /// repositories from here (or via BlocProvider), never by reaching into it deep @@ -24,7 +31,8 @@ final GetIt getIt = GetIt.instance; /// Wires the encrypted DB, keystore and repositories. Call once from `main` /// before `runApp`; the DB key must exist before the DB opens. Future configureDependencies() async { - final keyStore = SecureKeyStore(store: FlutterSecretStore()); + final secretStore = FlutterSecretStore(); + final keyStore = SecureKeyStore(store: secretStore); final dbKeyHex = await keyStore.databaseKeyHex(); final rootSeedHex = await keyStore.rootSeedHex(); @@ -48,12 +56,23 @@ Future configureDependencies() async { const fileService = FilePickerFileService(); + // OCR label suggestions only where a native Tesseract engine exists; every + // other platform (desktop, web) degrades to the no-op and hides the button. + // The language pack(s) follow the user's locale(s) — never a fixed region — + // limited to what we actually bundle, always unioned with the Latin baseline. + final LabelTextExtractor labelExtractor = + (Platform.isAndroid || Platform.isIOS) + ? TesseractLabelExtractor(language: await _resolveOcrLanguage()) + : const NoOpLabelTextExtractor(); + getIt ..registerSingleton(keyStore) ..registerSingleton(database) ..registerSingleton(speciesRepository) ..registerSingleton(varietyRepository) ..registerSingleton(fileService) + ..registerSingleton(labelExtractor) + ..registerSingleton(OnboardingStore(secretStore)) ..registerSingleton( ExportImportService(repository: varietyRepository, files: fileService), ); @@ -63,3 +82,18 @@ Future _databaseFile() async { final dir = await getApplicationDocumentsDirectory(); return File(p.join(dir.path, 'tane_inventory.sqlite')); } + +/// Picks the Tesseract language pack(s) for the current locale(s), limited to +/// the bundled packs listed in `tessdata_config.json`. Prefers the app's chosen +/// language, then the device's ordered locales. +Future _resolveOcrLanguage() async { + final available = parseAvailableOcrLanguages( + await rootBundle.loadString('assets/tessdata_config.json'), + ); + final preferred = [ + LocaleSettings.currentLocale.languageCode, + for (final l in WidgetsBinding.instance.platformDispatcher.locales) + l.languageCode, + ]; + return resolveOcrLanguages(preferredLocales: preferred, available: available); +} diff --git a/apps/app_seeds/lib/domain/crop_calendar.dart b/apps/app_seeds/lib/domain/crop_calendar.dart new file mode 100644 index 0000000..3fd645d --- /dev/null +++ b/apps/app_seeds/lib/domain/crop_calendar.dart @@ -0,0 +1,39 @@ +/// Crop-calendar months are stored compactly as a 12-bit mask in a single +/// nullable integer column (bit 0 = January … bit 11 = December). A phase can +/// happen in several months (e.g. sow lettuce in March, April and September), +/// so the calendar is a *set* of months, not one. `null` or `0` means "not +/// recorded". Pure functions — no I/O — so they are trivially testable. +library; + +/// Packs a set of 1..12 month numbers into a bitmask, or null when empty (so an +/// unset phase stays null in the DB rather than 0). Values outside 1..12 are +/// ignored. +int? monthsToMask(Iterable months) { + var mask = 0; + for (final m in months) { + if (m >= 1 && m <= 12) mask |= 1 << (m - 1); + } + return mask == 0 ? null : mask; +} + +/// Unpacks a month bitmask into an ascending list of 1..12 month numbers. +/// Returns an empty list for null or 0. +List maskToMonths(int? mask) { + if (mask == null || mask == 0) return const []; + return [ + for (var m = 1; m <= 12; m++) + if (mask & (1 << (m - 1)) != 0) m, + ]; +} + +/// Whether month [month] (1..12) is set in [mask]. +bool maskHasMonth(int? mask, int month) => + mask != null && month >= 1 && month <= 12 && mask & (1 << (month - 1)) != 0; + +/// Toggles month [month] (1..12) in [mask], returning the new mask (null when +/// the result is empty). +int? toggleMonth(int? mask, int month) { + if (month < 1 || month > 12) return mask; + final next = (mask ?? 0) ^ (1 << (month - 1)); + return next == 0 ? null : next; +} diff --git a/apps/app_seeds/lib/domain/seed_viability.dart b/apps/app_seeds/lib/domain/seed_viability.dart new file mode 100644 index 0000000..c669e00 --- /dev/null +++ b/apps/app_seeds/lib/domain/seed_viability.dart @@ -0,0 +1,41 @@ +/// Viability status of a seed lot, derived purely from its age versus the +/// species' typical seed longevity (bundled reference data). Turns passive +/// storage into active stewardship: it surfaces which lots to sow or regenerate +/// before they lapse, instead of just accumulating them. +enum SeedViability { + /// Comfortably within the species' typical viability window. + fresh, + + /// In the final year of the window — sow or reproduce this season. + expiringSoon, + + /// Past the typical viability window — germination is likely dropping fast. + expired, + + /// Not enough data to judge (no harvest year, or no reference figure). + unknown, +} + +/// Computes the viability status of a seed lot harvested in [harvestYear] for a +/// species whose typical longevity is [viabilityYears], as of [currentYear]. +/// +/// A conservative, age-based signal: the bundled figure is a single number, not +/// a decay curve, so this only distinguishes "fine / use soon / past it". +/// [expiringSoon] flags the last year of the window so the grower can prioritise +/// what to reproduce before it lapses. Any actual [GerminationTest] a grower +/// records remains the ground truth and is shown alongside this estimate. +SeedViability seedViability({ + required int? harvestYear, + required int? viabilityYears, + required int currentYear, +}) { + if (harvestYear == null || viabilityYears == null || viabilityYears <= 0) { + return SeedViability.unknown; + } + final age = currentYear - harvestYear; + // A harvest stamped in the future is treated as fresh, not expired. + if (age < 0) return SeedViability.fresh; + if (age >= viabilityYears) return SeedViability.expired; + if (age >= viabilityYears - 1) return SeedViability.expiringSoon; + return SeedViability.fresh; +} diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index d1369a3..98c155a 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -44,24 +44,24 @@ "aboutOpen": "About Tanemaki" }, "backup": { - "title": "Backup", - "exportCsv": "Export as CSV", - "exportCsvSubtitle": "For spreadsheets — no photos", - "exportJson": "Export as JSON", - "exportJsonSubtitle": "Complete copy, can be imported back", - "importJson": "Import from JSON", - "importJsonSubtitle": "Merge a saved copy into this inventory", - "importCsv": "Import from CSV", - "importCsvSubtitle": "Add a list from a spreadsheet", - "importConfirmTitle": "Import a saved copy?", + "title": "Backup & restore", + "exportJson": "Save a backup", + "exportJsonSubtitle": "A complete copy to keep safe, restore later or move to another device", + "importJson": "Restore a backup", + "importJsonSubtitle": "Bring a saved copy back — nothing gets duplicated", + "exportCsv": "Export to a spreadsheet", + "exportCsvSubtitle": "A simple list for Excel or LibreOffice — no photos", + "importCsv": "Import a list", + "importCsvSubtitle": "Add entries from a spreadsheet", + "importConfirmTitle": "Restore a backup?", "importConfirmBody": "Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.", - "importCsvConfirmTitle": "Import a CSV list?", + "importCsvConfirmTitle": "Import a list?", "importCsvConfirmBody": "Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.", "importAction": "Import", "exportSaved": "Copy saved", "cancelled": "Cancelled", "importDone": "Imported: {added} new, {updated} updated", - "importCsvDone": "Imported {count} from CSV", + "importCsvDone": "Added {count} entries", "importFailed": "This file could not be read as a Tanemaki copy", "failed": "Something went wrong" }, @@ -112,7 +112,19 @@ "empty": "No seeds yet. Tap + to add your first.", "noMatches": "No seeds match your filters.", "clearFilters": "Clear filters", - "uncategorized": "Uncategorized" + "uncategorized": "Uncategorized", + "needsReproductionFilter": "To regrow" + }, + "draft": { + "capture": "Capture photos", + "captured": "{n} captured to catalogue", + "triageTitle": "To catalogue", + "triageCount": "{n} to catalogue", + "untitled": "Unnamed", + "nameField": "Name this seed", + "nameHint": "What is it?", + "suggestFromPhoto": "Suggest name from photo", + "discard": "Discard" }, "quickAdd": { "title": "Add a seed", @@ -151,13 +163,21 @@ "none": "No germination tests yet.", "result": "{percent}%" }, + "viability": { + "expiringSoon": "Use or reproduce this season", + "expiringSoonYears": "Use or reproduce this season · keeps ~{years} yr", + "expired": "Past typical viability — reproduce", + "expiredYears": "Past typical viability (~{years} yr) — reproduce" + }, "editVariety": { "title": "Edit seed", "name": "Name", "category": "Category", "notes": "Notes", "species": "Species (from catalog)", - "speciesHint": "Search a species…" + "speciesHint": "Search a species…", + "organic": "Organic", + "organicHint": "Grown organically (eco)" }, "addLot": { "title": "Add lot", @@ -191,6 +211,65 @@ "bareRoot": "Bare-root", "rootBall": "Root-ball" }, + "provenance": { + "section": "Where it's from", + "seedsFrom": "Seeds from", + "seedsFromHint": "Who grew or gave them", + "place": "Place", + "placeHint": "Where they come from (with region)", + "addSeedsFrom": "Seeds from", + "addPlace": "Place" + }, + "abundance": { + "add": "How much I have", + "title": "How much I have", + "none": "Not set", + "plentyToShare": "Plenty to share", + "enoughToShare": "Enough to share a little", + "enoughForMe": "Enough for me", + "runningLow": "Running low" + }, + "cropCalendar": { + "add": "Crop calendar", + "title": "Crop calendar", + "sow": "Sow", + "transplant": "Transplant", + "flowering": "Flowering", + "fruiting": "Fruiting", + "seedHarvest": "Seed harvest", + "unset": "—" + }, + "needsReproduction": { + "label": "To regrow this season", + "hint": "Grow it out before the seed runs out", + "badge": "To regrow" + }, + "preservation": { + "add": "How it's kept", + "title": "How it's kept", + "none": "Unspecified", + "jarWithDesiccant": "Jar with drying agent", + "glassJar": "Glass jar", + "paperEnvelope": "Paper envelope", + "paperBag": "Paper bag", + "plasticBag": "Plastic bag" + }, + "conditionCheck": { + "advanced": "Storage & seed-bank details", + "title": "Storage checks", + "add": "Add check", + "containers": "Jars / containers", + "desiccant": "Drying agent", + "none": "No storage checks yet.", + "summary": "{count} jar(s) · {state}" + }, + "desiccant": { + "none": "None", + "add": "Add some", + "replace": "Replace it", + "dry": "Blue — dry", + "fresh": "Just renewed" + }, "unit": { "aFew": "a few", "some": "some", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 4a0570d..7645894 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -45,23 +45,23 @@ }, "backup": { "title": "Copia de seguridad", - "exportCsv": "Exportar a CSV", - "exportCsvSubtitle": "Para hojas de cálculo — sin fotos", - "exportJson": "Exportar a JSON", - "exportJsonSubtitle": "Copia completa, se puede volver a importar", - "importJson": "Importar desde JSON", - "importJsonSubtitle": "Fusiona una copia guardada con este inventario", - "importCsv": "Importar desde CSV", - "importCsvSubtitle": "Añade una lista desde una hoja de cálculo", - "importConfirmTitle": "¿Importar una copia guardada?", + "exportJson": "Guardar una copia de seguridad", + "exportJsonSubtitle": "Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo", + "importJson": "Restaurar una copia", + "importJsonSubtitle": "Recupera una copia guardada — no se duplica nada", + "exportCsv": "Exportar a una hoja de cálculo", + "exportCsvSubtitle": "Una lista sencilla para Excel o LibreOffice — sin fotos", + "importCsv": "Importar una lista", + "importCsvSubtitle": "Añade entradas desde una hoja de cálculo", + "importConfirmTitle": "¿Restaurar una copia?", "importConfirmBody": "Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.", - "importCsvConfirmTitle": "¿Importar una lista CSV?", + "importCsvConfirmTitle": "¿Importar una lista?", "importCsvConfirmBody": "Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.", "importAction": "Importar", "exportSaved": "Copia guardada", "cancelled": "Cancelado", "importDone": "Importado: {added} nuevas, {updated} actualizadas", - "importCsvDone": "Importadas {count} desde CSV", + "importCsvDone": "Añadidas {count} entradas", "importFailed": "Este fichero no se pudo leer como una copia de Tanemaki", "failed": "Algo ha salido mal" }, @@ -112,7 +112,19 @@ "empty": "Aún no hay semillas. Toca + para añadir la primera.", "noMatches": "Ninguna semilla coincide con los filtros.", "clearFilters": "Quitar filtros", - "uncategorized": "Sin categoría" + "uncategorized": "Sin categoría", + "needsReproductionFilter": "Por reproducir" + }, + "draft": { + "capture": "Capturar fotos", + "captured": "{n} capturadas por catalogar", + "triageTitle": "Por catalogar", + "triageCount": "{n} por catalogar", + "untitled": "Sin nombre", + "nameField": "Nombra esta semilla", + "nameHint": "¿Qué es?", + "suggestFromPhoto": "Sugerir nombre de la foto", + "discard": "Descartar" }, "quickAdd": { "title": "Añadir una semilla", @@ -151,13 +163,21 @@ "none": "Aún no hay pruebas de germinación.", "result": "{percent}%" }, + "viability": { + "expiringSoon": "Úsala o multiplícala esta temporada", + "expiringSoonYears": "Úsala o multiplícala esta temporada · dura ~{years} años", + "expired": "Supera su viabilidad típica — a multiplicar", + "expiredYears": "Supera su viabilidad típica (~{years} años) — a multiplicar" + }, "editVariety": { "title": "Editar semilla", "name": "Nombre", "category": "Categoría", "notes": "Notas", "species": "Especie (del catálogo)", - "speciesHint": "Buscar una especie…" + "speciesHint": "Buscar una especie…", + "organic": "Ecológica", + "organicHint": "Cultivada de forma ecológica (eco)" }, "addLot": { "title": "Añadir lote", @@ -191,6 +211,65 @@ "bareRoot": "Raíz desnuda", "rootBall": "Cepellón" }, + "provenance": { + "section": "De dónde viene", + "seedsFrom": "Semillas de", + "seedsFromHint": "Quién las cultivó o las dio", + "place": "Lugar", + "placeHint": "De dónde vienen (con provincia)", + "addSeedsFrom": "Semillas de", + "addPlace": "Lugar" + }, + "abundance": { + "add": "Cuánta tengo", + "title": "Cuánta tengo", + "none": "Sin indicar", + "plentyToShare": "De sobra para compartir", + "enoughToShare": "Bastante, para compartir con moderación", + "enoughForMe": "Suficiente para mí", + "runningLow": "Queda poca" + }, + "cropCalendar": { + "add": "Calendario de cultivo", + "title": "Calendario de cultivo", + "sow": "Siembra", + "transplant": "Trasplante", + "flowering": "Floración", + "fruiting": "Fructificación", + "seedHarvest": "Cosecha de semilla", + "unset": "—" + }, + "needsReproduction": { + "label": "Reproducir esta temporada", + "hint": "Cultívala antes de que se acabe la semilla", + "badge": "Por reproducir" + }, + "preservation": { + "add": "Cómo se conserva", + "title": "Cómo se conserva", + "none": "Sin especificar", + "jarWithDesiccant": "Bote con desecante", + "glassJar": "Bote de cristal", + "paperEnvelope": "Sobre de papel", + "paperBag": "Bolsa de papel", + "plasticBag": "Bolsa de plástico" + }, + "conditionCheck": { + "advanced": "Conservación y detalles del banco", + "title": "Revisiones de conservación", + "add": "Añadir revisión", + "containers": "Botes / recipientes", + "desiccant": "Desecante", + "none": "Aún no hay revisiones.", + "summary": "{count} bote(s) · {state}" + }, + "desiccant": { + "none": "No tiene", + "add": "Se pone", + "replace": "Se cambia", + "dry": "Azul — seco", + "fresh": "Recién puesto" + }, "unit": { "aFew": "unas pocas", "some": "algunas", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 32c5872..48268b8 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 408 (204 per locale) +/// Strings: 530 (265 per locale) /// -/// Built on 2026-07-09 at 12:44 UTC +/// Built on 2026-07-09 at 17:18 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index ec54931..cd98732 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -50,14 +50,23 @@ class Translations with BaseTranslations { late final Translations$about$en about = Translations$about$en.internal(_root); late final Translations$intro$en intro = Translations$intro$en.internal(_root); late final Translations$inventory$en inventory = Translations$inventory$en.internal(_root); + late final Translations$draft$en draft = Translations$draft$en.internal(_root); late final Translations$quickAdd$en quickAdd = Translations$quickAdd$en.internal(_root); late final Translations$detail$en detail = Translations$detail$en.internal(_root); late final Translations$germination$en germination = Translations$germination$en.internal(_root); + late final Translations$viability$en viability = Translations$viability$en.internal(_root); late final Translations$editVariety$en editVariety = Translations$editVariety$en.internal(_root); late final Translations$addLot$en addLot = Translations$addLot$en.internal(_root); late final Translations$harvest$en harvest = Translations$harvest$en.internal(_root); late final Translations$lotType$en lotType = Translations$lotType$en.internal(_root); late final Translations$presentation$en presentation = Translations$presentation$en.internal(_root); + late final Translations$provenance$en provenance = Translations$provenance$en.internal(_root); + late final Translations$abundance$en abundance = Translations$abundance$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); + late final Translations$conditionCheck$en conditionCheck = Translations$conditionCheck$en.internal(_root); + late final Translations$desiccant$en desiccant = Translations$desiccant$en.internal(_root); late final Translations$unit$en unit = Translations$unit$en.internal(_root); } @@ -219,41 +228,41 @@ class Translations$backup$en { // Translations - /// en: 'Backup' - String get title => 'Backup'; + /// en: 'Backup & restore' + String get title => 'Backup & restore'; - /// en: 'Export as CSV' - String get exportCsv => 'Export as CSV'; + /// en: 'Save a backup' + String get exportJson => 'Save a backup'; - /// en: 'For spreadsheets — no photos' - String get exportCsvSubtitle => 'For spreadsheets — no photos'; + /// en: 'A complete copy to keep safe, restore later or move to another device' + String get exportJsonSubtitle => 'A complete copy to keep safe, restore later or move to another device'; - /// en: 'Export as JSON' - String get exportJson => 'Export as JSON'; + /// en: 'Restore a backup' + String get importJson => 'Restore a backup'; - /// en: 'Complete copy, can be imported back' - String get exportJsonSubtitle => 'Complete copy, can be imported back'; + /// en: 'Bring a saved copy back — nothing gets duplicated' + String get importJsonSubtitle => 'Bring a saved copy back — nothing gets duplicated'; - /// en: 'Import from JSON' - String get importJson => 'Import from JSON'; + /// en: 'Export to a spreadsheet' + String get exportCsv => 'Export to a spreadsheet'; - /// en: 'Merge a saved copy into this inventory' - String get importJsonSubtitle => 'Merge a saved copy into this inventory'; + /// en: 'A simple list for Excel or LibreOffice — no photos' + String get exportCsvSubtitle => 'A simple list for Excel or LibreOffice — no photos'; - /// en: 'Import from CSV' - String get importCsv => 'Import from CSV'; + /// en: 'Import a list' + String get importCsv => 'Import a list'; - /// en: 'Add a list from a spreadsheet' - String get importCsvSubtitle => 'Add a list from a spreadsheet'; + /// en: 'Add entries from a spreadsheet' + String get importCsvSubtitle => 'Add entries from a spreadsheet'; - /// en: 'Import a saved copy?' - String get importConfirmTitle => 'Import a saved copy?'; + /// en: 'Restore a backup?' + String get importConfirmTitle => 'Restore a backup?'; /// en: 'Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.' String get importConfirmBody => 'Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.'; - /// en: 'Import a CSV list?' - String get importCsvConfirmTitle => 'Import a CSV list?'; + /// en: 'Import a list?' + String get importCsvConfirmTitle => 'Import a list?'; /// en: 'Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.' String get importCsvConfirmBody => 'Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.'; @@ -270,8 +279,8 @@ class Translations$backup$en { /// en: 'Imported: {added} new, {updated} updated' String importDone({required Object added, required Object updated}) => 'Imported: ${added} new, ${updated} updated'; - /// en: 'Imported {count} from CSV' - String importCsvDone({required Object count}) => 'Imported ${count} from CSV'; + /// en: 'Added {count} entries' + String importCsvDone({required Object count}) => 'Added ${count} entries'; /// en: 'This file could not be read as a Tanemaki copy' String get importFailed => 'This file could not be read as a Tanemaki copy'; @@ -370,6 +379,45 @@ class Translations$inventory$en { /// en: 'Uncategorized' String get uncategorized => 'Uncategorized'; + + /// en: 'To regrow' + String get needsReproductionFilter => 'To regrow'; +} + +// Path: draft +class Translations$draft$en { + Translations$draft$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Capture photos' + String get capture => 'Capture photos'; + + /// en: '{n} captured to catalogue' + String captured({required Object n}) => '${n} captured to catalogue'; + + /// en: 'To catalogue' + String get triageTitle => 'To catalogue'; + + /// en: '{n} to catalogue' + String triageCount({required Object n}) => '${n} to catalogue'; + + /// en: 'Unnamed' + String get untitled => 'Unnamed'; + + /// en: 'Name this seed' + String get nameField => 'Name this seed'; + + /// en: 'What is it?' + String get nameHint => 'What is it?'; + + /// en: 'Suggest name from photo' + String get suggestFromPhoto => 'Suggest name from photo'; + + /// en: 'Discard' + String get discard => 'Discard'; } // Path: quickAdd @@ -492,6 +540,27 @@ class Translations$germination$en { String result({required Object percent}) => '${percent}%'; } +// Path: viability +class Translations$viability$en { + Translations$viability$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Use or reproduce this season' + String get expiringSoon => 'Use or reproduce this season'; + + /// en: 'Use or reproduce this season · keeps ~{years} yr' + String expiringSoonYears({required Object years}) => 'Use or reproduce this season · keeps ~${years} yr'; + + /// en: 'Past typical viability — reproduce' + String get expired => 'Past typical viability — reproduce'; + + /// en: 'Past typical viability (~{years} yr) — reproduce' + String expiredYears({required Object years}) => 'Past typical viability (~${years} yr) — reproduce'; +} + // Path: editVariety class Translations$editVariety$en { Translations$editVariety$en.internal(this._root); @@ -517,6 +586,12 @@ class Translations$editVariety$en { /// en: 'Search a species…' String get speciesHint => 'Search a species…'; + + /// en: 'Organic' + String get organic => 'Organic'; + + /// en: 'Grown organically (eco)' + String get organicHint => 'Grown organically (eco)'; } // Path: addLot @@ -630,6 +705,204 @@ class Translations$presentation$en { String get rootBall => 'Root-ball'; } +// Path: provenance +class Translations$provenance$en { + Translations$provenance$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Where it's from' + String get section => 'Where it\'s from'; + + /// en: 'Seeds from' + String get seedsFrom => 'Seeds from'; + + /// en: 'Who grew or gave them' + String get seedsFromHint => 'Who grew or gave them'; + + /// en: 'Place' + String get place => 'Place'; + + /// en: 'Where they come from (with region)' + String get placeHint => 'Where they come from (with region)'; + + /// en: 'Seeds from' + String get addSeedsFrom => 'Seeds from'; + + /// en: 'Place' + String get addPlace => 'Place'; +} + +// Path: abundance +class Translations$abundance$en { + Translations$abundance$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'How much I have' + String get add => 'How much I have'; + + /// en: 'How much I have' + String get title => 'How much I have'; + + /// en: 'Not set' + String get none => 'Not set'; + + /// en: 'Plenty to share' + String get plentyToShare => 'Plenty to share'; + + /// en: 'Enough to share a little' + String get enoughToShare => 'Enough to share a little'; + + /// en: 'Enough for me' + String get enoughForMe => 'Enough for me'; + + /// en: 'Running low' + String get runningLow => 'Running low'; +} + +// Path: cropCalendar +class Translations$cropCalendar$en { + Translations$cropCalendar$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Crop calendar' + String get add => 'Crop calendar'; + + /// en: 'Crop calendar' + String get title => 'Crop calendar'; + + /// en: 'Sow' + String get sow => 'Sow'; + + /// en: 'Transplant' + String get transplant => 'Transplant'; + + /// en: 'Flowering' + String get flowering => 'Flowering'; + + /// en: 'Fruiting' + String get fruiting => 'Fruiting'; + + /// en: 'Seed harvest' + String get seedHarvest => 'Seed harvest'; + + /// en: '—' + String get unset => '—'; +} + +// Path: needsReproduction +class Translations$needsReproduction$en { + Translations$needsReproduction$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'To regrow this season' + String get label => 'To regrow this season'; + + /// en: 'Grow it out before the seed runs out' + String get hint => 'Grow it out before the seed runs out'; + + /// en: 'To regrow' + String get badge => 'To regrow'; +} + +// Path: preservation +class Translations$preservation$en { + Translations$preservation$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'How it's kept' + String get add => 'How it\'s kept'; + + /// en: 'How it's kept' + String get title => 'How it\'s kept'; + + /// en: 'Unspecified' + String get none => 'Unspecified'; + + /// en: 'Jar with drying agent' + String get jarWithDesiccant => 'Jar with drying agent'; + + /// en: 'Glass jar' + String get glassJar => 'Glass jar'; + + /// en: 'Paper envelope' + String get paperEnvelope => 'Paper envelope'; + + /// en: 'Paper bag' + String get paperBag => 'Paper bag'; + + /// en: 'Plastic bag' + String get plasticBag => 'Plastic bag'; +} + +// Path: conditionCheck +class Translations$conditionCheck$en { + Translations$conditionCheck$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Storage & seed-bank details' + String get advanced => 'Storage & seed-bank details'; + + /// en: 'Storage checks' + String get title => 'Storage checks'; + + /// en: 'Add check' + String get add => 'Add check'; + + /// en: 'Jars / containers' + String get containers => 'Jars / containers'; + + /// en: 'Drying agent' + String get desiccant => 'Drying agent'; + + /// en: 'No storage checks yet.' + String get none => 'No storage checks yet.'; + + /// en: '{count} jar(s) · {state}' + String summary({required Object count, required Object state}) => '${count} jar(s) · ${state}'; +} + +// Path: desiccant +class Translations$desiccant$en { + Translations$desiccant$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'None' + String get none => 'None'; + + /// en: 'Add some' + String get add => 'Add some'; + + /// en: 'Replace it' + String get replace => 'Replace it'; + + /// en: 'Blue — dry' + String get dry => 'Blue — dry'; + + /// en: 'Just renewed' + String get fresh => 'Just renewed'; +} + // Path: unit class Translations$unit$en { Translations$unit$en.internal(this._root); @@ -1165,24 +1438,24 @@ extension on Translations { 'settings.about' => 'About', 'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.', 'settings.aboutOpen' => 'About Tanemaki', - 'backup.title' => 'Backup', - 'backup.exportCsv' => 'Export as CSV', - 'backup.exportCsvSubtitle' => 'For spreadsheets — no photos', - 'backup.exportJson' => 'Export as JSON', - 'backup.exportJsonSubtitle' => 'Complete copy, can be imported back', - 'backup.importJson' => 'Import from JSON', - 'backup.importJsonSubtitle' => 'Merge a saved copy into this inventory', - 'backup.importCsv' => 'Import from CSV', - 'backup.importCsvSubtitle' => 'Add a list from a spreadsheet', - 'backup.importConfirmTitle' => 'Import a saved copy?', + 'backup.title' => 'Backup & restore', + 'backup.exportJson' => 'Save a backup', + 'backup.exportJsonSubtitle' => 'A complete copy to keep safe, restore later or move to another device', + 'backup.importJson' => 'Restore a backup', + 'backup.importJsonSubtitle' => 'Bring a saved copy back — nothing gets duplicated', + 'backup.exportCsv' => 'Export to a spreadsheet', + 'backup.exportCsvSubtitle' => 'A simple list for Excel or LibreOffice — no photos', + 'backup.importCsv' => 'Import a list', + 'backup.importCsvSubtitle' => 'Add entries from a spreadsheet', + 'backup.importConfirmTitle' => 'Restore a backup?', 'backup.importConfirmBody' => 'Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.', - 'backup.importCsvConfirmTitle' => 'Import a CSV list?', + 'backup.importCsvConfirmTitle' => 'Import a list?', 'backup.importCsvConfirmBody' => 'Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.', 'backup.importAction' => 'Import', 'backup.exportSaved' => 'Copy saved', 'backup.cancelled' => 'Cancelled', 'backup.importDone' => ({required Object added, required Object updated}) => 'Imported: ${added} new, ${updated} updated', - 'backup.importCsvDone' => ({required Object count}) => 'Imported ${count} from CSV', + 'backup.importCsvDone' => ({required Object count}) => 'Added ${count} entries', 'backup.importFailed' => 'This file could not be read as a Tanemaki copy', 'backup.failed' => 'Something went wrong', 'about.title' => 'About', @@ -1216,6 +1489,16 @@ extension on Translations { 'inventory.noMatches' => 'No seeds match your filters.', 'inventory.clearFilters' => 'Clear filters', 'inventory.uncategorized' => 'Uncategorized', + 'inventory.needsReproductionFilter' => 'To regrow', + 'draft.capture' => 'Capture photos', + 'draft.captured' => ({required Object n}) => '${n} captured to catalogue', + 'draft.triageTitle' => 'To catalogue', + 'draft.triageCount' => ({required Object n}) => '${n} to catalogue', + 'draft.untitled' => 'Unnamed', + 'draft.nameField' => 'Name this seed', + 'draft.nameHint' => 'What is it?', + 'draft.suggestFromPhoto' => 'Suggest name from photo', + 'draft.discard' => 'Discard', 'quickAdd.title' => 'Add a seed', 'quickAdd.labelField' => 'Name', 'quickAdd.labelRequired' => 'Give it a name', @@ -1247,12 +1530,18 @@ extension on Translations { 'germination.germinated' => 'Germinated', 'germination.none' => 'No germination tests yet.', 'germination.result' => ({required Object percent}) => '${percent}%', + 'viability.expiringSoon' => 'Use or reproduce this season', + 'viability.expiringSoonYears' => ({required Object years}) => 'Use or reproduce this season · keeps ~${years} yr', + 'viability.expired' => 'Past typical viability — reproduce', + 'viability.expiredYears' => ({required Object years}) => 'Past typical viability (~${years} yr) — reproduce', 'editVariety.title' => 'Edit seed', 'editVariety.name' => 'Name', 'editVariety.category' => 'Category', 'editVariety.notes' => 'Notes', 'editVariety.species' => 'Species (from catalog)', 'editVariety.speciesHint' => 'Search a species…', + 'editVariety.organic' => 'Organic', + 'editVariety.organicHint' => 'Grown organically (eco)', 'addLot.title' => 'Add lot', 'addLot.year' => 'Harvest date', 'addLot.quantity' => 'How much?', @@ -1285,6 +1574,51 @@ extension on Translations { 'presentation.plug' => 'Plug', 'presentation.bareRoot' => 'Bare-root', 'presentation.rootBall' => 'Root-ball', + 'provenance.section' => 'Where it\'s from', + 'provenance.seedsFrom' => 'Seeds from', + 'provenance.seedsFromHint' => 'Who grew or gave them', + 'provenance.place' => 'Place', + 'provenance.placeHint' => 'Where they come from (with region)', + 'provenance.addSeedsFrom' => 'Seeds from', + 'provenance.addPlace' => 'Place', + 'abundance.add' => 'How much I have', + 'abundance.title' => 'How much I have', + 'abundance.none' => 'Not set', + 'abundance.plentyToShare' => 'Plenty to share', + 'abundance.enoughToShare' => 'Enough to share a little', + 'abundance.enoughForMe' => 'Enough for me', + 'abundance.runningLow' => 'Running low', + 'cropCalendar.add' => 'Crop calendar', + 'cropCalendar.title' => 'Crop calendar', + 'cropCalendar.sow' => 'Sow', + 'cropCalendar.transplant' => 'Transplant', + 'cropCalendar.flowering' => 'Flowering', + 'cropCalendar.fruiting' => 'Fruiting', + 'cropCalendar.seedHarvest' => 'Seed harvest', + 'cropCalendar.unset' => '—', + 'needsReproduction.label' => 'To regrow this season', + 'needsReproduction.hint' => 'Grow it out before the seed runs out', + 'needsReproduction.badge' => 'To regrow', + 'preservation.add' => 'How it\'s kept', + 'preservation.title' => 'How it\'s kept', + 'preservation.none' => 'Unspecified', + 'preservation.jarWithDesiccant' => 'Jar with drying agent', + 'preservation.glassJar' => 'Glass jar', + 'preservation.paperEnvelope' => 'Paper envelope', + 'preservation.paperBag' => 'Paper bag', + 'preservation.plasticBag' => 'Plastic bag', + 'conditionCheck.advanced' => 'Storage & seed-bank details', + 'conditionCheck.title' => 'Storage checks', + 'conditionCheck.add' => 'Add check', + 'conditionCheck.containers' => 'Jars / containers', + 'conditionCheck.desiccant' => 'Drying agent', + 'conditionCheck.none' => 'No storage checks yet.', + 'conditionCheck.summary' => ({required Object count, required Object state}) => '${count} jar(s) · ${state}', + 'desiccant.none' => 'None', + 'desiccant.add' => 'Add some', + 'desiccant.replace' => 'Replace it', + 'desiccant.dry' => 'Blue — dry', + 'desiccant.fresh' => 'Just renewed', 'unit.aFew' => 'a few', 'unit.some' => 'some', 'unit.plenty' => 'plenty', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index 3c5b2f8..5ccb0fe 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -49,14 +49,23 @@ class TranslationsEs extends Translations with BaseTranslations 'Copia de seguridad'; - @override String get exportCsv => 'Exportar a CSV'; - @override String get exportCsvSubtitle => 'Para hojas de cálculo — sin fotos'; - @override String get exportJson => 'Exportar a JSON'; - @override String get exportJsonSubtitle => 'Copia completa, se puede volver a importar'; - @override String get importJson => 'Importar desde JSON'; - @override String get importJsonSubtitle => 'Fusiona una copia guardada con este inventario'; - @override String get importCsv => 'Importar desde CSV'; - @override String get importCsvSubtitle => 'Añade una lista desde una hoja de cálculo'; - @override String get importConfirmTitle => '¿Importar una copia guardada?'; + @override String get exportJson => 'Guardar una copia de seguridad'; + @override String get exportJsonSubtitle => 'Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo'; + @override String get importJson => 'Restaurar una copia'; + @override String get importJsonSubtitle => 'Recupera una copia guardada — no se duplica nada'; + @override String get exportCsv => 'Exportar a una hoja de cálculo'; + @override String get exportCsvSubtitle => 'Una lista sencilla para Excel o LibreOffice — sin fotos'; + @override String get importCsv => 'Importar una lista'; + @override String get importCsvSubtitle => 'Añade entradas desde una hoja de cálculo'; + @override String get importConfirmTitle => '¿Restaurar una copia?'; @override String get importConfirmBody => 'Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.'; - @override String get importCsvConfirmTitle => '¿Importar una lista CSV?'; + @override String get importCsvConfirmTitle => '¿Importar una lista?'; @override String get importCsvConfirmBody => 'Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.'; @override String get importAction => 'Importar'; @override String get exportSaved => 'Copia guardada'; @override String get cancelled => 'Cancelado'; @override String importDone({required Object added, required Object updated}) => 'Importado: ${added} nuevas, ${updated} actualizadas'; - @override String importCsvDone({required Object count}) => 'Importadas ${count} desde CSV'; + @override String importCsvDone({required Object count}) => 'Añadidas ${count} entradas'; @override String get importFailed => 'Este fichero no se pudo leer como una copia de Tanemaki'; @override String get failed => 'Algo ha salido mal'; } @@ -222,6 +231,25 @@ class _Translations$inventory$es extends Translations$inventory$en { @override String get noMatches => 'Ninguna semilla coincide con los filtros.'; @override String get clearFilters => 'Quitar filtros'; @override String get uncategorized => 'Sin categoría'; + @override String get needsReproductionFilter => 'Por reproducir'; +} + +// Path: draft +class _Translations$draft$es extends Translations$draft$en { + _Translations$draft$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get capture => 'Capturar fotos'; + @override String captured({required Object n}) => '${n} capturadas por catalogar'; + @override String get triageTitle => 'Por catalogar'; + @override String triageCount({required Object n}) => '${n} por catalogar'; + @override String get untitled => 'Sin nombre'; + @override String get nameField => 'Nombra esta semilla'; + @override String get nameHint => '¿Qué es?'; + @override String get suggestFromPhoto => 'Sugerir nombre de la foto'; + @override String get discard => 'Descartar'; } // Path: quickAdd @@ -282,6 +310,19 @@ class _Translations$germination$es extends Translations$germination$en { @override String result({required Object percent}) => '${percent}%'; } +// Path: viability +class _Translations$viability$es extends Translations$viability$en { + _Translations$viability$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get expiringSoon => 'Úsala o multiplícala esta temporada'; + @override String expiringSoonYears({required Object years}) => 'Úsala o multiplícala esta temporada · dura ~${years} años'; + @override String get expired => 'Supera su viabilidad típica — a multiplicar'; + @override String expiredYears({required Object years}) => 'Supera su viabilidad típica (~${years} años) — a multiplicar'; +} + // Path: editVariety class _Translations$editVariety$es extends Translations$editVariety$en { _Translations$editVariety$es._(TranslationsEs root) : this._root = root, super.internal(root); @@ -295,6 +336,8 @@ class _Translations$editVariety$es extends Translations$editVariety$en { @override String get notes => 'Notas'; @override String get species => 'Especie (del catálogo)'; @override String get speciesHint => 'Buscar una especie…'; + @override String get organic => 'Ecológica'; + @override String get organicHint => 'Cultivada de forma ecológica (eco)'; } // Path: addLot @@ -367,6 +410,114 @@ class _Translations$presentation$es extends Translations$presentation$en { @override String get rootBall => 'Cepellón'; } +// Path: provenance +class _Translations$provenance$es extends Translations$provenance$en { + _Translations$provenance$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get section => 'De dónde viene'; + @override String get seedsFrom => 'Semillas de'; + @override String get seedsFromHint => 'Quién las cultivó o las dio'; + @override String get place => 'Lugar'; + @override String get placeHint => 'De dónde vienen (con provincia)'; + @override String get addSeedsFrom => 'Semillas de'; + @override String get addPlace => 'Lugar'; +} + +// Path: abundance +class _Translations$abundance$es extends Translations$abundance$en { + _Translations$abundance$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get add => 'Cuánta tengo'; + @override String get title => 'Cuánta tengo'; + @override String get none => 'Sin indicar'; + @override String get plentyToShare => 'De sobra para compartir'; + @override String get enoughToShare => 'Bastante, para compartir con moderación'; + @override String get enoughForMe => 'Suficiente para mí'; + @override String get runningLow => 'Queda poca'; +} + +// Path: cropCalendar +class _Translations$cropCalendar$es extends Translations$cropCalendar$en { + _Translations$cropCalendar$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get add => 'Calendario de cultivo'; + @override String get title => 'Calendario de cultivo'; + @override String get sow => 'Siembra'; + @override String get transplant => 'Trasplante'; + @override String get flowering => 'Floración'; + @override String get fruiting => 'Fructificación'; + @override String get seedHarvest => 'Cosecha de semilla'; + @override String get unset => '—'; +} + +// Path: needsReproduction +class _Translations$needsReproduction$es extends Translations$needsReproduction$en { + _Translations$needsReproduction$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get label => 'Reproducir esta temporada'; + @override String get hint => 'Cultívala antes de que se acabe la semilla'; + @override String get badge => 'Por reproducir'; +} + +// Path: preservation +class _Translations$preservation$es extends Translations$preservation$en { + _Translations$preservation$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get add => 'Cómo se conserva'; + @override String get title => 'Cómo se conserva'; + @override String get none => 'Sin especificar'; + @override String get jarWithDesiccant => 'Bote con desecante'; + @override String get glassJar => 'Bote de cristal'; + @override String get paperEnvelope => 'Sobre de papel'; + @override String get paperBag => 'Bolsa de papel'; + @override String get plasticBag => 'Bolsa de plástico'; +} + +// Path: conditionCheck +class _Translations$conditionCheck$es extends Translations$conditionCheck$en { + _Translations$conditionCheck$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get advanced => 'Conservación y detalles del banco'; + @override String get title => 'Revisiones de conservación'; + @override String get add => 'Añadir revisión'; + @override String get containers => 'Botes / recipientes'; + @override String get desiccant => 'Desecante'; + @override String get none => 'Aún no hay revisiones.'; + @override String summary({required Object count, required Object state}) => '${count} bote(s) · ${state}'; +} + +// Path: desiccant +class _Translations$desiccant$es extends Translations$desiccant$en { + _Translations$desiccant$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get none => 'No tiene'; + @override String get add => 'Se pone'; + @override String get replace => 'Se cambia'; + @override String get dry => 'Azul — seco'; + @override String get fresh => 'Recién puesto'; +} + // Path: unit class _Translations$unit$es extends Translations$unit$en { _Translations$unit$es._(TranslationsEs root) : this._root = root, super.internal(root); @@ -778,23 +929,23 @@ extension on TranslationsEs { 'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.', 'settings.aboutOpen' => 'Acerca de Tanemaki', 'backup.title' => 'Copia de seguridad', - 'backup.exportCsv' => 'Exportar a CSV', - 'backup.exportCsvSubtitle' => 'Para hojas de cálculo — sin fotos', - 'backup.exportJson' => 'Exportar a JSON', - 'backup.exportJsonSubtitle' => 'Copia completa, se puede volver a importar', - 'backup.importJson' => 'Importar desde JSON', - 'backup.importJsonSubtitle' => 'Fusiona una copia guardada con este inventario', - 'backup.importCsv' => 'Importar desde CSV', - 'backup.importCsvSubtitle' => 'Añade una lista desde una hoja de cálculo', - 'backup.importConfirmTitle' => '¿Importar una copia guardada?', + 'backup.exportJson' => 'Guardar una copia de seguridad', + 'backup.exportJsonSubtitle' => 'Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo', + 'backup.importJson' => 'Restaurar una copia', + 'backup.importJsonSubtitle' => 'Recupera una copia guardada — no se duplica nada', + 'backup.exportCsv' => 'Exportar a una hoja de cálculo', + 'backup.exportCsvSubtitle' => 'Una lista sencilla para Excel o LibreOffice — sin fotos', + 'backup.importCsv' => 'Importar una lista', + 'backup.importCsvSubtitle' => 'Añade entradas desde una hoja de cálculo', + 'backup.importConfirmTitle' => '¿Restaurar una copia?', 'backup.importConfirmBody' => 'Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.', - 'backup.importCsvConfirmTitle' => '¿Importar una lista CSV?', + 'backup.importCsvConfirmTitle' => '¿Importar una lista?', 'backup.importCsvConfirmBody' => 'Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.', 'backup.importAction' => 'Importar', 'backup.exportSaved' => 'Copia guardada', 'backup.cancelled' => 'Cancelado', 'backup.importDone' => ({required Object added, required Object updated}) => 'Importado: ${added} nuevas, ${updated} actualizadas', - 'backup.importCsvDone' => ({required Object count}) => 'Importadas ${count} desde CSV', + 'backup.importCsvDone' => ({required Object count}) => 'Añadidas ${count} entradas', 'backup.importFailed' => 'Este fichero no se pudo leer como una copia de Tanemaki', 'backup.failed' => 'Algo ha salido mal', 'about.title' => 'Acerca de', @@ -828,6 +979,16 @@ extension on TranslationsEs { 'inventory.noMatches' => 'Ninguna semilla coincide con los filtros.', 'inventory.clearFilters' => 'Quitar filtros', 'inventory.uncategorized' => 'Sin categoría', + 'inventory.needsReproductionFilter' => 'Por reproducir', + 'draft.capture' => 'Capturar fotos', + 'draft.captured' => ({required Object n}) => '${n} capturadas por catalogar', + 'draft.triageTitle' => 'Por catalogar', + 'draft.triageCount' => ({required Object n}) => '${n} por catalogar', + 'draft.untitled' => 'Sin nombre', + 'draft.nameField' => 'Nombra esta semilla', + 'draft.nameHint' => '¿Qué es?', + 'draft.suggestFromPhoto' => 'Sugerir nombre de la foto', + 'draft.discard' => 'Descartar', 'quickAdd.title' => 'Añadir una semilla', 'quickAdd.labelField' => 'Nombre', 'quickAdd.labelRequired' => 'Ponle un nombre', @@ -859,12 +1020,18 @@ extension on TranslationsEs { 'germination.germinated' => 'Germinadas', 'germination.none' => 'Aún no hay pruebas de germinación.', 'germination.result' => ({required Object percent}) => '${percent}%', + 'viability.expiringSoon' => 'Úsala o multiplícala esta temporada', + 'viability.expiringSoonYears' => ({required Object years}) => 'Úsala o multiplícala esta temporada · dura ~${years} años', + 'viability.expired' => 'Supera su viabilidad típica — a multiplicar', + 'viability.expiredYears' => ({required Object years}) => 'Supera su viabilidad típica (~${years} años) — a multiplicar', 'editVariety.title' => 'Editar semilla', 'editVariety.name' => 'Nombre', 'editVariety.category' => 'Categoría', 'editVariety.notes' => 'Notas', 'editVariety.species' => 'Especie (del catálogo)', 'editVariety.speciesHint' => 'Buscar una especie…', + 'editVariety.organic' => 'Ecológica', + 'editVariety.organicHint' => 'Cultivada de forma ecológica (eco)', 'addLot.title' => 'Añadir lote', 'addLot.year' => 'Fecha de cosecha', 'addLot.quantity' => '¿Cuánta?', @@ -897,6 +1064,51 @@ extension on TranslationsEs { 'presentation.plug' => 'Alvéolo', 'presentation.bareRoot' => 'Raíz desnuda', 'presentation.rootBall' => 'Cepellón', + 'provenance.section' => 'De dónde viene', + 'provenance.seedsFrom' => 'Semillas de', + 'provenance.seedsFromHint' => 'Quién las cultivó o las dio', + 'provenance.place' => 'Lugar', + 'provenance.placeHint' => 'De dónde vienen (con provincia)', + 'provenance.addSeedsFrom' => 'Semillas de', + 'provenance.addPlace' => 'Lugar', + 'abundance.add' => 'Cuánta tengo', + 'abundance.title' => 'Cuánta tengo', + 'abundance.none' => 'Sin indicar', + 'abundance.plentyToShare' => 'De sobra para compartir', + 'abundance.enoughToShare' => 'Bastante, para compartir con moderación', + 'abundance.enoughForMe' => 'Suficiente para mí', + 'abundance.runningLow' => 'Queda poca', + 'cropCalendar.add' => 'Calendario de cultivo', + 'cropCalendar.title' => 'Calendario de cultivo', + 'cropCalendar.sow' => 'Siembra', + 'cropCalendar.transplant' => 'Trasplante', + 'cropCalendar.flowering' => 'Floración', + 'cropCalendar.fruiting' => 'Fructificación', + 'cropCalendar.seedHarvest' => 'Cosecha de semilla', + 'cropCalendar.unset' => '—', + 'needsReproduction.label' => 'Reproducir esta temporada', + 'needsReproduction.hint' => 'Cultívala antes de que se acabe la semilla', + 'needsReproduction.badge' => 'Por reproducir', + 'preservation.add' => 'Cómo se conserva', + 'preservation.title' => 'Cómo se conserva', + 'preservation.none' => 'Sin especificar', + 'preservation.jarWithDesiccant' => 'Bote con desecante', + 'preservation.glassJar' => 'Bote de cristal', + 'preservation.paperEnvelope' => 'Sobre de papel', + 'preservation.paperBag' => 'Bolsa de papel', + 'preservation.plasticBag' => 'Bolsa de plástico', + 'conditionCheck.advanced' => 'Conservación y detalles del banco', + 'conditionCheck.title' => 'Revisiones de conservación', + 'conditionCheck.add' => 'Añadir revisión', + 'conditionCheck.containers' => 'Botes / recipientes', + 'conditionCheck.desiccant' => 'Desecante', + 'conditionCheck.none' => 'Aún no hay revisiones.', + 'conditionCheck.summary' => ({required Object count, required Object state}) => '${count} bote(s) · ${state}', + 'desiccant.none' => 'No tiene', + 'desiccant.add' => 'Se pone', + 'desiccant.replace' => 'Se cambia', + 'desiccant.dry' => 'Azul — seco', + 'desiccant.fresh' => 'Recién puesto', 'unit.aFew' => 'unas pocas', 'unit.some' => 'algunas', 'unit.plenty' => 'muchas', diff --git a/apps/app_seeds/lib/main.dart b/apps/app_seeds/lib/main.dart index 7c2ae84..3239bd7 100644 --- a/apps/app_seeds/lib/main.dart +++ b/apps/app_seeds/lib/main.dart @@ -5,16 +5,20 @@ import 'data/species_repository.dart'; import 'data/variety_repository.dart'; import 'di/injector.dart'; import 'i18n/strings.g.dart'; +import 'services/onboarding_store.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); LocaleSettings.useDeviceLocaleSync(); await configureDependencies(); + final onboarding = getIt(); runApp( TranslationProvider( child: TaneApp( repository: getIt(), species: getIt(), + onboarding: onboarding, + showIntro: !await onboarding.introSeen(), ), ), ); diff --git a/apps/app_seeds/lib/services/ocr/label_text_extractor.dart b/apps/app_seeds/lib/services/ocr/label_text_extractor.dart new file mode 100644 index 0000000..67d039a --- /dev/null +++ b/apps/app_seeds/lib/services/ocr/label_text_extractor.dart @@ -0,0 +1,27 @@ +import 'dart:typed_data'; + +/// Extracts a suggested variety name from a photo of its packet/label — an +/// optional, best-effort enrichment for the draft-triage flow. Kept behind this +/// interface so the app degrades gracefully: platforms without an on-device OCR +/// engine (desktop, web) get [NoOpLabelTextExtractor], and the UI hides the +/// "suggest name" affordance when [isSupported] is false. +abstract class LabelTextExtractor { + /// Whether this build can actually run OCR (true only where a native engine + /// is wired). The UI uses it to decide whether to offer the suggestion. + bool get isSupported; + + /// Returns a suggested name read from [photo], or null when nothing usable + /// was found or OCR is unavailable. Never throws — failures yield null. + Future suggestLabel(Uint8List photo); +} + +/// The fallback where no on-device OCR exists: always unsupported, always null. +class NoOpLabelTextExtractor implements LabelTextExtractor { + const NoOpLabelTextExtractor(); + + @override + bool get isSupported => false; + + @override + Future suggestLabel(Uint8List photo) async => null; +} diff --git a/apps/app_seeds/lib/services/ocr/ocr_language.dart b/apps/app_seeds/lib/services/ocr/ocr_language.dart new file mode 100644 index 0000000..a11518c --- /dev/null +++ b/apps/app_seeds/lib/services/ocr/ocr_language.dart @@ -0,0 +1,110 @@ +/// Locale-aware selection of Tesseract OCR language packs. +/// +/// Tane is international by design: the OCR must read seed packets in whatever +/// language the user runs the app, not a fixed `eng`/`spa`. This maps the app's +/// preferred locales to the traineddata packs we actually bundle and degrades +/// gracefully — an unbundled locale simply falls back to the Latin baseline +/// instead of failing. As more packs are bundled (incl. RTL/CJK scripts) they +/// light up automatically with no code change here. +library; + +import 'dart:convert'; + +const _trainedDataSuffix = '.traineddata'; + +/// ISO 639-1 (locale) → Tesseract language code (mostly ISO 639-2/T). Covers the +/// scripts we plausibly bundle; extend as new packs are added. Kept +/// intentionally broad — international first, not one region. +const Map isoToTesseract = { + // Latin-script European + 'en': 'eng', + 'es': 'spa', + 'ca': 'cat', + 'gl': 'glg', + 'eu': 'eus', + 'pt': 'por', + 'fr': 'fra', + 'de': 'deu', + 'it': 'ita', + 'nl': 'nld', + 'pl': 'pol', + 'cs': 'ces', + 'ro': 'ron', + 'sv': 'swe', + 'da': 'dan', + 'fi': 'fin', + 'tr': 'tur', + // Cyrillic / Greek + 'ru': 'rus', + 'uk': 'ukr', + 'el': 'ell', + // RTL scripts + 'ar': 'ara', + 'he': 'heb', + 'fa': 'fas', + 'ur': 'urd', + // CJK & other non-Latin + 'zh': 'chi_sim', + 'ja': 'jpn', + 'ko': 'kor', + 'hi': 'hin', + 'th': 'tha', +}; + +/// Parses the bundled OCR config (`assets/tessdata_config.json`) into the set of +/// available Tesseract language codes (basenames, e.g. `eng`, `spa`). The config +/// is the single source of truth for what is shipped, so this stays in sync with +/// the assets automatically. +Set parseAvailableOcrLanguages(String configJson) { + final data = jsonDecode(configJson) as Map; + final files = (data['files'] as List?)?.cast() ?? const []; + return files + .map( + (f) => f.endsWith(_trainedDataSuffix) + ? f.substring(0, f.length - _trainedDataSuffix.length) + : f, + ) + .where((code) => code.isNotEmpty) + .toSet(); +} + +/// Resolves the `+`-joined Tesseract language string for [preferredLocales] +/// (ordered most-preferred first; each an ISO 639-1 code or a fuller tag like +/// `es_ES`/`zh-Hans`), limited to the [available] bundled packs. +/// +/// Rules, in order: +/// 1. Each preferred locale that maps to an available pack is included, in +/// order, de-duplicated. +/// 2. The Latin [baseline] (`eng`) is appended if bundled — Latin text is +/// common on packets even in non-Latin locales, and it's a safe default. +/// 3. If nothing matched and the baseline is unavailable, fall back to every +/// available pack (sorted) so OCR still runs with whatever we shipped. +/// +/// Returns an empty string only when [available] is empty. +String resolveOcrLanguages({ + required List preferredLocales, + required Set available, + String baseline = 'eng', +}) { + final ordered = []; + for (final locale in preferredLocales) { + final code = tesseractCodeForLocale(locale); + if (code != null && available.contains(code) && !ordered.contains(code)) { + ordered.add(code); + } + } + if (available.contains(baseline) && !ordered.contains(baseline)) { + ordered.add(baseline); + } + if (ordered.isEmpty) { + ordered.addAll(available.toList()..sort()); + } + return ordered.join('+'); +} + +/// Maps a locale tag to its Tesseract code, or null if unmapped. Accepts full +/// tags (`es_ES`, `zh-Hans-CN`) and uses the primary language subtag. +String? tesseractCodeForLocale(String localeTag) { + final primary = localeTag.split(RegExp('[_-]')).first.toLowerCase(); + return isoToTesseract[primary]; +} diff --git a/apps/app_seeds/lib/services/ocr/tesseract_label_extractor.dart b/apps/app_seeds/lib/services/ocr/tesseract_label_extractor.dart new file mode 100644 index 0000000..842ddaf --- /dev/null +++ b/apps/app_seeds/lib/services/ocr/tesseract_label_extractor.dart @@ -0,0 +1,212 @@ +import 'dart:io'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; +import 'package:image/image.dart' as img; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +import 'label_text_extractor.dart'; + +/// On-device OCR via Tesseract (offline, no cloud). Reads the **largest** text +/// on a packet/label photo — the variety name is the biggest print, not the +/// longest line — so boilerplate like "Organic Product" or "Herb Seeds" never +/// wins. +/// +/// Security note: the plugin takes a file path, so the photo is written to a +/// private temp file and **deleted immediately** in `finally` — it never lives +/// as plaintext at rest beyond the OCR call. The bundled traineddata the plugin +/// copies to app storage is public reference data, not user content. +class TesseractLabelExtractor implements LabelTextExtractor { + const TesseractLabelExtractor({ + this.language = 'eng+spa', + this.rotations = const [0, 90, 180, 270, 45, 135, 225, 315], + }); + + /// Tesseract language(s), '+'-joined; needs matching bundled traineddata. + final String language; + + /// Angles (degrees) to OCR the photo at, keeping the most confident read. + /// Tesseract only reads axis-aligned text, so this makes a packet shot + /// sideways, upside-down or ~45° tilted still work. More angles = slower + /// (one OCR pass each); trim to `[0, 90, 180, 270]` if latency matters. + final List rotations; + + @override + bool get isSupported => true; + + @override + Future suggestLabel(Uint8List photo) async { + final base = _preprocess(photo); + if (base == null) return null; + File? temp; + try { + final dir = await getTemporaryDirectory(); + temp = File(p.join(dir.path, 'ocr_${identityHashCode(photo)}.png')); + HocrLabel? best; + for (final angle in rotations) { + final image = angle == 0 ? base : img.copyRotate(base, angle: angle); + await temp.writeAsBytes(img.encodePng(image), flush: true); + // hOCR carries per-word bounding boxes + confidence, so we can rank by + // font size, drop low-confidence noise, and compare rotations. + final hocr = await FlutterTesseractOcr.extractHocr( + temp.path, + language: language, + args: {'preserve_interword_spaces': '1'}, + ); + final candidate = readHocrLabel(hocr); + if (candidate != null && + (best == null || candidate.score > best.score)) { + best = candidate; + } + } + return best?.label; + } on Object { + return null; + } finally { + try { + if (temp != null && await temp.exists()) await temp.delete(); + } on Object { + // Best effort: nothing to recover if cleanup fails. + } + } + } +} + +/// Words in an hOCR document ignored as packet boilerplate (lower-cased, +/// accent-stripped compare). Kept deliberately small and generic. +const _boilerplate = { + 'organic', 'product', 'products', 'herb', 'herbs', 'seed', 'seeds', + 'bio', 'net', 'weight', 'wt', 'www', 'com', 'gr', 'grs', 'grams', + 'variety', 'quality', +}; + +/// Selects the variety name from Tesseract [hocr]: keep the words whose glyph +/// height is close to the tallest (the title print), drop boilerplate, and join +/// them in reading order (top-to-bottom, left-to-right). Returns null when +/// nothing usable remains. Pure — unit-tested without a native engine. +/// A label read from one hOCR pass, with a [score] (sum of the kept words' +/// confidences) used to compare rotations — the most confident wins. +class HocrLabel { + const HocrLabel(this.label, this.score); + final String label; + final int score; +} + +/// Thin wrapper returning just the label (see [readHocrLabel]). +String? pickLabelFromHocr(String hocr, {int minConfidence = 20}) => + readHocrLabel(hocr, minConfidence: minConfidence)?.label; + +HocrLabel? readHocrLabel(String hocr, {int minConfidence = 20}) { + final wordRe = RegExp( + r"""class=['"]ocrx_word['"][^>]*\bbbox\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)(?:[^>]*?x_wconf\s+(\d+))?[^>]*>([^<]*)<""", + ); + final words = <_Word>[]; + for (final m in wordRe.allMatches(hocr)) { + final x0 = int.parse(m.group(1)!); + final y0 = int.parse(m.group(2)!); + final y1 = int.parse(m.group(4)!); + // Missing confidence → treat as passing (some hOCR omits x_wconf). + final confidence = m.group(5) == null ? 100 : int.parse(m.group(5)!); + final text = _unescape(m.group(6)!).trim(); + if (text.isEmpty) continue; + words.add( + _Word(x0: x0, y0: y0, height: y1 - y0, text: text, confidence: confidence), + ); + } + if (words.isEmpty) return null; + + final maxHeight = words.map((w) => w.height).reduce(math.max); + // Keep prominent words (>= 60% of the tallest) with ≥3 letters, above a low + // confidence floor, that are not boilerplate. The length rule kills 1–2 char + // OCR fragments (the "Re Ma" garbage) while keeping a plausibly-misread title + // word like "Sucumber" (which the user just edits to "Cucumber"). + final kept = words + .where( + (w) => + w.height >= 0.6 * maxHeight && + w.confidence >= minConfidence && + _letterCount(w.text) >= 3 && + !_boilerplate.contains(_fold(w.text)), + ) + .toList(); + if (kept.isEmpty) return null; + + // Reading order: group into rows by vertical position, then left-to-right. + final band = maxHeight * 0.6; + kept.sort((a, b) { + if ((a.y0 - b.y0).abs() > band) return a.y0.compareTo(b.y0); + return a.x0.compareTo(b.x0); + }); + + final chosen = kept.take(6).toList(); + final label = _titleCase(_clean(chosen.map((w) => w.text).join(' '))); + if (label.isEmpty) return null; + final score = chosen.fold(0, (sum, w) => sum + w.confidence); + return HocrLabel(label, score); +} + +class _Word { + const _Word({ + required this.x0, + required this.y0, + required this.height, + required this.text, + required this.confidence, + }); + final int x0; + final int y0; + final int height; + final String text; + final int confidence; +} + +/// Grayscale + contrast + upscale to help Tesseract on blurry/small photos. +/// Returns the decoded image (ready to rotate/encode), or null if it can't be +/// decoded. +img.Image? _preprocess(Uint8List bytes) { + final decoded = img.decodeImage(bytes); + if (decoded == null) return null; + var image = decoded; + // Upscale so glyphs are big enough for the LSTM engine. + if (image.width < 1200) { + image = img.copyResize(image, width: 1200); + } + image = img.grayscale(image); + return img.adjustColor(image, contrast: 1.4); +} + +/// Case- and accent-folded token for boilerplate matching. +String _fold(String s) { + const from = 'áàäâãéèëêíìïîóòöôõúùüûñç'; + const to = 'aaaaaeeeeiiiiooooouuuunc'; + final lower = s.toLowerCase(); + final buffer = StringBuffer(); + for (final ch in lower.split('')) { + final i = from.indexOf(ch); + buffer.write(i == -1 ? ch : to[i]); + } + return buffer.toString().replaceAll(RegExp(r'[^a-z0-9]'), ''); +} + +int _letterCount(String s) => + RegExp(r'\p{L}', unicode: true).allMatches(s).length; + +String _clean(String s) => s + .replaceAll(RegExp(r"[^\p{L}\p{N}\s'-]", unicode: true), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + +String _titleCase(String s) => s + .toLowerCase() + .split(' ') + .map((w) => w.isEmpty ? w : '${w[0].toUpperCase()}${w.substring(1)}') + .join(' '); + +String _unescape(String s) => s + .replaceAll('&', '&') + .replaceAll(''', "'") + .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('>', '>'); diff --git a/apps/app_seeds/lib/services/onboarding_store.dart b/apps/app_seeds/lib/services/onboarding_store.dart new file mode 100644 index 0000000..47611a0 --- /dev/null +++ b/apps/app_seeds/lib/services/onboarding_store.dart @@ -0,0 +1,18 @@ +import '../security/secret_store.dart'; + +/// Remembers whether the intro/onboarding carousel has been shown, so it only +/// appears on first launch. Backed by the OS keystore (via [SecretStore]) to +/// honour the "no plaintext at rest" rule — there is no shared_preferences here. +class OnboardingStore { + OnboardingStore(this._store); + + final SecretStore _store; + + static const _introSeenKey = 'tane.intro_seen'; + + /// Whether the user has already seen (or skipped) the intro carousel. + Future introSeen() async => await _store.read(_introSeenKey) == '1'; + + /// Records that the intro has been shown; subsequent launches skip it. + Future markIntroSeen() => _store.write(_introSeenKey, '1'); +} diff --git a/apps/app_seeds/lib/state/inventory_cubit.dart b/apps/app_seeds/lib/state/inventory_cubit.dart index a455116..f539880 100644 --- a/apps/app_seeds/lib/state/inventory_cubit.dart +++ b/apps/app_seeds/lib/state/inventory_cubit.dart @@ -12,13 +12,21 @@ import '../db/enums.dart'; class InventoryState extends Equatable { const InventoryState({ this.items = const [], + this.drafts = const [], this.query = '', this.categoryFilter = const {}, this.typeFilter = const {}, + this.organicOnly = false, + this.needsReproductionOnly = false, this.loading = true, }); final List items; + + /// Photo-first captures still awaiting a name — the "to catalogue" tray, + /// shown above the list and never mixed into [items]. + final List drafts; + final String query; /// Categories to keep; empty = all categories. @@ -28,6 +36,12 @@ class InventoryState extends Equatable { /// least one lot of a selected form. final Set typeFilter; + /// When true, keep only varieties flagged organic ("eco"). + final bool organicOnly; + + /// When true, keep only varieties flagged "needs reproducing this season". + final bool needsReproductionOnly; + final bool loading; /// Categories present across all items, in display order (deduped), so the @@ -53,22 +67,39 @@ class InventoryState extends Equatable { if (typeFilter.isNotEmpty && i.lotTypes.intersection(typeFilter).isEmpty) { return false; } + if (organicOnly && !i.isOrganic) return false; + if (needsReproductionOnly && !i.needsReproduction) return false; return true; }).toList(); } + /// Whether any item is flagged organic, so the UI only offers the eco filter + /// chip when it would actually match something. + bool get hasOrganic => items.any((i) => i.isOrganic); + + /// Whether any item is flagged "needs reproducing", so the UI only offers + /// that filter chip when it would match something. + bool get hasNeedsReproduction => items.any((i) => i.needsReproduction); + InventoryState copyWith({ List? items, + List? drafts, String? query, Set? categoryFilter, Set? typeFilter, + bool? organicOnly, + bool? needsReproductionOnly, bool? loading, }) { return InventoryState( items: items ?? this.items, + drafts: drafts ?? this.drafts, query: query ?? this.query, categoryFilter: categoryFilter ?? this.categoryFilter, typeFilter: typeFilter ?? this.typeFilter, + organicOnly: organicOnly ?? this.organicOnly, + needsReproductionOnly: + needsReproductionOnly ?? this.needsReproductionOnly, loading: loading ?? this.loading, ); } @@ -76,9 +107,12 @@ class InventoryState extends Equatable { @override List get props => [ items, + drafts, query, categoryFilter, typeFilter, + organicOnly, + needsReproductionOnly, loading, ]; } @@ -90,10 +124,14 @@ class InventoryCubit extends Cubit { _sub = _repo.watchInventory().listen( (items) => emit(state.copyWith(items: items, loading: false)), ); + _draftsSub = _repo.watchDrafts().listen( + (drafts) => emit(state.copyWith(drafts: drafts)), + ); } final VarietyRepository _repo; late final StreamSubscription> _sub; + late final StreamSubscription> _draftsSub; void search(String query) => emit(state.copyWith(query: query)); @@ -111,13 +149,29 @@ class InventoryCubit extends Cubit { emit(state.copyWith(typeFilter: next)); } - /// Clears both filters (search is left untouched). - void clearFilters() => - emit(state.copyWith(categoryFilter: const {}, typeFilter: const {})); + /// Toggles the "organic only" filter. + void toggleOrganicOnly() => + emit(state.copyWith(organicOnly: !state.organicOnly)); + + /// Toggles the "needs reproducing only" filter. + void toggleNeedsReproductionOnly() => emit( + state.copyWith(needsReproductionOnly: !state.needsReproductionOnly), + ); + + /// Clears all filters (search is left untouched). + void clearFilters() => emit( + state.copyWith( + categoryFilter: const {}, + typeFilter: const {}, + organicOnly: false, + needsReproductionOnly: false, + ), + ); @override Future close() async { await _sub.cancel(); + await _draftsSub.cancel(); return super.close(); } } diff --git a/apps/app_seeds/lib/state/variety_detail_cubit.dart b/apps/app_seeds/lib/state/variety_detail_cubit.dart index 8a6ed8d..b39c687 100644 --- a/apps/app_seeds/lib/state/variety_detail_cubit.dart +++ b/apps/app_seeds/lib/state/variety_detail_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:commons_core/commons_core.dart'; +import 'package:drift/drift.dart' show Value; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -44,13 +45,30 @@ class VarietyDetailCubit extends Cubit { final String varietyId; late final StreamSubscription _sub; - Future updateFields({String? label, String? category, String? notes}) => - _repo.updateVariety( - id: varietyId, - label: label, - category: category, - notes: notes, - ); + Future updateFields({ + String? label, + String? category, + String? notes, + bool? isOrganic, + bool? needsReproduction, + Value sowMonths = const Value.absent(), + Value transplantMonths = const Value.absent(), + Value floweringMonths = const Value.absent(), + Value fruitingMonths = const Value.absent(), + Value seedHarvestMonths = const Value.absent(), + }) => _repo.updateVariety( + id: varietyId, + label: label, + category: category, + notes: notes, + isOrganic: isOrganic, + needsReproduction: needsReproduction, + sowMonths: sowMonths, + transplantMonths: transplantMonths, + floweringMonths: floweringMonths, + fruitingMonths: fruitingMonths, + seedHarvestMonths: seedHarvestMonths, + ); Future addLot({ LotType type = LotType.seed, @@ -58,6 +76,10 @@ class VarietyDetailCubit extends Cubit { int? month, Quantity? quantity, Presentation? presentation, + String? originName, + String? originPlace, + Abundance? abundance, + PreservationFormat? preservationFormat, }) => _repo.addLot( varietyId: varietyId, type: type, @@ -65,6 +87,10 @@ class VarietyDetailCubit extends Cubit { harvestMonth: month, quantity: quantity, presentation: presentation, + originName: originName, + originPlace: originPlace, + abundance: abundance, + preservationFormat: preservationFormat, ); Future updateLot({ @@ -74,6 +100,10 @@ class VarietyDetailCubit extends Cubit { int? month, Quantity? quantity, Presentation? presentation, + String? originName, + String? originPlace, + Abundance? abundance, + PreservationFormat? preservationFormat, }) => _repo.updateLot( lotId: lotId, type: type, @@ -81,6 +111,10 @@ class VarietyDetailCubit extends Cubit { harvestMonth: month, quantity: quantity, presentation: presentation, + originName: originName, + originPlace: originPlace, + abundance: abundance, + preservationFormat: preservationFormat, ); Future deleteLot(String lotId) => _repo.softDeleteLot(lotId); @@ -120,6 +154,18 @@ class VarietyDetailCubit extends Cubit { germinatedCount: germinatedCount, ); + Future addConditionCheck({ + required String lotId, + int? checkedOn, + int? containerCount, + DesiccantState? desiccantState, + }) => _repo.addConditionCheck( + lotId: lotId, + checkedOn: checkedOn, + containerCount: containerCount, + desiccantState: desiccantState, + ); + Future deleteVariety() async { await _repo.softDeleteVariety(varietyId); emit( diff --git a/apps/app_seeds/lib/ui/app_drawer.dart b/apps/app_seeds/lib/ui/app_drawer.dart index d731166..9bf5bfb 100644 --- a/apps/app_seeds/lib/ui/app_drawer.dart +++ b/apps/app_seeds/lib/ui/app_drawer.dart @@ -47,6 +47,14 @@ class AppDrawer extends StatelessWidget { _DrawerItem(icon: const Icon(Icons.group), label: t.menu.following), const Spacer(), const Divider(height: 1), + _DrawerItem( + icon: const Icon(Icons.auto_stories_outlined), + label: t.intro.menuEntry, + onTap: () { + Navigator.of(context).pop(); + context.push('/intro'); + }, + ), _DrawerItem( icon: const Icon(Icons.settings), label: t.menu.settings, diff --git a/apps/app_seeds/lib/ui/backup_section.dart b/apps/app_seeds/lib/ui/backup_section.dart index 27b9ccd..34ddd51 100644 --- a/apps/app_seeds/lib/ui/backup_section.dart +++ b/apps/app_seeds/lib/ui/backup_section.dart @@ -4,9 +4,11 @@ import '../di/injector.dart'; import '../i18n/strings.g.dart'; import '../services/export_import_service.dart'; -/// The three backup actions shown in Settings: export CSV, export JSON and -/// import JSON. Import asks for confirmation first (it merges into the live -/// inventory), then reports what happened in a SnackBar. +/// The backup actions shown in Settings. The primary pair — save/restore a +/// full copy — comes first; the spreadsheet list export/import is a secondary +/// convenience below. File formats (JSON/CSV) are an implementation detail and +/// never surface in the UI. Restore/import asks for confirmation first (it +/// merges into the live inventory), then reports what happened in a SnackBar. class BackupSection extends StatelessWidget { const BackupSection({this.service, super.key}); @@ -22,23 +24,24 @@ class BackupSection extends StatelessWidget { return Column( children: [ ListTile( - leading: const Icon(Icons.table_view_outlined), - title: Text(t.backup.exportCsv), - subtitle: Text(t.backup.exportCsvSubtitle), - onTap: () => _runExport(context, () => _service.exportCsv()), - ), - ListTile( - leading: const Icon(Icons.file_download_outlined), + leading: const Icon(Icons.save_alt_outlined), title: Text(t.backup.exportJson), subtitle: Text(t.backup.exportJsonSubtitle), onTap: () => _runExport(context, () => _service.exportJson()), ), ListTile( - leading: const Icon(Icons.file_upload_outlined), + leading: const Icon(Icons.settings_backup_restore_outlined), title: Text(t.backup.importJson), subtitle: Text(t.backup.importJsonSubtitle), onTap: () => _runImport(context), ), + const Divider(), + ListTile( + leading: const Icon(Icons.table_view_outlined), + title: Text(t.backup.exportCsv), + subtitle: Text(t.backup.exportCsvSubtitle), + onTap: () => _runExport(context, () => _service.exportCsv()), + ), ListTile( leading: const Icon(Icons.playlist_add_outlined), title: Text(t.backup.importCsv), diff --git a/apps/app_seeds/lib/ui/draft_triage.dart b/apps/app_seeds/lib/ui/draft_triage.dart new file mode 100644 index 0000000..e00a71f --- /dev/null +++ b/apps/app_seeds/lib/ui/draft_triage.dart @@ -0,0 +1,245 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; + +import '../data/variety_repository.dart'; +import '../i18n/strings.g.dart'; +import '../services/ocr/label_text_extractor.dart'; +import 'photo_pick.dart'; + +/// Rapid "capture now, catalogue later": shoot/pick a burst of photos and turn +/// each into a draft variety. Returns how many were captured. +Future captureDraftBurst( + BuildContext context, + VarietyRepository repository, +) async { + final photos = await pickPhotos(context); + for (final bytes in photos) { + await repository.addDraftVariety(bytes); + } + return photos.length; +} + +/// The "to catalogue" tray: lists photo-first drafts (live from the DB); tapping +/// one prompts for a name and promotes it into the inventory. +Future showTriageSheet( + BuildContext context, + VarietyRepository repository, +) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => _TriageSheet(repository: repository), + ); +} + +class _TriageSheet extends StatelessWidget { + const _TriageSheet({required this.repository}); + + final VarietyRepository repository; + + @override + Widget build(BuildContext context) { + final t = context.t; + return StreamBuilder>( + stream: repository.watchDrafts(), + builder: (context, snapshot) { + final drafts = snapshot.data ?? const []; + return Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: MediaQuery.of(context).viewInsets.bottom + 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + t.draft.triageTitle, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 12), + if (drafts.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Text( + t.inventory.empty, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + ) + else + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: drafts.length, + itemBuilder: (context, i) => _DraftTile( + draft: drafts[i], + repository: repository, + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _DraftTile extends StatelessWidget { + const _DraftTile({required this.draft, required this.repository}); + + final VarietyListItem draft; + final VarietyRepository repository; + + @override + Widget build(BuildContext context) { + final t = context.t; + final photo = draft.photo; + return ListTile( + key: Key('draft.tile.${draft.id}'), + leading: photo == null + ? const Icon(Icons.image_outlined) + : ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory( + photo, + width: 48, + height: 48, + fit: BoxFit.cover, + ), + ), + title: Text(t.draft.untitled), + trailing: IconButton( + icon: const Icon(Icons.delete_outline), + tooltip: t.draft.discard, + onPressed: () => repository.softDeleteVariety(draft.id), + ), + onTap: () async { + final name = await showDialog( + context: context, + builder: (_) => NameDraftDialog(photo: photo), + ); + if (name != null && name.trim().isNotEmpty) { + await repository.nameDraft(draft.id, name.trim()); + } + }, + ); + } +} + +/// Prompts for a draft's name, showing its photo. Where an on-device OCR engine +/// exists, a "Suggest name from photo" button pre-fills the field (always +/// editable, never auto-confirmed). +class NameDraftDialog extends StatefulWidget { + const NameDraftDialog({required this.photo, this.extractor, super.key}); + + final Uint8List? photo; + + /// Injectable for tests; otherwise resolved from the service locator (or a + /// no-op when none is registered). + final LabelTextExtractor? extractor; + + @override + State createState() => _NameDraftDialogState(); +} + +class _NameDraftDialogState extends State { + final _controller = TextEditingController(); + bool _suggesting = false; + + LabelTextExtractor get _extractor => + widget.extractor ?? + (GetIt.instance.isRegistered() + ? GetIt.instance() + : const NoOpLabelTextExtractor()); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _suggest() async { + final photo = widget.photo; + if (photo == null) return; + setState(() => _suggesting = true); + final suggestion = await _extractor.suggestLabel(photo); + if (!mounted) return; + setState(() { + _suggesting = false; + if (suggestion != null && suggestion.trim().isNotEmpty) { + _controller.text = suggestion.trim(); + } + }); + } + + @override + Widget build(BuildContext context) { + final t = context.t; + final canSuggest = widget.photo != null && _extractor.isSupported; + return AlertDialog( + title: Text(t.draft.nameField), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.photo != null) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory( + widget.photo!, + height: 140, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 12), + TextField( + key: const Key('draft.nameField'), + controller: _controller, + autofocus: true, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + hintText: t.draft.nameHint, + border: const OutlineInputBorder(), + ), + onSubmitted: (value) => Navigator.of(context).pop(value), + ), + if (canSuggest) ...[ + const SizedBox(height: 8), + Align( + alignment: AlignmentDirectional.centerStart, + child: TextButton.icon( + key: const Key('draft.suggestFromPhoto'), + onPressed: _suggesting ? null : _suggest, + icon: _suggesting + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.auto_fix_high_outlined), + label: Text(t.draft.suggestFromPhoto), + ), + ), + ], + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(t.common.cancel), + ), + FilledButton( + key: const Key('draft.nameSave'), + onPressed: () => Navigator.of(context).pop(_controller.text), + child: Text(t.common.save), + ), + ], + ); + } +} diff --git a/apps/app_seeds/lib/ui/harvest_date_picker.dart b/apps/app_seeds/lib/ui/harvest_date_picker.dart index 122013b..17e37a6 100644 --- a/apps/app_seeds/lib/ui/harvest_date_picker.dart +++ b/apps/app_seeds/lib/ui/harvest_date_picker.dart @@ -51,12 +51,12 @@ class HarvestDateField extends StatelessWidget { }, icon: const Icon(Icons.event_outlined, size: 20), label: Align( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, child: Text(harvestDateLabel(t, year: year, month: month)), ), style: OutlinedButton.styleFrom( foregroundColor: seedGreenDark, - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), minimumSize: const Size.fromHeight(0), ), diff --git a/apps/app_seeds/lib/ui/intro_screen.dart b/apps/app_seeds/lib/ui/intro_screen.dart new file mode 100644 index 0000000..40864ae --- /dev/null +++ b/apps/app_seeds/lib/ui/intro_screen.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +import '../i18n/strings.g.dart'; +import 'seed_glyph.dart'; +import 'theme.dart'; + +/// One onboarding page: a large emblem (a bundled seed glyph or a Material +/// symbol), a title and a short body. Illustrations use the app's own icon set +/// rather than photos — no image licensing, and it keeps the bundle light. +class _Slide { + const _Slide({ + this.glyph, + this.icon, + required this.title, + required this.body, + }); + + /// A `seedks` glyph char (see [SeedGlyphs]); mutually exclusive with [icon]. + final String? glyph; + + /// A Material symbol, used where no seed glyph fits (privacy, sprout). + final IconData? icon; + final String title; + final String body; +} + +/// The first-run intro carousel (also reachable later from the drawer): a few +/// swipeable cards explaining what Tanemaki is, how privacy works, how sharing +/// works and the Plantare. [onDone] fires when the user skips or finishes, and +/// is where the caller marks the intro as seen and leaves the screen. +class IntroScreen extends StatefulWidget { + const IntroScreen({required this.onDone, super.key}); + + final VoidCallback onDone; + + @override + State createState() => _IntroScreenState(); +} + +class _IntroScreenState extends State { + final _controller = PageController(); + int _page = 0; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + List<_Slide> _slides(Translations t) => [ + _Slide( + glyph: SeedGlyphs.scattered, + title: t.intro.slides.welcome.title, + body: t.intro.slides.welcome.body, + ), + _Slide( + glyph: SeedGlyphs.jars, + title: t.intro.slides.inventory.title, + body: t.intro.slides.inventory.body, + ), + _Slide( + icon: Icons.lock_outline, + title: t.intro.slides.privacy.title, + body: t.intro.slides.privacy.body, + ), + _Slide( + glyph: SeedGlyphs.share, + title: t.intro.slides.share.title, + body: t.intro.slides.share.body, + ), + _Slide( + icon: Symbols.potted_plant, + title: t.intro.slides.plantare.title, + body: t.intro.slides.plantare.body, + ), + ]; + + void _next(int count) { + if (_page >= count - 1) { + widget.onDone(); + return; + } + _controller.nextPage( + duration: const Duration(milliseconds: 280), + curve: Curves.easeInOut, + ); + } + + @override + Widget build(BuildContext context) { + final t = context.t; + final slides = _slides(t); + final isLast = _page == slides.length - 1; + return Scaffold( + backgroundColor: seedCanvas, + body: SafeArea( + child: Column( + children: [ + Align( + alignment: AlignmentDirectional.centerEnd, + child: TextButton( + onPressed: widget.onDone, + child: Text(t.intro.skip), + ), + ), + Expanded( + child: PageView.builder( + controller: _controller, + onPageChanged: (i) => setState(() => _page = i), + itemCount: slides.length, + itemBuilder: (context, i) => _SlideView(slide: slides[i]), + ), + ), + _Dots(count: slides.length, active: _page), + Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 24), + child: SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => _next(slides.length), + child: Text(isLast ? t.intro.start : t.intro.next), + ), + ), + ), + ], + ), + ), + ); + } +} + +/// The centred emblem + title + body for a single [slide]. +class _SlideView extends StatelessWidget { + const _SlideView({required this.slide}); + + final _Slide slide; + + @override + Widget build(BuildContext context) { + // Centre when there's room, scroll when the viewport is short (small phones + // in landscape, split-screen) so the card never overflows. + return LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 160, + height: 160, + decoration: const BoxDecoration( + color: seedPrimaryContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: slide.glyph != null + ? SeedGlyph(slide.glyph!, size: 76, color: seedGreen) + : Icon(slide.icon, size: 72, color: seedGreen), + ), + const SizedBox(height: 36), + Text( + slide.title, + textAlign: TextAlign.center, + style: const TextStyle( + color: seedTitle, + fontSize: 24, + fontWeight: FontWeight.w600, + height: 1.2, + ), + ), + const SizedBox(height: 14), + Text( + slide.body, + textAlign: TextAlign.center, + style: const TextStyle( + color: seedOnSurfaceVariant, + fontSize: 16, + height: 1.45, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} + +/// The page-position dots: the active one is a wider green pill. +class _Dots extends StatelessWidget { + const _Dots({required this.count, required this.active}); + + final int count; + final int active; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (var i = 0; i < count; i++) + AnimatedContainer( + duration: const Duration(milliseconds: 220), + margin: const EdgeInsets.symmetric(horizontal: 4), + width: i == active ? 22 : 8, + height: 8, + decoration: BoxDecoration( + color: i == active ? seedGreen : seedOutline, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ); + } +} diff --git a/apps/app_seeds/lib/ui/inventory_list_screen.dart b/apps/app_seeds/lib/ui/inventory_list_screen.dart index a3442ed..7c421d2 100644 --- a/apps/app_seeds/lib/ui/inventory_list_screen.dart +++ b/apps/app_seeds/lib/ui/inventory_list_screen.dart @@ -4,9 +4,11 @@ import 'package:go_router/go_router.dart'; import '../data/variety_repository.dart'; import '../db/enums.dart'; +import '../domain/seed_viability.dart'; import '../i18n/strings.g.dart'; import '../state/inventory_cubit.dart'; import 'app_drawer.dart'; +import 'draft_triage.dart'; import 'quantity_picker.dart'; import 'quick_add_sheet.dart'; import 'seed_glyph.dart'; @@ -21,7 +23,17 @@ class InventoryListScreen extends StatelessWidget { Widget build(BuildContext context) { final t = context.t; return Scaffold( - appBar: AppBar(title: Text(t.inventory.title)), + appBar: AppBar( + title: Text(t.inventory.title), + actions: [ + IconButton( + key: const Key('inventory.captureBurst'), + icon: const Icon(Icons.add_a_photo_outlined), + tooltip: t.draft.capture, + onPressed: () => _captureBurst(context), + ), + ], + ), drawer: const AppDrawer(), floatingActionButton: FloatingActionButton( key: const Key('inventory.addFab'), @@ -39,6 +51,8 @@ class InventoryListScreen extends StatelessWidget { } return Column( children: [ + if (state.drafts.isNotEmpty) + _TriageBanner(count: state.drafts.length), Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), child: TextField( @@ -75,6 +89,45 @@ class InventoryListScreen extends StatelessWidget { ), ); } + + Future _captureBurst(BuildContext context) async { + final t = context.t; + final repository = context.read(); + final messenger = ScaffoldMessenger.of(context); + final count = await captureDraftBurst(context, repository); + if (count > 0) { + messenger.showSnackBar( + SnackBar(content: Text(t.draft.captured(n: count))), + ); + } + } +} + +/// A tappable banner announcing how many photo-first captures are waiting to be +/// named. Opens the "to catalogue" tray. +class _TriageBanner extends StatelessWidget { + const _TriageBanner({required this.count}); + + final int count; + + @override + Widget build(BuildContext context) { + final t = context.t; + return Material( + color: seedAvatar, + child: ListTile( + key: const Key('inventory.triageBanner'), + leading: const Icon(Icons.inventory_2_outlined, color: seedGreen), + title: Text( + t.draft.triageCount(n: count), + style: const TextStyle(fontWeight: FontWeight.w500), + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => + showTriageSheet(context, context.read()), + ), + ); + } } /// A horizontally scrolling row of filter chips: one per category in use plus @@ -94,9 +147,42 @@ class _FilterBar extends StatelessWidget { for (final type in LotType.values) if (state.items.any((i) => i.lotTypes.contains(type))) type, ]; - if (categories.isEmpty && forms.isEmpty) return const SizedBox.shrink(); + // Only offer the eco chip when some variety is flagged organic. + final hasOrganic = state.hasOrganic; + // Only offer the "to regrow" chip when something is flagged for it. + final hasNeedsReproduction = state.hasNeedsReproduction; + if (categories.isEmpty && + forms.isEmpty && + !hasOrganic && + !hasNeedsReproduction) { + return const SizedBox.shrink(); + } final chips = [ + if (hasOrganic) + FilterChip( + key: const Key('inventory.filter.organic'), + avatar: Icon( + Icons.eco, + size: 18, + color: state.organicOnly ? null : seedGreen, + ), + label: Text(t.editVariety.organic), + selected: state.organicOnly, + onSelected: (_) => cubit.toggleOrganicOnly(), + ), + if (hasNeedsReproduction) + FilterChip( + key: const Key('inventory.filter.needsReproduction'), + avatar: Icon( + Icons.autorenew, + size: 18, + color: state.needsReproductionOnly ? null : seedGreen, + ), + label: Text(t.inventory.needsReproductionFilter), + selected: state.needsReproductionOnly, + onSelected: (_) => cubit.toggleNeedsReproductionOnly(), + ), for (final category in categories) FilterChip( key: Key('inventory.filter.category.$category'), @@ -113,7 +199,10 @@ class _FilterBar extends StatelessWidget { ), ]; final hasActiveFilter = - state.categoryFilter.isNotEmpty || state.typeFilter.isNotEmpty; + state.categoryFilter.isNotEmpty || + state.typeFilter.isNotEmpty || + state.organicOnly || + state.needsReproductionOnly; return SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -122,7 +211,7 @@ class _FilterBar extends StatelessWidget { children: [ for (final chip in chips) Padding( - padding: const EdgeInsets.only(right: 8), + padding: const EdgeInsetsDirectional.only(end: 8), child: chip, ), if (hasActiveFilter) @@ -231,18 +320,61 @@ class _VarietyTile extends StatelessWidget { item.scientificName!, style: const TextStyle(fontStyle: FontStyle.italic), ), - trailing: IconButton( - icon: const Icon(Icons.edit_outlined), - // Action colour: this is a tap target, not secondary text. - color: seedGreen, - tooltip: context.t.common.edit, - onPressed: open, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (item.isOrganic) + Padding( + padding: const EdgeInsetsDirectional.only(end: 4), + child: Tooltip( + key: Key('inventory.organic.${item.id}'), + message: context.t.editVariety.organic, + child: const Icon(Icons.eco, size: 20, color: seedGreen), + ), + ), + _ViabilityDot(item.viability), + IconButton( + icon: const Icon(Icons.edit_outlined), + // Action colour: this is a tap target, not secondary text. + color: seedGreen, + tooltip: context.t.common.edit, + onPressed: open, + ), + ], ), onTap: open, ); } } +/// A small aging indicator on a list tile: an amber clock when some seed lot is +/// in its last viable year, a red alert when one is past typical viability. +/// Renders nothing for fresh or unknown lots. +class _ViabilityDot extends StatelessWidget { + const _ViabilityDot(this.status); + + final SeedViability status; + + @override + Widget build(BuildContext context) { + final t = context.t; + final expired = status == SeedViability.expired; + if (status != SeedViability.expiringSoon && !expired) { + return const SizedBox.shrink(); + } + final scheme = Theme.of(context).colorScheme; + return Tooltip( + key: Key('inventory.viability.${expired ? 'expired' : 'soon'}'), + message: expired ? t.viability.expired : t.viability.expiringSoon, + child: Icon( + expired ? Icons.error_outline : Icons.schedule, + size: 20, + color: expired ? scheme.error : const Color(0xFFB26A00), + ), + ); + } +} + /// Rounded-square photo thumbnail, or a green initial circle when there is none. class _Avatar extends StatelessWidget { const _Avatar({required this.item}); diff --git a/apps/app_seeds/lib/ui/photo_pick.dart b/apps/app_seeds/lib/ui/photo_pick.dart index 6e57cb2..f01f684 100644 --- a/apps/app_seeds/lib/ui/photo_pick.dart +++ b/apps/app_seeds/lib/ui/photo_pick.dart @@ -44,3 +44,58 @@ Future pickPhoto(BuildContext context) async { return null; } } + +/// Rapid multi-capture for the "capture now, catalogue later" flow: pick many +/// photos from the gallery at once, or shoot a burst with the camera (it +/// reopens after each shot until the user cancels). Returns every captured +/// image's bytes, or an empty list if cancelled/unavailable. +Future> pickPhotos(BuildContext context) async { + final t = context.t; + final source = await showModalBottomSheet( + context: context, + builder: (sheetContext) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + key: const Key('photos.source.camera'), + leading: const Icon(Icons.photo_camera_outlined), + title: Text(t.photo.camera), + onTap: () => Navigator.of(sheetContext).pop(ImageSource.camera), + ), + ListTile( + key: const Key('photos.source.gallery'), + leading: const Icon(Icons.photo_library_outlined), + title: Text(t.photo.gallery), + onTap: () => Navigator.of(sheetContext).pop(ImageSource.gallery), + ), + ], + ), + ), + ); + if (source == null) return const []; + final picker = ImagePicker(); + try { + if (source == ImageSource.gallery) { + final files = await picker.pickMultiImage( + maxWidth: 1280, + imageQuality: 80, + ); + return [for (final file in files) await file.readAsBytes()]; + } + // Camera burst: keep reopening the camera until the user cancels. + final shots = []; + while (true) { + final file = await picker.pickImage( + source: ImageSource.camera, + maxWidth: 1280, + imageQuality: 80, + ); + if (file == null) break; + shots.add(await file.readAsBytes()); + } + return shots; + } on Object { + return const []; + } +} diff --git a/apps/app_seeds/lib/ui/quick_add_sheet.dart b/apps/app_seeds/lib/ui/quick_add_sheet.dart index f4dc1c2..d8d5542 100644 --- a/apps/app_seeds/lib/ui/quick_add_sheet.dart +++ b/apps/app_seeds/lib/ui/quick_add_sheet.dart @@ -72,7 +72,7 @@ class QuickAddSheet extends StatelessWidget { ), const SizedBox(height: 8), Align( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, child: TextButton.icon( onPressed: cubit.toggleExpanded, icon: Icon( @@ -197,7 +197,7 @@ class _MoreSection extends StatelessWidget { ), ), Align( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, child: OutlinedButton.icon( onPressed: () async { final bytes = await pickPhoto(context); diff --git a/apps/app_seeds/lib/ui/seed_glyph.dart b/apps/app_seeds/lib/ui/seed_glyph.dart index 8283e8f..828446c 100644 --- a/apps/app_seeds/lib/ui/seed_glyph.dart +++ b/apps/app_seeds/lib/ui/seed_glyph.dart @@ -56,7 +56,7 @@ String? seedGlyphForKind(QuantityKind kind) => switch (kind) { QuantityKind.cup => SeedGlyphs.mug, QuantityKind.jar => SeedGlyphs.jar, QuantityKind.sack => SeedGlyphs.sack, - QuantityKind.packet => SeedGlyphs.envelope, + QuantityKind.packet => SeedGlyphs.pouring, _ => null, }; diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index 41d941d..08abdc7 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -7,6 +8,8 @@ import 'package:url_launcher/url_launcher.dart'; import '../data/species_repository.dart'; import '../data/variety_repository.dart'; import '../db/enums.dart'; +import '../domain/crop_calendar.dart'; +import '../domain/seed_viability.dart'; import '../i18n/strings.g.dart'; import '../state/variety_detail_cubit.dart'; import 'harvest_date_picker.dart'; @@ -108,6 +111,11 @@ class _DetailView extends StatelessWidget { _SectionTitle(t.detail.notes), Text(detail.notes!), ], + if (detail.hasCropCalendar) ...[ + const SizedBox(height: 16), + _SectionTitle(t.cropCalendar.title), + _CropCalendarView(detail: detail), + ], const SizedBox(height: 16), _SectionTitle(t.detail.links), Wrap( @@ -149,40 +157,10 @@ class _DetailView extends StatelessWidget { Text(t.detail.noLots) else for (final lot in detail.lots) - ListTile( - key: Key('lot.${lot.id}'), - contentPadding: EdgeInsets.zero, - onTap: () => _showLotSheet(context, cubit, existing: lot), - leading: lot.quantity == null - ? const Icon(Icons.inventory_2_outlined) - : QuantityKindIcon(lot.quantity!.kind, size: 28), - title: Row( - children: [ - _LotTypeChip(type: lot.type), - const SizedBox(width: 8), - Expanded(child: Text(_lotSubtitle(t, lot))), - ], - ), - subtitle: lot.storageLocation == null - ? null - : Text(lot.storageLocation!), - // Germination only applies to seed lots, not to plantel. - trailing: lot.type != LotType.seed - ? null - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (lot.latestGerminationRate != null) - _GerminationBadge(rate: lot.latestGerminationRate!), - IconButton( - key: Key('lot.germination.${lot.id}'), - icon: const Icon(Icons.grass_outlined), - tooltip: t.germination.title, - onPressed: () => - _showGerminationSheet(context, cubit, lot), - ), - ], - ), + _LotTile( + cubit: cubit, + lot: lot, + viabilityYears: detail.viabilityYears, ), ], ), @@ -196,15 +174,167 @@ String _lotSubtitle(Translations t, VarietyLot lot) { harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth) else t.detail.noYear, - if (lot.quantity != null) quantityDisplay(t, lot.quantity!), + if (lot.quantity != null) + quantityDisplay(t, lot.quantity!) + else if (lot.abundance != null) + abundanceLabel(t, lot.abundance!), if (lot.presentation != null) presentationLabel(t, lot.presentation!), ]; return parts.join(' · '); } +/// The provenance line for a lot ("from · place"), or null when neither is set. +String? _lotOrigin(VarietyLot lot) { + final parts = [?lot.originName, ?lot.originPlace]; + return parts.isEmpty ? null : parts.join(' · '); +} + String _germinationPercent(Translations t, double rate) => t.germination.result(percent: (rate * 100).round()); +/// One lot row: kind, harvest/quantity summary, storage location, an aging +/// (viability) warning for seed lots, and the germination test affordance. +class _LotTile extends StatelessWidget { + const _LotTile({ + required this.cubit, + required this.lot, + required this.viabilityYears, + }); + + final VarietyDetailCubit cubit; + final VarietyLot lot; + + /// Typical seed longevity of the variety's species (years), or null. + final int? viabilityYears; + + @override + Widget build(BuildContext context) { + final t = context.t; + // Aging only makes sense for seed lots (living material is not stored dry). + final viability = lot.type == LotType.seed + ? seedViability( + harvestYear: lot.harvestYear, + viabilityYears: viabilityYears, + currentYear: DateTime.now().year, + ) + : SeedViability.unknown; + final warning = _ViabilityWarning.forStatus(viability, viabilityYears); + + return ListTile( + key: Key('lot.${lot.id}'), + contentPadding: EdgeInsets.zero, + onTap: () => _showLotSheet(context, cubit, existing: lot), + leading: lot.quantity == null + ? const Icon(Icons.inventory_2_outlined) + : QuantityKindIcon(lot.quantity!.kind, size: 28), + title: Row( + children: [ + _LotTypeChip(type: lot.type), + const SizedBox(width: 8), + Expanded(child: Text(_lotSubtitle(t, lot))), + ], + ), + subtitle: + (lot.storageLocation == null && + warning == null && + _lotOrigin(lot) == null) + ? null + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_lotOrigin(lot) case final origin?) + Row( + children: [ + const Icon(Icons.place_outlined, size: 14), + const SizedBox(width: 4), + Expanded(child: Text(origin)), + ], + ), + if (lot.storageLocation case final loc?) Text(loc), + ?warning, + ], + ), + // Germination only applies to seed lots, not to plantel. + trailing: lot.type != LotType.seed + ? null + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (lot.latestGerminationRate != null) + _GerminationBadge(rate: lot.latestGerminationRate!), + IconButton( + key: Key('lot.germination.${lot.id}'), + icon: const Icon(Icons.grass_outlined), + tooltip: t.germination.title, + onPressed: () => _showGerminationSheet(context, cubit, lot), + ), + ], + ), + ); + } +} + +/// An inline "sow/reproduce this seed soon" line for an aging seed lot. Renders +/// nothing (returns null) when the lot is fresh or its age can't be judged. +class _ViabilityWarning extends StatelessWidget { + const _ViabilityWarning._({ + required this.status, + required this.viabilityYears, + }); + + static _ViabilityWarning? forStatus(SeedViability status, int? years) { + if (status != SeedViability.expiringSoon && + status != SeedViability.expired) { + return null; + } + return _ViabilityWarning._(status: status, viabilityYears: years); + } + + final SeedViability status; + final int? viabilityYears; + + @override + Widget build(BuildContext context) { + final t = context.t; + final scheme = Theme.of(context).colorScheme; + final expired = status == SeedViability.expired; + final color = expired ? scheme.error : const Color(0xFFB26A00); + final years = viabilityYears; + final text = expired + ? (years == null + ? t.viability.expired + : t.viability.expiredYears(years: years)) + : (years == null + ? t.viability.expiringSoon + : t.viability.expiringSoonYears(years: years)); + return Padding( + key: Key('lot.viability.${expired ? 'expired' : 'soon'}'), + padding: const EdgeInsets.only(top: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + expired ? Icons.error_outline : Icons.schedule, + size: 14, + color: color, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + text, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} + /// A small pill making a lot's kind (seeds vs plants/plantel) unmistakable. class _LotTypeChip extends StatelessWidget { const _LotTypeChip({required this.type}); @@ -447,6 +577,14 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { List _suggestions = const []; String? _pickedSpeciesId; + late bool _isOrganic = widget.detail.isOrganic; + late bool _needsReproduction = widget.detail.needsReproduction; + late int? _sowMonths = widget.detail.sowMonths; + late int? _transplantMonths = widget.detail.transplantMonths; + late int? _floweringMonths = widget.detail.floweringMonths; + late int? _fruitingMonths = widget.detail.fruitingMonths; + late int? _seedHarvestMonths = widget.detail.seedHarvestMonths; + late bool _calendarOpen = widget.detail.hasCropCalendar; @override void dispose() { @@ -477,6 +615,13 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { label: name.isEmpty ? null : name, category: _nullIfBlank(_category.text), notes: _nullIfBlank(_notes.text), + isOrganic: _isOrganic, + needsReproduction: _needsReproduction, + sowMonths: Value(_sowMonths), + transplantMonths: Value(_transplantMonths), + floweringMonths: Value(_floweringMonths), + fruitingMonths: Value(_fruitingMonths), + seedHarvestMonths: Value(_seedHarvestMonths), ); if (_pickedSpeciesId != null) { widget.cubit.linkSpecies(_pickedSpeciesId!); @@ -507,6 +652,7 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { TextField( key: const Key('editVariety.name'), controller: _name, + textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( labelText: t.editVariety.name, border: const OutlineInputBorder( @@ -539,6 +685,7 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { const SizedBox(height: 12), TextField( controller: _category, + textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( labelText: t.editVariety.category, border: const OutlineInputBorder( @@ -558,6 +705,62 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { ), ), ), + const SizedBox(height: 4), + SwitchListTile( + key: const Key('editVariety.organic'), + contentPadding: EdgeInsets.zero, + secondary: const Icon(Icons.eco_outlined, color: seedGreen), + title: Text(t.editVariety.organic), + subtitle: Text(t.editVariety.organicHint), + value: _isOrganic, + onChanged: (v) => setState(() => _isOrganic = v), + ), + SwitchListTile( + key: const Key('editVariety.needsReproduction'), + contentPadding: EdgeInsets.zero, + secondary: const Icon(Icons.autorenew, color: seedGreen), + title: Text(t.needsReproduction.label), + subtitle: Text(t.needsReproduction.hint), + value: _needsReproduction, + onChanged: (v) => setState(() => _needsReproduction = v), + ), + // Crop calendar: one collapsed reveal, not five dropdowns up front. + ExpansionTile( + key: const Key('editVariety.cropCalendar'), + initiallyExpanded: _calendarOpen, + tilePadding: EdgeInsets.zero, + childrenPadding: const EdgeInsets.only(bottom: 8), + leading: const Icon(Icons.calendar_month_outlined), + title: Text(t.cropCalendar.title), + onExpansionChanged: (v) => _calendarOpen = v, + children: [ + _MonthMultiSelect( + label: t.cropCalendar.sow, + mask: _sowMonths, + onChanged: (m) => setState(() => _sowMonths = m), + ), + _MonthMultiSelect( + label: t.cropCalendar.transplant, + mask: _transplantMonths, + onChanged: (m) => setState(() => _transplantMonths = m), + ), + _MonthMultiSelect( + label: t.cropCalendar.flowering, + mask: _floweringMonths, + onChanged: (m) => setState(() => _floweringMonths = m), + ), + _MonthMultiSelect( + label: t.cropCalendar.fruiting, + mask: _fruitingMonths, + onChanged: (m) => setState(() => _fruitingMonths = m), + ), + _MonthMultiSelect( + label: t.cropCalendar.seedHarvest, + mask: _seedHarvestMonths, + onChanged: (m) => setState(() => _seedHarvestMonths = m), + ), + ], + ), const SizedBox(height: 16), Row( children: [ @@ -580,6 +783,322 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { } } +/// A labelled multi-select of the 12 months for one crop-calendar phase — a +/// phase can span several months. [mask] is a 12-bit month mask; tapping a +/// month toggles its bit. +class _MonthMultiSelect extends StatelessWidget { + const _MonthMultiSelect({ + required this.label, + required this.mask, + required this.onChanged, + }); + + final String label; + final int? mask; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final t = context.t; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: Theme.of(context).textTheme.labelLarge), + const SizedBox(height: 6), + Wrap( + spacing: 6, + runSpacing: 4, + children: [ + for (var m = 1; m <= 12; m++) + FilterChip( + key: Key('calendar.$label.$m'), + visualDensity: VisualDensity.compact, + labelPadding: const EdgeInsets.symmetric(horizontal: 2), + label: Text(_monthAbbrev(t.harvest.monthNames[m - 1])), + selected: maskHasMonth(mask, m), + onSelected: (_) => onChanged(toggleMonth(mask, m)), + ), + ], + ), + ], + ), + ); + } +} + +/// A short (≤3-char) label for a month name, for the compact calendar chips. +String _monthAbbrev(String name) => + name.length <= 3 ? name : name.substring(0, 3); + +/// Read-only crop calendar: one line per recorded phase ("Sow · Mar, Apr, Sep"). +/// Renders only the phases that have months set. +class _CropCalendarView extends StatelessWidget { + const _CropCalendarView({required this.detail}); + + final VarietyDetail detail; + + @override + Widget build(BuildContext context) { + final t = context.t; + final phases = <(String, int?)>[ + (t.cropCalendar.sow, detail.sowMonths), + (t.cropCalendar.transplant, detail.transplantMonths), + (t.cropCalendar.flowering, detail.floweringMonths), + (t.cropCalendar.fruiting, detail.fruitingMonths), + (t.cropCalendar.seedHarvest, detail.seedHarvestMonths), + ]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final (label, mask) in phases) + if (maskToMonths(mask).isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: '$label · ', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + TextSpan( + text: maskToMonths(mask) + .map((m) => _monthAbbrev(t.harvest.monthNames[m - 1])) + .join(', '), + ), + ], + ), + ), + ), + ], + ); + } +} + +/// Human label for a coarse-abundance level. +String abundanceLabel(Translations t, Abundance a) => switch (a) { + Abundance.plentyToShare => t.abundance.plentyToShare, + Abundance.enoughToShare => t.abundance.enoughToShare, + Abundance.enoughForMe => t.abundance.enoughForMe, + Abundance.runningLow => t.abundance.runningLow, +}; + +/// Human label for a seed preservation format. +String preservationLabel(Translations t, PreservationFormat p) => switch (p) { + PreservationFormat.jarWithDesiccant => t.preservation.jarWithDesiccant, + PreservationFormat.glassJar => t.preservation.glassJar, + PreservationFormat.paperEnvelope => t.preservation.paperEnvelope, + PreservationFormat.paperBag => t.preservation.paperBag, + PreservationFormat.plasticBag => t.preservation.plasticBag, +}; + +/// Human label for a drying-agent (silica) state. +String desiccantLabel(Translations t, DesiccantState d) => switch (d) { + DesiccantState.none => t.desiccant.none, + DesiccantState.add => t.desiccant.add, + DesiccantState.replace => t.desiccant.replace, + DesiccantState.dry => t.desiccant.dry, + DesiccantState.fresh => t.desiccant.fresh, +}; + +/// Single-select chips for the coarse-abundance level; tapping the current one +/// clears it (null). +class _AbundanceSelector extends StatelessWidget { + const _AbundanceSelector({required this.value, required this.onChanged}); + + final Abundance? value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final t = context.t; + return Wrap( + spacing: 8, + runSpacing: 4, + children: [ + for (final a in Abundance.values) + ChoiceChip( + key: Key('abundance.${a.name}'), + label: Text(abundanceLabel(t, a)), + selected: value == a, + onSelected: (sel) => onChanged(sel ? a : null), + ), + ], + ); + } +} + +/// Single-select chips for the preservation format; tapping the current one +/// clears it (null). +class _PreservationSelector extends StatelessWidget { + const _PreservationSelector({required this.value, required this.onChanged}); + + final PreservationFormat? value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final t = context.t; + return Wrap( + spacing: 8, + runSpacing: 4, + children: [ + for (final p in PreservationFormat.values) + ChoiceChip( + key: Key('preservation.${p.name}'), + label: Text(preservationLabel(t, p)), + selected: value == p, + onSelected: (sel) => onChanged(sel ? p : null), + ), + ], + ); + } +} + +/// Lists a lot's storage-condition checks (newest first) and offers to add one. +class _ConditionChecksBlock extends StatelessWidget { + const _ConditionChecksBlock({required this.cubit, required this.lot}); + + final VarietyDetailCubit cubit; + final VarietyLot lot; + + @override + Widget build(BuildContext context) { + final t = context.t; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + t.conditionCheck.title, + style: Theme.of(context).textTheme.labelLarge, + ), + ), + if (lot.conditionChecks.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text(t.conditionCheck.none), + ) + else + for (final check in lot.conditionChecks) + ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.inventory_2_outlined), + title: Text( + t.conditionCheck.summary( + count: check.containerCount?.toString() ?? '—', + state: check.desiccantState == null + ? '—' + : desiccantLabel(t, check.desiccantState!), + ), + ), + ), + Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + key: const Key('conditionCheck.open'), + icon: const Icon(Icons.add), + label: Text(t.conditionCheck.add), + onPressed: () => _showConditionSheet(context, cubit, lot), + ), + ), + ], + ); + } +} + +Future _showConditionSheet( + BuildContext context, + VarietyDetailCubit cubit, + VarietyLot lot, +) { + final t = context.t; + final containers = TextEditingController(); + DesiccantState? state; + return showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (sheetContext) => StatefulBuilder( + builder: (sheetContext, setState) => Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + t.conditionCheck.title, + style: Theme.of(sheetContext).textTheme.titleLarge, + ), + const SizedBox(height: 12), + TextField( + key: const Key('conditionCheck.containers'), + controller: containers, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: t.conditionCheck.containers, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + const SizedBox(height: 16), + Text( + t.conditionCheck.desiccant, + style: Theme.of(sheetContext).textTheme.labelLarge, + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 4, + children: [ + for (final d in DesiccantState.values) + ChoiceChip( + key: Key('desiccant.${d.name}'), + label: Text(desiccantLabel(t, d)), + selected: state == d, + onSelected: (sel) => setState(() => state = sel ? d : null), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + TextButton( + onPressed: () => Navigator.of(sheetContext).pop(), + child: Text(t.common.cancel), + ), + const Spacer(), + FilledButton( + key: const Key('conditionCheck.save'), + onPressed: () { + cubit.addConditionCheck( + lotId: lot.id, + checkedOn: DateTime.now().millisecondsSinceEpoch, + containerCount: int.tryParse(containers.text.trim()), + desiccantState: state, + ); + Navigator.of(sheetContext).pop(); + }, + child: Text(t.conditionCheck.add), + ), + ], + ), + ], + ), + ), + ), + ); +} + Future _showLotSheet( BuildContext context, VarietyDetailCubit cubit, { @@ -592,6 +1111,17 @@ Future _showLotSheet( var selectedQuantity = existing?.quantity; var selectedPresentation = existing?.presentation; final editing = existing != null; + // Provenance + abundance + preservation: hidden behind reveal-on-tap chips so + // the default form stays small (progressive disclosure). + final originNameCtrl = TextEditingController(text: existing?.originName ?? ''); + final originPlaceCtrl = TextEditingController( + text: existing?.originPlace ?? '', + ); + var selectedAbundance = existing?.abundance; + var selectedPreservation = existing?.preservationFormat; + var showOrigin = + existing?.originName != null || existing?.originPlace != null; + var showAbundance = existing?.abundance != null; return showModalBottomSheet( context: context, isScrollControlled: true, @@ -682,6 +1212,99 @@ Future _showLotSheet( value: selectedQuantity, onChanged: (q) => selectedQuantity = q, ), + const SizedBox(height: 12), + // Layer 1: reveal-on-tap extras, one field per chip. + Wrap( + spacing: 8, + children: [ + if (!showOrigin) + ActionChip( + key: const Key('lot.addOrigin'), + avatar: const Icon(Icons.place_outlined, size: 18), + label: Text(t.provenance.section), + onPressed: () => setState(() => showOrigin = true), + ), + if (!showAbundance) + ActionChip( + key: const Key('lot.addAbundance'), + avatar: const Icon(Icons.inventory_2_outlined, size: 18), + label: Text(t.abundance.add), + onPressed: () => + setState(() => showAbundance = true), + ), + ], + ), + if (showOrigin) ...[ + const SizedBox(height: 12), + TextField( + key: const Key('lot.originName'), + controller: originNameCtrl, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + labelText: t.provenance.seedsFrom, + helperText: t.provenance.seedsFromHint, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + const SizedBox(height: 12), + TextField( + key: const Key('lot.originPlace'), + controller: originPlaceCtrl, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + labelText: t.provenance.place, + helperText: t.provenance.placeHint, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + ], + if (showAbundance) ...[ + const SizedBox(height: 16), + Text( + t.abundance.title, + style: Theme.of(sheetContext).textTheme.labelLarge, + ), + const SizedBox(height: 8), + _AbundanceSelector( + value: selectedAbundance, + onChanged: (a) => setState(() => selectedAbundance = a), + ), + ], + // Layer 2: advanced seed-bank details, collapsed by default. + // Only for seed lots, and (condition checks) an existing lot. + if (selectedType == LotType.seed) + ExpansionTile( + key: const Key('lot.advanced'), + tilePadding: EdgeInsets.zero, + childrenPadding: const EdgeInsets.only(bottom: 8), + leading: const Icon(Icons.inventory_outlined), + title: Text(t.conditionCheck.advanced), + children: [ + const SizedBox(height: 4), + Align( + alignment: Alignment.centerLeft, + child: Text( + t.preservation.title, + style: + Theme.of(sheetContext).textTheme.labelLarge, + ), + ), + const SizedBox(height: 8), + _PreservationSelector( + value: selectedPreservation, + onChanged: (p) => + setState(() => selectedPreservation = p), + ), + if (editing) ...[ + const SizedBox(height: 8), + _ConditionChecksBlock(cubit: cubit, lot: existing), + ], + ], + ), ], ), ), @@ -697,6 +1320,8 @@ Future _showLotSheet( FilledButton( key: const Key('addLot.save'), onPressed: () { + final originName = _nullIfBlank(originNameCtrl.text); + final originPlace = _nullIfBlank(originPlaceCtrl.text); if (editing) { cubit.updateLot( lotId: existing.id, @@ -705,6 +1330,10 @@ Future _showLotSheet( month: selectedMonth, quantity: selectedQuantity, presentation: selectedPresentation, + originName: originName, + originPlace: originPlace, + abundance: selectedAbundance, + preservationFormat: selectedPreservation, ); } else { cubit.addLot( @@ -713,6 +1342,10 @@ Future _showLotSheet( month: selectedMonth, quantity: selectedQuantity, presentation: selectedPresentation, + originName: originName, + originPlace: originPlace, + abundance: selectedAbundance, + preservationFormat: selectedPreservation, ); } Navigator.of(sheetContext).pop(); @@ -748,6 +1381,7 @@ Future _showAddNameDialog( key: const Key('name.field'), controller: controller, autofocus: true, + textCapitalization: TextCapitalization.sentences, decoration: InputDecoration(labelText: t.editVariety.name), onSubmitted: (_) => submit(dialogContext), ), @@ -840,7 +1474,11 @@ class _DetailHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final hasText = detail.category != null || detail.scientificName != null; + final hasText = + detail.category != null || + detail.scientificName != null || + detail.isOrganic || + detail.needsReproduction; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -848,6 +1486,14 @@ class _DetailHeader extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (detail.isOrganic) ...[ + _OrganicChip(), + const SizedBox(height: 6), + ], + if (detail.needsReproduction) ...[ + _NeedsReproductionChip(), + const SizedBox(height: 6), + ], if (detail.category != null) Row( mainAxisSize: MainAxisSize.min, @@ -889,6 +1535,66 @@ class _DetailHeader extends StatelessWidget { } } +/// A small green "Organic" pill shown on an organic variety's detail header. +class _OrganicChip extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + key: const Key('detail.organic'), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: seedGreen.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.eco, size: 15, color: seedGreen), + const SizedBox(width: 4), + Text( + context.t.editVariety.organic, + style: const TextStyle( + color: seedGreen, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ], + ), + ); + } +} + +/// A small "to regrow" pill shown on a variety flagged as needing reproduction. +class _NeedsReproductionChip extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + key: const Key('detail.needsReproduction'), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: seedGreen.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.autorenew, size: 15, color: seedGreen), + const SizedBox(width: 4), + Text( + context.t.needsReproduction.badge, + style: const TextStyle( + color: seedGreen, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ], + ), + ); + } +} + /// A 140×140 photo carousel (mockup 07): swipe between photos, page dots, add a /// photo, and delete the current one. Shows just an "add" button when empty. class _PhotoGallery extends StatefulWidget { @@ -1131,6 +1837,11 @@ class _PhotoViewerState extends State<_PhotoViewer> { IconButton( key: const Key('photo.cover'), icon: Icon(isCover ? Icons.star : Icons.star_border), + // The cover star is disabled; without an explicit disabled + // color it renders faded/dark on the black bar. Amber both + // keeps the contrast and reads as "selected". + color: Colors.white, + disabledColor: Colors.amber, tooltip: isCover ? context.t.photo.isCover : context.t.photo.setAsCover, diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index e0ba7c8..20e28ce 100644 --- a/apps/app_seeds/pubspec.yaml +++ b/apps/app_seeds/pubspec.yaml @@ -44,6 +44,12 @@ dependencies: # Routing + media + paths. go_router: ^14.6.2 image_picker: ^1.1.2 + # On-device OCR (Tesseract, Apache-2.0/BSD) to suggest a name from a packet + # photo. Offline, no Google/cloud. Mobile-only; other platforms use a no-op. + flutter_tesseract_ocr: ^0.4.31 + # Pure-Dart image preprocessing (grayscale/contrast/upscale) before OCR to + # lift accuracy on blurry packet photos. + image: ^4.3.0 # Save/open dialogs for the inventory export/import (MIT license). file_picker: ^10.1.2 path: ^1.9.0 @@ -82,6 +88,10 @@ flutter_launcher_icons: image_path: assets/icon.png background_color: "#2F7D34" theme_color: "#2F7D34" + windows: + generate: true + image_path: assets/icon.png + icon_size: 256 # Native splash: white sprout on brand green, a darker green in dark mode. # Regenerate with: dart run flutter_native_splash:create @@ -102,6 +112,13 @@ flutter: - assets/logo.png # Bundled so the Linux runner can use it as the GTK window icon. - assets/icon.png + # Tesseract OCR language data (tessdata_fast: eng + spa). Bundled for + # offline, on-device label OCR on mobile. + - assets/tessdata/ + - assets/tessdata_config.json + # Real packet photos for the on-device OCR validation integration test. + # Remove before shipping if you don't want them in the bundle. + - assets/ocr_fixtures/ fonts: # Custom seed-themed icon glyphs (chars a–k). See docs/mockups/seedks-glyphs.png. - family: seedks diff --git a/apps/app_seeds/test/data/draft_capture_test.dart b/apps/app_seeds/test/data/draft_capture_test.dart new file mode 100644 index 0000000..6dae079 --- /dev/null +++ b/apps/app_seeds/test/data/draft_capture_test.dart @@ -0,0 +1,57 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/data/variety_repository.dart'; +import 'package:tane/db/database.dart'; + +import '../support/test_support.dart'; + +void main() { + late AppDatabase db; + late VarietyRepository repo; + + setUp(() { + db = newTestDatabase(); + repo = newTestRepository(db); + }); + tearDown(() => db.close()); + + Uint8List photo(int seed) => Uint8List.fromList([seed, seed, seed]); + + test('a captured draft is hidden from the list but shown in the tray', + () async { + final id = await repo.addDraftVariety(photo(1)); + + // Not in the main inventory… + expect(await repo.watchInventory().first, isEmpty); + + // …but in the "to catalogue" tray, with its photo and no name yet. + final drafts = await repo.watchDrafts().first; + expect(drafts, hasLength(1)); + expect(drafts.single.id, id); + expect(drafts.single.isDraft, isTrue); + expect(drafts.single.label, ''); + expect(drafts.single.photo, isNotNull); + }); + + test('naming a draft promotes it into the list and out of the tray', + () async { + final id = await repo.addDraftVariety(photo(2)); + await repo.nameDraft(id, 'Grandma tomato'); + + expect(await repo.watchDrafts().first, isEmpty); + final list = await repo.watchInventory().first; + expect(list.single.id, id); + expect(list.single.label, 'Grandma tomato'); + expect(list.single.isDraft, isFalse); + // The captured photo survived the promotion. + expect(list.single.photo, isNotNull); + }); + + test('the tray is ordered newest-capture first', () async { + final first = await repo.addDraftVariety(photo(1)); + final second = await repo.addDraftVariety(photo(2)); + final drafts = await repo.watchDrafts().first; + expect(drafts.map((d) => d.id).toList(), [second, first]); + }); +} diff --git a/apps/app_seeds/test/data/export_import/inventory_csv_codec_test.dart b/apps/app_seeds/test/data/export_import/inventory_csv_codec_test.dart index 49629c0..d64f7d9 100644 --- a/apps/app_seeds/test/data/export_import/inventory_csv_codec_test.dart +++ b/apps/app_seeds/test/data/export_import/inventory_csv_codec_test.dart @@ -25,6 +25,9 @@ void main() { cultivarName: null, category: 'Solanaceae', notes: notes, + isDraft: false, + isOrganic: false, + needsReproduction: false, ); Lot lot({String id = 'lot-1', String varietyId = 'var-1'}) => Lot( diff --git a/apps/app_seeds/test/data/export_import/inventory_csv_import_test.dart b/apps/app_seeds/test/data/export_import/inventory_csv_import_test.dart index ca59c7a..04995e3 100644 --- a/apps/app_seeds/test/data/export_import/inventory_csv_import_test.dart +++ b/apps/app_seeds/test/data/export_import/inventory_csv_import_test.dart @@ -123,6 +123,9 @@ void main() { cultivarName: 'Marmande', category: 'Solanaceae', notes: 'heirloom', + isDraft: false, + isOrganic: false, + needsReproduction: false, ); Lot lot({String id = 'lot-1'}) => Lot( diff --git a/apps/app_seeds/test/data/export_import/inventory_json_codec_test.dart b/apps/app_seeds/test/data/export_import/inventory_json_codec_test.dart index da06524..8e0f572 100644 --- a/apps/app_seeds/test/data/export_import/inventory_json_codec_test.dart +++ b/apps/app_seeds/test/data/export_import/inventory_json_codec_test.dart @@ -22,6 +22,9 @@ void main() { cultivarName: 'Marmande', category: 'Solanaceae', notes: 'Keeps well', + isDraft: false, + isOrganic: false, + needsReproduction: false, ); const lot = Lot( id: 'lot-1', diff --git a/apps/app_seeds/test/data/export_import/inventory_round_trip_test.dart b/apps/app_seeds/test/data/export_import/inventory_round_trip_test.dart index 7fcd1bd..64b1450 100644 --- a/apps/app_seeds/test/data/export_import/inventory_round_trip_test.dart +++ b/apps/app_seeds/test/data/export_import/inventory_round_trip_test.dart @@ -7,6 +7,8 @@ import 'package:tane/data/export_import/inventory_json_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/db/enums.dart'; +import 'package:tane/domain/crop_calendar.dart'; import '../../support/test_support.dart'; @@ -41,6 +43,13 @@ void main() { label: 'Grandma tomato', category: 'Solanaceae', notes: 'Sow in March', + needsReproduction: true, + // Sow in spring and again in autumn (a phase can span several months). + sowMonths: Value(monthsToMask(const [3, 4, 9])), + transplantMonths: Value(monthsToMask(const [5])), + floweringMonths: Value(monthsToMask(const [7])), + fruitingMonths: Value(monthsToMask(const [8])), + seedHarvestMonths: Value(monthsToMask(const [9, 10])), ); final lotId = await source.addLot( varietyId: varietyId, @@ -48,6 +57,16 @@ void main() { harvestMonth: 6, quantity: const Quantity(kind: QuantityKind.cob, count: 2), storageLocation: 'fridge', + originName: 'La Verde Coop', + originPlace: 'Cádiz', + abundance: Abundance.plentyToShare, + preservationFormat: PreservationFormat.jarWithDesiccant, + ); + await source.addConditionCheck( + lotId: lotId, + checkedOn: 1700000001000, + containerCount: 2, + desiccantState: DesiccantState.dry, ); await source.addVernacularName( varietyId, @@ -102,6 +121,7 @@ void main() { expect(targetRows.vernacularNames, sourceRows.vernacularNames); expect(targetRows.externalLinks, sourceRows.externalLinks); expect(targetRows.germinationTests, sourceRows.germinationTests); + expect(targetRows.conditionChecks, sourceRows.conditionChecks); expect(targetRows.movements, sourceRows.movements); expect(targetRows.attachments, sourceRows.attachments); diff --git a/apps/app_seeds/test/db/migration_test.dart b/apps/app_seeds/test/db/migration_test.dart index 019a98b..633da2b 100644 --- a/apps/app_seeds/test/db/migration_test.dart +++ b/apps/app_seeds/test/db/migration_test.dart @@ -11,40 +11,66 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - test('freshly created database matches the exported schema v5', () async { - final schema = await verifier.schemaAt(5); + test('freshly created database matches the exported schema v8', () async { + final schema = await verifier.schemaAt(8); final db = AppDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, 5); + await verifier.migrateAndValidate(db, 8); await db.close(); }); - test('upgrades v1 → v5 (Lot.type, harvestMonth, presentation, ' - 'Attachment.sortOrder) and matches the fresh schema', () async { + test('upgrades v1 → v8 (Lot.type, harvestMonth, presentation, ' + 'Attachment.sortOrder, Variety.isDraft/isOrganic, Species.viabilityYears, ' + 'v8 provenance/calendar/abundance/condition-checks) and matches the fresh ' + 'schema', () async { final connection = await verifier.startAt(1); final db = AppDatabase(connection); - await verifier.migrateAndValidate(db, 5); + await verifier.migrateAndValidate(db, 8); await db.close(); }); - test('upgrades v2 → v5 and matches the fresh schema', () async { + test('upgrades v2 → v8 and matches the fresh schema', () async { final connection = await verifier.startAt(2); final db = AppDatabase(connection); - await verifier.migrateAndValidate(db, 5); + await verifier.migrateAndValidate(db, 8); await db.close(); }); - test('upgrades v3 → v5 and matches the fresh schema', () async { + test('upgrades v3 → v8 and matches the fresh schema', () async { final connection = await verifier.startAt(3); final db = AppDatabase(connection); - await verifier.migrateAndValidate(db, 5); + await verifier.migrateAndValidate(db, 8); await db.close(); }); - test('upgrades v4 → v5 (adds Attachment.sortOrder) and matches the fresh ' - 'schema', () async { + test('upgrades v4 → v8 and matches the fresh schema', () async { final connection = await verifier.startAt(4); final db = AppDatabase(connection); - await verifier.migrateAndValidate(db, 5); + await verifier.migrateAndValidate(db, 8); + await db.close(); + }); + + test('upgrades v5 → v8 (adds Variety.isDraft) and matches the fresh schema', + () async { + final connection = await verifier.startAt(5); + final db = AppDatabase(connection); + await verifier.migrateAndValidate(db, 8); + await db.close(); + }); + + test('upgrades v6 → v8 (adds Variety.isOrganic, Species.viabilityYears) and ' + 'matches the fresh schema', () async { + final connection = await verifier.startAt(6); + final db = AppDatabase(connection); + await verifier.migrateAndValidate(db, 8); + await db.close(); + }); + + test('upgrades v7 → v8 (adds Variety.needsReproduction + crop calendar, ' + 'Lot.origin/abundance/preservationFormat, ConditionChecks) and matches ' + 'the fresh schema', () async { + final connection = await verifier.startAt(7); + final db = AppDatabase(connection); + await verifier.migrateAndValidate(db, 8); await db.close(); }); } diff --git a/apps/app_seeds/test/db/schema/schema.dart b/apps/app_seeds/test/db/schema/schema.dart index 9dccfb2..f76d308 100644 --- a/apps/app_seeds/test/db/schema/schema.dart +++ b/apps/app_seeds/test/db/schema/schema.dart @@ -9,6 +9,9 @@ import 'schema_v2.dart' as v2; import 'schema_v3.dart' as v3; import 'schema_v4.dart' as v4; import 'schema_v5.dart' as v5; +import 'schema_v6.dart' as v6; +import 'schema_v7.dart' as v7; +import 'schema_v8.dart' as v8; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -24,10 +27,16 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v4.DatabaseAtV4(db); case 5: return v5.DatabaseAtV5(db); + case 6: + return v6.DatabaseAtV6(db); + case 7: + return v7.DatabaseAtV7(db); + case 8: + return v8.DatabaseAtV8(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8]; } diff --git a/apps/app_seeds/test/db/schema/schema_v6.dart b/apps/app_seeds/test/db/schema/schema_v6.dart new file mode 100644 index 0000000..33e1f46 --- /dev/null +++ b/apps/app_seeds/test/db/schema/schema_v6.dart @@ -0,0 +1,1428 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Varieties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Varieties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn label = GeneratedColumn( + 'label', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn speciesId = GeneratedColumn( + 'species_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn cultivarName = GeneratedColumn( + 'cultivar_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn category = GeneratedColumn( + 'category', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isDraft = GeneratedColumn( + 'is_draft', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_draft IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + label, + speciesId, + cultivarName, + category, + notes, + isDraft, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'varieties'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Varieties createAlias(String alias) { + return Varieties(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class VarietyVernacularNames extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + VarietyVernacularNames(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn varietyId = GeneratedColumn( + 'variety_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn region = GeneratedColumn( + 'region', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + varietyId, + name, + language, + region, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'variety_vernacular_names'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + VarietyVernacularNames createAlias(String alias) { + return VarietyVernacularNames(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Species extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Species(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn scientificName = GeneratedColumn( + 'scientific_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn wikidataQid = GeneratedColumn( + 'wikidata_qid', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn gbifKey = GeneratedColumn( + 'gbif_key', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn family = GeneratedColumn( + 'family', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isBundled = GeneratedColumn( + 'is_bundled', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_bundled IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + scientificName, + wikidataQid, + gbifKey, + family, + isBundled, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'species'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Species createAlias(String alias) { + return Species(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class SpeciesCommonNames extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SpeciesCommonNames(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn speciesId = GeneratedColumn( + 'species_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + speciesId, + name, + language, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'species_common_names'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + SpeciesCommonNames createAlias(String alias) { + return SpeciesCommonNames(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Lots extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Lots(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn varietyId = GeneratedColumn( + 'variety_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'seed\'', + defaultValue: const CustomExpression('\'seed\''), + ); + late final GeneratedColumn harvestYear = GeneratedColumn( + 'harvest_year', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn harvestMonth = GeneratedColumn( + 'harvest_month', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityKind = GeneratedColumn( + 'quantity_kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityPrecise = GeneratedColumn( + 'quantity_precise', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityLabel = GeneratedColumn( + 'quantity_label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn presentation = GeneratedColumn( + 'presentation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn storageLocation = GeneratedColumn( + 'storage_location', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn offerStatus = GeneratedColumn( + 'offer_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'private\'', + defaultValue: const CustomExpression('\'private\''), + ); + late final GeneratedColumn seedbankId = GeneratedColumn( + 'seedbank_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + varietyId, + type, + harvestYear, + harvestMonth, + quantityKind, + quantityPrecise, + quantityLabel, + presentation, + storageLocation, + offerStatus, + seedbankId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lots'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Lots createAlias(String alias) { + return Lots(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class GerminationTests extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GerminationTests(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn testedOn = GeneratedColumn( + 'tested_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn sampleSize = GeneratedColumn( + 'sample_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn germinatedCount = GeneratedColumn( + 'germinated_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + lotId, + testedOn, + sampleSize, + germinatedCount, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'germination_tests'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + GerminationTests createAlias(String alias) { + return GerminationTests(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Movements extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Movements(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn occurredOn = GeneratedColumn( + 'occurred_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn counterpartyId = GeneratedColumn( + 'counterparty_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityKind = GeneratedColumn( + 'quantity_kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityPrecise = GeneratedColumn( + 'quantity_precise', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityLabel = GeneratedColumn( + 'quantity_label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn parentMovementId = GeneratedColumn( + 'parent_movement_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn plantareId = GeneratedColumn( + 'plantare_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + lastAuthor, + schemaRowVersion, + lotId, + type, + occurredOn, + counterpartyId, + quantityKind, + quantityPrecise, + quantityLabel, + parentMovementId, + plantareId, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'movements'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Movements createAlias(String alias) { + return Movements(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Parties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Parties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicKey = GeneratedColumn( + 'public_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'person\'', + defaultValue: const CustomExpression('\'person\''), + ); + late final GeneratedColumn note = GeneratedColumn( + 'note', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + displayName, + publicKey, + kind, + note, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'parties'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Parties createAlias(String alias) { + return Parties(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Attachments extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Attachments(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn parentType = GeneratedColumn( + 'parent_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn uri = GeneratedColumn( + 'uri', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn bytes = + GeneratedColumn( + 'bytes', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn mimeType = GeneratedColumn( + 'mime_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn sortOrder = GeneratedColumn( + 'sort_order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + parentType, + parentId, + kind, + uri, + bytes, + mimeType, + sortOrder, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'attachments'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Attachments createAlias(String alias) { + return Attachments(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class ExternalLinks extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ExternalLinks(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn parentType = GeneratedColumn( + 'parent_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn url = GeneratedColumn( + 'url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + parentType, + parentId, + url, + title, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'external_links'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + ExternalLinks createAlias(String alias) { + return ExternalLinks(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class DatabaseAtV6 extends GeneratedDatabase { + DatabaseAtV6(QueryExecutor e) : super(e); + late final Varieties varieties = Varieties(this); + late final VarietyVernacularNames varietyVernacularNames = + VarietyVernacularNames(this); + late final Species species = Species(this); + late final SpeciesCommonNames speciesCommonNames = SpeciesCommonNames(this); + late final Lots lots = Lots(this); + late final GerminationTests germinationTests = GerminationTests(this); + late final Movements movements = Movements(this); + late final Parties parties = Parties(this); + late final Attachments attachments = Attachments(this); + late final ExternalLinks externalLinks = ExternalLinks(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + varieties, + varietyVernacularNames, + species, + speciesCommonNames, + lots, + germinationTests, + movements, + parties, + attachments, + externalLinks, + ]; + @override + int get schemaVersion => 6; +} diff --git a/apps/app_seeds/test/db/schema/schema_v7.dart b/apps/app_seeds/test/db/schema/schema_v7.dart new file mode 100644 index 0000000..6e23ed9 --- /dev/null +++ b/apps/app_seeds/test/db/schema/schema_v7.dart @@ -0,0 +1,1447 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Varieties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Varieties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn label = GeneratedColumn( + 'label', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn speciesId = GeneratedColumn( + 'species_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn cultivarName = GeneratedColumn( + 'cultivar_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn category = GeneratedColumn( + 'category', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isDraft = GeneratedColumn( + 'is_draft', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_draft IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isOrganic = GeneratedColumn( + 'is_organic', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_organic IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + label, + speciesId, + cultivarName, + category, + notes, + isDraft, + isOrganic, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'varieties'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Varieties createAlias(String alias) { + return Varieties(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class VarietyVernacularNames extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + VarietyVernacularNames(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn varietyId = GeneratedColumn( + 'variety_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn region = GeneratedColumn( + 'region', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + varietyId, + name, + language, + region, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'variety_vernacular_names'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + VarietyVernacularNames createAlias(String alias) { + return VarietyVernacularNames(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Species extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Species(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn scientificName = GeneratedColumn( + 'scientific_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn wikidataQid = GeneratedColumn( + 'wikidata_qid', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn gbifKey = GeneratedColumn( + 'gbif_key', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn family = GeneratedColumn( + 'family', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isBundled = GeneratedColumn( + 'is_bundled', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_bundled IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn viabilityYears = GeneratedColumn( + 'viability_years', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + scientificName, + wikidataQid, + gbifKey, + family, + isBundled, + viabilityYears, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'species'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Species createAlias(String alias) { + return Species(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class SpeciesCommonNames extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SpeciesCommonNames(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn speciesId = GeneratedColumn( + 'species_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + speciesId, + name, + language, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'species_common_names'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + SpeciesCommonNames createAlias(String alias) { + return SpeciesCommonNames(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Lots extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Lots(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn varietyId = GeneratedColumn( + 'variety_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'seed\'', + defaultValue: const CustomExpression('\'seed\''), + ); + late final GeneratedColumn harvestYear = GeneratedColumn( + 'harvest_year', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn harvestMonth = GeneratedColumn( + 'harvest_month', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityKind = GeneratedColumn( + 'quantity_kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityPrecise = GeneratedColumn( + 'quantity_precise', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityLabel = GeneratedColumn( + 'quantity_label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn presentation = GeneratedColumn( + 'presentation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn storageLocation = GeneratedColumn( + 'storage_location', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn offerStatus = GeneratedColumn( + 'offer_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'private\'', + defaultValue: const CustomExpression('\'private\''), + ); + late final GeneratedColumn seedbankId = GeneratedColumn( + 'seedbank_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + varietyId, + type, + harvestYear, + harvestMonth, + quantityKind, + quantityPrecise, + quantityLabel, + presentation, + storageLocation, + offerStatus, + seedbankId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lots'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Lots createAlias(String alias) { + return Lots(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class GerminationTests extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GerminationTests(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn testedOn = GeneratedColumn( + 'tested_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn sampleSize = GeneratedColumn( + 'sample_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn germinatedCount = GeneratedColumn( + 'germinated_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + lotId, + testedOn, + sampleSize, + germinatedCount, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'germination_tests'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + GerminationTests createAlias(String alias) { + return GerminationTests(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Movements extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Movements(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn occurredOn = GeneratedColumn( + 'occurred_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn counterpartyId = GeneratedColumn( + 'counterparty_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityKind = GeneratedColumn( + 'quantity_kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityPrecise = GeneratedColumn( + 'quantity_precise', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityLabel = GeneratedColumn( + 'quantity_label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn parentMovementId = GeneratedColumn( + 'parent_movement_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn plantareId = GeneratedColumn( + 'plantare_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + lastAuthor, + schemaRowVersion, + lotId, + type, + occurredOn, + counterpartyId, + quantityKind, + quantityPrecise, + quantityLabel, + parentMovementId, + plantareId, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'movements'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Movements createAlias(String alias) { + return Movements(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Parties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Parties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicKey = GeneratedColumn( + 'public_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'person\'', + defaultValue: const CustomExpression('\'person\''), + ); + late final GeneratedColumn note = GeneratedColumn( + 'note', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + displayName, + publicKey, + kind, + note, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'parties'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Parties createAlias(String alias) { + return Parties(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Attachments extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Attachments(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn parentType = GeneratedColumn( + 'parent_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn uri = GeneratedColumn( + 'uri', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn bytes = + GeneratedColumn( + 'bytes', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn mimeType = GeneratedColumn( + 'mime_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn sortOrder = GeneratedColumn( + 'sort_order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + parentType, + parentId, + kind, + uri, + bytes, + mimeType, + sortOrder, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'attachments'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Attachments createAlias(String alias) { + return Attachments(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class ExternalLinks extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ExternalLinks(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn parentType = GeneratedColumn( + 'parent_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn url = GeneratedColumn( + 'url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + parentType, + parentId, + url, + title, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'external_links'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + ExternalLinks createAlias(String alias) { + return ExternalLinks(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class DatabaseAtV7 extends GeneratedDatabase { + DatabaseAtV7(QueryExecutor e) : super(e); + late final Varieties varieties = Varieties(this); + late final VarietyVernacularNames varietyVernacularNames = + VarietyVernacularNames(this); + late final Species species = Species(this); + late final SpeciesCommonNames speciesCommonNames = SpeciesCommonNames(this); + late final Lots lots = Lots(this); + late final GerminationTests germinationTests = GerminationTests(this); + late final Movements movements = Movements(this); + late final Parties parties = Parties(this); + late final Attachments attachments = Attachments(this); + late final ExternalLinks externalLinks = ExternalLinks(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + varieties, + varietyVernacularNames, + species, + speciesCommonNames, + lots, + germinationTests, + movements, + parties, + attachments, + externalLinks, + ]; + @override + int get schemaVersion => 7; +} diff --git a/apps/app_seeds/test/db/schema/schema_v8.dart b/apps/app_seeds/test/db/schema/schema_v8.dart new file mode 100644 index 0000000..56de62f --- /dev/null +++ b/apps/app_seeds/test/db/schema/schema_v8.dart @@ -0,0 +1,1674 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Varieties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Varieties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn label = GeneratedColumn( + 'label', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn speciesId = GeneratedColumn( + 'species_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn cultivarName = GeneratedColumn( + 'cultivar_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn category = GeneratedColumn( + 'category', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isDraft = GeneratedColumn( + 'is_draft', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_draft IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isOrganic = GeneratedColumn( + 'is_organic', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_organic IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn needsReproduction = GeneratedColumn( + 'needs_reproduction', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (needs_reproduction IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn sowMonths = GeneratedColumn( + 'sow_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn transplantMonths = GeneratedColumn( + 'transplant_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn floweringMonths = GeneratedColumn( + 'flowering_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn fruitingMonths = GeneratedColumn( + 'fruiting_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn seedHarvestMonths = GeneratedColumn( + 'seed_harvest_months', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + label, + speciesId, + cultivarName, + category, + notes, + isDraft, + isOrganic, + needsReproduction, + sowMonths, + transplantMonths, + floweringMonths, + fruitingMonths, + seedHarvestMonths, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'varieties'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Varieties createAlias(String alias) { + return Varieties(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class VarietyVernacularNames extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + VarietyVernacularNames(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn varietyId = GeneratedColumn( + 'variety_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn region = GeneratedColumn( + 'region', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + varietyId, + name, + language, + region, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'variety_vernacular_names'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + VarietyVernacularNames createAlias(String alias) { + return VarietyVernacularNames(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Species extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Species(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn scientificName = GeneratedColumn( + 'scientific_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn wikidataQid = GeneratedColumn( + 'wikidata_qid', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn gbifKey = GeneratedColumn( + 'gbif_key', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn family = GeneratedColumn( + 'family', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isBundled = GeneratedColumn( + 'is_bundled', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_bundled IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn viabilityYears = GeneratedColumn( + 'viability_years', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + scientificName, + wikidataQid, + gbifKey, + family, + isBundled, + viabilityYears, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'species'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Species createAlias(String alias) { + return Species(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class SpeciesCommonNames extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SpeciesCommonNames(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn speciesId = GeneratedColumn( + 'species_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + speciesId, + name, + language, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'species_common_names'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + SpeciesCommonNames createAlias(String alias) { + return SpeciesCommonNames(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Lots extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Lots(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn varietyId = GeneratedColumn( + 'variety_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'seed\'', + defaultValue: const CustomExpression('\'seed\''), + ); + late final GeneratedColumn harvestYear = GeneratedColumn( + 'harvest_year', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn harvestMonth = GeneratedColumn( + 'harvest_month', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityKind = GeneratedColumn( + 'quantity_kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityPrecise = GeneratedColumn( + 'quantity_precise', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityLabel = GeneratedColumn( + 'quantity_label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn presentation = GeneratedColumn( + 'presentation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn storageLocation = GeneratedColumn( + 'storage_location', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn offerStatus = GeneratedColumn( + 'offer_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'private\'', + defaultValue: const CustomExpression('\'private\''), + ); + late final GeneratedColumn seedbankId = GeneratedColumn( + 'seedbank_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn originName = GeneratedColumn( + 'origin_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn originPlace = GeneratedColumn( + 'origin_place', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn abundance = GeneratedColumn( + 'abundance', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn preservationFormat = + GeneratedColumn( + 'preservation_format', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + varietyId, + type, + harvestYear, + harvestMonth, + quantityKind, + quantityPrecise, + quantityLabel, + presentation, + storageLocation, + offerStatus, + seedbankId, + originName, + originPlace, + abundance, + preservationFormat, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lots'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Lots createAlias(String alias) { + return Lots(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class GerminationTests extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GerminationTests(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn testedOn = GeneratedColumn( + 'tested_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn sampleSize = GeneratedColumn( + 'sample_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn germinatedCount = GeneratedColumn( + 'germinated_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + lotId, + testedOn, + sampleSize, + germinatedCount, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'germination_tests'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + GerminationTests createAlias(String alias) { + return GerminationTests(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class ConditionChecks extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ConditionChecks(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn checkedOn = GeneratedColumn( + 'checked_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn containerCount = GeneratedColumn( + 'container_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn desiccantState = GeneratedColumn( + 'desiccant_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + lotId, + checkedOn, + containerCount, + desiccantState, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'condition_checks'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + ConditionChecks createAlias(String alias) { + return ConditionChecks(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Movements extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Movements(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn lotId = GeneratedColumn( + 'lot_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn occurredOn = GeneratedColumn( + 'occurred_on', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn counterpartyId = GeneratedColumn( + 'counterparty_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityKind = GeneratedColumn( + 'quantity_kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityPrecise = GeneratedColumn( + 'quantity_precise', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quantityLabel = GeneratedColumn( + 'quantity_label', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn parentMovementId = GeneratedColumn( + 'parent_movement_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn plantareId = GeneratedColumn( + 'plantare_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + lastAuthor, + schemaRowVersion, + lotId, + type, + occurredOn, + counterpartyId, + quantityKind, + quantityPrecise, + quantityLabel, + parentMovementId, + plantareId, + notes, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'movements'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Movements createAlias(String alias) { + return Movements(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Parties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Parties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicKey = GeneratedColumn( + 'public_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'person\'', + defaultValue: const CustomExpression('\'person\''), + ); + late final GeneratedColumn note = GeneratedColumn( + 'note', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + displayName, + publicKey, + kind, + note, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'parties'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Parties createAlias(String alias) { + return Parties(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class Attachments extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Attachments(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn parentType = GeneratedColumn( + 'parent_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn uri = GeneratedColumn( + 'uri', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn bytes = + GeneratedColumn( + 'bytes', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn mimeType = GeneratedColumn( + 'mime_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn sortOrder = GeneratedColumn( + 'sort_order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + parentType, + parentId, + kind, + uri, + bytes, + mimeType, + sortOrder, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'attachments'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Attachments createAlias(String alias) { + return Attachments(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class ExternalLinks extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ExternalLinks(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn lastAuthor = GeneratedColumn( + 'last_author', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn schemaRowVersion = GeneratedColumn( + 'schema_row_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn parentType = GeneratedColumn( + 'parent_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn url = GeneratedColumn( + 'url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + lastAuthor, + isDeleted, + schemaRowVersion, + parentType, + parentId, + url, + title, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'external_links'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + ExternalLinks createAlias(String alias) { + return ExternalLinks(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class DatabaseAtV8 extends GeneratedDatabase { + DatabaseAtV8(QueryExecutor e) : super(e); + late final Varieties varieties = Varieties(this); + late final VarietyVernacularNames varietyVernacularNames = + VarietyVernacularNames(this); + late final Species species = Species(this); + late final SpeciesCommonNames speciesCommonNames = SpeciesCommonNames(this); + late final Lots lots = Lots(this); + late final GerminationTests germinationTests = GerminationTests(this); + late final ConditionChecks conditionChecks = ConditionChecks(this); + late final Movements movements = Movements(this); + late final Parties parties = Parties(this); + late final Attachments attachments = Attachments(this); + late final ExternalLinks externalLinks = ExternalLinks(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + varieties, + varietyVernacularNames, + species, + speciesCommonNames, + lots, + germinationTests, + conditionChecks, + movements, + parties, + attachments, + externalLinks, + ]; + @override + int get schemaVersion => 8; +} diff --git a/apps/app_seeds/test/domain/crop_calendar_test.dart b/apps/app_seeds/test/domain/crop_calendar_test.dart new file mode 100644 index 0000000..04d5fac --- /dev/null +++ b/apps/app_seeds/test/domain/crop_calendar_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/domain/crop_calendar.dart'; + +void main() { + test('an empty month set packs to null, not 0', () { + expect(monthsToMask(const []), isNull); + expect(monthsToMask(const [0, 13, -1]), isNull); // out of range ignored + }); + + test('months round-trip through the mask, ascending and deduped', () { + final mask = monthsToMask(const [9, 3, 4, 3]); + expect(maskToMonths(mask), [3, 4, 9]); + }); + + test('every single month packs to a distinct bit', () { + for (var m = 1; m <= 12; m++) { + expect(maskToMonths(monthsToMask([m])), [m]); + } + }); + + test('maskToMonths of null or 0 is empty', () { + expect(maskToMonths(null), isEmpty); + expect(maskToMonths(0), isEmpty); + }); + + test('maskHasMonth reflects set bits', () { + final mask = monthsToMask(const [3, 9]); + expect(maskHasMonth(mask, 3), isTrue); + expect(maskHasMonth(mask, 9), isTrue); + expect(maskHasMonth(mask, 4), isFalse); + expect(maskHasMonth(null, 3), isFalse); + expect(maskHasMonth(mask, 0), isFalse); + }); + + test('toggleMonth adds, removes, and clears to null when empty', () { + var mask = toggleMonth(null, 3); + expect(maskToMonths(mask), [3]); + mask = toggleMonth(mask, 9); + expect(maskToMonths(mask), [3, 9]); + mask = toggleMonth(mask, 3); + expect(maskToMonths(mask), [9]); + mask = toggleMonth(mask, 9); + expect(mask, isNull); // last month removed → null, not 0 + }); +} diff --git a/apps/app_seeds/test/services/ocr/ocr_language_test.dart b/apps/app_seeds/test/services/ocr/ocr_language_test.dart new file mode 100644 index 0000000..9af298f --- /dev/null +++ b/apps/app_seeds/test/services/ocr/ocr_language_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/services/ocr/ocr_language.dart'; + +void main() { + group('parseAvailableOcrLanguages', () { + test('strips the .traineddata suffix', () { + final available = parseAvailableOcrLanguages( + '{"files": ["eng.traineddata", "spa.traineddata"]}', + ); + expect(available, {'eng', 'spa'}); + }); + + test('empty / missing files yields empty set', () { + expect(parseAvailableOcrLanguages('{}'), isEmpty); + expect(parseAvailableOcrLanguages('{"files": []}'), isEmpty); + }); + }); + + group('tesseractCodeForLocale', () { + test('maps ISO 639-1 to Tesseract codes', () { + expect(tesseractCodeForLocale('es'), 'spa'); + expect(tesseractCodeForLocale('en'), 'eng'); + expect(tesseractCodeForLocale('ar'), 'ara'); + expect(tesseractCodeForLocale('zh'), 'chi_sim'); + }); + + test('accepts full locale tags and is case-insensitive', () { + expect(tesseractCodeForLocale('es_ES'), 'spa'); + expect(tesseractCodeForLocale('zh-Hans-CN'), 'chi_sim'); + expect(tesseractCodeForLocale('DE'), 'deu'); + }); + + test('unmapped locale returns null', () { + expect(tesseractCodeForLocale('xx'), isNull); + }); + }); + + group('resolveOcrLanguages', () { + const bundled = {'eng', 'spa'}; + + test('preferred locale comes first, baseline appended', () { + expect( + resolveOcrLanguages(preferredLocales: ['es'], available: bundled), + 'spa+eng', + ); + }); + + test('English preferred is not duplicated by the baseline', () { + expect( + resolveOcrLanguages(preferredLocales: ['en'], available: bundled), + 'eng', + ); + }); + + test('order follows preference, de-duplicated', () { + expect( + resolveOcrLanguages( + preferredLocales: ['es', 'en', 'es'], + available: bundled, + ), + 'spa+eng', + ); + }); + + test('unbundled locale falls back to the Latin baseline', () { + // German pack is not bundled here — degrade to eng, never fail. + expect( + resolveOcrLanguages(preferredLocales: ['de'], available: bundled), + 'eng', + ); + }); + + test('a future RTL/CJK pack lights up automatically once bundled', () { + expect( + resolveOcrLanguages( + preferredLocales: ['ar'], + available: {'eng', 'spa', 'ara'}, + ), + 'ara+eng', + ); + }); + + test('no baseline available: falls back to every bundled pack, sorted', () { + expect( + resolveOcrLanguages( + preferredLocales: ['fr'], + available: {'spa', 'deu'}, + ), + 'deu+spa', + ); + }); + + test('empty available yields empty string', () { + expect( + resolveOcrLanguages(preferredLocales: ['es'], available: const {}), + '', + ); + }); + }); +} diff --git a/apps/app_seeds/test/services/ocr/tesseract_label_extractor_test.dart b/apps/app_seeds/test/services/ocr/tesseract_label_extractor_test.dart new file mode 100644 index 0000000..789124c --- /dev/null +++ b/apps/app_seeds/test/services/ocr/tesseract_label_extractor_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/services/ocr/tesseract_label_extractor.dart'; + +/// Builds one hOCR word span with a bounding box (height = y1 - y0) and an +/// optional recognition confidence. +String word(String text, int x0, int y0, int x1, int y1, {int conf = 90}) => + "" + '$text'; + +String page(List words) => + "
${words.join('\n')}
"; + +void main() { + group('pickLabelFromHocr picks the largest print, not the longest line', () { + test('Aubergine Black Beauty packet → the variety name, not boilerplate', () { + // "Organic Product" (14 letters) is longer than "Black Beauty" (11) but + // printed smaller — the old longest-line heuristic wrongly picked it. + final hocr = page([ + word('BonPrime', 120, 300, 300, 335), // brand, tiny + word('Organic', 130, 1050, 320, 1095), // boilerplate, small + word('Product', 130, 1100, 340, 1145), + word('AUBERGINE', 380, 1180, 900, 1300), // the title (tallest) + word('BLACK', 380, 1330, 620, 1410), + word('BEAUTY', 640, 1330, 900, 1410), + word('HERB', 430, 1470, 560, 1510), // boilerplate, small + word('SEEDS', 580, 1470, 740, 1510), + ]); + expect(pickLabelFromHocr(hocr), 'Aubergine Black Beauty'); + }); + + test('Cucumber Supermarketer packet → the variety name', () { + final hocr = page([ + word('BonPrime', 120, 300, 300, 335), + word('Organic', 130, 1050, 320, 1095), + word('Product', 130, 1100, 340, 1145), + word('CUCUMBER', 300, 1200, 820, 1320), // tallest + word('SUPERMARKETER', 320, 1350, 900, 1430), + word('HERB', 430, 1470, 560, 1510), + word('SEEDS', 580, 1470, 740, 1510), + ]); + expect(pickLabelFromHocr(hocr), 'Cucumber Supermarketer'); + }); + + test('reads left-to-right within the same row', () { + final hocr = page([ + word('CHERRY', 400, 1000, 560, 1090), + word('TOMATO', 580, 1000, 760, 1090), + ]); + expect(pickLabelFromHocr(hocr), 'Cherry Tomato'); + }); + + test('a page of only boilerplate yields null', () { + final hocr = page([ + word('Organic', 130, 1050, 320, 1095), + word('Product', 130, 1100, 340, 1145), + word('HERB', 430, 1470, 560, 1510), + word('SEEDS', 580, 1470, 740, 1510), + ]); + expect(pickLabelFromHocr(hocr), isNull); + }); + + test('empty hOCR yields null', () { + expect(pickLabelFromHocr(page(const [])), isNull); + expect(pickLabelFromHocr(''), isNull); + }); + + test('short OCR fragments ("Re Ma") are dropped → no suggestion', () { + // Blurry misread into 1–2 char blobs: better nothing than "Re Ma". + final hocr = page([ + word('Re', 300, 1200, 420, 1320, conf: 40), + word('Ma', 460, 1200, 600, 1320, conf: 40), + ]); + expect(pickLabelFromHocr(hocr), isNull); + }); + + test('a plausibly-misread title word is kept (user edits it)', () { + // "Sucumber" (C read as S) is useful as an editable prefill. + final hocr = page([ + word('Sucumber', 300, 1200, 820, 1320, conf: 45), + word('Organic', 130, 1050, 320, 1095, conf: 80), + ]); + expect(pickLabelFromHocr(hocr), 'Sucumber'); + }); + + test('a near-zero confidence word is dropped by the floor', () { + final hocr = page([word('Xzqwp', 300, 1200, 820, 1320, conf: 8)]); + expect(pickLabelFromHocr(hocr), isNull); + }); + }); +} diff --git a/apps/app_seeds/test/support/test_support.dart b/apps/app_seeds/test/support/test_support.dart index 5506779..55af651 100644 --- a/apps/app_seeds/test/support/test_support.dart +++ b/apps/app_seeds/test/support/test_support.dart @@ -8,6 +8,8 @@ import 'package:tane/data/species_repository.dart'; import 'package:tane/data/variety_repository.dart'; import 'package:tane/db/database.dart'; import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/security/secret_store.dart'; +import 'package:tane/services/onboarding_store.dart'; import 'package:tane/state/inventory_cubit.dart'; import 'package:tane/state/variety_detail_cubit.dart'; import 'package:tane/ui/variety_detail_screen.dart'; @@ -33,6 +35,25 @@ VarietyRepository newTestRepository( SpeciesRepository newTestSpeciesRepository(AppDatabase db) => SpeciesRepository(db, idGen: IdGen()); +/// An in-memory [SecretStore] for tests (no OS keystore). +class InMemorySecretStore implements SecretStore { + final Map _values = {}; + + @override + Future read(String key) async => _values[key]; + + @override + Future write(String key, String value) async => _values[key] = value; +} + +/// A fresh [OnboardingStore] backed by in-memory storage. Defaults to +/// "intro already seen" so app-level tests land on home, not the carousel. +OnboardingStore newTestOnboardingStore({bool introSeen = true}) { + final store = InMemorySecretStore(); + if (introSeen) store.write('tane.intro_seen', '1'); + return OnboardingStore(store); +} + /// Wraps [child] with the providers a screen expects (repository, inventory /// cubit) plus i18n and Material localizations, pinned to [locale]. Widget wrapScreen({ diff --git a/apps/app_seeds/test/ui/backup_section_test.dart b/apps/app_seeds/test/ui/backup_section_test.dart index 4499b5f..93c4163 100644 --- a/apps/app_seeds/test/ui/backup_section_test.dart +++ b/apps/app_seeds/test/ui/backup_section_test.dart @@ -66,24 +66,27 @@ void main() { ); } - testWidgets('settings shows the backup section with its three actions', ( + testWidgets('settings shows the backup section with its actions', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(800, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(wrap(SettingsScreen(exportImport: service))); - expect(find.text('Backup'), findsOneWidget); - expect(find.text('Export as CSV'), findsOneWidget); - expect(find.text('Export as JSON'), findsOneWidget); - expect(find.text('Import from JSON'), findsOneWidget); + expect(find.text('Backup & restore'), findsOneWidget); + expect(find.text('Save a backup'), findsOneWidget); + expect(find.text('Restore a backup'), findsOneWidget); + expect(find.text('Export to a spreadsheet'), findsOneWidget); + expect(find.text('Import a list'), findsOneWidget); }); - testWidgets('export CSV saves a .csv file and confirms', (tester) async { + testWidgets('spreadsheet export saves a .csv file and confirms', ( + tester, + ) async { await tester.pumpWidget( wrap(Scaffold(body: BackupSection(service: service))), ); - await tester.tap(find.text('Export as CSV')); + await tester.tap(find.text('Export to a spreadsheet')); await tester.pumpAndSettle(); expect(files.savedName, endsWith('.csv')); @@ -91,11 +94,11 @@ void main() { expect(find.text('Copy saved'), findsOneWidget); }); - testWidgets('export JSON saves a versioned .json file', (tester) async { + testWidgets('saving a backup writes a versioned .json file', (tester) async { await tester.pumpWidget( wrap(Scaffold(body: BackupSection(service: service))), ); - await tester.tap(find.text('Export as JSON')); + await tester.tap(find.text('Save a backup')); await tester.pumpAndSettle(); expect(files.savedName, endsWith('.json')); @@ -120,9 +123,9 @@ void main() { await tester.pumpWidget( wrap(Scaffold(body: BackupSection(service: service))), ); - await tester.tap(find.text('Import from JSON')); + await tester.tap(find.text('Restore a backup')); await tester.pumpAndSettle(); - expect(find.text('Import a saved copy?'), findsOneWidget); + expect(find.text('Restore a backup?'), findsOneWidget); await tester.tap(find.text('Import')); await tester.pumpAndSettle(); @@ -137,7 +140,7 @@ void main() { await tester.pumpWidget( wrap(Scaffold(body: BackupSection(service: service))), ); - await tester.tap(find.text('Import from JSON')); + await tester.tap(find.text('Restore a backup')); await tester.pumpAndSettle(); await tester.tap(find.text('Import')); await tester.pumpAndSettle(); diff --git a/apps/app_seeds/test/ui/draft_ocr_test.dart b/apps/app_seeds/test/ui/draft_ocr_test.dart new file mode 100644 index 0000000..36fa4cd --- /dev/null +++ b/apps/app_seeds/test/ui/draft_ocr_test.dart @@ -0,0 +1,95 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/services/ocr/label_text_extractor.dart'; +import 'package:tane/ui/draft_triage.dart'; + +/// A stand-in OCR engine so the dialog can be tested without a native backend. +class _FakeExtractor implements LabelTextExtractor { + _FakeExtractor({this.result, this.supported = true}); + final String? result; + final bool supported; + @override + bool get isSupported => supported; + @override + Future suggestLabel(Uint8List photo) async => result; +} + +Widget _host(Widget child) { + LocaleSettings.setLocaleSync(AppLocale.en); + return TranslationProvider( + child: MaterialApp( + locale: AppLocale.en.flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: Scaffold(body: child), + ), + ); +} + +void main() { + // A valid 1×1 PNG so Image.memory can decode it in the widget test. + final photo = Uint8List.fromList([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, + 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, + 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, + 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82, + ]); + + testWidgets('a supported engine offers a suggestion that pre-fills the name', + (tester) async { + await tester.pumpWidget( + _host( + NameDraftDialog( + photo: photo, + extractor: _FakeExtractor(result: 'Aubergine Black Beauty'), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('draft.suggestFromPhoto')), findsOneWidget); + await tester.tap(find.byKey(const Key('draft.suggestFromPhoto'))); + await tester.pumpAndSettle(); + + expect(find.text('Aubergine Black Beauty'), findsOneWidget); + }); + + testWidgets('an unsupported engine hides the suggestion button', + (tester) async { + await tester.pumpWidget( + _host( + NameDraftDialog( + photo: photo, + extractor: _FakeExtractor(supported: false), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('draft.suggestFromPhoto')), findsNothing); + }); + + testWidgets('a null suggestion leaves the field untouched (no crash)', + (tester) async { + await tester.pumpWidget( + _host(NameDraftDialog(photo: photo, extractor: _FakeExtractor())), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('draft.suggestFromPhoto'))); + await tester.pumpAndSettle(); + // Nothing was entered; the save still returns the (empty) field. + expect(find.byKey(const Key('draft.nameField')), findsOneWidget); + }); +} diff --git a/apps/app_seeds/test/ui/draft_triage_test.dart b/apps/app_seeds/test/ui/draft_triage_test.dart new file mode 100644 index 0000000..c5f74b0 --- /dev/null +++ b/apps/app_seeds/test/ui/draft_triage_test.dart @@ -0,0 +1,72 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/ui/inventory_list_screen.dart'; + +import '../support/test_support.dart'; + +void main() { + // A valid 1×1 PNG so Image.memory decodes it in the widget test (the seed + // only nudges a trailing byte to keep captures distinct). + Uint8List photo(int seed) => Uint8List.fromList([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, + 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, + 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, + 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82, seed, + ]); + + testWidgets( + 'drafts show a triage banner, stay out of the list, and can be named', + (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + final db = newTestDatabase(); + addTearDown(db.close); + final repo = newTestRepository(db); + await repo.addDraftVariety(photo(1)); + await repo.addDraftVariety(photo(2)); + + await tester.pumpWidget( + wrapScreen(repository: repo, child: const InventoryListScreen()), + ); + await tester.pumpAndSettle(); + + // The two captures are in the tray, not the main list. + expect(find.byKey(const Key('inventory.triageBanner')), findsOneWidget); + expect(find.text('2 to catalogue'), findsOneWidget); + expect(find.text('No seeds yet. Tap + to add your first.'), findsOneWidget); + + // Open the tray and name the first draft. + await tester.tap(find.byKey(const Key('inventory.triageBanner'))); + await tester.pumpAndSettle(); + expect(find.text('Unnamed'), findsNWidgets(2)); + + await tester.tap(find.text('Unnamed').first); + await tester.pumpAndSettle(); + await tester.enterText( + find.byKey(const Key('draft.nameField')), + 'Grandma tomato', + ); + await tester.tap(find.byKey(const Key('draft.nameSave'))); + await tester.pumpAndSettle(); + + // One draft left in the tray… + expect(find.text('Unnamed'), findsOneWidget); + + // …and close the sheet: the named seed is now in the inventory, the + // banner shows one remaining. + await tester.tapAt(const Offset(20, 20)); + await tester.pumpAndSettle(); + expect(find.text('Grandma tomato'), findsOneWidget); + expect(find.text('1 to catalogue'), findsOneWidget); + + await disposeTree(tester); + }, + ); +} diff --git a/apps/app_seeds/test/ui/home_screen_test.dart b/apps/app_seeds/test/ui/home_screen_test.dart index 3603b7f..9e386e2 100644 --- a/apps/app_seeds/test/ui/home_screen_test.dart +++ b/apps/app_seeds/test/ui/home_screen_test.dart @@ -10,6 +10,7 @@ void main() { child: TaneApp( repository: newTestRepository(db), species: newTestSpeciesRepository(db), + onboarding: newTestOnboardingStore(), ), ); diff --git a/apps/app_seeds/test/ui/intro_screen_test.dart b/apps/app_seeds/test/ui/intro_screen_test.dart new file mode 100644 index 0000000..ba908bb --- /dev/null +++ b/apps/app_seeds/test/ui/intro_screen_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/app.dart'; +import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/ui/intro_screen.dart'; + +import '../support/test_support.dart'; + +void main() { + Widget standalone(VoidCallback onDone) => TranslationProvider( + child: MaterialApp(home: IntroScreen(onDone: onDone)), + ); + + testWidgets('carousel advances through slides and finishes', (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + var done = false; + + await tester.pumpWidget(standalone(() => done = true)); + await tester.pumpAndSettle(); + + // First slide + a "Next" button (not "Get started" yet). + expect(find.text('The seed that brought you here'), findsOneWidget); + expect(find.text('Next'), findsOneWidget); + expect(find.text('Get started'), findsNothing); + + // Advance to the last of the five slides. + for (var i = 0; i < 4; i++) { + await tester.tap(find.text('Next')); + await tester.pumpAndSettle(); + } + + expect(find.text('To sow is to multiply'), findsOneWidget); + expect(find.text('Get started'), findsOneWidget); + + await tester.tap(find.text('Get started')); + await tester.pumpAndSettle(); + expect(done, isTrue); + }); + + testWidgets('Skip finishes immediately', (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + var done = false; + + await tester.pumpWidget(standalone(() => done = true)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Skip')); + await tester.pumpAndSettle(); + expect(done, isTrue); + }); + + testWidgets('first launch opens on the intro and marks it seen', ( + tester, + ) async { + LocaleSettings.setLocaleSync(AppLocale.en); + final db = newTestDatabase(); + addTearDown(db.close); + final onboarding = newTestOnboardingStore(introSeen: false); + + await tester.pumpWidget( + TranslationProvider( + child: TaneApp( + repository: newTestRepository(db), + species: newTestSpeciesRepository(db), + onboarding: onboarding, + showIntro: true, + ), + ), + ); + await tester.pumpAndSettle(); + + // The carousel is shown, not the home menu. + expect(find.text('The seed that brought you here'), findsOneWidget); + + await tester.tap(find.text('Skip')); + await tester.pumpAndSettle(); + + // Lands on home, and the intro is now remembered as seen. + expect(find.text('Your inventory'), findsOneWidget); + expect(await onboarding.introSeen(), isTrue); + await disposeTree(tester); + }); +} diff --git a/apps/app_seeds/web/index.html b/apps/app_seeds/web/index.html new file mode 100644 index 0000000..94ce61d --- /dev/null +++ b/apps/app_seeds/web/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + Tanemaki + + + + + + + diff --git a/apps/app_seeds/web/manifest.json b/apps/app_seeds/web/manifest.json index e69de29..a7db6ff 100644 --- a/apps/app_seeds/web/manifest.json +++ b/apps/app_seeds/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "Tanemaki", + "short_name": "Tane", + "start_url": ".", + "display": "standalone", + "background_color": "#2F7D34", + "theme_color": "#2F7D34", + "description": "Local-first, encrypted, decentralized traditional-seed inventory.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/apps/app_seeds/windows/.gitignore b/apps/app_seeds/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/apps/app_seeds/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/apps/app_seeds/windows/CMakeLists.txt b/apps/app_seeds/windows/CMakeLists.txt new file mode 100644 index 0000000..a682cb5 --- /dev/null +++ b/apps/app_seeds/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(tane LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "tane") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/apps/app_seeds/windows/flutter/CMakeLists.txt b/apps/app_seeds/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/apps/app_seeds/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/apps/app_seeds/windows/flutter/generated_plugin_registrant.cc b/apps/app_seeds/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..15293a4 --- /dev/null +++ b/apps/app_seeds/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/apps/app_seeds/windows/flutter/generated_plugin_registrant.h b/apps/app_seeds/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/apps/app_seeds/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/app_seeds/windows/flutter/generated_plugins.cmake b/apps/app_seeds/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b7ef470 --- /dev/null +++ b/apps/app_seeds/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + flutter_secure_storage_windows + sqlcipher_flutter_libs + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/apps/app_seeds/windows/runner/CMakeLists.txt b/apps/app_seeds/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/apps/app_seeds/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/apps/app_seeds/windows/runner/Runner.rc b/apps/app_seeds/windows/runner/Runner.rc new file mode 100644 index 0000000..41772ee --- /dev/null +++ b/apps/app_seeds/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "Comunes" "\0" + VALUE "FileDescription", "Tanemaki — local-first traditional-seed inventory" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "tane" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 Comunes. Licensed under AGPL-3.0." "\0" + VALUE "OriginalFilename", "tane.exe" "\0" + VALUE "ProductName", "Tanemaki" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/apps/app_seeds/windows/runner/flutter_window.cpp b/apps/app_seeds/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/apps/app_seeds/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/apps/app_seeds/windows/runner/flutter_window.h b/apps/app_seeds/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/apps/app_seeds/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/apps/app_seeds/windows/runner/main.cpp b/apps/app_seeds/windows/runner/main.cpp new file mode 100644 index 0000000..87ba72c --- /dev/null +++ b/apps/app_seeds/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"tane", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/apps/app_seeds/windows/runner/resource.h b/apps/app_seeds/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/apps/app_seeds/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/apps/app_seeds/windows/runner/resources/app_icon.ico b/apps/app_seeds/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..9608276 Binary files /dev/null and b/apps/app_seeds/windows/runner/resources/app_icon.ico differ diff --git a/apps/app_seeds/windows/runner/runner.exe.manifest b/apps/app_seeds/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/apps/app_seeds/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/apps/app_seeds/windows/runner/utils.cpp b/apps/app_seeds/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/apps/app_seeds/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/apps/app_seeds/windows/runner/utils.h b/apps/app_seeds/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/apps/app_seeds/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/apps/app_seeds/windows/runner/win32_window.cpp b/apps/app_seeds/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/apps/app_seeds/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/apps/app_seeds/windows/runner/win32_window.h b/apps/app_seeds/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/apps/app_seeds/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/pubspec.lock b/pubspec.lock index 6c803a5..54b329d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -440,6 +440,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + flutter_tesseract_ocr: + dependency: transitive + description: + name: flutter_tesseract_ocr + sha256: "63552ab444f3fa3b2e86485ad3a40cfcf70408fcd14cd061d87bfd3c7ff22a0e" + url: "https://pub.dev" + source: hosted + version: "0.4.31" flutter_test: dependency: transitive description: flutter