Skip to content
29 changes: 20 additions & 9 deletions lib/core/images/booru_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class BooruImage extends ConsumerWidget {
this.imageHeight,
this.forceCover = false,
this.forceFill = false,
this.fitWidthForTallImages = false,
this.forceLoadPlaceholder = false,
this.placeholderWidget,
this.controller,
Expand All @@ -46,6 +47,7 @@ class BooruImage extends ConsumerWidget {
final double? imageHeight;
final bool forceCover;
final bool forceFill;
final bool fitWidthForTallImages;
final bool forceLoadPlaceholder;
final Widget? placeholderWidget;
final ExtendedImageController? controller;
Expand Down Expand Up @@ -78,6 +80,7 @@ class BooruImage extends ConsumerWidget {
imageHeight: imageHeight,
forceCover: forceCover,
forceFill: forceFill,
fitWidthForTallImages: fitWidthForTallImages,
isLargeImage: imageQualitySettings != ImageQuality.low,
forceLoadPlaceholder: forceLoadPlaceholder,
headers: ref.watch(httpHeadersProvider(config)),
Expand All @@ -102,6 +105,7 @@ class BooruRawImage extends StatelessWidget {
this.imageHeight,
this.forceCover = false,
this.forceFill = false,
this.fitWidthForTallImages = false,
this.headers = const {},
this.isLargeImage = false,
this.forceLoadPlaceholder = false,
Expand All @@ -122,6 +126,7 @@ class BooruRawImage extends StatelessWidget {
final double? imageHeight;
final bool forceCover;
final bool forceFill;
final bool fitWidthForTallImages;
final Map<String, String> headers;
final bool isLargeImage;
final bool forceLoadPlaceholder;
Expand All @@ -138,20 +143,26 @@ class BooruRawImage extends StatelessWidget {
);

return NullableAspectRatio(
aspectRatio: forceCover || fit == BoxFit.contain ? null : aspectRatio,
aspectRatio: forceCover || fit == BoxFit.contain || fitWidthForTallImages
? null
: aspectRatio,
child: LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth.roundToDouble();
final height = constraints.maxHeight.roundToDouble();

final fit =
this.fit ??
// If the image is larger than the layout, just fill it to prevent distortion
(forceFill &&
_shouldForceFill(
constraints.biggest,
imageWidth,
imageHeight,
)
// Always use fit width when flag is enabled
(fitWidthForTallImages
? BoxFit.fitWidth
// If the image is larger than the layout, just fill it to prevent distortion
: forceFill &&
_shouldForceFill(
constraints.biggest,
imageWidth,
imageHeight,
)
? BoxFit.fill
// Cover is for the standard grid that crops the image to fit the aspect ratio
: forceCover
Expand All @@ -166,7 +177,7 @@ class BooruRawImage extends StatelessWidget {
headers: headers,
borderRadius: borderRadius,
width: width,
height: height,
height: fitWidthForTallImages ? null : height,
fit: fit,
gaplessPlayback: gaplessPlayback,
fetchStrategy: _fetchStrategy,
Expand Down
137 changes: 137 additions & 0 deletions lib/core/posts/details/src/utils/tall_media_classifier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Flutter imports:
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

// Project imports:
import '../../../../settings/settings.dart';

class TallMediaDisposition {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the class name, it sounds strange. Maybe MediaViewportConfiguration

const TallMediaDisposition._({
required this.isTall,
required this.shouldFitToWidth,
required this.aspectRatio,
required this.viewportRatio,
required this.pixelCount,
required this.scrollExtent,
});

const TallMediaDisposition.standard({
required double aspectRatio,
required double viewportRatio,
required double pixelCount,
}) : this._(
isTall: false,
shouldFitToWidth: false,
aspectRatio: aspectRatio,
viewportRatio: viewportRatio,
pixelCount: pixelCount,
scrollExtent: 0,
);

factory TallMediaDisposition.tall({
required double aspectRatio,
required double viewportRatio,
required double pixelCount,
required double scrollExtent,
}) => TallMediaDisposition._(
isTall: true,
shouldFitToWidth: true,
aspectRatio: aspectRatio,
viewportRatio: viewportRatio,
pixelCount: pixelCount,
scrollExtent: scrollExtent,
);

final bool isTall;
final bool shouldFitToWidth;
final double aspectRatio;
final double viewportRatio;
final double pixelCount;
final double scrollExtent;

bool get hasScrollableExtent => scrollExtent > 0;
}

class TallMediaClassifier {
const TallMediaClassifier({
required this.settings,
required this.viewportSize,
});

final TallMediaSettings settings;
final Size viewportSize;

TallMediaDisposition classify({
required double width,
required double height,
required bool isVideo,
}) {
if (width <= 0 || height <= 0 || isVideo) {
final aspectRatio = width <= 0 || height <= 0 ? 0.0 : height / width;
final viewportRatio = viewportSize.height == 0
? 0.0
: height / viewportSize.height;
return TallMediaDisposition.standard(
aspectRatio: aspectRatio,
viewportRatio: viewportRatio,
pixelCount: width * height,
);
}

final aspectRatio = height / width;
final viewportRatio = viewportSize.height == 0
? 0.0
: height / viewportSize.height;
final pixelCount = width * height;
final projectedHeight = _projectedHeight(width, height);
final rawScrollExtent = projectedHeight - viewportSize.height;
final hasScroll = rawScrollExtent > settings.minScrollExtentPx;

final meetsAspect = aspectRatio >= settings.aspectRatioThreshold;
final meetsHeight = height >= settings.minHeightPx;
final meetsViewport = viewportRatio >= settings.minViewportHeightRatio;
final meetsPixels = pixelCount >= settings.minPixelCount;

final isTall =
hasScroll &&
((meetsAspect && meetsViewport) ||
(meetsAspect && meetsHeight && meetsPixels) ||
(meetsHeight && meetsViewport && meetsPixels));

if (kDebugMode) {
debugPrint(
'TallMediaClassifier: aspect=$aspectRatio viewportRatio=$viewportRatio '
'pixels=$pixelCount projectedHeight=$projectedHeight scroll=$rawScrollExtent '
'-> isTall=$isTall',
);
}

if (!isTall) {
return TallMediaDisposition.standard(
aspectRatio: aspectRatio,
viewportRatio: viewportRatio,
pixelCount: pixelCount,
);
}

final scrollExtent = rawScrollExtent > 0 ? rawScrollExtent : 0.0;

return TallMediaDisposition.tall(
aspectRatio: aspectRatio,
viewportRatio: viewportRatio,
pixelCount: pixelCount,
scrollExtent: scrollExtent > 0 ? scrollExtent : 0,
);
}

double _projectedHeight(double width, double height) {
final viewportWidth = viewportSize.width;
if (width <= 0 || height <= 0 || viewportWidth <= 0) {
return height;
}

final scale = viewportWidth / width;
final clampedScale = scale < 1 ? scale : 1.0;
return height * clampedScale;
}
}
13 changes: 9 additions & 4 deletions lib/core/posts/details/src/widgets/post_details_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class PostDetailsImage<T extends Post> extends ConsumerStatefulWidget {
super.key,
this.heroTag,
this.imageCacheManager,
this.fitWidthForTallImages = false,
});

final BooruConfigAuth config;
Expand All @@ -31,12 +32,15 @@ class PostDetailsImage<T extends Post> extends ConsumerStatefulWidget {
final String Function(T post)? thumbnailUrlBuilder;
final ImageCacheManager? imageCacheManager;
final T post;
final bool fitWidthForTallImages;

@override
ConsumerState<PostDetailsImage> createState() => _PostDetailsImageState();
ConsumerState<PostDetailsImage<T>> createState() =>
_PostDetailsImageState<T>();
}

class _PostDetailsImageState extends ConsumerState<PostDetailsImage> {
class _PostDetailsImageState<T extends Post>
extends ConsumerState<PostDetailsImage<T>> {
@override
Widget build(BuildContext context) {
final imageUrl = widget.imageUrlBuilder != null
Expand Down Expand Up @@ -117,10 +121,11 @@ class _PostDetailsImageState extends ConsumerState<PostDetailsImage> {
imageUrl: imageUrl,
placeholderUrl: placeholderImageUrl,
aspectRatio: post.aspectRatio,
forceCover: post.aspectRatio != null,
forceCover: false, // Never force cover when we want fit width
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be conditional based on fitWidthForTallImages

imageHeight: post.height,
imageWidth: post.width,
forceFill: true,
forceFill: false,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above?

fitWidthForTallImages: widget.fitWidthForTallImages,
borderRadius: BorderRadius.zero,
forceLoadPlaceholder: true,
imageCacheManager: widget.imageCacheManager,
Expand Down
Loading