Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:math';

import 'package:bluebubbles/app/wrappers/stateful_boilerplate.dart';
import 'package:bluebubbles/helpers/helpers.dart';
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/attachment/image_viewer_decode_dimensions.dart';
import 'package:bluebubbles/database/models.dart';
import 'package:bluebubbles/services/services.dart';
import 'package:flutter/cupertino.dart';
Expand Down Expand Up @@ -55,11 +55,12 @@ class _ImageViewerState extends OptimizedState<ImageViewer> with AutomaticKeepAl
final completer = Completer<Uint8List>();
controller!.queueImage(Tuple4(attachment, file, context, completer));
final newData = await completer.future;
if (newData.isEmpty) return;
if (!mounted || newData.isEmpty) return;
setState(() {
data = newData;
});
} else {
if (!mounted) return;
setState(() {
data = tmpData;
});
Expand All @@ -78,13 +79,20 @@ class _ImageViewerState extends OptimizedState<ImageViewer> with AutomaticKeepAl
height: min((attachment.height?.toDouble() ?? ns.width(context) * 0.5 / attachment.aspectRatio), ns.width(context) * 0.5 / attachment.aspectRatio),
);
}
final decodeDimensions = calculateImageViewerDecodeDimensions(
maximumDisplayWidth: ns.width(context) * 0.5,
pixelRatio: Get.pixelRatio,
sourceWidth: attachment.width,
sourceHeight: attachment.height,
aspectRatio: attachment.aspectRatio,
);
return Image.memory(
data!,
// prevents the image widget from "refreshing" when the provider changes
gaplessPlayback: true,
filterQuality: FilterQuality.none,
cacheWidth: (min((attachment.width ?? 0), ns.width(context) * 0.5) * Get.pixelRatio / 2).round().abs().nonZero,
cacheHeight: (min((attachment.height ?? 0), ns.width(context) * 0.5 / attachment.aspectRatio) * Get.pixelRatio / 2).round().abs().nonZero,
cacheWidth: decodeDimensions.width,
cacheHeight: decodeDimensions.height,
fit: BoxFit.cover,
frameBuilder: (context, w, frame, wasSyncLoaded) {
return AnimatedCrossFade(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'dart:math';

class ImageViewerDecodeDimensions {
const ImageViewerDecodeDimensions(
{required this.width, required this.height});

final int width;
final int height;
}

ImageViewerDecodeDimensions calculateImageViewerDecodeDimensions({
required double maximumDisplayWidth,
required double pixelRatio,
required int? sourceWidth,
required int? sourceHeight,
required double aspectRatio,
}) {
final safeMaximumDisplayWidth =
maximumDisplayWidth.isFinite && maximumDisplayWidth > 0
? maximumDisplayWidth
: 1.0;
final safePixelRatio =
pixelRatio.isFinite && pixelRatio > 0 ? pixelRatio : 1.0;
final safeAspectRatio =
aspectRatio.isFinite && aspectRatio > 0 ? aspectRatio : 1.0;
final displayWidth = min(
sourceWidth != null && sourceWidth > 0
? sourceWidth.toDouble()
: safeMaximumDisplayWidth,
safeMaximumDisplayWidth,
);
final fallbackHeight = safeMaximumDisplayWidth / safeAspectRatio;
final displayHeight = min(
sourceHeight != null && sourceHeight > 0
? sourceHeight.toDouble()
: fallbackHeight,
fallbackHeight,
);

int cacheDimension(double displayDimension) =>
(displayDimension * safePixelRatio / 2).round().clamp(1, 1024).toInt();

return ImageViewerDecodeDimensions(
width: cacheDimension(displayWidth),
height: cacheDimension(displayHeight),
);
}
9 changes: 8 additions & 1 deletion lib/app/layouts/fullscreen_media/fullscreen_holder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import "package:flutter/material.dart";
import 'package:flutter/services.dart';
import 'package:gesture_x_detector/gesture_x_detector.dart';
import 'package:get/get.dart';
import 'package:photo_view/photo_view.dart' show PhotoViewGestureDetectorScope;

class FullscreenMediaHolder extends StatefulWidget {
FullscreenMediaHolder({
Expand Down Expand Up @@ -151,7 +152,12 @@ class FullscreenMediaHolderState extends OptimizedState<FullscreenMediaHolder> {
}
return KeyEventResult.ignored;
},
child: PageView.builder(
child: PhotoViewGestureDetectorScope(
// Lets a contained image yield horizontal drags to the
// PageView. A zoomed image keeps its own pan gesture, and
// FullscreenImage disables this PageView while zoomed.
axis: Axis.horizontal,
child: PageView.builder(
physics: physics ??
(attachments.length == 1
? const NeverScrollableScrollPhysics()
Expand Down Expand Up @@ -302,6 +308,7 @@ class FullscreenMediaHolderState extends OptimizedState<FullscreenMediaHolder> {
);
}
},
),
),
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/attachment/image_viewer_decode_dimensions.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
test('bounds decoded dimensions to the displayed image size', () {
final dimensions = calculateImageViewerDecodeDimensions(
maximumDisplayWidth: 200,
pixelRatio: 3,
sourceWidth: 4000,
sourceHeight: 3000,
aspectRatio: 4 / 3,
);

expect(dimensions.width, 300);
expect(dimensions.height, 225);
});

test('uses safe positive fallbacks for missing or invalid metadata', () {
final dimensions = calculateImageViewerDecodeDimensions(
maximumDisplayWidth: double.nan,
pixelRatio: double.infinity,
sourceWidth: 0,
sourceHeight: null,
aspectRatio: 0,
);

expect(dimensions.width, 1);
expect(dimensions.height, 1);
});

test('caps large device-scaled decode dimensions', () {
final dimensions = calculateImageViewerDecodeDimensions(
maximumDisplayWidth: 4000,
pixelRatio: 4,
sourceWidth: 4000,
sourceHeight: 4000,
aspectRatio: 1,
);

expect(dimensions.width, 1024);
expect(dimensions.height, 1024);
});
}