import 'package:commons_core/commons_core.dart'; import 'package:flutter/material.dart'; import '../i18n/strings.g.dart'; import '../services/offer_thumbnail.dart'; import 'theme.dart'; /// Human words for an offer's reciprocity mode. Shared by the market list and /// the offer detail page so both read the same way. String offerTypeLabel(Translations t, OfferType type) => switch (type) { OfferType.gift => t.share.gift, OfferType.exchange => t.share.exchange, OfferType.sale => t.share.sell, OfferType.wanted => t.market.wanted, OfferType.lend => t.share.exchange, }; /// A small square thumbnail of an offer's photo for the market list card. /// The image usually rides inline in the offer as a `data:` URI (decentralized, /// no server), so it renders straight from memory; an `http(s)` URL is still /// supported for interop and degrades gracefully (spinner, then placeholder). class OfferThumbnail extends StatelessWidget { const OfferThumbnail({ required this.url, required this.semanticLabel, this.size = 56, super.key, }); final String url; final String semanticLabel; final double size; @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(10), child: _offerImage( url, semanticLabel: semanticLabel, width: size, height: size, ), ); } } /// Full-width "hero" image atop the offer detail page. Same source handling as /// [OfferThumbnail]; hidden entirely when the offer has no photo. class OfferHeroImage extends StatelessWidget { const OfferHeroImage({ required this.url, required this.semanticLabel, super.key, }); final String url; final String semanticLabel; @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(14), child: AspectRatio( aspectRatio: 4 / 3, child: _offerImage(url, semanticLabel: semanticLabel), ), ); } } /// Renders an offer image from either an inline `data:` URI (via memory) or a /// remote URL (via network), always cropping to fill and falling back to a /// neutral placeholder — a broken image must never block the card. Widget _offerImage( String url, { required String semanticLabel, double? width, double? height, }) { final inline = decodeDataUri(url); if (inline != null) { return Image.memory( inline, width: width, height: height, fit: BoxFit.cover, semanticLabel: semanticLabel, errorBuilder: (context, _, _) => _OfferImagePlaceholder(width: width, height: height), ); } return Image.network( url, width: width, height: height, fit: BoxFit.cover, semanticLabel: semanticLabel, loadingBuilder: (context, child, progress) => progress == null ? child : _OfferImagePlaceholder(width: width, height: height, loading: true), errorBuilder: (context, _, _) => _OfferImagePlaceholder(width: width, height: height), ); } /// Neutral fill shown while a remote offer image loads or when it can't be /// decoded/fetched — a marketplace is useless if a broken image blocks the card. class _OfferImagePlaceholder extends StatelessWidget { const _OfferImagePlaceholder({this.width, this.height, this.loading = false}); final double? width; final double? height; final bool loading; @override Widget build(BuildContext context) { return Container( width: width, height: height, color: seedPrimaryContainer.withValues(alpha: 0.4), alignment: Alignment.center, child: loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.image_not_supported_outlined, color: seedMuted), ); } } /// The small pill showing an offer's reciprocity mode (give/swap/sell/wanted). class OfferTypeChip extends StatelessWidget { const OfferTypeChip({required this.label, super.key}); final String label; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: seedPrimaryContainer, borderRadius: BorderRadius.circular(20), ), child: Text( label, style: const TextStyle( color: seedOnPrimaryContainer, fontSize: 12, fontWeight: FontWeight.w600, ), ), ); } }