Repository: momentobooth/momentobooth
Pull request #605: Fix ProgressRing size in some capture previews
Author: login: h3x4d3c1m4l
Created: 2025-04-06T08:52:33Z | Merged: 2025-04-06T19:18:35Z



Diff:
diff --git a/lib/views/components/imaging/capture_view_box.dart b/lib/views/components/imaging/capture_view_box.dart
new file mode 100644
index 00000000..dddae89d
--- /dev/null
+++ b/lib/views/components/imaging/capture_view_box.dart
@@ -0,0 +1,48 @@
+import 'package:fluent_ui/fluent_ui.dart';
+import 'package:momento_booth/main.dart';
+import 'package:momento_booth/managers/settings_manager.dart';
+import 'package:momento_booth/views/components/imaging/image_with_loader_fallback.dart';
+import 'package:momento_booth/views/components/imaging/rotate_flip_crop.dart';
+
+class CaptureViewBox extends StatefulWidget {
+
+  final ImageWithLoaderFallback Function(VoidCallback setImageDecoded) imageBuilder;
+
+  const CaptureViewBox({super.key, required this.imageBuilder});
+
+  @override
+  State<CaptureViewBox> createState() => _CaptureViewBoxState();
+
+}
+
+class _CaptureViewBoxState extends State<CaptureViewBox> {
+
+  bool _isImageDecoded = false;
+
+  void _setImageDecoded() {
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      setState(() => _isImageDecoded = true);
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      fit: StackFit.passthrough,
+      children: [
+        if (!_isImageDecoded)
+          const Center(child: ProgressRing()),
+        Visibility.maintain(
+          visible: _isImageDecoded,
+          child: RotateFlipCrop(
+            rotate: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureRotate,
+            flip: getIt<SettingsManager>().settings.hardware.captureFlip,
+            aspectRatio: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureAspectRatio,
+            child: widget.imageBuilder(_setImageDecoded),
+          ),
+        ),
+      ],
+    );
+  }
+
+}
diff --git a/lib/views/components/imaging/image_with_loader_fallback.dart b/lib/views/components/imaging/image_with_loader_fallback.dart
index f851804c..caa346cb 100644
--- a/lib/views/components/imaging/image_with_loader_fallback.dart
+++ b/lib/views/components/imaging/image_with_loader_fallback.dart
@@ -15,20 +15,20 @@ class ImageWithLoaderFallback extends StatefulWidget {
   final int? cacheHeight;
 
   final BoxFit? fit;
-  final VoidCallback? decodeCallback;
+  final VoidCallback? onImageDecoded;
   final _Type _type;
 
-  const ImageWithLoaderFallback.memory(this.bytes, {super.key, this.fit, this.decodeCallback, this.cacheWidth, this.cacheHeight})
+  const ImageWithLoaderFallback.memory(this.bytes, {super.key, this.fit, this.onImageDecoded, this.cacheWidth, this.cacheHeight})
       : _type = _Type.memory,
         file = null,
         assetPath = null;
 
-  const ImageWithLoaderFallback.file(this.file, {super.key, this.fit, this.decodeCallback, this.cacheWidth, this.cacheHeight})
+  const ImageWithLoaderFallback.file(this.file, {super.key, this.fit, this.onImageDecoded, this.cacheWidth, this.cacheHeight})
       : _type = _Type.file,
         bytes = null,
         assetPath = null;
 
-  const ImageWithLoaderFallback.asset(this.assetPath, {super.key, this.fit, this.decodeCallback, this.cacheWidth, this.cacheHeight})
+  const ImageWithLoaderFallback.asset(this.assetPath, {super.key, this.fit, this.onImageDecoded, this.cacheWidth, this.cacheHeight})
       : _type = _Type.asset,
         bytes = null,
         file = null;
@@ -75,7 +75,7 @@ class _ImageWithLoaderFallbackState extends State<ImageWithLoaderFallback> {
       ),
     };
 
-    _listener = ImageStreamListener((image, synchronousCall) => widget.decodeCallback?.call());
+    _listener = ImageStreamListener((image, synchronousCall) => widget.onImageDecoded?.call());
     _imageStream = _imageWidget.image.resolve(ImageConfiguration.empty);
     _imageStream.addListener(_listener);
   }
