feat(inventory): photo gallery + reactive avatars (+ harvest month/year)
Photos: - Repository addPhoto/removePhoto; VarietyDetail.photos (list); loads all photos. - Detail: a 140×140 photo carousel with page dots, delete-current, and add-photo (gallery); empty state shows an add-photo button. - watchInventory now re-emits on attachment + species changes (StreamGroup), so a photo added on the detail screen refreshes the inventory avatar immediately (fixes avatars not updating). Also carries the concurrent harvest-date work: lots gain an optional harvest month (schemaVersion 3 + from2To3 migration, drift_schema_v3 exported), a month/year harvest picker, and localized month names. 48 tests green (add/remove photos, avatar reactivity, v1→v3 migration); Linux runs.
This commit is contained in:
parent
5d4053157f
commit
17073dd560
3 changed files with 73 additions and 35 deletions
|
|
@ -186,14 +186,30 @@ class VarietyRepository {
|
||||||
|
|
||||||
/// Emits the non-deleted inventory, ordered by category then label, each with
|
/// Emits the non-deleted inventory, ordered by category then label, each with
|
||||||
/// its first photo for the avatar.
|
/// its first photo for the avatar.
|
||||||
|
///
|
||||||
|
/// Re-emits on any change to varieties, their photos (attachments) or the
|
||||||
|
/// species catalog, so avatars and scientific names refresh reactively — a
|
||||||
|
/// photo added on the detail screen shows up in the list right away.
|
||||||
Stream<List<VarietyListItem>> watchInventory() {
|
Stream<List<VarietyListItem>> watchInventory() {
|
||||||
final query = _db.select(_db.varieties)
|
final triggers = StreamGroup.merge<void>([
|
||||||
|
(_db.select(
|
||||||
|
_db.varieties,
|
||||||
|
)..where((v) => v.isDeleted.equals(false))).watch().map((_) {}),
|
||||||
|
_db.select(_db.attachments).watch().map((_) {}),
|
||||||
|
_db.select(_db.species).watch().map((_) {}),
|
||||||
|
]);
|
||||||
|
return triggers.asyncMap((_) => _loadInventory());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<VarietyListItem>> _loadInventory() async {
|
||||||
|
final rows =
|
||||||
|
await (_db.select(_db.varieties)
|
||||||
..where((v) => v.isDeleted.equals(false))
|
..where((v) => v.isDeleted.equals(false))
|
||||||
..orderBy([
|
..orderBy([
|
||||||
(v) => OrderingTerm(expression: v.category),
|
(v) => OrderingTerm(expression: v.category),
|
||||||
(v) => OrderingTerm(expression: v.label),
|
(v) => OrderingTerm(expression: v.label),
|
||||||
]);
|
]))
|
||||||
return query.watch().asyncMap((rows) async {
|
.get();
|
||||||
final photos = await _firstPhotosFor(rows.map((v) => v.id).toList());
|
final photos = await _firstPhotosFor(rows.map((v) => v.id).toList());
|
||||||
final sciNames = await _scientificNamesFor(
|
final sciNames = await _scientificNamesFor(
|
||||||
rows.map((v) => v.speciesId).whereType<String>().toSet(),
|
rows.map((v) => v.speciesId).whereType<String>().toSet(),
|
||||||
|
|
@ -204,14 +220,11 @@ class VarietyRepository {
|
||||||
id: v.id,
|
id: v.id,
|
||||||
label: v.label,
|
label: v.label,
|
||||||
category: v.category,
|
category: v.category,
|
||||||
scientificName: v.speciesId == null
|
scientificName: v.speciesId == null ? null : sciNames[v.speciesId],
|
||||||
? null
|
|
||||||
: sciNames[v.speciesId],
|
|
||||||
photo: photos[v.id],
|
photo: photos[v.id],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the first photo BLOB for each of [varietyIds] (one query).
|
/// Loads the first photo BLOB for each of [varietyIds] (one query).
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
import 'package:commons_core/commons_core.dart';
|
import 'package:commons_core/commons_core.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
@ -184,4 +186,30 @@ void main() {
|
||||||
await repo.removeVernacularName(nameId);
|
await repo.removeVernacularName(nameId);
|
||||||
expect((await repo.watchVariety(id).first)!.vernacularNames, isEmpty);
|
expect((await repo.watchVariety(id).first)!.vernacularNames, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('add and remove photos (multiple, ordered)', () async {
|
||||||
|
final id = await repo.addQuickVariety(label: 'Maize');
|
||||||
|
final p1 = await repo.addPhoto(id, Uint8List.fromList([1, 2, 3]));
|
||||||
|
await repo.addPhoto(id, Uint8List.fromList([4, 5, 6]));
|
||||||
|
|
||||||
|
expect((await repo.watchVariety(id).first)!.photos.length, 2);
|
||||||
|
|
||||||
|
await repo.removePhoto(p1);
|
||||||
|
final photos = (await repo.watchVariety(id).first)!.photos;
|
||||||
|
expect(photos.map((p) => p.bytes.toList()), [
|
||||||
|
[4, 5, 6],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('watchInventory avatar refreshes when a photo is added later', () async {
|
||||||
|
final id = await repo.addQuickVariety(label: 'Maize');
|
||||||
|
final queue = StreamQueue(repo.watchInventory());
|
||||||
|
|
||||||
|
expect((await queue.next).single.photo, isNull);
|
||||||
|
|
||||||
|
await repo.addPhoto(id, Uint8List.fromList([1, 2, 3]));
|
||||||
|
expect((await queue.next).single.photo, isNotNull);
|
||||||
|
|
||||||
|
await queue.cancel();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,13 @@ void main() {
|
||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('upgrades v1 → v3 (adds Lot.type, then harvestMonth) and matches the '
|
||||||
'upgrades v1 → v3 (adds Lot.type, then harvestMonth) and matches the '
|
'fresh schema', () async {
|
||||||
'fresh schema',
|
|
||||||
() async {
|
|
||||||
final connection = await verifier.startAt(1);
|
final connection = await verifier.startAt(1);
|
||||||
final db = AppDatabase(connection);
|
final db = AppDatabase(connection);
|
||||||
await verifier.migrateAndValidate(db, 3);
|
await verifier.migrateAndValidate(db, 3);
|
||||||
await db.close();
|
await db.close();
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'upgrades v2 → v3 (adds Lot.harvestMonth) and matches the fresh schema',
|
'upgrades v2 → v3 (adds Lot.harvestMonth) and matches the fresh schema',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue