diff --git a/apps/app_seeds/android/app/build.gradle.kts b/apps/app_seeds/android/app/build.gradle.kts index ecc470d..b0e4713 100644 --- a/apps/app_seeds/android/app/build.gradle.kts +++ b/apps/app_seeds/android/app/build.gradle.kts @@ -73,17 +73,6 @@ android { } else { signingConfigs.getByName("debug") } - // R8: shrink + optimize + obfuscate the Java/Kotlin bytecode and strip - // unused resources — Play's "app optimization" recommendation (smaller - // download, less memory). Native .so libs are untouched, so this does - // not change device/ABI compatibility. Keep rules for reflection/JNI - // heavy deps (OCR, SQLCipher, notifications) live in proguard-rules.pro. - isMinifyEnabled = true - isShrinkResources = true - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) } } } diff --git a/apps/app_seeds/android/app/proguard-rules.pro b/apps/app_seeds/android/app/proguard-rules.pro deleted file mode 100644 index 3d061e3..0000000 --- a/apps/app_seeds/android/app/proguard-rules.pro +++ /dev/null @@ -1,40 +0,0 @@ -# R8/ProGuard keep rules for Tane (org.comunes.tane). -# -# R8 is enabled for release builds (see build.gradle.kts). It shrinks/optimizes -# the Java/Kotlin bytecode and can break code reached only via reflection or JNI -# — the native plugins below load their Java classes from C, so R8 can't see the -# references and would strip/rename them. Keep them explicitly. Start -# conservative; trim only after a release smoke test proves a class is unused. - -# --- Flutter engine & embedding (defensive; usually handled by the plugin) --- --keep class io.flutter.** { *; } --keep class io.flutter.plugins.** { *; } --dontwarn io.flutter.embedding.** - -# --- On-device OCR: tesseract4android (JNI) --- -# libtesseract/libleptonica call back into these Java classes by name. --keep class com.googlecode.tesseract.android.** { *; } --keep class com.googlecode.leptonica.android.** { *; } --keep class io.paratoner.flutter_tesseract_ocr.** { *; } - -# --- QR scanning: zxing_barcode_scanner (pure ZXing, platform view) --- --keep class com.shirisharyal.zxing_barcode_scanner.** { *; } - -# --- Encrypted DB: SQLCipher / sqlite3 native loader --- -# sqlite3 is reached over FFI (dlopen), but keep the loader plugin classes. --keep class eu.simonbinder.sqlite3_flutter_libs.** { *; } --keep class net.zetetic.** { *; } --dontwarn net.zetetic.** - -# --- Local notifications --- -# Published keep rules for flutter_local_notifications' Gson-serialized models. --keep class com.dexterous.** { *; } --keep class com.google.gson.** { *; } --keep class * extends com.google.gson.TypeAdapter --keepattributes Signature --keepattributes *Annotation* --dontwarn com.google.errorprone.annotations.** - -# --- Core library desugaring --- --dontwarn java.lang.invoke.** --dontwarn build.IgnoreJava8API diff --git a/apps/app_seeds/android/app/src/main/AndroidManifest.xml b/apps/app_seeds/android/app/src/main/AndroidManifest.xml index 3ea677f..677d645 100644 --- a/apps/app_seeds/android/app/src/main/AndroidManifest.xml +++ b/apps/app_seeds/android/app/src/main/AndroidManifest.xml @@ -5,19 +5,6 @@ - - - - - - - when (call.method) { "getCoarseLatLon" -> getCoarseLatLon(result) - "hasCamera" -> result.success(hasCameraHardware()) else -> result.notImplemented() } } @@ -73,15 +72,6 @@ class MainActivity : FlutterActivity() { } } - /** - * Whether this device has any camera. Camera-less devices (Chromebooks, - * Android Automotive, many TVs) are supported — see AndroidManifest's - * uses-feature required="false" — so the QR scan and camera-capture UI - * hide themselves when this is false instead of offering broken actions. - */ - private fun hasCameraHardware(): Boolean = - packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) - private fun hasLocationPermission(): Boolean = ContextCompat.checkSelfPermission( this, diff --git a/apps/app_seeds/drift_schemas/drift_schema_v14.json b/apps/app_seeds/drift_schemas/drift_schema_v14.json deleted file mode 100644 index 623b534..0000000 --- a/apps/app_seeds/drift_schemas/drift_schema_v14.json +++ /dev/null @@ -1,2509 +0,0 @@ -{ - "_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" - } - }, - { - "name": "price_amount", - "getter_name": "priceAmount", - "moor_type": "double", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "price_currency", - "getter_name": "priceCurrency", - "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": "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": "garden_outcomes", - "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": "year", - "getter_name": "year", - "moor_type": "int", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "rating", - "getter_name": "rating", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const EnumNameConverter(GardenOutcomeRating.values)", - "dart_type_name": "GardenOutcomeRating" - } - }, - { - "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": "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": 9, - "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": 10, - "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": "thumbnail", - "getter_name": "thumbnail", - "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": 11, - "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" - ] - } - }, - { - "id": 12, - "references": [], - "type": "table", - "data": { - "name": "plantares", - "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": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "direction", - "getter_name": "direction", - "moor_type": "string", - "nullable": false, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const EnumNameConverter(PlantareDirection.values)", - "dart_type_name": "PlantareDirection" - } - }, - { - "name": "counterparty", - "getter_name": "counterparty", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "owed_description", - "getter_name": "owedDescription", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "made_on", - "getter_name": "madeOn", - "moor_type": "int", - "nullable": false, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "due_by", - "getter_name": "dueBy", - "moor_type": "int", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "status", - "getter_name": "status", - "moor_type": "string", - "nullable": false, - "customConstraints": null, - "default_dart": "const CustomExpression('\\'open\\'')", - "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const EnumNameConverter(PlantareStatus.values)", - "dart_type_name": "PlantareStatus" - } - }, - { - "name": "settled_on", - "getter_name": "settledOn", - "moor_type": "int", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "note", - "getter_name": "note", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "pledge_id", - "getter_name": "pledgeId", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "debtor_key", - "getter_name": "debtorKey", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "creditor_key", - "getter_name": "creditorKey", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "debtor_signature", - "getter_name": "debtorSignature", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "creditor_signature", - "getter_name": "creditorSignature", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "movement_id", - "getter_name": "movementId", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "remote_state", - "getter_name": "remoteState", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const EnumNameConverter(PlantareRemoteState.values)", - "dart_type_name": "PlantareRemoteState" - } - }, - { - "name": "return_kind", - "getter_name": "returnKind", - "moor_type": "string", - "nullable": false, - "customConstraints": null, - "default_dart": "const CustomExpression('\\'similar\\'')", - "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const EnumNameConverter(PlantareReturnKind.values)", - "dart_type_name": "PlantareReturnKind" - } - }, - { - "name": "work_hours", - "getter_name": "workHours", - "moor_type": "double", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - } - ], - "is_virtual": false, - "without_rowid": false, - "constraints": [], - "explicit_pk": [ - "id" - ] - } - }, - { - "id": 13, - "references": [], - "type": "table", - "data": { - "name": "sales", - "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": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "direction", - "getter_name": "direction", - "moor_type": "string", - "nullable": false, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const EnumNameConverter(SaleDirection.values)", - "dart_type_name": "SaleDirection" - } - }, - { - "name": "counterparty", - "getter_name": "counterparty", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "amount", - "getter_name": "amount", - "moor_type": "double", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "currency", - "getter_name": "currency", - "moor_type": "string", - "nullable": true, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "name": "sold_on", - "getter_name": "soldOn", - "moor_type": "int", - "nullable": false, - "customConstraints": null, - "default_dart": null, - "default_client_dart": null, - "dsl_features": [] - }, - { - "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": 14, - "references": [ - 0 - ], - "type": "index", - "data": { - "on": 0, - "name": "idx_varieties_deleted_draft", - "sql": null, - "unique": false, - "columns": [ - { - "column": "is_deleted", - "order_by": null - }, - { - "column": "is_draft", - "order_by": null - } - ] - } - }, - { - "id": 15, - "references": [ - 4 - ], - "type": "index", - "data": { - "on": 4, - "name": "idx_lots_variety", - "sql": null, - "unique": false, - "columns": [ - { - "column": "variety_id", - "order_by": null - } - ] - } - }, - { - "id": 16, - "references": [ - 10 - ], - "type": "index", - "data": { - "on": 10, - "name": "idx_attachments_parent", - "sql": null, - "unique": false, - "columns": [ - { - "column": "parent_type", - "order_by": null - }, - { - "column": "parent_id", - "order_by": null - }, - { - "column": "kind", - "order_by": null - } - ] - } - } - ], - "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, \"price_amount\" REAL NULL, \"price_currency\" 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": "garden_outcomes", - "sql": [ - { - "dialect": "sqlite", - "sql": "CREATE TABLE IF NOT EXISTS \"garden_outcomes\" (\"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, \"year\" INTEGER NULL, \"rating\" 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, \"thumbnail\" 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\"));" - } - ] - }, - { - "name": "plantares", - "sql": [ - { - "dialect": "sqlite", - "sql": "CREATE TABLE IF NOT EXISTS \"plantares\" (\"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 NULL, \"direction\" TEXT NOT NULL, \"counterparty\" TEXT NULL, \"owed_description\" TEXT NULL, \"made_on\" INTEGER NOT NULL, \"due_by\" INTEGER NULL, \"status\" TEXT NOT NULL DEFAULT 'open', \"settled_on\" INTEGER NULL, \"note\" TEXT NULL, \"pledge_id\" TEXT NULL, \"debtor_key\" TEXT NULL, \"creditor_key\" TEXT NULL, \"debtor_signature\" TEXT NULL, \"creditor_signature\" TEXT NULL, \"movement_id\" TEXT NULL, \"remote_state\" TEXT NULL, \"return_kind\" TEXT NOT NULL DEFAULT 'similar', \"work_hours\" REAL NULL, PRIMARY KEY (\"id\"));" - } - ] - }, - { - "name": "sales", - "sql": [ - { - "dialect": "sqlite", - "sql": "CREATE TABLE IF NOT EXISTS \"sales\" (\"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 NULL, \"direction\" TEXT NOT NULL, \"counterparty\" TEXT NULL, \"amount\" REAL NULL, \"currency\" TEXT NULL, \"sold_on\" INTEGER NOT NULL, \"note\" TEXT NULL, PRIMARY KEY (\"id\"));" - } - ] - }, - { - "name": "idx_varieties_deleted_draft", - "sql": [ - { - "dialect": "sqlite", - "sql": "CREATE INDEX idx_varieties_deleted_draft ON varieties (is_deleted, is_draft)" - } - ] - }, - { - "name": "idx_lots_variety", - "sql": [ - { - "dialect": "sqlite", - "sql": "CREATE INDEX idx_lots_variety ON lots (variety_id)" - } - ] - }, - { - "name": "idx_attachments_parent", - "sql": [ - { - "dialect": "sqlite", - "sql": "CREATE INDEX idx_attachments_parent ON attachments (parent_type, parent_id, kind)" - } - ] - } - ] -} \ No newline at end of file diff --git a/apps/app_seeds/fastlane/Fastfile b/apps/app_seeds/fastlane/Fastfile index 5f3437a..6a52523 100644 --- a/apps/app_seeds/fastlane/Fastfile +++ b/apps/app_seeds/fastlane/Fastfile @@ -16,24 +16,13 @@ default_platform(:android) AAB_PATH = "build/app/outputs/bundle/release/app-release.aab".freeze platform :android do - desc "Upload the signed AAB + store listing to Google Play (production, 100%)" + desc "Upload the signed AAB + store listing to Google Play (internal track)" lane :deploy_play do - supply( - track: "production", - aab: AAB_PATH, - release_status: "completed", # publish to 100% of users (subject to review) - skip_upload_apk: true, # we ship the AAB, not a raw APK - skip_upload_changelogs: false, - ) - end - - desc "Upload the signed AAB to the internal test track (manual QA safety net)" - lane :deploy_internal do supply( track: "internal", aab: AAB_PATH, release_status: "completed", # internal testing has no review delay - skip_upload_apk: true, + skip_upload_apk: true, # we ship the AAB, not a raw APK skip_upload_changelogs: false, ) end diff --git a/apps/app_seeds/lib/bootstrap.dart b/apps/app_seeds/lib/bootstrap.dart index e0ecc26..57ab427 100644 --- a/apps/app_seeds/lib/bootstrap.dart +++ b/apps/app_seeds/lib/bootstrap.dart @@ -9,7 +9,6 @@ import 'data/variety_repository.dart'; import 'di/injector.dart'; import 'i18n/strings.g.dart'; import 'services/auto_backup_service.dart'; -import 'services/camera_availability.dart'; import 'services/coarse_location.dart'; import 'services/inbox_service.dart'; import 'services/locale_store.dart'; @@ -51,11 +50,6 @@ class _BootstrapState extends State { Future _boot() async { await configureDependencies(); - // Detect camera presence once, before the home screen builds, so camera-less - // devices (Chromebooks, Automotive, some TVs) hide the QR scan / camera UI - // instead of offering broken actions. Non-Android platforms keep the default. - await initCameraAvailability(); - // The saved language lives in the keystore, so it can only be applied once // DI is up. Until now the splash showed in the device language (it has no // text), so there is no visible flip. diff --git a/apps/app_seeds/lib/data/variety_repository.dart b/apps/app_seeds/lib/data/variety_repository.dart index da4b511..dd5e877 100644 --- a/apps/app_seeds/lib/data/variety_repository.dart +++ b/apps/app_seeds/lib/data/variety_repository.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:async/async.dart'; import 'package:commons_core/commons_core.dart'; import 'package:drift/drift.dart'; @@ -654,27 +652,13 @@ class VarietyRepository { required this.idGen, required this.nodeId, int Function()? nowMillis, - Uint8List? Function(Uint8List)? thumbnailBuilder, }) : _now = nowMillis ?? (() => DateTime.now().millisecondsSinceEpoch), - _thumbnailBuilder = thumbnailBuilder, _clock = Hlc.zero(nodeId); final AppDatabase _db; final IdGen idGen; final String nodeId; final int Function() _now; - - /// Builds the small list-avatar thumbnail from a full photo. Injected (from - /// `services/offer_thumbnail.dart` in production) so the data layer stays free - /// of the image codec and unit tests run without decoding. Null → no - /// thumbnail is stored and the list falls back to the full photo. - final Uint8List? Function(Uint8List)? _thumbnailBuilder; - - /// The thumbnail for [photoBytes], or null when no builder is wired or the - /// bytes aren't decodable. Wrapped in a Value for direct use in a companion. - Value _thumbValue(Uint8List photoBytes) => - Value(_thumbnailBuilder?.call(photoBytes)); - Hlc _clock; /// Emits the non-deleted inventory, ordered by category then label, each with @@ -712,11 +696,7 @@ class VarietyRepository { _db.select(_db.species).watch().map((_) {}), _db.select(_db.lots).watch().map((_) {}), ]); - // Coalesce bursts: a single quick-add or handover touches several of these - // tables at once, and each would otherwise re-run the full (7-query) load. - // Debouncing collapses the burst into one reload — decisive with a large - // inventory. - return _debounce(triggers, const Duration(milliseconds: 250)).asyncMap( + return triggers.asyncMap( (_) async => (items: await _loadInventory(), drafts: await _loadDrafts()), ); } @@ -949,10 +929,7 @@ class VarietyRepository { return rows.map((l) => l.varietyId).toSet(); } - /// Loads the first photo's small [thumbnail] for each of [varietyIds] (one - /// query) — for the inventory-list avatar. Falls back to the full-resolution - /// [bytes] only when a thumbnail hasn't been generated yet (older rows, - /// pending lazy backfill), so the list stays correct meanwhile. + /// Loads the first photo BLOB for each of [varietyIds] (one query). Future> _firstPhotosFor( List varietyIds, ) async { @@ -973,69 +950,17 @@ class VarietyRepository { .get(); final byVariety = {}; for (final row in rows) { - final image = row.thumbnail ?? row.bytes; - if (image != null) byVariety.putIfAbsent(row.parentId, () => image); + final bytes = row.bytes; + if (bytes != null) byVariety.putIfAbsent(row.parentId, () => bytes); } return byVariety; } - /// Generates the missing list thumbnails for photos that predate the - /// thumbnail column (or arrived via sync/backup restore, which never carry - /// one). Processes in small batches so decoding doesn't block the UI; safe to - /// call at startup as fire-and-forget. No-op when no thumbnail builder is - /// wired. Returns how many thumbnails were written. - Future backfillThumbnails({int batchSize = 20}) async { - final build = _thumbnailBuilder; - if (build == null) return 0; - var written = 0; - while (true) { - final batch = - await (_db.select(_db.attachments) - ..where( - (a) => - a.kind.equalsValue(AttachmentKind.photo) & - a.isDeleted.equals(false) & - a.thumbnail.isNull() & - a.bytes.isNotNull(), - ) - ..limit(batchSize)) - .get(); - if (batch.isEmpty) break; - for (final row in batch) { - final thumb = build(row.bytes!); - // No decodable image → store the full bytes as the "thumbnail" so this - // row isn't re-scanned forever. It's rare (corrupt photo) and still - // bounded by the avatar's cacheWidth at render time. - await (_db.update(_db.attachments)..where((a) => a.id.equals(row.id))) - .write(AttachmentsCompanion(thumbnail: Value(thumb ?? row.bytes))); - written++; - } - if (batch.length < batchSize) break; - } - return written; - } - /// The cover photo (lowest `sortOrder`) for a single variety, or null when it - /// has none. Used by the publish step to host an offer's image, so it returns - /// the FULL-resolution [bytes] (not the list thumbnail). - Future coverPhotoFor(String varietyId) async { - final rows = - await (_db.select(_db.attachments) - ..where( - (a) => - a.parentId.equals(varietyId) & - a.parentType.equalsValue(ParentType.variety) & - a.kind.equalsValue(AttachmentKind.photo) & - a.isDeleted.equals(false), - ) - ..orderBy([ - (a) => OrderingTerm(expression: a.sortOrder), - (a) => OrderingTerm(expression: a.createdAt), - ]) - ..limit(1)) - .get(); - return rows.isEmpty ? null : rows.first.bytes; - } + /// has none. Used by the publish step to host an offer's image; reuses the + /// same "first photo" rule as the inventory avatar. + Future coverPhotoFor(String varietyId) async => + (await _firstPhotosFor([varietyId]))[varietyId]; /// Maps each of [speciesIds] to its scientific name (one query). Future> _scientificNamesFor( @@ -1115,7 +1040,6 @@ class VarietyRepository { parentId: varietyId, kind: AttachmentKind.photo, bytes: Value(photoBytes), - thumbnail: _thumbValue(photoBytes), mimeType: const Value('image/jpeg'), ), ); @@ -1156,7 +1080,6 @@ class VarietyRepository { parentId: varietyId, kind: AttachmentKind.photo, bytes: Value(photoBytes), - thumbnail: _thumbValue(photoBytes), mimeType: const Value('image/jpeg'), ), ); @@ -1872,7 +1795,6 @@ class VarietyRepository { parentId: varietyId, kind: AttachmentKind.photo, bytes: Value(bytes), - thumbnail: _thumbValue(bytes), mimeType: const Value('image/jpeg'), ), ); @@ -3084,46 +3006,3 @@ class VarietyRepository { return maxPacked == null ? null : Hlc.parse(maxPacked); } } - -/// Emits the latest event from [source] only once [duration] has elapsed with -/// no newer event — collapsing a burst of rapid change-triggers into a single -/// downstream reload. Trailing-edge; single-subscription (matches the merged -/// Drift trigger stream it wraps). -Stream _debounce(Stream source, Duration duration) { - late StreamController controller; - StreamSubscription? sub; - Timer? timer; - T? pending; - var hasPending = false; - - void flush() { - if (hasPending) { - hasPending = false; - controller.add(pending as T); - } - } - - controller = StreamController( - onListen: () { - sub = source.listen( - (value) { - pending = value; - hasPending = true; - timer?.cancel(); - timer = Timer(duration, flush); - }, - onError: controller.addError, - onDone: () { - timer?.cancel(); - flush(); - controller.close(); - }, - ); - }, - onCancel: () async { - timer?.cancel(); - await sub?.cancel(); - }, - ); - return controller.stream; -} diff --git a/apps/app_seeds/lib/db/database.dart b/apps/app_seeds/lib/db/database.dart index 53c061f..01c0d85 100644 --- a/apps/app_seeds/lib/db/database.dart +++ b/apps/app_seeds/lib/db/database.dart @@ -32,7 +32,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 = 14; + static const int currentSchemaVersion = 13; @override int get schemaVersion => currentSchemaVersion; @@ -196,40 +196,9 @@ class AppDatabase extends _$AppDatabase { await m.createTable(gardenOutcomes); } } - // v14: scalability for large inventories. A local, regenerable [thumbnail] - // BLOB on attachments (so the list never decodes full-resolution photos) - // plus indexes on the columns the inventory list filters/joins on. All - // additive and guarded, so a half-migrated dev database re-runs cleanly - // (see the v7 note above). Existing thumbnails are backfilled lazily in - // the background, not here (image decoding in a migration is slow/fragile). - if (from < 14) { - if (!await _hasColumn('attachments', 'thumbnail')) { - await m.addColumn(attachments, attachments.thumbnail); - } - // Declared as `@TableIndex` on the tables, so a fresh install gets them - // via createAll(); existing databases get them here. Guarded against a - // half-migrated dev database (see the v7 note above). - for (final index in [ - idxVarietiesDeletedDraft, - idxAttachmentsParent, - idxLotsVariety, - ]) { - if (!await _hasIndex(index.entityName)) await m.create(index); - } - } }, ); - /// Whether an index named [name] already exists — keeps the additive v14 - /// index creation idempotent against partially-migrated databases. - Future _hasIndex(String name) async { - final rows = await customSelect( - "SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?", - variables: [Variable.withString(name)], - ).get(); - return rows.isNotEmpty; - } - /// Whether a table named [table] already exists. Keeps the additive v8 /// table creation idempotent against partially-migrated databases. Future _hasTable(String table) async { diff --git a/apps/app_seeds/lib/db/database.g.dart b/apps/app_seeds/lib/db/database.g.dart index 17b35c8..da165ef 100644 --- a/apps/app_seeds/lib/db/database.g.dart +++ b/apps/app_seeds/lib/db/database.g.dart @@ -7975,17 +7975,6 @@ class $AttachmentsTable extends Attachments type: DriftSqlType.blob, requiredDuringInsert: false, ); - static const VerificationMeta _thumbnailMeta = const VerificationMeta( - 'thumbnail', - ); - @override - late final GeneratedColumn thumbnail = GeneratedColumn( - 'thumbnail', - aliasedName, - true, - type: DriftSqlType.blob, - requiredDuringInsert: false, - ); static const VerificationMeta _mimeTypeMeta = const VerificationMeta( 'mimeType', ); @@ -8022,7 +8011,6 @@ class $AttachmentsTable extends Attachments kind, uri, bytes, - thumbnail, mimeType, sortOrder, ]; @@ -8102,12 +8090,6 @@ class $AttachmentsTable extends Attachments bytes.isAcceptableOrUnknown(data['bytes']!, _bytesMeta), ); } - if (data.containsKey('thumbnail')) { - context.handle( - _thumbnailMeta, - thumbnail.isAcceptableOrUnknown(data['thumbnail']!, _thumbnailMeta), - ); - } if (data.containsKey('mime_type')) { context.handle( _mimeTypeMeta, @@ -8177,10 +8159,6 @@ class $AttachmentsTable extends Attachments DriftSqlType.blob, data['${effectivePrefix}bytes'], ), - thumbnail: attachedDatabase.typeMapping.read( - DriftSqlType.blob, - data['${effectivePrefix}thumbnail'], - ), mimeType: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}mime_type'], @@ -8215,13 +8193,6 @@ class Attachment extends DataClass implements Insertable { final AttachmentKind kind; final String? uri; final Uint8List? bytes; - - /// A small JPEG thumbnail of [bytes] (photos only), decoded once on save so - /// the inventory list never has to decode the full-resolution photo for a - /// 48px avatar. Purely derived, local and regenerable — it is EXCLUDED from - /// CRDT sync payloads and backups (see [SyncColumns] usage); a peer or a - /// restored backup regenerates it lazily via `backfillThumbnails`. - final Uint8List? thumbnail; final String? mimeType; /// Display order among sibling attachments (lower first). The lowest-ordered @@ -8241,7 +8212,6 @@ class Attachment extends DataClass implements Insertable { required this.kind, this.uri, this.bytes, - this.thumbnail, this.mimeType, required this.sortOrder, }); @@ -8271,9 +8241,6 @@ class Attachment extends DataClass implements Insertable { if (!nullToAbsent || bytes != null) { map['bytes'] = Variable(bytes); } - if (!nullToAbsent || thumbnail != null) { - map['thumbnail'] = Variable(thumbnail); - } if (!nullToAbsent || mimeType != null) { map['mime_type'] = Variable(mimeType); } @@ -8296,9 +8263,6 @@ class Attachment extends DataClass implements Insertable { bytes: bytes == null && nullToAbsent ? const Value.absent() : Value(bytes), - thumbnail: thumbnail == null && nullToAbsent - ? const Value.absent() - : Value(thumbnail), mimeType: mimeType == null && nullToAbsent ? const Value.absent() : Value(mimeType), @@ -8327,7 +8291,6 @@ class Attachment extends DataClass implements Insertable { ), uri: serializer.fromJson(json['uri']), bytes: serializer.fromJson(json['bytes']), - thumbnail: serializer.fromJson(json['thumbnail']), mimeType: serializer.fromJson(json['mimeType']), sortOrder: serializer.fromJson(json['sortOrder']), ); @@ -8351,7 +8314,6 @@ class Attachment extends DataClass implements Insertable { ), 'uri': serializer.toJson(uri), 'bytes': serializer.toJson(bytes), - 'thumbnail': serializer.toJson(thumbnail), 'mimeType': serializer.toJson(mimeType), 'sortOrder': serializer.toJson(sortOrder), }; @@ -8369,7 +8331,6 @@ class Attachment extends DataClass implements Insertable { AttachmentKind? kind, Value uri = const Value.absent(), Value bytes = const Value.absent(), - Value thumbnail = const Value.absent(), Value mimeType = const Value.absent(), int? sortOrder, }) => Attachment( @@ -8384,7 +8345,6 @@ class Attachment extends DataClass implements Insertable { kind: kind ?? this.kind, uri: uri.present ? uri.value : this.uri, bytes: bytes.present ? bytes.value : this.bytes, - thumbnail: thumbnail.present ? thumbnail.value : this.thumbnail, mimeType: mimeType.present ? mimeType.value : this.mimeType, sortOrder: sortOrder ?? this.sortOrder, ); @@ -8407,7 +8367,6 @@ class Attachment extends DataClass implements Insertable { kind: data.kind.present ? data.kind.value : this.kind, uri: data.uri.present ? data.uri.value : this.uri, bytes: data.bytes.present ? data.bytes.value : this.bytes, - thumbnail: data.thumbnail.present ? data.thumbnail.value : this.thumbnail, mimeType: data.mimeType.present ? data.mimeType.value : this.mimeType, sortOrder: data.sortOrder.present ? data.sortOrder.value : this.sortOrder, ); @@ -8427,7 +8386,6 @@ class Attachment extends DataClass implements Insertable { ..write('kind: $kind, ') ..write('uri: $uri, ') ..write('bytes: $bytes, ') - ..write('thumbnail: $thumbnail, ') ..write('mimeType: $mimeType, ') ..write('sortOrder: $sortOrder') ..write(')')) @@ -8447,7 +8405,6 @@ class Attachment extends DataClass implements Insertable { kind, uri, $driftBlobEquality.hash(bytes), - $driftBlobEquality.hash(thumbnail), mimeType, sortOrder, ); @@ -8466,7 +8423,6 @@ class Attachment extends DataClass implements Insertable { other.kind == this.kind && other.uri == this.uri && $driftBlobEquality.equals(other.bytes, this.bytes) && - $driftBlobEquality.equals(other.thumbnail, this.thumbnail) && other.mimeType == this.mimeType && other.sortOrder == this.sortOrder); } @@ -8483,7 +8439,6 @@ class AttachmentsCompanion extends UpdateCompanion { final Value kind; final Value uri; final Value bytes; - final Value thumbnail; final Value mimeType; final Value sortOrder; final Value rowid; @@ -8499,7 +8454,6 @@ class AttachmentsCompanion extends UpdateCompanion { this.kind = const Value.absent(), this.uri = const Value.absent(), this.bytes = const Value.absent(), - this.thumbnail = const Value.absent(), this.mimeType = const Value.absent(), this.sortOrder = const Value.absent(), this.rowid = const Value.absent(), @@ -8516,7 +8470,6 @@ class AttachmentsCompanion extends UpdateCompanion { required AttachmentKind kind, this.uri = const Value.absent(), this.bytes = const Value.absent(), - this.thumbnail = const Value.absent(), this.mimeType = const Value.absent(), this.sortOrder = const Value.absent(), this.rowid = const Value.absent(), @@ -8539,7 +8492,6 @@ class AttachmentsCompanion extends UpdateCompanion { Expression? kind, Expression? uri, Expression? bytes, - Expression? thumbnail, Expression? mimeType, Expression? sortOrder, Expression? rowid, @@ -8556,7 +8508,6 @@ class AttachmentsCompanion extends UpdateCompanion { if (kind != null) 'kind': kind, if (uri != null) 'uri': uri, if (bytes != null) 'bytes': bytes, - if (thumbnail != null) 'thumbnail': thumbnail, if (mimeType != null) 'mime_type': mimeType, if (sortOrder != null) 'sort_order': sortOrder, if (rowid != null) 'rowid': rowid, @@ -8575,7 +8526,6 @@ class AttachmentsCompanion extends UpdateCompanion { Value? kind, Value? uri, Value? bytes, - Value? thumbnail, Value? mimeType, Value? sortOrder, Value? rowid, @@ -8592,7 +8542,6 @@ class AttachmentsCompanion extends UpdateCompanion { kind: kind ?? this.kind, uri: uri ?? this.uri, bytes: bytes ?? this.bytes, - thumbnail: thumbnail ?? this.thumbnail, mimeType: mimeType ?? this.mimeType, sortOrder: sortOrder ?? this.sortOrder, rowid: rowid ?? this.rowid, @@ -8639,9 +8588,6 @@ class AttachmentsCompanion extends UpdateCompanion { if (bytes.present) { map['bytes'] = Variable(bytes.value); } - if (thumbnail.present) { - map['thumbnail'] = Variable(thumbnail.value); - } if (mimeType.present) { map['mime_type'] = Variable(mimeType.value); } @@ -8668,7 +8614,6 @@ class AttachmentsCompanion extends UpdateCompanion { ..write('kind: $kind, ') ..write('uri: $uri, ') ..write('bytes: $bytes, ') - ..write('thumbnail: $thumbnail, ') ..write('mimeType: $mimeType, ') ..write('sortOrder: $sortOrder, ') ..write('rowid: $rowid') @@ -11490,18 +11435,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $ExternalLinksTable externalLinks = $ExternalLinksTable(this); late final $PlantaresTable plantares = $PlantaresTable(this); late final $SalesTable sales = $SalesTable(this); - late final Index idxVarietiesDeletedDraft = Index( - 'idx_varieties_deleted_draft', - 'CREATE INDEX idx_varieties_deleted_draft ON varieties (is_deleted, is_draft)', - ); - late final Index idxLotsVariety = Index( - 'idx_lots_variety', - 'CREATE INDEX idx_lots_variety ON lots (variety_id)', - ); - late final Index idxAttachmentsParent = Index( - 'idx_attachments_parent', - 'CREATE INDEX idx_attachments_parent ON attachments (parent_type, parent_id, kind)', - ); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -11521,9 +11454,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { externalLinks, plantares, sales, - idxVarietiesDeletedDraft, - idxLotsVariety, - idxAttachmentsParent, ]; } @@ -15188,7 +15118,6 @@ typedef $$AttachmentsTableCreateCompanionBuilder = required AttachmentKind kind, Value uri, Value bytes, - Value thumbnail, Value mimeType, Value sortOrder, Value rowid, @@ -15206,7 +15135,6 @@ typedef $$AttachmentsTableUpdateCompanionBuilder = Value kind, Value uri, Value bytes, - Value thumbnail, Value mimeType, Value sortOrder, Value rowid, @@ -15278,11 +15206,6 @@ class $$AttachmentsTableFilterComposer builder: (column) => ColumnFilters(column), ); - ColumnFilters get thumbnail => $composableBuilder( - column: $table.thumbnail, - builder: (column) => ColumnFilters(column), - ); - ColumnFilters get mimeType => $composableBuilder( column: $table.mimeType, builder: (column) => ColumnFilters(column), @@ -15358,11 +15281,6 @@ class $$AttachmentsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get thumbnail => $composableBuilder( - column: $table.thumbnail, - builder: (column) => ColumnOrderings(column), - ); - ColumnOrderings get mimeType => $composableBuilder( column: $table.mimeType, builder: (column) => ColumnOrderings(column), @@ -15423,9 +15341,6 @@ class $$AttachmentsTableAnnotationComposer GeneratedColumn get bytes => $composableBuilder(column: $table.bytes, builder: (column) => column); - GeneratedColumn get thumbnail => - $composableBuilder(column: $table.thumbnail, builder: (column) => column); - GeneratedColumn get mimeType => $composableBuilder(column: $table.mimeType, builder: (column) => column); @@ -15475,7 +15390,6 @@ class $$AttachmentsTableTableManager Value kind = const Value.absent(), Value uri = const Value.absent(), Value bytes = const Value.absent(), - Value thumbnail = const Value.absent(), Value mimeType = const Value.absent(), Value sortOrder = const Value.absent(), Value rowid = const Value.absent(), @@ -15491,7 +15405,6 @@ class $$AttachmentsTableTableManager kind: kind, uri: uri, bytes: bytes, - thumbnail: thumbnail, mimeType: mimeType, sortOrder: sortOrder, rowid: rowid, @@ -15509,7 +15422,6 @@ class $$AttachmentsTableTableManager required AttachmentKind kind, Value uri = const Value.absent(), Value bytes = const Value.absent(), - Value thumbnail = const Value.absent(), Value mimeType = const Value.absent(), Value sortOrder = const Value.absent(), Value rowid = const Value.absent(), @@ -15525,7 +15437,6 @@ class $$AttachmentsTableTableManager kind: kind, uri: uri, bytes: bytes, - thumbnail: thumbnail, mimeType: mimeType, sortOrder: sortOrder, rowid: rowid, diff --git a/apps/app_seeds/lib/db/tables.dart b/apps/app_seeds/lib/db/tables.dart index 125f418..4d2c49f 100644 --- a/apps/app_seeds/lib/db/tables.dart +++ b/apps/app_seeds/lib/db/tables.dart @@ -5,10 +5,6 @@ import 'sync_columns.dart'; /// The identity/accession — one row per distinct thing in the inventory. /// Only [label] is mandatory (progressive disclosure). -/// -/// The index covers the inventory list's hot filter — non-deleted, non-draft -/// rows — so it stays fast with a large catalogue. -@TableIndex(name: 'idx_varieties_deleted_draft', columns: {#isDeleted, #isDraft}) class Varieties extends Table with SyncColumns { TextColumn get label => text()(); TextColumn get speciesId => text().nullable()(); // → Species @@ -81,10 +77,6 @@ class SpeciesCommonNames extends Table with SyncColumns { /// A homogeneous batch held for a Variety — its own year and its own unit. /// Quantity (commons_core value type) is flattened into columns here. -/// -/// Indexed by [varietyId] — the inventory list joins lots per variety (types, -/// shared status, viability), so this avoids a full scan per reload. -@TableIndex(name: 'idx_lots_variety', columns: {#varietyId}) class Lots extends Table with SyncColumns { TextColumn get varietyId => text()(); TextColumn get type => @@ -189,26 +181,12 @@ class Parties extends Table with SyncColumns { /// rest by SQLCipher) via [bytes]; external files use [uri]. Storing bytes here /// keeps the "no plaintext at rest" rule for photos in Block 1; an external /// encrypted file store is a later optimization. -/// -/// Indexed by (parentType, parentId, kind) — the list's "first photo per -/// variety" lookup filters on exactly these. -@TableIndex( - name: 'idx_attachments_parent', - columns: {#parentType, #parentId, #kind}, -) class Attachments extends Table with SyncColumns { TextColumn get parentType => textEnum()(); TextColumn get parentId => text()(); TextColumn get kind => textEnum()(); TextColumn get uri => text().nullable()(); BlobColumn get bytes => blob().nullable()(); - - /// A small JPEG thumbnail of [bytes] (photos only), decoded once on save so - /// the inventory list never has to decode the full-resolution photo for a - /// 48px avatar. Purely derived, local and regenerable — it is EXCLUDED from - /// CRDT sync payloads and backups (see [SyncColumns] usage); a peer or a - /// restored backup regenerates it lazily via `backfillThumbnails`. - BlobColumn get thumbnail => blob().nullable()(); TextColumn get mimeType => text().nullable()(); /// Display order among sibling attachments (lower first). The lowest-ordered diff --git a/apps/app_seeds/lib/di/injector.dart b/apps/app_seeds/lib/di/injector.dart index 844a4fb..528baf5 100644 --- a/apps/app_seeds/lib/di/injector.dart +++ b/apps/app_seeds/lib/di/injector.dart @@ -28,7 +28,6 @@ import '../services/file_service.dart'; import '../services/inbox_service.dart'; import '../services/locale_store.dart'; import '../services/notification_service.dart'; -import '../services/offer_thumbnail.dart'; import '../services/ocr/label_text_extractor.dart'; import '../services/ocr/ocr_language.dart'; import '../services/ocr/tesseract_label_extractor.dart'; @@ -153,11 +152,7 @@ Future configureDependencies() async { database, idGen: IdGen(), nodeId: nodeId, - thumbnailBuilder: inventoryThumbnailBytes, ); - // Backfill list thumbnails for photos that predate the thumbnail column (or - // arrived via a restored backup). Fire-and-forget so startup isn't blocked. - unawaited(varietyRepository.backfillThumbnails()); const fileService = FilePickerFileService(); diff --git a/apps/app_seeds/lib/main.dart b/apps/app_seeds/lib/main.dart index 7509544..0a4c2db 100644 --- a/apps/app_seeds/lib/main.dart +++ b/apps/app_seeds/lib/main.dart @@ -1,4 +1,3 @@ -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'bootstrap.dart'; @@ -7,16 +6,6 @@ import 'ui/restart_widget.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - // Draw behind the system bars (edge-to-edge) with transparent bars, so the app - // fills the screen on Android 15+ (where edge-to-edge is enforced) and stays - // consistent elsewhere. Scaffolds use SafeArea to keep content off the insets. - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Color(0x00000000), - systemNavigationBarColor: Color(0x00000000), - ), - ); // Start from the device language, then let an explicit in-app choice win — // it's the only reliable way to reach languages the OS picker doesn't offer // (e.g. Asturian). The saved choice is restored inside [Bootstrap], after DI. diff --git a/apps/app_seeds/lib/services/camera_availability.dart b/apps/app_seeds/lib/services/camera_availability.dart deleted file mode 100644 index d9704e6..0000000 --- a/apps/app_seeds/lib/services/camera_availability.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -/// Whether this device has any camera. Resolved once at startup (via the native -/// channel, `PackageManager.FEATURE_CAMERA_ANY`) and cached so `build()` methods -/// can read it synchronously — the QR scan button ([qrScanSupported]) and the -/// photo-source sheet hide their camera options when it is false. -/// -/// Defaults to `true` so a normal phone never loses camera UI over a transient -/// channel error; only genuinely camera-less devices (Chromebooks, Android -/// Automotive, some TVs — kept installable by the `uses-feature required=false` -/// in AndroidManifest) flip it to false. Non-Android platforms keep the default -/// (iOS devices have a camera; desktop/web gate camera UI elsewhere). -bool _deviceHasCamera = true; - -bool get deviceHasCamera => _deviceHasCamera; - -@visibleForTesting -set deviceHasCamera(bool value) => _deviceHasCamera = value; - -const _channel = MethodChannel('org.comunes.tane/coarse_location'); - -/// Queries the native camera-presence check and caches it. Called once during -/// bootstrap, before the first real frame, so synchronous readers see the right -/// value. Android-only; elsewhere the default stands. Never throws — a channel -/// error leaves the safe default in place. -Future initCameraAvailability() async { - if (defaultTargetPlatform != TargetPlatform.android) return; - try { - final has = await _channel.invokeMethod('hasCamera'); - if (has != null) _deviceHasCamera = has; - } catch (_) { - // Keep the default; a normal phone shouldn't lose camera UI over this. - } -} diff --git a/apps/app_seeds/lib/services/offer_thumbnail.dart b/apps/app_seeds/lib/services/offer_thumbnail.dart index 27a6b04..81e756a 100644 --- a/apps/app_seeds/lib/services/offer_thumbnail.dart +++ b/apps/app_seeds/lib/services/offer_thumbnail.dart @@ -34,26 +34,6 @@ String? offerThumbnailDataUri( } } -/// Builds a small JPEG thumbnail (longest edge [edge]px) for the inventory-list -/// avatar, so the list never has to decode the full-resolution photo. Returns -/// raw JPEG bytes, or null on undecodable input (the caller falls back to the -/// full photo). Kept as local bytes — regenerable, so it is excluded from sync -/// and backups. -Uint8List? inventoryThumbnailBytes( - Uint8List bytes, { - int edge = 96, - int quality = 72, -}) { - try { - final decoded = img.decodeImage(bytes); - if (decoded == null) return null; - final resized = _fitWithin(decoded, edge); - return Uint8List.fromList(img.encodeJpg(resized, quality: quality)); - } catch (_) { - return null; - } -} - /// Extracts the raw bytes from a `data:...;base64,…` URI, or null when [uri] is /// not a base64 data URI. Used by the UI to render an inline thumbnail with /// `Image.memory` instead of a network fetch. diff --git a/apps/app_seeds/lib/state/offers_cubit.dart b/apps/app_seeds/lib/state/offers_cubit.dart index d6c462b..d5fecc2 100644 --- a/apps/app_seeds/lib/state/offers_cubit.dart +++ b/apps/app_seeds/lib/state/offers_cubit.dart @@ -24,16 +24,12 @@ class OffersState extends Equatable { this.blockedAuthors = const {}, this.hiddenOfferKeys = const {}, this.searching = false, - this.loadingMore = false, - this.nextPageCursor, this.publishing = false, this.hasSearched = false, this.error, }); - /// Offers discovered so far for [areaGeohash], newest first. Bounded in size - /// (see [OffersCubit.maxOffersKept]) so a busy area can't grow the list — and - /// the inline photo thumbnails it carries — without limit. + /// Offers discovered so far for [areaGeohash], newest appended as they arrive. final List offers; /// The coarse area currently being browsed. @@ -61,21 +57,8 @@ class OffersState extends Equatable { final Set hiddenOfferKeys; final bool searching; - - /// True while a "load more" page is in flight, so the UI shows a footer - /// spinner and doesn't fire overlapping page requests. - final bool loadingMore; - - /// Cursor (Unix seconds) for the next older page, or null when the newest - /// page was short (nothing older) or the in-memory cap was reached — in both - /// cases there is no more to load. - final int? nextPageCursor; - final bool publishing; - /// Whether more offers can be paged in (drives the infinite-scroll trigger). - bool get canLoadMore => nextPageCursor != null; - /// True once a discovery has been started, so the UI can tell "no search yet" /// from "searched, found nothing". final bool hasSearched; @@ -132,8 +115,6 @@ class OffersState extends Equatable { Set? blockedAuthors, Set? hiddenOfferKeys, bool? searching, - bool? loadingMore, - int? Function()? nextPageCursor, bool? publishing, bool? hasSearched, String? Function()? error, @@ -148,9 +129,6 @@ class OffersState extends Equatable { blockedAuthors: blockedAuthors ?? this.blockedAuthors, hiddenOfferKeys: hiddenOfferKeys ?? this.hiddenOfferKeys, searching: searching ?? this.searching, - loadingMore: loadingMore ?? this.loadingMore, - nextPageCursor: - nextPageCursor != null ? nextPageCursor() : this.nextPageCursor, publishing: publishing ?? this.publishing, hasSearched: hasSearched ?? this.hasSearched, error: error != null ? error() : this.error, @@ -168,8 +146,6 @@ class OffersState extends Equatable { blockedAuthors, hiddenOfferKeys, searching, - loadingMore, - nextPageCursor, publishing, hasSearched, error, @@ -207,23 +183,10 @@ class OffersCubit extends Cubit { StreamSubscription? _sub; Timer? _searchTimeout; - /// Upper bound on offers held in memory. Each offer can carry an inline - /// (~40 KB) photo thumbnail, so an unbounded list in a busy area is a real - /// out-of-memory risk on mobile. Paging stops once the list reaches this, and - /// live offers beyond it evict the oldest — the newest stay visible. - static const int maxOffersKept = 400; - /// Whether a live transport is available (relay configured and reachable). bool get isOnline => _transport != null; - /// Current time in Unix seconds — the granularity Nostr filters use for - /// `since`/`until`. - int _now() => DateTime.now().millisecondsSinceEpoch ~/ 1000; - - /// Starts (or restarts) discovery for [geohashPrefix]. Fetches the newest page - /// up front (bounded), then keeps a live subscription for offers published - /// from now on — older results arrive via [loadNextPage], not by draining the - /// whole area into memory. + /// Starts (or restarts) discovery for [geohashPrefix]. Results stream in. Future discover(String geohashPrefix) async { final transport = _transport; if (transport == null) { @@ -244,38 +207,15 @@ class OffersCubit extends Cubit { searching: true, hasSearched: true, )); - - final query = DiscoveryQuery( - geohashPrefix: geohashPrefix, - types: state.typeFilter, + _sub = transport.discover(DiscoveryQuery(geohashPrefix: geohashPrefix)).listen( + (offer) => + emit(state.copyWith(offers: _merge(state.offers, offer), searching: false)), + onError: (Object e) => + emit(state.copyWith(searching: false, error: () => '$e')), ); - // Live subscription first, bounded to offers published from now on, so a new - // listing shows up immediately without re-dumping the whole area. - final since = _now(); - _sub = transport.discover(query, since: since).listen( - (offer) => emit(state.copyWith( - offers: _capped(_prepend(state.offers, offer)), - searching: false, - )), - onError: (Object e) => - emit(state.copyWith(searching: false, error: () => '$e')), - ); - - try { - final page = await transport.discoverPage(query); - if (!isClosed) { - emit(state.copyWith( - offers: _capped(_mergePage(state.offers, page.offers)), - nextPageCursor: () => page.nextCursor, - searching: false, - )); - } - } catch (e) { - if (!isClosed) emit(state.copyWith(searching: false, error: () => '$e')); - } - - // Safety net: if the first page hangs and no live offer arrives, still stop - // the spinner so the screen shows the empty state, not an endless search. + // The discover stream stays open for live offers and never signals "done", + // so stop the spinner after a beat: no results → show the empty state, not + // an endless "searching". _searchTimeout = Timer(const Duration(seconds: 6), () { if (!isClosed && state.searching) { emit(state.copyWith(searching: false)); @@ -283,64 +223,18 @@ class OffersCubit extends Cubit { }); } - /// Loads the next (older) page of offers for the current area. Called by the - /// list as it nears the end. No-op when offline, already loading, or there is - /// nothing older to fetch. - Future loadNextPage() async { - final transport = _transport; - final cursor = state.nextPageCursor; - if (transport == null || cursor == null || state.loadingMore) return; - emit(state.copyWith(loadingMore: true)); - try { - final page = await transport.discoverPage(DiscoveryQuery( - geohashPrefix: state.areaGeohash, - types: state.typeFilter, - until: cursor, - )); - final merged = _mergePage(state.offers, page.offers); - // Stop paging once the cap is reached — the list is already as large as we - // keep — otherwise carry the transport's cursor onward. - final reachedCap = merged.length >= maxOffersKept; - emit(state.copyWith( - offers: _capped(merged), - nextPageCursor: () => reachedCap ? null : page.nextCursor, - loadingMore: false, - )); - } catch (e) { - emit(state.copyWith(loadingMore: false, error: () => '$e')); - } - } - - /// The offer's identity for de-duplication: NIP-99 addressable events are keyed - /// by (author, `d`-tag id), so the same listing from two relays — or a stored - /// copy plus a live echo — collapses to one entry. - static String _key(Offer o) => '${o.authorPubkeyHex}:${o.id}'; - - /// Prepends a freshly-arrived (newer) [incoming] offer, dropping any existing - /// entry with the same identity so a live echo never doubles the listing. - static List _prepend(List current, Offer incoming) => [ - incoming, + /// Appends [incoming] to [current], replacing any existing offer with the same + /// author + id. Relays legitimately resend addressable events (a stored copy + /// plus a live echo after publishing), so a plain append would double the + /// listing; keeping one entry per (author, id) is the NIP-99 semantics. + static List _merge(List current, Offer incoming) => [ for (final o in current) - if (_key(o) != _key(incoming)) o, + if (!(o.id == incoming.id && + o.authorPubkeyHex == incoming.authorPubkeyHex)) + o, + incoming, ]; - /// Appends an older [page] after [current] (older offers sort below newer), - /// skipping any already present. Keeps the newest-first ordering. - static List _mergePage(List current, List page) { - final seen = {for (final o in current) _key(o)}; - return [ - ...current, - for (final o in page) - if (seen.add(_key(o))) o, - ]; - } - - /// Caps the list to [maxOffersKept], keeping the newest (front) and dropping - /// the oldest (tail) — bounds memory in a busy area. - static List _capped(List offers) => offers.length <= maxOffersKept - ? offers - : offers.sublist(0, maxOffersKept); - /// Narrows the visible offers to those whose summary matches [query]. Purely /// local over the already-discovered list; does not re-hit the transport. void search(String query) => emit(state.copyWith(query: query)); diff --git a/apps/app_seeds/lib/ui/calendar_screen.dart b/apps/app_seeds/lib/ui/calendar_screen.dart index 717fb5b..d01df9e 100644 --- a/apps/app_seeds/lib/ui/calendar_screen.dart +++ b/apps/app_seeds/lib/ui/calendar_screen.dart @@ -235,14 +235,8 @@ class _CalendarRow extends StatelessWidget { leading: CircleAvatar( radius: 20, backgroundColor: swatch.fill, - // Decode to the 40px avatar size (× dpr), not the full photo. - foregroundImage: entry.photo == null - ? null - : ResizeImage( - MemoryImage(entry.photo!), - width: (40 * MediaQuery.devicePixelRatioOf(context)).round(), - height: (40 * MediaQuery.devicePixelRatioOf(context)).round(), - ), + foregroundImage: + entry.photo == null ? null : MemoryImage(entry.photo!), child: entry.photo == null ? Text( entry.label.isEmpty diff --git a/apps/app_seeds/lib/ui/draft_triage.dart b/apps/app_seeds/lib/ui/draft_triage.dart index 1605e52..9f677b1 100644 --- a/apps/app_seeds/lib/ui/draft_triage.dart +++ b/apps/app_seeds/lib/ui/draft_triage.dart @@ -108,10 +108,6 @@ class _DraftTile extends StatelessWidget { photo, width: 48, height: 48, - cacheWidth: - (48 * MediaQuery.devicePixelRatioOf(context)).round(), - cacheHeight: - (48 * MediaQuery.devicePixelRatioOf(context)).round(), fit: BoxFit.cover, ), ), @@ -196,8 +192,6 @@ class _NameDraftDialogState extends State { child: Image.memory( widget.photo!, height: 140, - cacheHeight: - (140 * MediaQuery.devicePixelRatioOf(context)).round(), fit: BoxFit.cover, ), ), diff --git a/apps/app_seeds/lib/ui/inventory_list_screen.dart b/apps/app_seeds/lib/ui/inventory_list_screen.dart index 2ec0925..bcda588 100644 --- a/apps/app_seeds/lib/ui/inventory_list_screen.dart +++ b/apps/app_seeds/lib/ui/inventory_list_screen.dart @@ -582,47 +582,27 @@ class _InventoryBody extends StatelessWidget { } // Items arrive ordered by category then label; insert a header whenever the - // category changes. Precompute a flat row model (header or item) so the - // ListView can build tiles lazily — with a large inventory, building every - // tile upfront (the old `ListView(children:)`) stalled the first paint. - final rows = <_InventoryRow>[]; + // category changes. + final rows = []; String? currentCategory; for (final item in items) { final category = item.category ?? t.inventory.uncategorized; if (category != currentCategory) { currentCategory = category; - rows.add(_InventoryRow.header(category)); + rows.add(_CategoryHeader(title: category)); } - rows.add(_InventoryRow.variety(item)); - } - return ListView.builder( - itemCount: rows.length, - itemBuilder: (context, i) { - final row = rows[i]; - final header = row.header; - if (header != null) { - return _CategoryHeader(title: header); - } - return _VarietyTile( - item: row.item!, + rows.add( + _VarietyTile( + item: item, selectionMode: selectionMode, - selected: selectedIds.contains(row.item!.id), - ); - }, - ); + selected: selectedIds.contains(item.id), + ), + ); + } + return ListView(children: rows); } } -/// A single row in the flattened inventory list: either a category [header] or -/// a variety [item] (exactly one is non-null). -class _InventoryRow { - const _InventoryRow.header(this.header) : item = null; - const _InventoryRow.variety(this.item) : header = null; - - final String? header; - final VarietyListItem? item; -} - class _CategoryHeader extends StatelessWidget { const _CategoryHeader({required this.title}); @@ -787,21 +767,10 @@ class _Avatar extends StatelessWidget { // Decorative: the tile title already announces the variety name, so keep // the thumbnail / initial out of the semantics tree. if (photo != null) { - // Decode straight to the 48px avatar size (× device pixel ratio) instead - // of holding the full-resolution photo in the image cache — decisive for - // memory with a large inventory of photographed varieties. - final cachePx = (48 * MediaQuery.devicePixelRatioOf(context)).round(); return ExcludeSemantics( child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: Image.memory( - photo, - width: 48, - height: 48, - cacheWidth: cachePx, - cacheHeight: cachePx, - fit: BoxFit.cover, - ), + child: Image.memory(photo, width: 48, height: 48, fit: BoxFit.cover), ), ); } diff --git a/apps/app_seeds/lib/ui/market_screen.dart b/apps/app_seeds/lib/ui/market_screen.dart index 23941a5..b4a7eeb 100644 --- a/apps/app_seeds/lib/ui/market_screen.dart +++ b/apps/app_seeds/lib/ui/market_screen.dart @@ -414,44 +414,23 @@ class MarketBody extends StatelessWidget { ), ], ) - : NotificationListener( - // Page in older offers as the list nears its end, so a - // large area streams in on demand rather than all at - // once. The cubit guards against overlapping loads. - onNotification: (n) { - if (state.canLoadMore && - !state.loadingMore && - n.metrics.axis == Axis.vertical && - n.metrics.extentAfter < 500) { - cubit.loadNextPage(); - } - return false; + : ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: visible.length, + separatorBuilder: (_, _) => const SizedBox(height: 12), + itemBuilder: (context, i) { + final o = visible[i]; + final mine = o.authorPubkeyHex == selfPubkey; + return _OfferCard( + offer: o, + mine: mine, + onTap: () async { + await context.push('/market/offer', extra: o); + await onOfferClosed?.call(); + }, + ); }, - child: ListView.separated( - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), - // A trailing spinner row while the next page loads. - itemCount: visible.length + (state.loadingMore ? 1 : 0), - separatorBuilder: (_, _) => const SizedBox(height: 12), - itemBuilder: (context, i) { - if (i >= visible.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center(child: CircularProgressIndicator()), - ); - } - final o = visible[i]; - final mine = o.authorPubkeyHex == selfPubkey; - return _OfferCard( - offer: o, - mine: mine, - onTap: () async { - await context.push('/market/offer', extra: o); - await onOfferClosed?.call(); - }, - ); - }, - ), ), ), ), diff --git a/apps/app_seeds/lib/ui/market_widgets.dart b/apps/app_seeds/lib/ui/market_widgets.dart index 92da91d..20c9f31 100644 --- a/apps/app_seeds/lib/ui/market_widgets.dart +++ b/apps/app_seeds/lib/ui/market_widgets.dart @@ -36,7 +36,6 @@ class OfferThumbnail extends StatelessWidget { return ClipRRect( borderRadius: BorderRadius.circular(10), child: _offerImage( - context, url, semanticLabel: semanticLabel, width: size, @@ -64,7 +63,7 @@ class OfferHeroImage extends StatelessWidget { borderRadius: BorderRadius.circular(14), child: AspectRatio( aspectRatio: 4 / 3, - child: _offerImage(context, url, semanticLabel: semanticLabel), + child: _offerImage(url, semanticLabel: semanticLabel), ), ); } @@ -74,7 +73,6 @@ class OfferHeroImage extends StatelessWidget { /// remote URL (via network), always cropping to fill and falling back to a /// neutral placeholder — a broken image must never block the card. Widget _offerImage( - BuildContext context, String url, { required String semanticLabel, double? width, @@ -82,16 +80,10 @@ Widget _offerImage( }) { final inline = decodeDataUri(url); if (inline != null) { - // For a sized thumbnail, decode down to its on-screen pixels instead of the - // photo's full resolution (Play's "bitmap downscaling"). The hero image - // passes no width, so it decodes at native size as intended. - final dpr = MediaQuery.devicePixelRatioOf(context); return Image.memory( inline, width: width, height: height, - cacheWidth: width == null ? null : (width * dpr).round(), - cacheHeight: height == null ? null : (height * dpr).round(), fit: BoxFit.cover, semanticLabel: semanticLabel, errorBuilder: (context, _, _) => diff --git a/apps/app_seeds/lib/ui/peer_avatar.dart b/apps/app_seeds/lib/ui/peer_avatar.dart index 09bd3ef..d501b9d 100644 --- a/apps/app_seeds/lib/ui/peer_avatar.dart +++ b/apps/app_seeds/lib/ui/peer_avatar.dart @@ -34,19 +34,7 @@ class PeerAvatar extends StatelessWidget { if (pic.startsWith('data:')) { final bytes = decodeDataUri(pic); if (bytes != null) { - // Decode the photo down to the avatar's on-screen size (in physical - // pixels) instead of full resolution — the "bitmap downscaling" Play - // recommends. A tiny disc never needs a multi-megapixel decode. - final side = (radius * 2 * MediaQuery.devicePixelRatioOf(context)) - .round(); - return CircleAvatar( - radius: radius, - backgroundImage: ResizeImage( - MemoryImage(bytes), - width: side, - height: side, - ), - ); + return CircleAvatar(radius: radius, backgroundImage: MemoryImage(bytes)); } } else if (pic.startsWith(avatarGlyphPrefix)) { final glyph = avatarGlyphChar(pic.substring(avatarGlyphPrefix.length)); diff --git a/apps/app_seeds/lib/ui/photo_pick.dart b/apps/app_seeds/lib/ui/photo_pick.dart index a7ae779..f01f684 100644 --- a/apps/app_seeds/lib/ui/photo_pick.dart +++ b/apps/app_seeds/lib/ui/photo_pick.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../i18n/strings.g.dart'; -import '../services/camera_availability.dart'; /// Asks the user to take a photo or pick one from the gallery, then returns its /// bytes (or null if cancelled/unavailable). Camera failures — e.g. on desktop, @@ -17,15 +16,12 @@ Future pickPhoto(BuildContext context) async { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Only offer the camera when the device actually has one; camera-less - // devices (Chromebooks, Automotive, some TVs) fall back to gallery. - if (deviceHasCamera) - ListTile( - key: const Key('photo.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('photo.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('photo.source.gallery'), leading: const Icon(Icons.photo_library_outlined), @@ -61,15 +57,12 @@ Future> pickPhotos(BuildContext context) async { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Only offer the camera when the device actually has one; camera-less - // devices (Chromebooks, Automotive, some TVs) fall back to gallery. - if (deviceHasCamera) - 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.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), diff --git a/apps/app_seeds/lib/ui/qr_scan.dart b/apps/app_seeds/lib/ui/qr_scan.dart index f765d81..05126ed 100644 --- a/apps/app_seeds/lib/ui/qr_scan.dart +++ b/apps/app_seeds/lib/ui/qr_scan.dart @@ -7,17 +7,12 @@ import 'package:zxing_barcode_scanner/zxing_barcode_scanner.dart'; import '../data/species_repository.dart'; import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; -import '../services/camera_availability.dart'; import '../services/seed_label_scan.dart'; /// Whether this platform can open the camera scanner. Mobile-only for now /// (pure-ZXing platform views; no Google Play Services) — elsewhere the scan -/// button simply isn't offered, same pattern as the OCR capture. On Android it -/// also requires an actual camera, so camera-less devices (Chromebooks, -/// Automotive, some TVs) don't get a scan button that can't open — see -/// [deviceHasCamera]. -bool get qrScanSupported => - !kIsWeb && ((Platform.isAndroid && deviceHasCamera) || Platform.isIOS); +/// button simply isn't offered, same pattern as the OCR capture. +bool get qrScanSupported => !kIsWeb && (Platform.isAndroid || Platform.isIOS); /// Opens the full-screen camera scanner and returns the first decoded QR /// payload, or null if the person backed out. (Ğ1nkgo's scanner pattern on diff --git a/apps/app_seeds/lib/ui/quick_add_sheet.dart b/apps/app_seeds/lib/ui/quick_add_sheet.dart index 349d247..49574ab 100644 --- a/apps/app_seeds/lib/ui/quick_add_sheet.dart +++ b/apps/app_seeds/lib/ui/quick_add_sheet.dart @@ -199,8 +199,6 @@ class _MoreSection extends StatelessWidget { child: Image.memory( state.photoBytes!, height: 120, - cacheHeight: - (120 * MediaQuery.devicePixelRatioOf(context)).round(), fit: BoxFit.cover, ), ), diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index e9d6634..740713b 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -2244,16 +2244,10 @@ class _PhotoGalleryState extends State<_PhotoGallery> { itemBuilder: (_, i) => GestureDetector( key: Key('photo.thumb.$i'), onTap: () => _openPhotoViewer(context, cubit, i), - // Decode to the 140px thumbnail size (× dpr), not full photo - // resolution — the full-res decode happens in the viewer. child: Image.memory( photos[i].bytes, width: 140, height: 140, - cacheWidth: - (140 * MediaQuery.devicePixelRatioOf(context)).round(), - cacheHeight: - (140 * MediaQuery.devicePixelRatioOf(context)).round(), fit: BoxFit.cover, ), ), diff --git a/apps/app_seeds/test/data/inventory_scale_test.dart b/apps/app_seeds/test/data/inventory_scale_test.dart index 576e9b7..d6928df 100644 --- a/apps/app_seeds/test/data/inventory_scale_test.dart +++ b/apps/app_seeds/test/data/inventory_scale_test.dart @@ -19,8 +19,8 @@ void main() { }); tearDown(() => db.close()); - test('the inventory view loads 10000 varieties quickly', () async { - const count = 10000; + test('the inventory view loads 3000 varieties quickly', () async { + const count = 3000; for (var i = 0; i < count; i++) { final id = await repo.addQuickVariety( label: 'Variety $i', @@ -43,12 +43,11 @@ void main() { sw.stop(); expect(view.items, hasLength(count)); - // Generous ceiling for CI (the point is to catch an N+1 regression, not to - // micro-benchmark). Includes the ~250ms debounce on the first emit; typical - // local runs are well under 2s for 10k rows with the v14 indexes. + // Generous ceiling for CI; typical local runs are well under 500ms. The + // point is to catch an accidental N+1 regression, not to micro-benchmark. expect( sw.elapsedMilliseconds, - lessThan(6000), + lessThan(3000), reason: 'inventory view took ${sw.elapsedMilliseconds}ms for $count rows', ); }); diff --git a/apps/app_seeds/test/db/migration_test.dart b/apps/app_seeds/test/db/migration_test.dart index 4653582..7b7f0cd 100644 --- a/apps/app_seeds/test/db/migration_test.dart +++ b/apps/app_seeds/test/db/migration_test.dart @@ -11,19 +11,19 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - test('freshly created database matches the exported schema v14', () async { - final schema = await verifier.schemaAt(14); + test('freshly created database matches the exported schema v13', () async { + final schema = await verifier.schemaAt(13); final db = AppDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, 14); + await verifier.migrateAndValidate(db, 13); await db.close(); }); - // Every historical version upgrades cleanly to the current schema (v14). - for (var from = 1; from <= 13; from++) { - test('upgrades v$from → v14 and matches the fresh schema', () async { + // Every historical version upgrades cleanly to the current schema (v13). + for (var from = 1; from <= 12; from++) { + test('upgrades v$from → v13 and matches the fresh schema', () async { final connection = await verifier.startAt(from); final db = AppDatabase(connection); - await verifier.migrateAndValidate(db, 14); + await verifier.migrateAndValidate(db, 13); await db.close(); }); } diff --git a/apps/app_seeds/test/db/schema/schema.dart b/apps/app_seeds/test/db/schema/schema.dart index 938ab12..ba56c19 100644 --- a/apps/app_seeds/test/db/schema/schema.dart +++ b/apps/app_seeds/test/db/schema/schema.dart @@ -17,7 +17,6 @@ import 'schema_v10.dart' as v10; import 'schema_v11.dart' as v11; import 'schema_v12.dart' as v12; import 'schema_v13.dart' as v13; -import 'schema_v14.dart' as v14; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -49,12 +48,10 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v12.DatabaseAtV12(db); case 13: return v13.DatabaseAtV13(db); - case 14: - return v14.DatabaseAtV14(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; } diff --git a/apps/app_seeds/test/db/schema/schema_v14.dart b/apps/app_seeds/test/db/schema/schema_v14.dart deleted file mode 100644 index ad1dee1..0000000 --- a/apps/app_seeds/test/db/schema/schema_v14.dart +++ /dev/null @@ -1,2248 +0,0 @@ -// 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', - ); - late final GeneratedColumn priceAmount = GeneratedColumn( - 'price_amount', - aliasedName, - true, - type: DriftSqlType.double, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn priceCurrency = GeneratedColumn( - 'price_currency', - 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, - priceAmount, - priceCurrency, - ]; - @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 GardenOutcomes extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - GardenOutcomes(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 year = GeneratedColumn( - 'year', - aliasedName, - true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn rating = GeneratedColumn( - 'rating', - 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, - year, - rating, - notes, - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'garden_outcomes'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - GardenOutcomes createAlias(String alias) { - return GardenOutcomes(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 thumbnail = - GeneratedColumn( - 'thumbnail', - 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, - thumbnail, - 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 Plantares extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Plantares(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, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn direction = GeneratedColumn( - 'direction', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL', - ); - late final GeneratedColumn counterparty = GeneratedColumn( - 'counterparty', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn owedDescription = GeneratedColumn( - 'owed_description', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn madeOn = GeneratedColumn( - 'made_on', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL', - ); - late final GeneratedColumn dueBy = GeneratedColumn( - 'due_by', - aliasedName, - true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn status = GeneratedColumn( - 'status', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL DEFAULT \'open\'', - defaultValue: const CustomExpression('\'open\''), - ); - late final GeneratedColumn settledOn = GeneratedColumn( - 'settled_on', - aliasedName, - true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn note = GeneratedColumn( - 'note', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn pledgeId = GeneratedColumn( - 'pledge_id', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn debtorKey = GeneratedColumn( - 'debtor_key', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn creditorKey = GeneratedColumn( - 'creditor_key', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn debtorSignature = GeneratedColumn( - 'debtor_signature', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn creditorSignature = - GeneratedColumn( - 'creditor_signature', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn movementId = GeneratedColumn( - 'movement_id', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn remoteState = GeneratedColumn( - 'remote_state', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn returnKind = GeneratedColumn( - 'return_kind', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL DEFAULT \'similar\'', - defaultValue: const CustomExpression('\'similar\''), - ); - late final GeneratedColumn workHours = GeneratedColumn( - 'work_hours', - aliasedName, - true, - type: DriftSqlType.double, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - @override - List get $columns => [ - id, - createdAt, - updatedAt, - lastAuthor, - isDeleted, - schemaRowVersion, - varietyId, - direction, - counterparty, - owedDescription, - madeOn, - dueBy, - status, - settledOn, - note, - pledgeId, - debtorKey, - creditorKey, - debtorSignature, - creditorSignature, - movementId, - remoteState, - returnKind, - workHours, - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'plantares'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Plantares createAlias(String alias) { - return Plantares(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY(id)']; - @override - bool get dontWriteConstraints => true; -} - -class Sales extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Sales(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, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn direction = GeneratedColumn( - 'direction', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL', - ); - late final GeneratedColumn counterparty = GeneratedColumn( - 'counterparty', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn amount = GeneratedColumn( - 'amount', - aliasedName, - true, - type: DriftSqlType.double, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn currency = GeneratedColumn( - 'currency', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL', - ); - late final GeneratedColumn soldOn = GeneratedColumn( - 'sold_on', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL', - ); - 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, - varietyId, - direction, - counterparty, - amount, - currency, - soldOn, - note, - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'sales'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Sales createAlias(String alias) { - return Sales(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY(id)']; - @override - bool get dontWriteConstraints => true; -} - -class DatabaseAtV14 extends GeneratedDatabase { - DatabaseAtV14(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 GardenOutcomes gardenOutcomes = GardenOutcomes(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); - late final Plantares plantares = Plantares(this); - late final Sales sales = Sales(this); - late final Index idxVarietiesDeletedDraft = Index( - 'idx_varieties_deleted_draft', - 'CREATE INDEX idx_varieties_deleted_draft ON varieties (is_deleted, is_draft)', - ); - late final Index idxLotsVariety = Index( - 'idx_lots_variety', - 'CREATE INDEX idx_lots_variety ON lots (variety_id)', - ); - late final Index idxAttachmentsParent = Index( - 'idx_attachments_parent', - 'CREATE INDEX idx_attachments_parent ON attachments (parent_type, parent_id, kind)', - ); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [ - varieties, - varietyVernacularNames, - species, - speciesCommonNames, - lots, - germinationTests, - conditionChecks, - gardenOutcomes, - movements, - parties, - attachments, - externalLinks, - plantares, - sales, - idxVarietiesDeletedDraft, - idxLotsVariety, - idxAttachmentsParent, - ]; - @override - int get schemaVersion => 14; -} diff --git a/apps/app_seeds/test/screenshots/screenshots_test.dart b/apps/app_seeds/test/screenshots/screenshots_test.dart index 7589506..fac0139 100644 --- a/apps/app_seeds/test/screenshots/screenshots_test.dart +++ b/apps/app_seeds/test/screenshots/screenshots_test.dart @@ -156,11 +156,7 @@ class _SeededOffersCubit extends OffersCubit { /// A transport that is present (so the market reads as online) but never used. class _NoopOfferTransport implements OfferTransport { @override - Stream discover(DiscoveryQuery query, {int? since}) => - const Stream.empty(); - @override - Future discoverPage(DiscoveryQuery query) async => - const OfferPage(offers: []); + Stream discover(DiscoveryQuery query) => const Stream.empty(); @override Future publish(Offer offer) => throw UnimplementedError(); @override diff --git a/apps/app_seeds/test/state/offers_cubit_test.dart b/apps/app_seeds/test/state/offers_cubit_test.dart index 650088a..d38a96e 100644 --- a/apps/app_seeds/test/state/offers_cubit_test.dart +++ b/apps/app_seeds/test/state/offers_cubit_test.dart @@ -25,7 +25,7 @@ class FakeOfferTransport implements OfferTransport { } @override - Stream discover(DiscoveryQuery query, {int? since}) { + Stream discover(DiscoveryQuery query) { final controller = StreamController(); for (final o in _offers) { if (o.approxGeohash.startsWith(query.geohashPrefix)) controller.add(o); @@ -33,15 +33,6 @@ class FakeOfferTransport implements OfferTransport { return controller.stream; // left open (live), like a real subscription } - @override - Future discoverPage(DiscoveryQuery query) async { - final matches = [ - for (final o in _offers) - if (o.approxGeohash.startsWith(query.geohashPrefix)) o, - ]; - return OfferPage(offers: matches); // short page → nothing older to load - } - @override Future retract(String offerId) async => _offers.removeWhere((o) => o.id == offerId); @@ -63,7 +54,7 @@ class DuplicatingOfferTransport implements OfferTransport { } @override - Stream discover(DiscoveryQuery query, {int? since}) { + Stream discover(DiscoveryQuery query) { final controller = StreamController(); for (final o in _offers) { if (o.approxGeohash.startsWith(query.geohashPrefix)) { @@ -74,17 +65,6 @@ class DuplicatingOfferTransport implements OfferTransport { return controller.stream; } - @override - Future discoverPage(DiscoveryQuery query) async { - // The stored copy (deduped at relay level); the live echo (the duplicate) - // still arrives via [discover], so the cubit's dedup is what's under test. - final matches = [ - for (final o in _offers) - if (o.approxGeohash.startsWith(query.geohashPrefix)) o, - ]; - return OfferPage(offers: matches); - } - @override Future retract(String offerId) async => _offers.removeWhere((o) => o.id == offerId); @@ -93,59 +73,6 @@ class DuplicatingOfferTransport implements OfferTransport { Future close() async {} } -/// A transport that serves offers in `until`-cursored pages (like a relay's -/// stored events), so the cubit's pagination and memory cap can be exercised. -/// Each seeded offer gets a descending createdAt; [discover] (live) is empty. -class PaginatingOfferTransport implements OfferTransport { - PaginatingOfferTransport(int count, {this.geohash = 'sp3e9'}) { - // Newest (highest createdAt) first: offer 0 is newest. - for (var i = 0; i < count; i++) { - _byCreatedAt.add(( - createdAt: 1000000 - i, - offer: Offer( - id: 'o$i', - authorPubkeyHex: 'ab' * 32, - summary: 'offer $i', - type: OfferType.gift, - approxGeohash: geohash, - ), - )); - } - } - - final String geohash; - final List<({int createdAt, Offer offer})> _byCreatedAt = []; - - @override - Stream discover(DiscoveryQuery query, {int? since}) => - const Stream.empty(); - - @override - Future discoverPage(DiscoveryQuery query) async { - final matched = _byCreatedAt - .where((e) => e.offer.approxGeohash.startsWith(query.geohashPrefix)) - .where((e) => query.until == null || e.createdAt <= query.until!) - .toList() - ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); - final page = matched.take(query.limit).toList(); - final nextCursor = page.length >= query.limit && page.isNotEmpty - ? page.last.createdAt - 1 - : null; - return OfferPage( - offers: [for (final e in page) e.offer], - nextCursor: nextCursor, - ); - } - - @override - Future publish(Offer offer) async => - PublishResult(accepted: true, transportRef: offer.id); - @override - Future retract(String offerId) async {} - @override - Future close() async {} -} - void main() { group('OfferMapper', () { test('maps local sharing intent to the network offer type', () { @@ -774,65 +701,4 @@ void main() { await cubit.close(); }); }); - - group('OffersCubit pagination', () { - test('discover loads the first page and exposes a next-page cursor', - () async { - final cubit = OffersCubit(PaginatingOfferTransport(250)); - await cubit.discover('sp3'); - await pumpEventQueue(); - // One page (default limit 100) — not the whole 250 — is kept in memory. - expect(cubit.state.offers, hasLength(100)); - expect(cubit.state.canLoadMore, isTrue); - expect(cubit.state.searching, isFalse); - await cubit.close(); - }); - - test('loadNextPage accumulates older offers until the source is exhausted', - () async { - final cubit = OffersCubit(PaginatingOfferTransport(250)); - await cubit.discover('sp3'); - await pumpEventQueue(); - - await cubit.loadNextPage(); - expect(cubit.state.offers, hasLength(200)); - expect(cubit.state.canLoadMore, isTrue); - - await cubit.loadNextPage(); - // Only 250 exist: the third page is short, so there is nothing older. - expect(cubit.state.offers, hasLength(250)); - expect(cubit.state.canLoadMore, isFalse); - - // Paging past the end is a harmless no-op. - await cubit.loadNextPage(); - expect(cubit.state.offers, hasLength(250)); - await cubit.close(); - }); - - test('the in-memory list is capped and paging stops at the cap', () async { - final cubit = OffersCubit(PaginatingOfferTransport(600)); - await cubit.discover('sp3'); - await pumpEventQueue(); - // Keep paging until the cubit says there is no more. - var guard = 0; - while (cubit.state.canLoadMore && guard++ < 20) { - await cubit.loadNextPage(); - } - expect(cubit.state.offers, hasLength(OffersCubit.maxOffersKept)); - expect(cubit.state.canLoadMore, isFalse); - await cubit.close(); - }); - - test('offers are de-duplicated across pages by (author, id)', () async { - // Two pages that overlap on the boundary id must not double it. - final cubit = OffersCubit(PaginatingOfferTransport(150)); - await cubit.discover('sp3'); - await pumpEventQueue(); - await cubit.loadNextPage(); - final ids = cubit.state.offers.map((o) => o.id).toList(); - expect(ids.toSet(), hasLength(ids.length)); // no duplicates - expect(cubit.state.offers, hasLength(150)); - await cubit.close(); - }); - }); } diff --git a/apps/app_seeds/test/ui/inventory_list_lazy_test.dart b/apps/app_seeds/test/ui/inventory_list_lazy_test.dart deleted file mode 100644 index a9475e1..0000000 --- a/apps/app_seeds/test/ui/inventory_list_lazy_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:tane/ui/inventory_list_screen.dart'; - -import '../support/test_support.dart'; - -/// The inventory list must render lazily: with a large catalogue only the tiles -/// near the viewport are built, not one widget per row. Guards the regression -/// from `ListView(children: …)` (every tile built upfront) back in. -void main() { - testWidgets('builds only the on-screen tiles for a large inventory', - (tester) async { - final db = newTestDatabase(); - final repo = newTestRepository(db); - // Enough rows that an eager list would build hundreds of tiles at once. - for (var i = 0; i < 300; i++) { - await repo.addQuickVariety( - label: 'Variety ${i.toString().padLeft(3, '0')}', - category: i.isEven ? 'Poaceae' : 'Fabaceae', - ); - } - - await tester.pumpWidget( - wrapScreen(repository: repo, child: const InventoryListScreen()), - ); - // Let the debounced inventory stream emit and the async load resolve - // (bounded pumps — the screen holds a live Drift stream, so pumpAndSettle - // would hang; see testing.md). - await tester.pump(); - await tester.pump(const Duration(milliseconds: 400)); - await tester.pump(const Duration(milliseconds: 100)); - - // Every variety tile is a ListTile; a lazy list builds only a viewport-worth. - final built = find.byType(ListTile).evaluate().length; - expect(built, greaterThan(0), reason: 'the list rendered some tiles'); - expect( - built, - lessThan(50), - reason: 'lazy list should build ~a screenful, not all 300 ($built built)', - ); - - await disposeTree(tester); - await db.close(); - }); -} diff --git a/docs/release.md b/docs/release.md index 3e70f01..8b44a4f 100644 --- a/docs/release.md +++ b/docs/release.md @@ -15,11 +15,7 @@ git tag v0.1.0 && git push origin v0.1.0 ``` Forgejo Actions ([`../.forgejo/workflows/release.yml`](../.forgejo/workflows/release.yml)) -builds the signed AAB/APK and uploads to Play's **production** track at **100% -rollout**. No passwords are typed. Because a tag now publishes to everyone -(subject to Google review), run the [manual smoke test](#manual-smoke-test-before-shipping) -**before** tagging. To stage on the internal track first, run -`bundle exec fastlane deploy_internal` by hand. +builds the signed AAB/APK and uploads to Play's **internal** track. No passwords are typed. ## Versioning @@ -127,24 +123,6 @@ listing (titles, descriptions, changelogs, screenshots) is read straight from `fastlane/metadata/android/`. Data Safety and content-rating answers: [`legal/internal/play-compliance.md`](legal/internal/play-compliance.md). -Lanes: -- `deploy_play` — upload the AAB to **production** at 100% (what CI runs on tag). -- `deploy_internal` — same AAB to the **internal** test track (manual QA safety net). -- `deploy_metadata` — push only the store listing (no binary). -- `promote_production` — promote an internal release to production without a rebuild. - -### Country/region availability (Play Console, not this repo) - -Which countries the app is offered in is **not** in the repo and `fastlane supply` -does not manage it — it's a Play Console setting. To add countries: -**Play Console → (Tane) → Production → Countries / regions → Add countries/regions** -(under "Availability"). Play asks you to pick countries when you first move a -release to production; widen the list there for new markets. - -Note: the locales under `fastlane/metadata/android/` (`en-US`, `es-ES`) -are **listing languages**, not countries — adding a locale improves the store -page but does not by itself make the app available in a new country. - ## Publish to F-Droid (official repo) F-Droid builds from source after a merge request to `fdroiddata`. The build recipe diff --git a/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart b/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart index 305f842..c9408b9 100644 --- a/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart +++ b/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart @@ -13,12 +13,10 @@ class NostrOfferTransport implements OfferTransport { final NostrChannel _conn; final Nip99Codec _codec = Nip99Codec(); - Filter _filter(DiscoveryQuery q, {int? since}) => Filter( + Filter _filter(DiscoveryQuery q) => Filter( kinds: const [Nip99Codec.kindActive], tagFilters: {'g': [q.geohashPrefix]}, limit: q.limit, - since: since, - until: q.until, ); @override @@ -39,34 +37,11 @@ class NostrOfferTransport implements OfferTransport { } @override - Stream discover(DiscoveryQuery query, {int? since}) => _conn - .subscribe(_filter(query, since: since)) + Stream discover(DiscoveryQuery query) => _conn + .subscribe(_filter(query)) .map(_codec.decode) .where((o) => query.types.isEmpty || query.types.contains(o.type)); - @override - Future discoverPage(DiscoveryQuery query) async { - final events = await _conn.reqOnce(_filter(query)) - // Newest first; the relays dedup within themselves and reqOnce dedups - // across them, but order is not guaranteed, so sort here. - ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); - final offers = []; - for (final e in events) { - final offer = _codec.decode(e); - if (_matchesType(offer, query)) offers.add(offer); - } - // A full page means more may exist older than the oldest event we saw; a - // short page means we've reached the end. Base the cursor on ALL events - // (not just type-matched ones) so type filtering never makes us re-fetch. - final nextCursor = events.length >= query.limit && events.isNotEmpty - ? events.last.createdAt - 1 - : null; - return OfferPage(offers: offers, nextCursor: nextCursor); - } - - bool _matchesType(Offer o, DiscoveryQuery q) => - q.types.isEmpty || q.types.contains(o.type); - /// Collects matches up to EOSE (tests/one-shot browse). Future> discoverUntilEose(DiscoveryQuery query) async { final events = await _conn.reqOnce(_filter(query)); diff --git a/packages/commons_core/lib/src/social/offer.dart b/packages/commons_core/lib/src/social/offer.dart index 97472d9..5baa1b1 100644 --- a/packages/commons_core/lib/src/social/offer.dart +++ b/packages/commons_core/lib/src/social/offer.dart @@ -88,28 +88,10 @@ class DiscoveryQuery { required this.geohashPrefix, this.types = const {}, this.limit = 100, - this.until, }); /// Coarse geohash prefix to search near (e.g. "u09" ≈ tens of km). final String geohashPrefix; final Set types; final int limit; - - /// Pagination cursor (NIP-01 `until`): return only offers published at or - /// before this Unix time (seconds). Null asks for the newest page. Comes from - /// the previous page's [OfferPage.nextCursor]. - final int? until; -} - -/// One page of discovered offers plus the cursor to fetch the next (older) page. -class OfferPage { - const OfferPage({required this.offers, this.nextCursor}); - - /// The page's offers, newest first. - final List offers; - - /// Pass as the next query's [DiscoveryQuery.until] to page further back. Null - /// when the relays returned less than a full page — there is nothing older. - final int? nextCursor; } diff --git a/packages/commons_core/lib/src/social/offer_transport.dart b/packages/commons_core/lib/src/social/offer_transport.dart index 3d7928e..3dc3740 100644 --- a/packages/commons_core/lib/src/social/offer_transport.dart +++ b/packages/commons_core/lib/src/social/offer_transport.dart @@ -13,16 +13,8 @@ abstract interface class OfferTransport { Future publish(Offer offer); /// Streams offers matching [query]: stored matches first, then live ones, - /// until the caller cancels the subscription. Pass [since] (Unix seconds) to - /// stream only offers published after it — used to keep a live subscription - /// for NEW offers while older ones are browsed via [discoverPage]. - Stream discover(DiscoveryQuery query, {int? since}); - - /// Fetches ONE page of stored offers matching [query] (up to EOSE), newest - /// first, with a cursor to page further back. Bounded — unlike [discover] it - /// does not hold a live subscription — so the UI can scroll a large result - /// set without accumulating every offer in memory. - Future discoverPage(DiscoveryQuery query); + /// until the caller cancels the subscription. + Stream discover(DiscoveryQuery query); /// Retracts a previously published offer (relays that already replicated it /// drop it over time). diff --git a/packages/commons_core/test/social/nostr_offer_transport_page_test.dart b/packages/commons_core/test/social/nostr_offer_transport_page_test.dart deleted file mode 100644 index 25f9642..0000000 --- a/packages/commons_core/test/social/nostr_offer_transport_page_test.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:commons_core/commons_core.dart'; -import 'package:nostr/nostr.dart'; -import 'package:test/test.dart'; - -/// A stand-in relay channel: [reqOnce] honours the filter's `until` and `limit` -/// (newest first), like a relay serving stored events, so [discoverPage]'s -/// ordering and cursor maths can be asserted deterministically. -class _FakeChannel implements NostrChannel { - _FakeChannel(this._events); - - final List _events; - - @override - Future> reqOnce(Filter filter) async { - final until = filter.until; - final matched = _events - .where((e) => until == null || e.createdAt <= until) - .toList() - ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); - return matched.take(filter.limit ?? matched.length).toList(); - } - - @override - String get privateKeyHex => '00' * 32; - @override - String get publicKeyHex => 'ab' * 32; - @override - Future<({bool accepted, String message})> publish(Event event) async => - (accepted: true, message: ''); - @override - Stream subscribe(Filter filter) => const Stream.empty(); - @override - Future close() async {} -} - -Event _evt(String id, int createdAt, {String type = 'gift'}) => Event( - 'evt-$id', - 'ab' * 32, - createdAt, - Nip99Codec.kindActive, - [ - ['d', id], - ['g', 'sp3e9'], - ['offer_type', type], - ['title', 'offer $id'], - ], - 'offer $id', - '00' * 64, - verify: false, - ); - -void main() { - group('NostrOfferTransport.discoverPage', () { - test('returns offers newest-first with a cursor to the older page', - () async { - final transport = NostrOfferTransport(_FakeChannel([ - _evt('a', 100), // oldest - _evt('c', 300), // newest - _evt('b', 200), - ])); - - final page1 = await transport.discoverPage( - const DiscoveryQuery(geohashPrefix: 'sp3e9', limit: 2), - ); - // A full page (2 of the 3) sorted newest-first, with a cursor set. - expect(page1.offers.map((o) => o.id), ['c', 'b']); - expect(page1.nextCursor, 199, reason: 'oldest kept (200) minus one'); - - final page2 = await transport.discoverPage( - DiscoveryQuery(geohashPrefix: 'sp3e9', limit: 2, until: page1.nextCursor), - ); - // Only the last one remains — a short page, so nothing older. - expect(page2.offers.map((o) => o.id), ['a']); - expect(page2.nextCursor, isNull); - }); - - test('a short first page reports no next cursor', () async { - final transport = NostrOfferTransport(_FakeChannel([ - _evt('a', 100), - _evt('b', 200), - ])); - final page = await transport.discoverPage( - const DiscoveryQuery(geohashPrefix: 'sp3e9', limit: 100), - ); - expect(page.offers, hasLength(2)); - expect(page.nextCursor, isNull); - }); - - test('type filtering narrows the offers but not the cursor', () async { - // A full page of gifts with one sale mixed in; asking for sales only must - // still advance the cursor by the oldest EVENT seen, not the oldest match, - // so paging never skips unmatched offers. - final transport = NostrOfferTransport(_FakeChannel([ - _evt('g1', 300), - _evt('s1', 200, type: 'sale'), - _evt('g2', 100), - ])); - final page = await transport.discoverPage( - const DiscoveryQuery( - geohashPrefix: 'sp3e9', - limit: 3, - types: {OfferType.sale}, - ), - ); - expect(page.offers.map((o) => o.id), ['s1']); - expect(page.nextCursor, 99, reason: 'oldest event (100) minus one'); - }); - }); -}