Files
Client/lib/widget/prosemirror/pm_image_view.dart
T

136 lines
3.8 KiB
Dart

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<void>(
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);
}
}