diff --git a/lib/views/components/imaging/photo_collage.dart b/lib/views/components/imaging/photo_collage.dart
index 31781b69..7aaffba0 100644
--- a/lib/views/components/imaging/photo_collage.dart
+++ b/lib/views/components/imaging/photo_collage.dart
@@ -25,8 +25,8 @@ import 'package:momento_booth/src/rust/models/images.dart';
 import 'package:momento_booth/src/rust/utils/image_processing.dart';
 import 'package:momento_booth/utils/environment_info.dart';
 import 'package:momento_booth/utils/logger.dart';
+import 'package:momento_booth/views/components/imaging/capture_view_box.dart';
 import 'package:momento_booth/views/components/imaging/image_with_loader_fallback.dart';
-import 'package:momento_booth/views/components/imaging/photo_container.dart';
 import 'package:momento_booth/views/photo_booth_screen/theme/momento_booth_theme_data.dart';
 import 'package:path/path.dart';
 import 'package:screenshot/screenshot.dart';
@@ -223,9 +223,13 @@ class PhotoCollageState extends State<PhotoCollage> with Logger {
   }
 
   Widget _getChosenImage(int index, {BoxFit? fit, VoidCallback? decodeCallback}) {
-    return widget.debug == null
-        ? PhotoContainer.memory(photos[chosen[index]].data, fit: fit, decodeCallback: decodeCallback)
-        : PhotoContainer.asset('assets/bitmap/placeholder.png', fit: fit, decodeCallback: decodeCallback);
+    return CaptureViewBox(
+      imageBuilder: (setImageDecoded) {
+        return widget.debug == null
+            ? ImageWithLoaderFallback.memory(photos[chosen[index]].data, fit: fit, onImageDecoded: setImageDecoded)
+            : ImageWithLoaderFallback.asset('assets/bitmap/placeholder.png', fit: fit, onImageDecoded: setImageDecoded);
+      },
+    );
   }
 
   Widget _getZeroLayout(AppLocalizations localizations) {
diff --git a/lib/views/components/imaging/photo_container.dart b/lib/views/components/imaging/photo_container.dart
deleted file mode 100644
index 77387ddb..00000000
--- a/lib/views/components/imaging/photo_container.dart
+++ /dev/null
@@ -1,65 +0,0 @@
-import 'dart:io';
-import 'dart:typed_data';
-
-import 'package:fluent_ui/fluent_ui.dart';
-import 'package:momento_booth/main.dart';
-import 'package:momento_booth/managers/settings_manager.dart';
-import 'package:momento_booth/views/components/imaging/image_with_loader_fallback.dart';
-import 'package:momento_booth/views/components/imaging/rotate_flip_crop.dart';
-
-enum _Type { memory, file, asset }
-
-class PhotoContainer extends StatelessWidget {
-
-  final Uint8List? bytes;
-  final File? file;
-  final String? assetPath;
-
-  final BoxFit? fit;
-  final VoidCallback? decodeCallback;
-  final _Type _type;
-
-  const PhotoContainer.memory(this.bytes, {super.key, this.fit, this.decodeCallback})
-      : _type = _Type.memory,
-        file = null,
-        assetPath = null;
-
-  const PhotoContainer.file(this.file, {super.key, this.fit, this.decodeCallback})
-      : _type = _Type.file,
-        bytes = null,
-        assetPath = null;
-
-  const PhotoContainer.asset(this.assetPath, {super.key, this.fit, this.decodeCallback})
-      : _type = _Type.asset,
-        bytes = null,
-        file = null;
-
-  @override
-  Widget build(BuildContext context) {
-    ImageWithLoaderFallback img = switch(_type) {
-      _Type.memory => ImageWithLoaderFallback.memory(
-        bytes,
-        fit: fit,
-        decodeCallback: decodeCallback,
-      ),
-      _Type.file => ImageWithLoaderFallback.file(
-        file,
-        fit: fit,
-        decodeCallback: decodeCallback,
-      ),
-      _Type.asset => ImageWithLoaderFallback.asset(
-        assetPath,
-        fit: fit,
-        decodeCallback: decodeCallback,
-      ),
-    };
-
-    return RotateFlipCrop(
-      rotate: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureRotate,
-      flip: getIt<SettingsManager>().settings.hardware.captureFlip,
-      aspectRatio: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureAspectRatio,
-      child: img,
-    );
-  }
-
-}
diff --git a/lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart b/lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart
index bd2f97df..0c44849b 100644
--- a/lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart
+++ b/lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart
@@ -6,8 +6,9 @@ import 'package:momento_booth/main.dart';
 import 'package:momento_booth/managers/photos_manager.dart';
 import 'package:momento_booth/managers/settings_manager.dart';
 import 'package:momento_booth/views/base/screen_view_base.dart';
+import 'package:momento_booth/views/components/imaging/capture_view_box.dart';
+import 'package:momento_booth/views/components/imaging/image_with_loader_fallback.dart';
 import 'package:momento_booth/views/components/imaging/photo_collage.dart';
-import 'package:momento_booth/views/components/imaging/photo_container.dart';
 import 'package:momento_booth/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_controller.dart';
 import 'package:momento_booth/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view_model.dart';
 
@@ -108,7 +109,9 @@ class CollageMakerScreenView extends ScreenViewBase<CollageMakerScreenViewModel,
                   SizedBox.expand(
                     child: AspectRatio(
                       aspectRatio: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureAspectRatio,
-                      child: PhotoContainer.memory(getIt<PhotosManager>().photos[i].data),
+                      child: CaptureViewBox(imageBuilder: (imageDecoded) {
+                        return ImageWithLoaderFallback.memory(getIt<PhotosManager>().photos[i].data, onImageDecoded: imageDecoded);
+                      }),
                     ),
                   ),
                   AnimatedOpacity(
diff --git a/lib/views/photo_booth_screen/screens/multi_capture_screen/multi_capture_screen_view.dart b/lib/views/photo_booth_screen/screens/multi_capture_screen/multi_capture_screen_view.dart
index ca15205d..f94ff6e1 100644
--- a/lib/views/photo_booth_screen/screens/multi_capture_screen/multi_capture_screen_view.dart
+++ b/lib/views/photo_booth_screen/screens/multi_capture_screen/multi_capture_screen_view.dart
@@ -6,8 +6,9 @@ import 'package:momento_booth/main.dart';
 import 'package:momento_booth/managers/photos_manager.dart';
 import 'package:momento_booth/views/base/screen_view_base.dart';
 import 'package:momento_booth/views/components/dialogs/loading_dialog.dart';
+import 'package:momento_booth/views/components/imaging/capture_view_box.dart';
+import 'package:momento_booth/views/components/imaging/image_with_loader_fallback.dart';
 import 'package:momento_booth/views/components/imaging/live_view.dart';
-import 'package:momento_booth/views/components/imaging/photo_container.dart';
 import 'package:momento_booth/views/components/indicators/capture_counter.dart';
 import 'package:momento_booth/views/photo_booth_screen/screens/multi_capture_screen/multi_capture_screen_controller.dart';
 import 'package:momento_booth/views/photo_booth_screen/screens/multi_capture_screen/multi_capture_screen_view_model.dart';
@@ -75,7 +76,9 @@ class MultiCaptureScreenView extends ScreenViewBase<MultiCaptureScreenViewModel,
               padding: const EdgeInsets.symmetric(vertical: 10),
               child: AspectRatio(
                 aspectRatio: viewModel.aspectRatio,
-                child: PhotoContainer.memory(getIt<PhotosManager>().photos[i].data),
+                child: CaptureViewBox(imageBuilder: (imageDecoded) {
+                  return ImageWithLoaderFallback.memory(getIt<PhotosManager>().photos[i].data, onImageDecoded: imageDecoded);
+                }),
               ),
             ),
           ),


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: Copilot wasn't able to review any files in this pull request.
<details>
<summary>Files not reviewed (5)</summary>

* **lib/views/components/imaging/capture_preview_box.dart**: Language not supported
* **lib/views/components/imaging/photo_collage.dart**: Language not supported
* **lib/views/components/imaging/photo_container.dart**: Language not supported
* **lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart**: Language not supported
* **lib/views/photo_booth_screen/screens/multi_capture_screen/multi_capture_screen_view.dart**: Language not supported
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T08:52:35Z
user: login: ccromjongh
body: I suppose this works. Originally the ring was in the `ImageWithLoaderFallback` widget. Is having the ring in the `CapturePreviewBox` a better solution than scaling the ring in the `ImageWithLoaderFallback`?
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-06T12:53:47Z
user: login: h3x4d3c1m4l
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T18:19:04Z
user: login: ccromjongh
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T18:47:49Z
user: login: h3x4d3c1m4l
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T18:56:06Z

Review comments:
diff_hunk: @@ -108,7 +108,7 @@ class CollageMakerScreenView extends ScreenViewBase<CollageMakerScreenViewModel,
                   SizedBox.expand(
                     child: AspectRatio(
                       aspectRatio: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureAspectRatio,
-                      child: PhotoContainer.memory(getIt<PhotosManager>().photos[i].data),
+                      child: CapturePreviewBox.memory(getIt<PhotosManager>().photos[i].data),
path: lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart
user: login: ccromjongh
body: It ain't really a preview here now is it?
created_at: 2025-04-06T12:40:13Z
author_association: COLLABORATOR
diff_hunk: @@ -108,7 +108,7 @@ class CollageMakerScreenView extends ScreenViewBase<CollageMakerScreenViewModel,
                   SizedBox.expand(
                     child: AspectRatio(
                       aspectRatio: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureAspectRatio,
-                      child: PhotoContainer.memory(getIt<PhotosManager>().photos[i].data),
+                      child: CapturePreviewBox.memory(getIt<PhotosManager>().photos[i].data),
path: lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart
user: login: h3x4d3c1m4l
body: Ah right, let's call it... `CaptureViewBox` then? I think the `Box` part nicely aligns with Flutter's own standards (`FittingBox`, `SizedBox`, ...).
created_at: 2025-04-06T18:19:04Z
author_association: COLLABORATOR
diff_hunk: @@ -108,7 +108,7 @@ class CollageMakerScreenView extends ScreenViewBase<CollageMakerScreenViewModel,
                   SizedBox.expand(
                     child: AspectRatio(
                       aspectRatio: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureAspectRatio,
-                      child: PhotoContainer.memory(getIt<PhotosManager>().photos[i].data),
+                      child: CapturePreviewBox.memory(getIt<PhotosManager>().photos[i].data),
path: lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart
user: login: ccromjongh
body: Those are pure containers, however, this is both the container and the image 🤔 
created_at: 2025-04-06T18:47:48Z
author_association: COLLABORATOR
diff_hunk: @@ -108,7 +108,7 @@ class CollageMakerScreenView extends ScreenViewBase<CollageMakerScreenViewModel,
                   SizedBox.expand(
                     child: AspectRatio(
                       aspectRatio: getIt<SettingsManager>().settings.hardware.liveViewAndCaptureAspectRatio,
-                      child: PhotoContainer.memory(getIt<PhotosManager>().photos[i].data),
+                      child: CapturePreviewBox.memory(getIt<PhotosManager>().photos[i].data),
path: lib/views/photo_booth_screen/screens/collage_maker_screen/collage_maker_screen_view.dart
user: login: h3x4d3c1m4l
body: Hmm, let's simplify it, I think this widget shouldn't be the image at all...
created_at: 2025-04-06T18:56:05Z
author_association: COLLABORATOR

PR comments:
user: login: h3x4d3c1m4l
created_at: 2025-04-06T18:22:29Z
body: > Is having the ring in the `CapturePreviewBox` a better solution than scaling the ring in the `ImageWithLoaderFallback`?

I tried getting the scaling right within `ImageWithLoaderFallback`, however due to `ImageWithLoaderFallback` being wrapped in (what is currently still called) `CapturePreviewBox`, I couldn't.
author_association: COLLABORATOR
user: login: ccromjongh
created_at: 2025-04-06T18:46:53Z
body: > I tried getting the scaling right within `ImageWithLoaderFallback`, however due to `ImageWithLoaderFallback` being wrapped in (what is currently still called) `CapturePreviewBox`, I couldn't.

Well one could put a `scale` parameter, idk what's more convoluted haha
author_association: COLLABORATOR
user: login: h3x4d3c1m4l
created_at: 2025-04-06T18:51:15Z
body: > Well one could put a `scale` parameter, idk what's more convoluted haha

So, the crappy scaling happens due to the scaling/rotation/aspect ratio stuff that get's applied to `ImageWithLoaderFallback` gets applied to the `ProgressRing` as well. The only clean issue to fix it, is to avoid it being wrapped by these scaling/rotation/aspect ratio widgets.
author_association: COLLABORATOR