Browse Source

Merge branch 'main' into uncategorized

Neeraj Gupta 2 năm trước cách đây
mục cha
commit
98a22c34a5

+ 1 - 0
lib/services/remote_sync_service.dart

@@ -477,6 +477,7 @@ class RemoteSyncService {
     final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length;
     if (toBeUploaded > 0) {
       Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload));
+      await _uploader.checkNetworkForUpload();
       // verify if files upload is allowed based on their subscription plan and
       // storage limit. To avoid creating new endpoint, we are using
       // fetchUploadUrls as alternative method.

+ 10 - 1
lib/theme/colors.dart

@@ -21,6 +21,7 @@ class EnteColorScheme {
 
   // Fill Colors
   final Color fillBase;
+  final Color fillBasePressed;
   final Color fillMuted;
   final Color fillFaint;
   final Color fillFaintPressed;
@@ -44,6 +45,7 @@ class EnteColorScheme {
   final Color warning700;
   final Color warning500;
   final Color warning400;
+  final Color warning800;
   final Color caution500;
 
   //other colors
@@ -62,6 +64,7 @@ class EnteColorScheme {
     this.textFaint,
     this.blurTextBase,
     this.fillBase,
+    this.fillBasePressed,
     this.fillMuted,
     this.fillFaint,
     this.fillFaintPressed,
@@ -78,9 +81,10 @@ class EnteColorScheme {
     this.primary500 = _primary500,
     this.primary400 = _primary400,
     this.primary300 = _primary300,
+    this.warning800 = _warning800,
     this.warning700 = _warning700,
     this.warning500 = _warning500,
-    this.warning400 = _warning700,
+    this.warning400 = _warning400,
     this.caution500 = _caution500,
   });
 }
@@ -97,6 +101,7 @@ const EnteColorScheme lightScheme = EnteColorScheme(
   textFaintLight,
   blurTextBaseLight,
   fillBaseLight,
+  fillBasePressedLight,
   fillMutedLight,
   fillFaintLight,
   fillFaintPressedLight,
@@ -123,6 +128,7 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   textFaintDark,
   blurTextBaseDark,
   fillBaseDark,
+  fillBasePressedDark,
   fillMutedDark,
   fillFaintDark,
   fillFaintPressedDark,
@@ -168,11 +174,13 @@ const Color blurTextBaseDark = Color.fromRGBO(255, 255, 255, 0.95);
 
 // Fill Colors
 const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
+const Color fillBasePressedLight = Color.fromRGBO(0, 0, 0, 0.87);
 const Color fillMutedLight = Color.fromRGBO(0, 0, 0, 0.12);
 const Color fillFaintLight = Color.fromRGBO(0, 0, 0, 0.04);
 const Color fillFaintPressedLight = Color.fromRGBO(0, 0, 0, 0.08);
 
 const Color fillBaseDark = Color.fromRGBO(255, 255, 255, 1);
+const Color fillBasePressedDark = Color.fromRGBO(255, 255, 255, 0.9);
 const Color fillMutedDark = Color.fromRGBO(255, 255, 255, 0.16);
 const Color fillFaintDark = Color.fromRGBO(255, 255, 255, 0.12);
 const Color fillFaintPressedDark = Color.fromRGBO(255, 255, 255, 0.06);
@@ -212,6 +220,7 @@ const Color _warning700 = Color.fromRGBO(234, 63, 63, 1);
 const Color _warning500 = Color.fromRGBO(255, 101, 101, 1);
 const Color warning500 = Color.fromRGBO(255, 101, 101, 1);
 const Color _warning400 = Color.fromRGBO(255, 111, 111, 1);
+const Color _warning800 = Color(0xFFF53434);
 
 const Color _caution500 = Color.fromRGBO(255, 194, 71, 1);
 

+ 59 - 5
lib/ui/collections/collection_item_widget.dart

@@ -4,18 +4,22 @@ import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/gallery_type.dart';
 import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/viewer/file/file_icons_widget.dart';
 import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
 import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
 import 'package:photos/ui/viewer/gallery/collection_page.dart';
 import 'package:photos/utils/navigation_util.dart';
+import 'package:visibility_detector/visibility_detector.dart';
 
 class CollectionItem extends StatelessWidget {
   final CollectionWithThumbnail c;
   final double sideOfThumbnail;
+  final bool shouldRender;
 
   CollectionItem(
     this.c,
     this.sideOfThumbnail, {
+    this.shouldRender = false,
     Key? key,
   }) : super(key: Key(c.collection.id.toString()));
 
@@ -39,11 +43,10 @@ class CollectionItem extends StatelessWidget {
                   child: Hero(
                     tag: heroTag,
                     child: c.thumbnail != null
-                        ? ThumbnailWidget(
-                            c.thumbnail,
-                            shouldShowArchiveStatus: c.collection.isArchived(),
-                            showFavForAlbumOnly: true,
-                            key: Key(heroTag),
+                        ? CollectionItemThumbnailWidget(
+                            c: c,
+                            heroTag: heroTag,
+                            shouldRender: shouldRender,
                           )
                         : const NoThumbnailWidget(),
                   ),
@@ -98,3 +101,54 @@ class CollectionItem extends StatelessWidget {
     );
   }
 }
+
+class CollectionItemThumbnailWidget extends StatefulWidget {
+  const CollectionItemThumbnailWidget({
+    Key? key,
+    required this.c,
+    required this.heroTag,
+    this.shouldRender = false,
+  }) : super(key: key);
+
+  final CollectionWithThumbnail c;
+  final String heroTag;
+  final bool shouldRender;
+
+  @override
+  State<CollectionItemThumbnailWidget> createState() =>
+      _CollectionItemThumbnailWidgetState();
+}
+
+class _CollectionItemThumbnailWidgetState
+    extends State<CollectionItemThumbnailWidget> {
+  bool _shouldRender = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _shouldRender = widget.shouldRender;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return VisibilityDetector(
+      key: Key("collection_item" + widget.c.thumbnail!.tag),
+      onVisibilityChanged: (visibility) {
+        final shouldRender = visibility.visibleFraction > 0;
+        if (mounted && shouldRender && !_shouldRender) {
+          setState(() {
+            _shouldRender = shouldRender;
+          });
+        }
+      },
+      child: _shouldRender
+          ? ThumbnailWidget(
+              widget.c.thumbnail,
+              shouldShowArchiveStatus: widget.c.collection.isArchived(),
+              showFavForAlbumOnly: true,
+              key: Key(widget.heroTag),
+            )
+          : const ThumbnailPlaceHolder(),
+    );
+  }
+}

+ 6 - 1
lib/ui/collections/remote_collections_grid_view_widget.dart

@@ -14,6 +14,7 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget {
   static const maxThumbnailWidth = 224.0;
   static const fixedGapBetweenAlbum = 8.0;
   static const minGapForHorizontalPadding = 8.0;
+  static const collectionItemsToPreload = 20;
 
   final List<CollectionWithThumbnail>? collections;
 
@@ -45,7 +46,11 @@ class RemoteCollectionsGridViewWidget extends StatelessWidget {
         // to disable GridView's scrolling
         itemBuilder: (context, index) {
           if (index < collections!.length) {
-            return CollectionItem(collections![index], sideOfThumbnail);
+            return CollectionItem(
+              collections![index],
+              sideOfThumbnail,
+              shouldRender: index < collectionItemsToPreload,
+            );
           } else {
             return const CreateNewAlbumWidget();
           }

+ 1 - 1
lib/ui/components/action_sheet_widget.dart

@@ -152,7 +152,7 @@ class ContentContainerWidget extends StatelessWidget {
             ? const SizedBox.shrink()
             : Text(
                 title!,
-                style: textTheme.h3Bold
+                style: textTheme.largeBold
                     .copyWith(color: textBaseDark), //constant color
               ),
         title == null || body == null

+ 37 - 21
lib/ui/components/button_widget.dart

@@ -115,7 +115,6 @@ class ButtonWidget extends StatelessWidget {
         buttonType.defaultBorderColor(colorScheme, buttonSize);
     buttonStyle.pressedBorderColor = buttonType.pressedBorderColor(
       colorScheme: colorScheme,
-      inverseColorScheme: inverseColorScheme,
       buttonSize: buttonSize,
     );
     buttonStyle.disabledBorderColor =
@@ -205,27 +204,20 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
   final _debouncer = Debouncer(const Duration(milliseconds: 300));
   ExecutionState executionState = ExecutionState.idle;
 
+  @override
+  void initState() {
+    _setButtonTheme();
+    super.initState();
+  }
+
+  @override
+  void didUpdateWidget(covariant ButtonChildWidget oldWidget) {
+    _setButtonTheme();
+    super.didUpdateWidget(oldWidget);
+  }
+
   @override
   Widget build(BuildContext context) {
-    progressStatus = widget.progressStatus;
-    checkIconColor = widget.buttonStyle.checkIconColor ??
-        widget.buttonStyle.defaultIconColor;
-    loadingIconColor = widget.buttonStyle.defaultIconColor;
-    if (widget.isDisabled) {
-      buttonColor = widget.buttonStyle.disabledButtonColor ??
-          widget.buttonStyle.defaultButtonColor;
-      borderColor = widget.buttonStyle.disabledBorderColor ??
-          widget.buttonStyle.defaultBorderColor;
-      iconColor = widget.buttonStyle.disabledIconColor ??
-          widget.buttonStyle.defaultIconColor;
-      labelStyle = widget.buttonStyle.disabledLabelStyle ??
-          widget.buttonStyle.defaultLabelStyle;
-    } else {
-      buttonColor = widget.buttonStyle.defaultButtonColor;
-      borderColor = widget.buttonStyle.defaultBorderColor;
-      iconColor = widget.buttonStyle.defaultIconColor;
-      labelStyle = widget.buttonStyle.defaultLabelStyle;
-    }
     if (executionState == ExecutionState.successful) {
       Future.delayed(Duration(seconds: widget.isInAlert ? 1 : 2), () {
         setState(() {
@@ -241,7 +233,9 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
       child: Container(
         decoration: BoxDecoration(
           borderRadius: const BorderRadius.all(Radius.circular(4)),
-          border: Border.all(color: borderColor),
+          border: widget.buttonType == ButtonType.tertiaryCritical
+              ? Border.all(color: borderColor)
+              : null,
         ),
         child: AnimatedContainer(
           duration: const Duration(milliseconds: 16),
@@ -383,6 +377,28 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
     );
   }
 
+  void _setButtonTheme() {
+    progressStatus = widget.progressStatus;
+    checkIconColor = widget.buttonStyle.checkIconColor ??
+        widget.buttonStyle.defaultIconColor;
+    loadingIconColor = widget.buttonStyle.defaultIconColor;
+    if (widget.isDisabled) {
+      buttonColor = widget.buttonStyle.disabledButtonColor ??
+          widget.buttonStyle.defaultButtonColor;
+      borderColor = widget.buttonStyle.disabledBorderColor ??
+          widget.buttonStyle.defaultBorderColor;
+      iconColor = widget.buttonStyle.disabledIconColor ??
+          widget.buttonStyle.defaultIconColor;
+      labelStyle = widget.buttonStyle.disabledLabelStyle ??
+          widget.buttonStyle.defaultLabelStyle;
+    } else {
+      buttonColor = widget.buttonStyle.defaultButtonColor;
+      borderColor = widget.buttonStyle.defaultBorderColor;
+      iconColor = widget.buttonStyle.defaultIconColor;
+      labelStyle = widget.buttonStyle.defaultLabelStyle;
+    }
+  }
+
   bool get _shouldRegisterGestures =>
       !widget.isDisabled && executionState == ExecutionState.idle;
 

+ 1 - 1
lib/ui/components/dialog_widget.dart

@@ -117,7 +117,7 @@ class ContentContainer extends StatelessWidget {
                 ],
               ),
         icon == null ? const SizedBox.shrink() : const SizedBox(height: 19),
-        Text(title, style: textTheme.h3Bold),
+        Text(title, style: textTheme.largeBold),
         body != null ? const SizedBox(height: 19) : const SizedBox.shrink(),
         body != null
             ? Text(

+ 21 - 16
lib/ui/components/models/button_type.dart

@@ -55,6 +55,15 @@ enum ButtonType {
     if (isPrimary) {
       return colorScheme.primary700;
     }
+    if (isSecondary) {
+      return colorScheme.fillFaintPressed;
+    }
+    if (isNeutral) {
+      return colorScheme.fillBasePressed;
+    }
+    if (this == ButtonType.critical) {
+      return colorScheme.warning800;
+    }
     return null;
   }
 
@@ -85,20 +94,10 @@ enum ButtonType {
   //Returning null to fallback to default color
   Color? pressedBorderColor({
     required EnteColorScheme colorScheme,
-    required EnteColorScheme inverseColorScheme,
     required ButtonSize buttonSize,
   }) {
-    if (isPrimary) {
-      return colorScheme.strokeMuted;
-    }
-    if (buttonSize == ButtonSize.small && this == ButtonType.tertiaryCritical) {
-      return null;
-    }
-    if (isSecondary || isCritical) {
-      return colorScheme.strokeBase;
-    }
-    if (isNeutral) {
-      return inverseColorScheme.strokeBase;
+    if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
+      return colorScheme.warning700;
     }
     return null;
   }
@@ -133,8 +132,11 @@ enum ButtonType {
 
   //Returning null to fallback to default color
   Color? pressedIconColor(EnteColorScheme colorScheme, ButtonSize buttonSize) {
-    if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
-      return colorScheme.strokeBase;
+    if (this == ButtonType.tertiaryCritical) {
+      return colorScheme.warning700;
+    }
+    if (this == ButtonType.tertiary && buttonSize == ButtonSize.small) {
+      return colorScheme.fillBasePressed;
     }
     return null;
   }
@@ -176,8 +178,11 @@ enum ButtonType {
     EnteColorScheme colorScheme,
     ButtonSize buttonSize,
   ) {
-    if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
-      return textTheme.bodyBold.copyWith(color: colorScheme.strokeBase);
+    if (this == ButtonType.tertiaryCritical) {
+      return textTheme.bodyBold.copyWith(color: colorScheme.warning700);
+    }
+    if (this == ButtonType.tertiary && buttonSize == ButtonSize.small) {
+      return textTheme.bodyBold.copyWith(color: colorScheme.fillBasePressed);
     }
     return null;
   }

+ 0 - 28
lib/ui/home/status_bar_widget.dart

@@ -240,34 +240,6 @@ class RefreshIndicatorWidget extends StatelessWidget {
   }
 }
 
-class BrandingWidget extends StatelessWidget {
-  const BrandingWidget({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Row(
-      children: [
-        Container(
-          height: kContainerHeight,
-          padding: const EdgeInsets.only(left: 12, top: 4),
-          child: const Align(
-            alignment: Alignment.centerLeft,
-            child: Text(
-              "ente",
-              style: TextStyle(
-                fontWeight: FontWeight.bold,
-                fontFamily: 'Montserrat',
-                fontSize: 24,
-                height: 1,
-              ),
-            ),
-          ),
-        ),
-      ],
-    );
-  }
-}
-
 class SyncStatusCompletedWidget extends StatelessWidget {
   const SyncStatusCompletedWidget({Key? key}) : super(key: key);
 

+ 2 - 2
lib/ui/huge_listview/lazy_loading_gallery.dart

@@ -352,7 +352,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
 
   Widget _getRecyclableView() {
     return VisibilityDetector(
-      key: UniqueKey(),
+      key: Key("gallery" + widget.filesInDay.first.tag),
       onVisibilityChanged: (visibility) {
         final shouldRender = visibility.visibleFraction > 0;
         if (mounted && shouldRender != _shouldRender) {
@@ -370,7 +370,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
   Widget _getNonRecyclableView() {
     if (!_shouldRender!) {
       return VisibilityDetector(
-        key: UniqueKey(),
+        key: Key("gallery" + widget.filesInDay.first.tag),
         onVisibilityChanged: (visibility) {
           if (mounted && visibility.visibleFraction > 0 && !_shouldRender!) {
             setState(() {

+ 5 - 2
lib/ui/viewer/file/file_icons_widget.dart

@@ -23,7 +23,10 @@ class UnSyncedIcon extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return const _BottomLeftOverlayIcon(Icons.cloud_off_outlined);
+    return const _BottomLeftOverlayIcon(
+      Icons.cloud_off_outlined,
+      baseSize: 18,
+    );
   }
 }
 
@@ -186,7 +189,7 @@ class _BottomLeftOverlayIcon extends StatelessWidget {
 
         if (constraints.hasBoundedWidth) {
           final w = constraints.maxWidth;
-          if (w > 120) {
+          if (w > 125) {
             size = 24;
           } else if (w < 75) {
             inset = 3;

+ 44 - 3
lib/ui/viewer/file/zoomable_image.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:io';
 
 import 'package:flutter/material.dart';
@@ -13,6 +14,7 @@ import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/utils/file_util.dart';
+import 'package:photos/utils/image_util.dart';
 import 'package:photos/utils/thumbnail_util.dart';
 
 class ZoomableImage extends StatefulWidget {
@@ -35,7 +37,7 @@ class ZoomableImage extends StatefulWidget {
 
 class _ZoomableImageState extends State<ZoomableImage>
     with SingleTickerProviderStateMixin {
-  final Logger _logger = Logger("ZoomableImage");
+  late Logger _logger;
   late File _photo;
   ImageProvider? _imageProvider;
   bool _loadedSmallThumbnail = false;
@@ -45,10 +47,13 @@ class _ZoomableImageState extends State<ZoomableImage>
   bool _loadedFinalImage = false;
   ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
   bool _isZooming = false;
+  PhotoViewController _photoViewController = PhotoViewController();
+  int? _thumbnailWidth;
 
   @override
   void initState() {
     _photo = widget.photo;
+    _logger = Logger("ZoomableImage_" + _photo.displayName);
     debugPrint('initState for ${_photo.toString()}');
     _scaleStateChangedCallback = (value) {
       if (widget.shouldDisableScroll != null) {
@@ -61,6 +66,12 @@ class _ZoomableImageState extends State<ZoomableImage>
     super.initState();
   }
 
+  @override
+  void dispose() {
+    _photoViewController.dispose();
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
     if (_photo.isRemoteFile) {
@@ -75,6 +86,7 @@ class _ZoomableImageState extends State<ZoomableImage>
         axis: Axis.vertical,
         child: PhotoView(
           imageProvider: _imageProvider,
+          controller: _photoViewController,
           scaleStateChangedCallback: _scaleStateChangedCallback,
           minScale: PhotoViewComputedScale.contained,
           gaplessPlayback: true,
@@ -106,6 +118,7 @@ class _ZoomableImageState extends State<ZoomableImage>
       if (cachedThumbnail != null) {
         _imageProvider = Image.memory(cachedThumbnail).image;
         _loadedSmallThumbnail = true;
+        _captureThumbnailDimensions(_imageProvider!);
       } else {
         getThumbnailFromServer(_photo).then((file) {
           final imageProvider = Image.memory(file).image;
@@ -115,6 +128,7 @@ class _ZoomableImageState extends State<ZoomableImage>
                 setState(() {
                   _imageProvider = imageProvider;
                   _loadedSmallThumbnail = true;
+                  _captureThumbnailDimensions(_imageProvider!);
                 });
               }
             }).catchError((e) {
@@ -125,7 +139,8 @@ class _ZoomableImageState extends State<ZoomableImage>
         });
       }
     }
-    if (!_loadedFinalImage) {
+    if (!_loadedFinalImage && !_loadingFinalImage) {
+      _loadingFinalImage = true;
       getFileFromServer(_photo).then((file) {
         _onFinalImageLoaded(
           Image.file(
@@ -209,16 +224,42 @@ class _ZoomableImageState extends State<ZoomableImage>
 
   void _onFinalImageLoaded(ImageProvider imageProvider) {
     if (mounted) {
-      precacheImage(imageProvider, context).then((value) {
+      precacheImage(imageProvider, context).then((value) async {
         if (mounted) {
+          await _updatePhotoViewController(imageProvider);
           setState(() {
             _imageProvider = imageProvider;
             _loadedFinalImage = true;
+            _logger.info("Final image loaded");
           });
         }
       });
     }
   }
 
+  Future<void> _captureThumbnailDimensions(ImageProvider imageProvider) async {
+    final imageInfo = await getImageInfo(imageProvider);
+    _thumbnailWidth = imageInfo.image.width;
+  }
+
+  Future<void> _updatePhotoViewController(ImageProvider imageProvider) async {
+    if (_thumbnailWidth == null || _photoViewController.scale == null) {
+      return;
+    }
+    final imageInfo = await getImageInfo(imageProvider);
+    final scale = _photoViewController.scale! /
+        (imageInfo.image.width / _thumbnailWidth!);
+    final currentPosition = _photoViewController.value.position;
+    final positionScaleFactor = 1 / scale;
+    final newPosition = currentPosition.scale(
+      positionScaleFactor,
+      positionScaleFactor,
+    );
+    _photoViewController = PhotoViewController(
+      initialPosition: newPosition,
+      initialScale: scale,
+    );
+  }
+
   bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif");
 }

+ 15 - 6
lib/utils/file_uploader.dart

@@ -271,18 +271,27 @@ class FileUploader {
     }
   }
 
-  Future<File> _tryToUpload(
-    File file,
-    int collectionID,
-    bool forcedUpload,
-  ) async {
+  Future<void> checkNetworkForUpload({bool isForceUpload = false}) async {
+    // Note: We don't support force uploading currently. During force upload,
+    // network check is skipped completely
+    if (isForceUpload) {
+      return;
+    }
     final connectivityResult = await (Connectivity().checkConnectivity());
     final canUploadUnderCurrentNetworkConditions =
         (connectivityResult == ConnectivityResult.wifi ||
             Configuration.instance.shouldBackupOverMobileData());
-    if (!canUploadUnderCurrentNetworkConditions && !forcedUpload) {
+    if (!canUploadUnderCurrentNetworkConditions) {
       throw WiFiUnavailableError();
     }
+  }
+
+  Future<File> _tryToUpload(
+    File file,
+    int collectionID,
+    bool forcedUpload,
+  ) async {
+    await checkNetworkForUpload(isForceUpload: forcedUpload);
     final fileOnDisk = await FilesDB.instance.getFile(file.generatedID!);
     final wasAlreadyUploaded = fileOnDisk != null &&
         fileOnDisk.uploadedFileID != null &&

+ 16 - 0
lib/utils/image_util.dart

@@ -0,0 +1,16 @@
+import 'dart:async';
+
+import 'package:flutter/widgets.dart';
+
+Future<ImageInfo> getImageInfo(ImageProvider imageProvider) {
+  final completer = Completer<ImageInfo>();
+  final imageStream = imageProvider.resolve(const ImageConfiguration());
+  final listener = ImageStreamListener(
+    ((imageInfo, _) {
+      completer.complete(imageInfo);
+    }),
+  );
+  imageStream.addListener(listener);
+  completer.future.whenComplete(() => imageStream.removeListener(listener));
+  return completer.future;
+}