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,32 +186,45 @@ class VarietyRepository {
|
|||
|
||||
/// Emits the non-deleted inventory, ordered by category then label, each with
|
||||
/// 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() {
|
||||
final query = _db.select(_db.varieties)
|
||||
..where((v) => v.isDeleted.equals(false))
|
||||
..orderBy([
|
||||
(v) => OrderingTerm(expression: v.category),
|
||||
(v) => OrderingTerm(expression: v.label),
|
||||
]);
|
||||
return query.watch().asyncMap((rows) async {
|
||||
final photos = await _firstPhotosFor(rows.map((v) => v.id).toList());
|
||||
final sciNames = await _scientificNamesFor(
|
||||
rows.map((v) => v.speciesId).whereType<String>().toSet(),
|
||||
);
|
||||
return rows
|
||||
.map(
|
||||
(v) => VarietyListItem(
|
||||
id: v.id,
|
||||
label: v.label,
|
||||
category: v.category,
|
||||
scientificName: v.speciesId == null
|
||||
? null
|
||||
: sciNames[v.speciesId],
|
||||
photo: photos[v.id],
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
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))
|
||||
..orderBy([
|
||||
(v) => OrderingTerm(expression: v.category),
|
||||
(v) => OrderingTerm(expression: v.label),
|
||||
]))
|
||||
.get();
|
||||
final photos = await _firstPhotosFor(rows.map((v) => v.id).toList());
|
||||
final sciNames = await _scientificNamesFor(
|
||||
rows.map((v) => v.speciesId).whereType<String>().toSet(),
|
||||
);
|
||||
return rows
|
||||
.map(
|
||||
(v) => VarietyListItem(
|
||||
id: v.id,
|
||||
label: v.label,
|
||||
category: v.category,
|
||||
scientificName: v.speciesId == null ? null : sciNames[v.speciesId],
|
||||
photo: photos[v.id],
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Loads the first photo BLOB for each of [varietyIds] (one query).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue