142 lines
4.3 KiB
Dart
142 lines
4.3 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});
|
|
|
|
/// Full-resolution source, used for the zoomable fullscreen view.
|
|
static ImageProvider? sourceProvider(PmImage node) {
|
|
final bytes = node.bytes;
|
|
if (bytes != null) return MemoryImage(bytes);
|
|
if (node.src.startsWith('http')) {
|
|
return CachedNetworkImageProvider(node.src);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Inline images never render wider than the screen, so they are decoded at
|
|
/// most at its physical width instead of the (often camera-sized) original.
|
|
/// [PmJsonView] precaches through this too, so both hit the same cache key.
|
|
static ImageProvider? inlineProvider(BuildContext context, PmImage node) {
|
|
final source = sourceProvider(node);
|
|
if (source == null) return null;
|
|
final width =
|
|
(MediaQuery.sizeOf(context).width *
|
|
MediaQuery.devicePixelRatioOf(context))
|
|
.round();
|
|
return ResizeImage(source, width: width);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final provider = sourceProvider(node);
|
|
final inline = inlineProvider(context, node);
|
|
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: inline == null
|
|
? _brokenImage(context)
|
|
: Image(
|
|
image: inline,
|
|
errorBuilder: (context, error, stack) =>
|
|
_brokenImage(context),
|
|
),
|
|
);
|
|
|
|
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);
|
|
},
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|