103 lines
2.8 KiB
Dart
103 lines
2.8 KiB
Dart
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.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 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.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;
|
|
}
|
|
|
|
final url = await ImageUrlResolver.instance.resolve(widget.imageUrl);
|
|
if (mounted) {
|
|
setState(() {
|
|
_resolvedUrl = url;
|
|
_loading = false;
|
|
_failed = url == null;
|
|
});
|
|
}
|
|
}
|
|
|
|
Widget _buildPlaceholder(BuildContext context) {
|
|
return widget.placeholder?.call(context) ?? Container(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
color: const Color(0xFFC4D9D4),
|
|
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
|
);
|
|
}
|
|
|
|
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!,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
fit: widget.fit,
|
|
placeholder: (ctx, _) => _buildPlaceholder(ctx),
|
|
errorWidget: (ctx, _, __) => _buildError(ctx),
|
|
);
|
|
}
|
|
}
|