feat(market): host and publish the cover photo with each offer

Wire the Blossom MediaTransport into publishing: shareable lots carry
their varietyId, publishLots uploads the lot's cover photo and puts the
returned URL on the offer (best-effort — a hosting failure still publishes
the offer, just without a photo). Add a configurable media server (Comunes
default, empty to opt out) to settings and the market advanced config.
This commit is contained in:
vjrj 2026-07-10 21:12:36 +02:00
parent 5bc1715637
commit fa53295439
17 changed files with 280 additions and 22 deletions

View file

@ -174,6 +174,7 @@ class SeedLabelEntry extends Equatable {
class ShareableLot extends Equatable {
const ShareableLot({
required this.lotId,
required this.varietyId,
required this.summary,
required this.offerStatus,
this.category,
@ -183,6 +184,10 @@ class ShareableLot extends Equatable {
final String lotId;
/// The lot's variety, so the publish step can fetch its cover photo to host
/// as the offer image. Not published itself never leaves the device.
final String varietyId;
/// What to publish: the variety label (never the full inventory).
final String summary;
@ -197,7 +202,7 @@ class ShareableLot extends Equatable {
@override
List<Object?> get props =>
[lotId, summary, offerStatus, category, harvestYear, isOrganic];
[lotId, varietyId, summary, offerStatus, category, harvestYear, isOrganic];
}
/// One germination test on a lot; [rate] is derived (0..1), null when it can't
@ -727,6 +732,12 @@ class VarietyRepository {
return byVariety;
}
/// 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; reuses the
/// same "first photo" rule as the inventory avatar.
Future<Uint8List?> coverPhotoFor(String varietyId) async =>
(await _firstPhotosFor([varietyId]))[varietyId];
/// Maps each of [speciesIds] to its scientific name (one query).
Future<Map<String, String>> _scientificNamesFor(
Set<String> speciesIds,
@ -1461,6 +1472,7 @@ class VarietyRepository {
if (byId[l.varietyId] case final v?)
ShareableLot(
lotId: l.id,
varietyId: v.id,
summary: v.label,
offerStatus: l.offerStatus,
category: v.category,