import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:real_estate_mobile/core/utils/image_url_resolver.dart'; /// Displays an image from an S3 key or full URL. /// Resolves S3 keys to presigned download URLs via the backend API. class S3Image extends StatefulWidget { final String? imageUrl; final double? width; final double? height; final BoxFit fit; final Alignment alignment; final Widget Function(BuildContext context)? placeholder; final Widget Function(BuildContext context)? errorWidget; const S3Image({ super.key, required this.imageUrl, this.width, this.height, this.fit = BoxFit.cover, this.alignment = Alignment.center, this.placeholder, this.errorWidget, }); @override State createState() => _S3ImageState(); } class _S3ImageState extends State { String? _resolvedUrl; bool _loading = true; bool _failed = false; bool _retried = false; @override void initState() { super.initState(); _resolve(); } @override void didUpdateWidget(S3Image oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.imageUrl != widget.imageUrl) { _resolve(); } } Future _resolve() async { if (widget.imageUrl == null || widget.imageUrl!.isEmpty) { if (mounted) setState(() { _loading = false; _failed = true; }); return; } // If already a full URL, skip resolution if (widget.imageUrl!.startsWith('http://') || widget.imageUrl!.startsWith('https://')) { if (mounted) setState(() { _resolvedUrl = widget.imageUrl; _loading = false; }); return; } try { final url = await ImageUrlResolver.instance.resolve(widget.imageUrl); if (mounted) { setState(() { _resolvedUrl = url; _loading = false; _failed = url == null; }); } } catch (_) { if (mounted) setState(() { _loading = false; _failed = true; }); } } Widget _buildShimmerPlaceholder() { return Shimmer.fromColors( baseColor: const Color(0xFFE0E0E0), highlightColor: const Color(0xFFF5F5F5), child: Container( width: widget.width, height: widget.height, color: Colors.white, ), ); } Widget _buildPlaceholder(BuildContext context) { return widget.placeholder?.call(context) ?? _buildShimmerPlaceholder(); } Widget _buildError(BuildContext context) { return widget.errorWidget?.call(context) ?? Container( width: widget.width, height: widget.height, color: const Color(0xFFC4D9D4), child: const Icon(Icons.person, size: 60, color: Color(0xFF00293D)), ); } @override Widget build(BuildContext context) { if (_loading) return _buildPlaceholder(context); if (_failed || _resolvedUrl == null) return _buildError(context); return CachedNetworkImage( imageUrl: _resolvedUrl!, cacheKey: widget.imageUrl ?? _resolvedUrl!, width: widget.width, height: widget.height, fit: widget.fit, alignment: widget.alignment, placeholder: (ctx, _) => _buildPlaceholder(ctx), errorWidget: (ctx, _, err) { // On error (expired URL), evict and retry once if (!_retried) { _retried = true; ImageUrlResolver.instance.evict(widget.imageUrl); _resolve(); return _buildPlaceholder(ctx); } return _buildError(ctx); }, ); } }