import 'package:flutter/material.dart'; import '../../../services/motorcycle_image_service.dart'; /// Shared motorcycle image widget used by Garage and motorcycle detail views. /// It always resolves through the authoritative MotorcycleImageService. class MotorcyclePhoto extends StatelessWidget { final int year; final String make; final String model; final double height; final BorderRadius borderRadius; final bool showAttribution; final BoxFit fit; const MotorcyclePhoto({ super.key, required this.year, required this.make, required this.model, this.height = 190, this.borderRadius = const BorderRadius.all(Radius.circular(14)), this.showAttribution = true, this.fit = BoxFit.cover, }); @override Widget build(BuildContext context) { final initial = MotorcycleImageService.instance.getCachedOrCuratedImage( year: year, make: make, model: model, ); return FutureBuilder( initialData: initial, future: MotorcycleImageService.instance.resolve( year: year, make: make, model: model, ), builder: (context, snapshot) { final image = snapshot.data; if (image == null && snapshot.connectionState == ConnectionState.waiting) { return ClipRRect( borderRadius: borderRadius, child: Container( height: height, width: double.infinity, color: const Color(0xFF111111), child: const Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: Color(0xFF00E5FF), ), ), ), ), ); } return ClipRRect( borderRadius: borderRadius, child: Container( height: height, width: double.infinity, color: const Color(0xFF111111), child: image == null ? const _FallbackMotorcycleImage() : Stack( fit: StackFit.expand, children: [ _buildImage(image), if (showAttribution && image.isPhoto) Positioned( left: 10, right: 10, bottom: 8, child: Align( alignment: Alignment.bottomLeft, child: DecoratedBox( decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.68), borderRadius: BorderRadius.circular(5), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), child: Text( _attribution(image), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white70, fontSize: 9, fontWeight: FontWeight.w600, ), ), ), ), ), ), ], ), ), ); }, ); } Widget _buildImage(MotorcycleImage image) { if (image.assetPath != null) { return Image.asset( image.assetPath!, fit: fit, errorBuilder: (_, _, _) => const _FallbackMotorcycleImage(), ); } if (image.imageUrl != null) { return Image.network( image.imageUrl!, headers: const { 'User-Agent': 'MotorSignal/1.0 (motorcycle garage)', }, fit: fit, filterQuality: FilterQuality.medium, loadingBuilder: (context, child, progress) { if (progress == null) return child; return const Center( child: CircularProgressIndicator( strokeWidth: 2, color: Colors.cyanAccent, ), ); }, errorBuilder: (_, _, _) => const _FallbackMotorcycleImage(), ); } return const _FallbackMotorcycleImage(); } static String _cleanText(String? text) { if (text == null || text.isEmpty) return ''; return text .replaceAll(RegExp(r'<[^>]*>'), '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll(''', "'") .replaceAll(''', "'") .replaceAll(' ', ' ') .replaceAll(RegExp(r'\s+'), ' ') .trim(); } String _attribution(MotorcycleImage image) { final credit = _cleanText(image.credit); final license = _cleanText(image.license); if (credit.isNotEmpty && license.isNotEmpty) { return 'Photo: $credit • $license'; } if (credit.isNotEmpty) return 'Photo: $credit'; final attribution = _cleanText(image.attribution); if (attribution.isNotEmpty) return 'Photo: $attribution'; return 'Photo: Wikimedia Commons'; } } class _FallbackMotorcycleImage extends StatelessWidget { const _FallbackMotorcycleImage(); @override Widget build(BuildContext context) { return const Center( child: Icon( Icons.two_wheeler, size: 64, color: Color(0xFF00E5FF), ), ); } }