import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; import 'pm_node.dart'; import 'pm_render_scope.dart'; class PmImageView extends StatelessWidget { final PmImage node; const PmImageView({required this.node, super.key}); @override Widget build(BuildContext context) { final provider = _imageProvider(); return LayoutBuilder( builder: (context, constraints) { final available = constraints.maxWidth.isFinite ? constraints.maxWidth : MediaQuery.sizeOf(context).width; final targetWidth = _resolveWidth(node.width, available); Widget image = ConstrainedBox( constraints: BoxConstraints(maxWidth: targetWidth ?? available), child: _imageWidget(context, provider), ); final href = node.href; if (href != null && href.isNotEmpty) { image = InkWell( onTap: () => PmRenderScope.maybeOf(context)?.onLinkTap?.call(href), child: image, ); } else if (provider != null) { image = InkWell( onTap: () => _openFullscreen(context, provider), child: image, ); } return Align(alignment: _alignment(node.align), child: image); }, ); } ImageProvider? _imageProvider() { if (node.bytes != null) return MemoryImage(node.bytes!); if (node.src.startsWith('http')) { return CachedNetworkImageProvider(node.src); } return null; } Widget _imageWidget(BuildContext context, ImageProvider? provider) { if (node.bytes != null) { return Image.memory( node.bytes!, errorBuilder: (context, error, stack) => _brokenImage(context), ); } if (node.src.startsWith('http')) { return CachedNetworkImage( imageUrl: node.src, errorWidget: (context, url, error) => _brokenImage(context), ); } return _brokenImage(context); } Widget _brokenImage(BuildContext context) { final theme = Theme.of(context); return DecoratedBox( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Padding( padding: const EdgeInsets.all(24), child: Icon( Icons.broken_image_outlined, color: theme.colorScheme.onSurfaceVariant, ), ), ); } void _openFullscreen(BuildContext context, ImageProvider provider) { showDialog( context: context, barrierColor: Colors.black, builder: (context) => Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.black, foregroundColor: Colors.white, ), body: PhotoView( minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 3, imageProvider: provider, backgroundDecoration: const BoxDecoration(color: Colors.black), ), ), ); } Alignment _alignment(String? align) { switch (align) { case 'center': return Alignment.center; case 'right': return Alignment.centerRight; default: return Alignment.centerLeft; } } double? _resolveWidth(String? width, double available) { if (width == null) return null; final match = RegExp(r'^(\d+(?:\.\d+)?)(px|%|em|rem)$').firstMatch(width); if (match == null) return null; final value = double.parse(match.group(1)!); final double resolved; switch (match.group(2)) { case '%': resolved = available * value / 100; case 'em': case 'rem': resolved = value * 16; default: resolved = value; } return resolved.clamp(1, available); } }