feat(block1): photo thumbnails in the inventory list
The list showed only initials even when a variety had a photo (quick-add stores one as an encrypted BLOB). Now the avatar shows the first photo, falling back to the initial — matching the "foto/inicial" list spec. - VarietyListItem carries the first photo bytes; watchInventory loads them in a single query per emission. - List tile renders a MemoryImage avatar when present. Tests: repository (photo bytes surfaced, null when none) and a widget test (avatar replaces the initial). Full suite: 37 passing, 0 skipped.
This commit is contained in:
parent
37427fa738
commit
8a19a6c85a
4 changed files with 87 additions and 10 deletions
|
|
@ -8,14 +8,22 @@ import '../db/enums.dart';
|
|||
|
||||
/// A lightweight row for the inventory list (only what the list renders).
|
||||
class VarietyListItem extends Equatable {
|
||||
const VarietyListItem({required this.id, required this.label, this.category});
|
||||
const VarietyListItem({
|
||||
required this.id,
|
||||
required this.label,
|
||||
this.category,
|
||||
this.photo,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String label;
|
||||
final String? category;
|
||||
|
||||
/// First photo (encrypted BLOB) for the avatar, or null → show an initial.
|
||||
final Uint8List? photo;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, label, category];
|
||||
List<Object?> get props => [id, label, category, photo];
|
||||
}
|
||||
|
||||
/// One germination test on a lot; [rate] is derived (0..1), null when it can't
|
||||
|
|
@ -135,7 +143,8 @@ class VarietyRepository {
|
|||
final int Function() _now;
|
||||
Hlc _clock;
|
||||
|
||||
/// Emits the non-deleted inventory, ordered by category then label.
|
||||
/// Emits the non-deleted inventory, ordered by category then label, each with
|
||||
/// its first photo for the avatar.
|
||||
Stream<List<VarietyListItem>> watchInventory() {
|
||||
final query = _db.select(_db.varieties)
|
||||
..where((v) => v.isDeleted.equals(false))
|
||||
|
|
@ -143,14 +152,41 @@ class VarietyRepository {
|
|||
(v) => OrderingTerm(expression: v.category),
|
||||
(v) => OrderingTerm(expression: v.label),
|
||||
]);
|
||||
return query.watch().map(
|
||||
(rows) => rows
|
||||
return query.watch().asyncMap((rows) async {
|
||||
final photos = await _firstPhotosFor(rows.map((v) => v.id).toList());
|
||||
return rows
|
||||
.map(
|
||||
(v) =>
|
||||
VarietyListItem(id: v.id, label: v.label, category: v.category),
|
||||
(v) => VarietyListItem(
|
||||
id: v.id,
|
||||
label: v.label,
|
||||
category: v.category,
|
||||
photo: photos[v.id],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Loads the first photo BLOB for each of [varietyIds] (one query).
|
||||
Future<Map<String, Uint8List>> _firstPhotosFor(
|
||||
List<String> varietyIds,
|
||||
) async {
|
||||
if (varietyIds.isEmpty) return const {};
|
||||
final rows =
|
||||
await (_db.select(_db.attachments)..where(
|
||||
(a) =>
|
||||
a.parentId.isIn(varietyIds) &
|
||||
a.parentType.equalsValue(ParentType.variety) &
|
||||
a.kind.equalsValue(AttachmentKind.photo) &
|
||||
a.isDeleted.equals(false),
|
||||
))
|
||||
.get();
|
||||
final byVariety = <String, Uint8List>{};
|
||||
for (final row in rows) {
|
||||
final bytes = row.bytes;
|
||||
if (bytes != null) byVariety.putIfAbsent(row.parentId, () => bytes);
|
||||
}
|
||||
return byVariety;
|
||||
}
|
||||
|
||||
/// The 20-second quick-add: a [label] (required) plus an optional [category],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue