feat(block2): publish my seeds to the network (completes publish->discover)

The other half of the market: share your marked lots as offers.
- VarietyRepository.shareableLots(): non-private lots WITH their id (stable,
  updatable offer id) + the variety label. Private lots never leave the device.
- OffersCubit.publishLots(): maps each via OfferMapper and publishes, tagged
  with the coarse area + signed by the derived key; counts acceptances; no-op
  offline or with no area.
- Market screen: a 'share my seeds' action (online only) — publishes, then
  re-discovers to show them; 'mark some seeds first' when nothing's shared.
- i18n en/es/pt.

14 tests green (repo query, cubit publish, market UI); analyzer clean for new code.
This commit is contained in:
vjrj 2026-07-10 10:18:30 +02:00
parent cb5f55e146
commit f8f73c4153
12 changed files with 284 additions and 5 deletions

View file

@ -117,6 +117,33 @@ class SharedCatalogEntry extends Equatable {
];
}
/// A lot the user has marked for sharing, carrying the lot [lotId] (so the
/// published offer has a stable, updatable id) plus the summary a network offer
/// needs. Distinct from [SharedCatalogEntry], which is for the printable local
/// catalog and has no id.
class ShareableLot extends Equatable {
const ShareableLot({
required this.lotId,
required this.summary,
required this.offerStatus,
this.category,
this.harvestYear,
});
final String lotId;
/// What to publish: the variety label (never the full inventory).
final String summary;
/// Never [OfferStatus.private] private lots don't leave the device.
final OfferStatus offerStatus;
final String? category;
final int? harvestYear;
@override
List<Object?> get props => [lotId, summary, offerStatus, category, harvestYear];
}
/// One germination test on a lot; [rate] is derived (0..1), null when it can't
/// be computed.
class GerminationEntry extends Equatable {
@ -1210,6 +1237,42 @@ class VarietyRepository {
return entries;
}
/// Lots the user marked to share (not private), each with its id and the
/// variety label the input for publishing offers to the network (Block 2).
Future<List<ShareableLot>> shareableLots() async {
final lots =
await (_db.select(_db.lots)..where(
(l) =>
l.isDeleted.equals(false) &
l.offerStatus.equalsValue(OfferStatus.private).not(),
))
.get();
if (lots.isEmpty) return const [];
final varietyIds = lots.map((l) => l.varietyId).toSet();
final varieties =
await (_db.select(_db.varieties)..where(
(v) =>
v.id.isIn(varietyIds) &
v.isDeleted.equals(false) &
v.isDraft.equals(false),
))
.get();
final byId = {for (final v in varieties) v.id: v};
return [
for (final l in lots)
if (byId[l.varietyId] case final v?)
ShareableLot(
lotId: l.id,
summary: v.label,
offerStatus: l.offerStatus,
category: v.category,
harvestYear: l.harvestYear,
),
];
}
/// Soft-deletes a lot (tombstone); it leaves the variety's lot list.
Future<void> softDeleteLot(String lotId) async {
final (_, updated) = await _stamp();