import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
/// Renders an already-approved public media URL.
///
/// This widget deliberately does not implement a persistent application cache.
/// Flutter's in-memory ImageCache and the platform/browser HTTP cache handle
/// normal public reads; private, quarantined and upload-candidate media must
/// not be passed here.
class PublicMediaImage extends StatelessWidget {
const PublicMediaImage({
super.key,
required this.url,
this.alt = '',
this.width,
this.height,
this.fit = BoxFit.cover,
this.emptyIcon = Icons.photo_outlined,
this.errorIcon = Icons.broken_image_outlined,
this.iconSize = 48,
this.iconColor,
this.backgroundColor,
this.borderColor,
this.fallbackHeight,
this.includeSemantics = true,
this.isSvg = false,
this.fallbackBuilder,
this.cacheWidth,
this.cacheHeight,
});
final String url;
final String alt;
final double? width;
final double? height;
final BoxFit fit;
final IconData emptyIcon;
final IconData errorIcon;
final double iconSize;
final Color? iconColor;
final Color? backgroundColor;
final Color? borderColor;
final double? fallbackHeight;
final bool includeSemantics;
final bool isSvg;
final WidgetBuilder? fallbackBuilder;
final int? cacheWidth;
final int? cacheHeight;
@override
Widget build(BuildContext context) {
final normalizedUrl = url.trim();
final child = normalizedUrl.isEmpty
? _fallback(context, emptyIcon)
: isSvg
? SvgPicture.network(
normalizedUrl,
width: width,
height: height,
fit: fit,
semanticsLabel: includeSemantics && alt.trim().isNotEmpty
? alt.trim()
: null,
placeholderBuilder: (_) => _fallback(context, emptyIcon),
errorBuilder: (context, error, stackTrace) =>
_fallback(context, errorIcon),
)
: Image.network(
normalizedUrl,
width: width,
height: height,
fit: fit,
cacheWidth: cacheWidth,
cacheHeight: cacheHeight,
// Flutter Web's CanvasKit byte decoder can report a failed image
// even when the same CORS-enabled JPEG decodes correctly in the
// browser. Prefer the browser image element on web; native
// platforms continue to use their normal network decoder.
webHtmlElementStrategy: WebHtmlElementStrategy.prefer,
gaplessPlayback: true,
semanticLabel: includeSemantics && alt.trim().isNotEmpty
? alt.trim()
: null,
errorBuilder: (context, error, stackTrace) =>
_fallback(context, errorIcon),
);
if (!includeSemantics || alt.trim().isEmpty || normalizedUrl.isEmpty) {
return child;
}
return Semantics(image: true, label: alt.trim(), child: child);
}
Widget _fallback(BuildContext context, IconData icon) {
if (fallbackBuilder != null) return fallbackBuilder!(context);
final scheme = Theme.of(context).colorScheme;
final fallbackWidth = width;
final fallbackBoxHeight = fallbackHeight ?? height;
return SizedBox(
width: fallbackWidth,
height: fallbackBoxHeight,
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor ?? scheme.surfaceContainerHighest,
border: Border.all(color: borderColor ?? scheme.outlineVariant),
),
child: Center(
child: Icon(icon, size: iconSize, color: iconColor ?? scheme.primary),
),
),
);
}
}
