diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 735f8cfa6..aea725a36 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -321,7 +321,7 @@ SPEC CHECKSUMS: FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3 fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433 flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 diff --git a/lib/core/cache/thumbnail_cache.dart b/lib/core/cache/thumbnail_cache.dart index 08a05fda9..3030f356b 100644 --- a/lib/core/cache/thumbnail_cache.dart +++ b/lib/core/cache/thumbnail_cache.dart @@ -5,7 +5,7 @@ import 'package:photos/core/constants.dart'; import 'package:photos/models/ente_file.dart'; class ThumbnailLruCache { - static final LRUMap _map = LRUMap(1000); + static final LRUMap _map = LRUMap(250); static Uint8List? get(EnteFile enteFile, [int? size]) { return _map.get( diff --git a/lib/ui/huge_listview/lazy_loading_gallery.dart b/lib/ui/huge_listview/lazy_loading_gallery.dart index 9b9d98ec5..dcf0f5d14 100644 --- a/lib/ui/huge_listview/lazy_loading_gallery.dart +++ b/lib/ui/huge_listview/lazy_loading_gallery.dart @@ -5,8 +5,8 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'package:local_hero/local_hero.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; @@ -33,6 +33,7 @@ class LazyLoadingGallery extends StatefulWidget { final String tag; final String logTag; final Stream currentIndexStream; + final int imagesPerRow; LazyLoadingGallery( this.files, @@ -44,6 +45,7 @@ class LazyLoadingGallery extends StatefulWidget { this.tag, this.currentIndexStream, { this.logTag = "", + this.imagesPerRow, Key key, }) : super(key: key ?? UniqueKey()); @@ -251,6 +253,7 @@ class _LazyLoadingGalleryState extends State { _files.length > kRecycleLimit, _toggleSelectAllFromDay, _areAllFromDaySelected, + widget.imagesPerRow, ), ); } @@ -278,6 +281,7 @@ class LazyLoadingGridView extends StatefulWidget { final bool shouldRecycle; final ValueNotifier toggleSelectAllFromDay; final ValueNotifier areAllFilesSelected; + final int imagesPerRow; LazyLoadingGridView( this.tag, @@ -287,7 +291,8 @@ class LazyLoadingGridView extends StatefulWidget { this.shouldRender, this.shouldRecycle, this.toggleSelectAllFromDay, - this.areAllFilesSelected, { + this.areAllFilesSelected, + this.imagesPerRow, { Key key, }) : super(key: key ?? UniqueKey()); @@ -383,10 +388,10 @@ class _LazyLoadingGridViewState extends State { return _buildFile(context, widget.filesInDay[index]); }, itemCount: widget.filesInDay.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisSpacing: 2, mainAxisSpacing: 2, - crossAxisCount: 4, + crossAxisCount: widget.imagesPerRow ?? 4, ), padding: const EdgeInsets.all(0), ); @@ -405,8 +410,8 @@ class _LazyLoadingGridViewState extends State { HapticFeedback.lightImpact(); _selectFile(file); }, - child: ClipRRect( - borderRadius: BorderRadius.circular(1), + child: LocalHero( + tag: widget.tag + file.tag, child: Stack( children: [ Hero( diff --git a/lib/ui/viewer/file/thumbnail_widget.dart b/lib/ui/viewer/file/thumbnail_widget.dart index 3c0fe5809..3458df28c 100644 --- a/lib/ui/viewer/file/thumbnail_widget.dart +++ b/lib/ui/viewer/file/thumbnail_widget.dart @@ -179,7 +179,6 @@ class _ThumbnailWidgetState extends State { _loadNetworkImage(); } else { if (await doesLocalFileExist(widget.file) == false) { - _logger.info("Deleting file " + widget.file.tag); FilesDB.instance.deleteLocalFile(widget.file); Bus.instance.fire( LocalPhotosUpdatedEvent( @@ -197,7 +196,6 @@ class _ThumbnailWidgetState extends State { final imageProvider = Image.memory(thumbData).image; _cacheAndRender(imageProvider); } - ThumbnailLruCache.put(widget.file, thumbData, thumbnailSmallSize); }).catchError((e) { _logger.warning("Could not load image: ", e); _errorLoadingLocalThumbnail = true; diff --git a/lib/ui/viewer/gallery/gallery.dart b/lib/ui/viewer/gallery/gallery.dart index c7f2c1739..68b4aa734 100644 --- a/lib/ui/viewer/gallery/gallery.dart +++ b/lib/ui/viewer/gallery/gallery.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:local_hero/local_hero.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; @@ -203,76 +205,118 @@ class _GalleryState extends State { return _getListView(); } + int _imagesPerRow = 4; + ScaleUpdateDetails _lastScaleUpdateDetails; + Widget _getListView() { - return HugeListView>( - key: _hugeListViewKey, - controller: _itemScroller, - startIndex: 0, - totalCount: _collatedFiles.length, - isDraggableScrollbarEnabled: _collatedFiles.length > 10, - waitBuilder: (_) { - return const EnteLoadingWidget(); - }, - emptyResultBuilder: (_) { - final List children = []; - if (widget.header != null) { - children.add(widget.header); - } - children.add( - Expanded( - child: widget.emptyState, - ), - ); - if (widget.footer != null) { - children.add(widget.footer); - } - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: children, - ); - }, - itemBuilder: (context, index) { - Widget gallery; - gallery = LazyLoadingGallery( - _collatedFiles[index], - index, - widget.reloadEvent, - widget.removalEventTypes, - widget.asyncLoader, - widget.selectedFiles, - widget.tagPrefix, - Bus.instance - .on() - .where((event) => event.tag == widget.tagPrefix) - .map((event) => event.index), - logTag: _logTag, - ); - if (widget.header != null && index == 0) { - gallery = Column(children: [widget.header, gallery]); - } - if (widget.footer != null && index == _collatedFiles.length - 1) { - gallery = Column(children: [gallery, widget.footer]); - } - return gallery; - }, - labelTextBuilder: (int index) { - return getMonthAndYear( - DateTime.fromMicrosecondsSinceEpoch( - _collatedFiles[index][0].creationTime, - ), - ); - }, - thumbBackgroundColor: - Theme.of(context).colorScheme.galleryThumbBackgroundColor, - thumbDrawColor: Theme.of(context).colorScheme.galleryThumbDrawColor, - thumbPadding: widget.header != null - ? const EdgeInsets.only(top: 60) - : const EdgeInsets.all(0), - bottomSafeArea: widget.scrollBottomSafeArea, - firstShown: (int firstIndex) { - Bus.instance - .fire(GalleryIndexUpdatedEvent(widget.tagPrefix, firstIndex)); - }, + return LocalHeroScope( + duration: const Duration(milliseconds: 100), + child: + RawGestureDetector( + gestures: { + AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers< + AllowMultipleGestureRecognizer>( + () => AllowMultipleGestureRecognizer(), //constructor + (AllowMultipleGestureRecognizer instance) { + instance.onUpdate = (details) { + if (details.pointerCount == 2) { + _lastScaleUpdateDetails = details; + } + }; + instance.onEnd = (details) { + if (_lastScaleUpdateDetails != null) { + if (_lastScaleUpdateDetails.verticalScale > 1 && + _lastScaleUpdateDetails.horizontalScale > 1) { + _logger.info("zoomed in"); + if (_imagesPerRow > 2) { + _imagesPerRow--; + } + } else if (_lastScaleUpdateDetails.verticalScale < 1 && + _lastScaleUpdateDetails.horizontalScale < 1) { + _logger.info("zoomed out"); + if (_imagesPerRow < 4) { + _imagesPerRow++; + } + } + _lastScaleUpdateDetails = null; + setState(() {}); + } + }; + }, + ) + }, + child: HugeListView>( + key: _hugeListViewKey, + controller: _itemScroller, + startIndex: 0, + totalCount: _collatedFiles.length, + isDraggableScrollbarEnabled: _collatedFiles.length > 10, + waitBuilder: (_) { + return const EnteLoadingWidget(); + }, + emptyResultBuilder: (_) { + final List children = []; + if (widget.header != null) { + children.add(widget.header); + } + children.add( + Expanded( + child: widget.emptyState, + ), + ); + if (widget.footer != null) { + children.add(widget.footer); + } + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: children, + ); + }, + itemBuilder: (context, index) { + Widget gallery; + gallery = LazyLoadingGallery( + _collatedFiles[index], + index, + widget.reloadEvent, + widget.removalEventTypes, + widget.asyncLoader, + widget.selectedFiles, + widget.tagPrefix, + Bus.instance + .on() + .where((event) => event.tag == widget.tagPrefix) + .map((event) => event.index), + logTag: _logTag, + imagesPerRow: _imagesPerRow, + ); + if (widget.header != null && index == 0) { + gallery = Column(children: [widget.header, gallery]); + } + if (widget.footer != null && index == _collatedFiles.length - 1) { + gallery = Column(children: [gallery, widget.footer]); + } + return gallery; + }, + labelTextBuilder: (int index) { + return getMonthAndYear( + DateTime.fromMicrosecondsSinceEpoch( + _collatedFiles[index][0].creationTime, + ), + ); + }, + thumbBackgroundColor: + Theme.of(context).colorScheme.galleryThumbBackgroundColor, + thumbDrawColor: Theme.of(context).colorScheme.galleryThumbDrawColor, + thumbPadding: widget.header != null + ? const EdgeInsets.only(top: 60) + : const EdgeInsets.all(0), + bottomSafeArea: widget.scrollBottomSafeArea, + firstShown: (int firstIndex) { + Bus.instance + .fire(GalleryIndexUpdatedEvent(widget.tagPrefix, firstIndex)); + }, + ), + ), ); } @@ -315,3 +359,14 @@ class GalleryIndexUpdatedEvent { GalleryIndexUpdatedEvent(this.tag, this.index); } + +// Custom Gesture Recognizer. +// rejectGesture() is overridden. When a gesture is rejected, this is the function that is called. By default, it disposes of the +// Recognizer and runs clean up. However we modified it so that instead the Recognizer is disposed of, it is actually manually added. +// The result is instead you have one Recognizer winning the Arena, you have two. It is a win-win. +class AllowMultipleGestureRecognizer extends ScaleGestureRecognizer { + @override + void rejectGesture(int pointer) { + acceptGesture(pointer); + } +} diff --git a/lib/utils/thumbnail_util.dart b/lib/utils/thumbnail_util.dart index f0225f976..d4314acf7 100644 --- a/lib/utils/thumbnail_util.dart +++ b/lib/utils/thumbnail_util.dart @@ -62,7 +62,7 @@ Future getThumbnailFromServer(File file) async { Future getThumbnailFromLocal( File file, { - int size = thumbnailSmallSize, + int size = thumbnailLargeSize, int quality = thumbnailQuality, }) async { final lruCachedThumbnail = ThumbnailLruCache.get(file, size); diff --git a/pubspec.lock b/pubspec.lock index 5bee9ad4d..f50076169 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" background_fetch: dependency: "direct main" description: @@ -98,14 +98,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" chewie: dependency: "direct main" description: @@ -119,7 +112,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: "direct main" description: @@ -308,7 +301,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" fast_base58: dependency: "direct main" description: @@ -742,6 +735,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.11" + local_hero: + dependency: "direct main" + description: + name: local_hero + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" logging: dependency: "direct main" description: @@ -762,14 +762,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" media_extension: dependency: "direct main" description: @@ -785,7 +785,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -913,7 +913,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: @@ -1254,7 +1254,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" sprintf: dependency: transitive description: @@ -1310,7 +1310,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" syncfusion_flutter_core: dependency: "direct main" description: @@ -1338,28 +1338,28 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: "direct dev" description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.21.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.16" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5b033590f..e19414cc3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,7 @@ dependencies: like_button: ^2.0.2 loading_animations: ^2.1.0 local_auth: ^1.1.5 + local_hero: ^0.2.0 logging: ^1.0.1 lottie: ^1.2.2 media_extension: