improved performance and battery usage on older devices

This commit is contained in:
2026-09-25 23:58:08 +02:00
parent ed8e52f475
commit 56a497985b
70 changed files with 1631 additions and 621 deletions
+32 -26
View File
@@ -10,9 +10,33 @@ class PmImageView extends StatelessWidget {
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 = _imageProvider();
final provider = sourceProvider(node);
final inline = inlineProvider(context, node);
return LayoutBuilder(
builder: (context, constraints) {
final available = constraints.maxWidth.isFinite
@@ -22,7 +46,13 @@ class PmImageView extends StatelessWidget {
Widget image = ConstrainedBox(
constraints: BoxConstraints(maxWidth: targetWidth ?? available),
child: _imageWidget(context, provider),
child: inline == null
? _brokenImage(context)
: Image(
image: inline,
errorBuilder: (context, error, stack) =>
_brokenImage(context),
),
);
final href = node.href;
@@ -43,30 +73,6 @@ class PmImageView extends StatelessWidget {
);
}
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(
+14 -20
View File
@@ -1,9 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import '../app_progress_indicator.dart';
import 'pm_document_view.dart';
import 'pm_image_view.dart';
import 'pm_node.dart';
/// Renders raw ProseMirror JSON and memoises the parsed tree across rebuilds.
@@ -31,7 +31,7 @@ class PmJsonView extends StatefulWidget {
class _PmJsonViewState extends State<PmJsonView> {
PmNode? _shown;
PmNode? _pending;
List<ImageProvider> _pendingProviders = const [];
List<PmImage> _pendingImages = const [];
bool _precacheStarted = false;
int _generation = 0;
@@ -62,17 +62,17 @@ class _PmJsonViewState extends State<PmJsonView> {
void _parse() {
final doc = PmNode.fromJson(widget.json);
final providers = <ImageProvider>[];
_collectProviders(doc, providers);
final images = <PmImage>[];
_collectImages(doc, images);
_generation++;
_precacheStarted = false;
if (providers.isEmpty) {
if (images.isEmpty) {
_shown = doc;
_pending = null;
_pendingProviders = const [];
_pendingImages = const [];
} else {
_pending = doc;
_pendingProviders = providers;
_pendingImages = images;
}
}
@@ -82,8 +82,9 @@ class _PmJsonViewState extends State<PmJsonView> {
_precacheStarted = true;
final generation = _generation;
Future.wait([
for (final provider in _pendingProviders)
precacheImage(provider, context, onError: (_, _) {}),
for (final image in _pendingImages)
if (PmImageView.inlineProvider(context, image) case final provider?)
precacheImage(provider, context, onError: (_, _) {}),
])
.timeout(PmJsonView.precacheTimeout, onTimeout: () => const [])
.whenComplete(() {
@@ -91,23 +92,16 @@ class _PmJsonViewState extends State<PmJsonView> {
setState(() {
_shown = pending;
_pending = null;
_pendingProviders = const [];
_pendingImages = const [];
});
}
});
}
void _collectProviders(PmNode node, List<ImageProvider> out) {
if (node is PmImage) {
final bytes = node.bytes;
if (bytes != null) {
out.add(MemoryImage(bytes));
} else if (node.src.startsWith('http')) {
out.add(CachedNetworkImageProvider(node.src));
}
}
void _collectImages(PmNode node, List<PmImage> out) {
if (node is PmImage) out.add(node);
for (final child in node.children) {
_collectProviders(child, out);
_collectImages(child, out);
}
}