Files
mobile-app/lib/core/widgets/s3_image.dart

119 lines
3.2 KiB
Dart

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<S3Image> createState() => _S3ImageState();
}
class _S3ImageState extends State<S3Image> {
String? _resolvedUrl;
bool _loading = true;
bool _failed = false;
@override
void initState() {
super.initState();
_resolve();
}
@override
void didUpdateWidget(S3Image oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.imageUrl != widget.imageUrl) {
_resolve();
}
}
Future<void> _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,
width: widget.width,
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
placeholder: (ctx, _) => _buildPlaceholder(ctx),
errorWidget: (ctx, _, _) => _buildError(ctx),
);
}
}