Explorar el Código

Refactor gallery (#1074)

Ashil hace 2 años
padre
commit
c77afae652

+ 0 - 573
lib/ui/huge_listview/lazy_loading_gallery.dart

@@ -1,573 +0,0 @@
-import 'dart:async';
-import 'dart:math';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:logging/logging.dart';
-import 'package:media_extension/media_extension.dart';
-import 'package:media_extension/media_extension_action_types.dart';
-import 'package:photos/core/configuration.dart';
-import 'package:photos/core/constants.dart';
-import 'package:photos/core/event_bus.dart';
-import 'package:photos/events/clear_selections_event.dart';
-import 'package:photos/events/files_updated_event.dart';
-import 'package:photos/extensions/string_ext.dart';
-import 'package:photos/models/file.dart';
-import 'package:photos/models/selected_files.dart';
-import 'package:photos/services/app_lifecycle_service.dart';
-import 'package:photos/theme/ente_theme.dart';
-import 'package:photos/ui/huge_listview/place_holder_widget.dart';
-import 'package:photos/ui/viewer/file/detail_page.dart';
-import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
-import "package:photos/ui/viewer/gallery/component/day_widget.dart";
-import 'package:photos/ui/viewer/gallery/gallery.dart';
-import 'package:photos/utils/file_util.dart';
-import 'package:photos/utils/navigation_util.dart';
-import 'package:visibility_detector/visibility_detector.dart';
-
-class LazyLoadingGallery extends StatefulWidget {
-  final List<File> files;
-  final int index;
-  final Stream<FilesUpdatedEvent>? reloadEvent;
-  final Set<EventType> removalEventTypes;
-  final GalleryLoader asyncLoader;
-  final SelectedFiles? selectedFiles;
-  final String tag;
-  final String? logTag;
-  final Stream<int> currentIndexStream;
-  final int photoGirdSize;
-  final bool areFilesCollatedByDay;
-  final bool limitSelectionToOne;
-  LazyLoadingGallery(
-    this.files,
-    this.index,
-    this.reloadEvent,
-    this.removalEventTypes,
-    this.asyncLoader,
-    this.selectedFiles,
-    this.tag,
-    this.currentIndexStream,
-    this.areFilesCollatedByDay, {
-    this.logTag = "",
-    this.photoGirdSize = photoGridSizeDefault,
-    this.limitSelectionToOne = false,
-    Key? key,
-  }) : super(key: key ?? UniqueKey());
-
-  @override
-  State<LazyLoadingGallery> createState() => _LazyLoadingGalleryState();
-}
-
-class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
-  static const kRecycleLimit = 400;
-  static const kNumberOfDaysToRenderBeforeAndAfter = 8;
-
-  late Logger _logger;
-
-  late List<File> _files;
-  late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
-  late StreamSubscription<int> _currentIndexSubscription;
-  bool? _shouldRender;
-  final ValueNotifier<bool> _toggleSelectAllFromDay = ValueNotifier(false);
-  final ValueNotifier<bool> _showSelectAllButton = ValueNotifier(false);
-  final ValueNotifier<bool> _areAllFromDaySelected = ValueNotifier(false);
-
-  @override
-  void initState() {
-    //this is for removing the 'select all from day' icon on unselecting all files with 'cancel'
-    widget.selectedFiles?.addListener(_selectedFilesListener);
-    super.initState();
-    _init();
-  }
-
-  void _init() {
-    _logger = Logger("LazyLoading_${widget.logTag}");
-    _shouldRender = true;
-    _files = widget.files;
-    _reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
-
-    _currentIndexSubscription =
-        widget.currentIndexStream.listen((currentIndex) {
-      final bool shouldRender = (currentIndex - widget.index).abs() <
-          kNumberOfDaysToRenderBeforeAndAfter;
-      if (mounted && shouldRender != _shouldRender) {
-        setState(() {
-          _shouldRender = shouldRender;
-        });
-      }
-    });
-  }
-
-  Future _onReload(FilesUpdatedEvent event) async {
-    final galleryDate =
-        DateTime.fromMicrosecondsSinceEpoch(_files[0].creationTime!);
-    final filesUpdatedThisDay = event.updatedFiles.where((file) {
-      final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
-      return fileDate.year == galleryDate.year &&
-          fileDate.month == galleryDate.month &&
-          fileDate.day == galleryDate.day;
-    });
-    if (filesUpdatedThisDay.isNotEmpty) {
-      if (kDebugMode) {
-        _logger.info(
-          filesUpdatedThisDay.length.toString() +
-              " files were updated due to ${event.reason} on " +
-              DateTime.fromMicrosecondsSinceEpoch(
-                galleryDate.microsecondsSinceEpoch,
-              ).toIso8601String(),
-        );
-      }
-      if (event.type == EventType.addedOrUpdated) {
-        final dayStartTime =
-            DateTime(galleryDate.year, galleryDate.month, galleryDate.day);
-        final result = await widget.asyncLoader(
-          dayStartTime.microsecondsSinceEpoch,
-          dayStartTime.microsecondsSinceEpoch + microSecondsInDay - 1,
-        );
-        if (mounted) {
-          setState(() {
-            _files = result.files;
-          });
-        }
-      } else if (widget.removalEventTypes.contains(event.type)) {
-        // Files were removed
-        final generatedFileIDs = <int?>{};
-        final uploadedFileIds = <int?>{};
-        for (final file in filesUpdatedThisDay) {
-          if (file.generatedID != null) {
-            generatedFileIDs.add(file.generatedID);
-          } else if (file.uploadedFileID != null) {
-            uploadedFileIds.add(file.uploadedFileID);
-          }
-        }
-        final List<File> files = [];
-        files.addAll(_files);
-        files.removeWhere(
-          (file) =>
-              generatedFileIDs.contains(file.generatedID) ||
-              uploadedFileIds.contains(file.uploadedFileID),
-        );
-        if (kDebugMode) {
-          _logger.finest(
-            "removed ${_files.length - files.length} due to ${event.reason}",
-          );
-        }
-        if (mounted) {
-          setState(() {
-            _files = files;
-          });
-        }
-      } else {
-        if (kDebugMode) {
-          debugPrint("Unexpected event ${event.type.name}");
-        }
-      }
-    }
-  }
-
-  @override
-  void dispose() {
-    _reloadEventSubscription?.cancel();
-    _currentIndexSubscription.cancel();
-    widget.selectedFiles?.removeListener(_selectedFilesListener);
-    _toggleSelectAllFromDay.dispose();
-    _showSelectAllButton.dispose();
-    _areAllFromDaySelected.dispose();
-    super.dispose();
-  }
-
-  @override
-  void didUpdateWidget(LazyLoadingGallery oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    if (!listEquals(_files, widget.files)) {
-      _reloadEventSubscription?.cancel();
-      _init();
-    }
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    if (_files.isEmpty) {
-      return const SizedBox.shrink();
-    }
-    return Column(
-      children: [
-        Row(
-          mainAxisAlignment: MainAxisAlignment.spaceBetween,
-          children: [
-            if (widget.areFilesCollatedByDay)
-              DayWidget(
-                timestamp: _files[0].creationTime!,
-                gridSize: widget.photoGirdSize,
-              ),
-            widget.limitSelectionToOne
-                ? const SizedBox.shrink()
-                : ValueListenableBuilder(
-                    valueListenable: _showSelectAllButton,
-                    builder: (context, dynamic value, _) {
-                      return !value
-                          ? const SizedBox.shrink()
-                          : GestureDetector(
-                              behavior: HitTestBehavior.translucent,
-                              child: SizedBox(
-                                width: 48,
-                                height: 44,
-                                child: ValueListenableBuilder(
-                                  valueListenable: _areAllFromDaySelected,
-                                  builder: (context, dynamic value, _) {
-                                    return value
-                                        ? const Icon(
-                                            Icons.check_circle,
-                                            size: 18,
-                                          )
-                                        : Icon(
-                                            Icons.check_circle_outlined,
-                                            color: getEnteColorScheme(context)
-                                                .strokeMuted,
-                                            size: 18,
-                                          );
-                                  },
-                                ),
-                              ),
-                              onTap: () {
-                                //this value has no significance
-                                //changing only to notify the listeners
-                                _toggleSelectAllFromDay.value =
-                                    !_toggleSelectAllFromDay.value;
-                              },
-                            );
-                    },
-                  )
-          ],
-        ),
-        _shouldRender!
-            ? _getGallery()
-            : PlaceHolderWidget(
-                _files.length,
-                widget.photoGirdSize,
-              ),
-      ],
-    );
-  }
-
-  Widget _getGallery() {
-    final List<Widget> childGalleries = [];
-    final subGalleryItemLimit = widget.photoGirdSize * subGalleryMultiplier;
-    for (int index = 0; index < _files.length; index += subGalleryItemLimit) {
-      childGalleries.add(
-        LazyLoadingGridView(
-          widget.tag,
-          _files.sublist(
-            index,
-            min(index + subGalleryItemLimit, _files.length),
-          ),
-          widget.asyncLoader,
-          widget.selectedFiles,
-          index == 0,
-          _files.length > kRecycleLimit,
-          _toggleSelectAllFromDay,
-          _areAllFromDaySelected,
-          widget.photoGirdSize,
-          limitSelectionToOne: widget.limitSelectionToOne,
-        ),
-      );
-    }
-
-    return Column(
-      children: childGalleries,
-    );
-  }
-
-  void _selectedFilesListener() {
-    if (widget.selectedFiles!.files.isEmpty) {
-      _showSelectAllButton.value = false;
-    } else {
-      _showSelectAllButton.value = true;
-    }
-  }
-}
-
-class LazyLoadingGridView extends StatefulWidget {
-  final String tag;
-  final List<File> filesInDay;
-  final GalleryLoader asyncLoader;
-  final SelectedFiles? selectedFiles;
-  final bool shouldRender;
-  final bool shouldRecycle;
-  final ValueNotifier toggleSelectAllFromDay;
-  final ValueNotifier areAllFilesSelected;
-  final int? photoGridSize;
-  final bool limitSelectionToOne;
-
-  LazyLoadingGridView(
-    this.tag,
-    this.filesInDay,
-    this.asyncLoader,
-    this.selectedFiles,
-    this.shouldRender,
-    this.shouldRecycle,
-    this.toggleSelectAllFromDay,
-    this.areAllFilesSelected,
-    this.photoGridSize, {
-    this.limitSelectionToOne = false,
-    Key? key,
-  }) : super(key: key ?? UniqueKey());
-
-  @override
-  State<LazyLoadingGridView> createState() => _LazyLoadingGridViewState();
-}
-
-class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
-  bool? _shouldRender;
-  int? _currentUserID;
-  late StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
-
-  @override
-  void initState() {
-    _shouldRender = widget.shouldRender;
-    _currentUserID = Configuration.instance.getUserID();
-    widget.selectedFiles?.addListener(_selectedFilesListener);
-    _clearSelectionsEvent =
-        Bus.instance.on<ClearSelectionsEvent>().listen((event) {
-      if (mounted) {
-        setState(() {});
-      }
-    });
-    widget.toggleSelectAllFromDay.addListener(_toggleSelectAllFromDayListener);
-    super.initState();
-  }
-
-  @override
-  void dispose() {
-    widget.selectedFiles?.removeListener(_selectedFilesListener);
-    _clearSelectionsEvent.cancel();
-    widget.toggleSelectAllFromDay
-        .removeListener(_toggleSelectAllFromDayListener);
-    super.dispose();
-  }
-
-  @override
-  void didUpdateWidget(LazyLoadingGridView oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    if (!listEquals(widget.filesInDay, oldWidget.filesInDay)) {
-      _shouldRender = widget.shouldRender;
-    }
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    if (widget.shouldRecycle) {
-      return _getRecyclableView();
-    } else {
-      return _getNonRecyclableView();
-    }
-  }
-
-  Widget _getRecyclableView() {
-    return VisibilityDetector(
-      key: Key("gallery" + widget.filesInDay.first.tag),
-      onVisibilityChanged: (visibility) {
-        final shouldRender = visibility.visibleFraction > 0;
-        if (mounted && shouldRender != _shouldRender) {
-          setState(() {
-            _shouldRender = shouldRender;
-          });
-        }
-      },
-      child: _shouldRender!
-          ? _getGridView()
-          : PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize!),
-    );
-  }
-
-  Widget _getNonRecyclableView() {
-    if (!_shouldRender!) {
-      return VisibilityDetector(
-        key: Key("gallery" + widget.filesInDay.first.tag),
-        onVisibilityChanged: (visibility) {
-          if (mounted && visibility.visibleFraction > 0 && !_shouldRender!) {
-            setState(() {
-              _shouldRender = true;
-            });
-          }
-        },
-        child:
-            PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize!),
-      );
-    } else {
-      return _getGridView();
-    }
-  }
-
-  Widget _getGridView() {
-    return GridView.builder(
-      shrinkWrap: true,
-      physics: const NeverScrollableScrollPhysics(),
-      // to disable GridView's scrolling
-      itemBuilder: (context, index) {
-        return _buildFile(context, widget.filesInDay[index]);
-      },
-      itemCount: widget.filesInDay.length,
-      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
-        crossAxisSpacing: 2,
-        mainAxisSpacing: 2,
-        crossAxisCount: widget.photoGridSize!,
-      ),
-      padding: const EdgeInsets.symmetric(vertical: (galleryGridSpacing / 2)),
-    );
-  }
-
-  Widget _buildFile(BuildContext context, File file) {
-    final isFileSelected = widget.selectedFiles?.isFileSelected(file) ?? false;
-    Color selectionColor = Colors.white;
-    if (isFileSelected &&
-        file.isUploaded &&
-        (file.ownerID != _currentUserID ||
-            file.pubMagicMetadata!.uploaderName != null)) {
-      final avatarColors = getEnteColorScheme(context).avatarColors;
-      final int randomID = file.ownerID != _currentUserID
-          ? file.ownerID!
-          : file.pubMagicMetadata!.uploaderName.sumAsciiValues;
-      selectionColor = avatarColors[(randomID).remainder(avatarColors.length)];
-    }
-    return GestureDetector(
-      onTap: () {
-        widget.limitSelectionToOne
-            ? _onTapWithSelectionLimit(file)
-            : _onTapNoSelectionLimit(file);
-      },
-      onLongPress: () {
-        widget.limitSelectionToOne
-            ? _onLongPressWithSelectionLimit(file)
-            : _onLongPressNoSelectionLimit(file);
-      },
-      child: ClipRRect(
-        borderRadius: BorderRadius.circular(1),
-        child: Stack(
-          children: [
-            Hero(
-              tag: widget.tag + file.tag,
-              child: ColorFiltered(
-                colorFilter: ColorFilter.mode(
-                  Colors.black.withOpacity(
-                    isFileSelected ? 0.4 : 0,
-                  ),
-                  BlendMode.darken,
-                ),
-                child: ThumbnailWidget(
-                  file,
-                  diskLoadDeferDuration: thumbnailDiskLoadDeferDuration,
-                  serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
-                  shouldShowLivePhotoOverlay: true,
-                  key: Key(widget.tag + file.tag),
-                  thumbnailSize: widget.photoGridSize! < photoGridSizeDefault
-                      ? thumbnailLargeSize
-                      : thumbnailSmallSize,
-                  shouldShowOwnerAvatar: !isFileSelected,
-                ),
-              ),
-            ),
-            Visibility(
-              visible: isFileSelected,
-              child: Positioned(
-                right: 4,
-                top: 4,
-                child: Icon(
-                  Icons.check_circle_rounded,
-                  size: 20,
-                  color: selectionColor, //same for both themes
-                ),
-              ),
-            )
-          ],
-        ),
-      ),
-    );
-  }
-
-  void _toggleFileSelection(File file) {
-    widget.selectedFiles!.toggleSelection(file);
-  }
-
-  void _onTapNoSelectionLimit(File file) async {
-    if (widget.selectedFiles?.files.isNotEmpty ?? false) {
-      _toggleFileSelection(file);
-    } else {
-      if (AppLifecycleService.instance.mediaExtensionAction.action ==
-          IntentAction.pick) {
-        final ioFile = await getFile(file);
-        MediaExtension().setResult("file://${ioFile!.path}");
-      } else {
-        _routeToDetailPage(file, context);
-      }
-    }
-  }
-
-  void _onTapWithSelectionLimit(File file) {
-    if (widget.selectedFiles!.files.isNotEmpty &&
-        widget.selectedFiles!.files.first != file) {
-      widget.selectedFiles!.clearAll();
-    }
-    _toggleFileSelection(file);
-  }
-
-  void _onLongPressNoSelectionLimit(File file) {
-    if (widget.selectedFiles!.files.isNotEmpty) {
-      _routeToDetailPage(file, context);
-    } else if (AppLifecycleService.instance.mediaExtensionAction.action ==
-        IntentAction.main) {
-      HapticFeedback.lightImpact();
-      _toggleFileSelection(file);
-    }
-  }
-
-  Future<void> _onLongPressWithSelectionLimit(File file) async {
-    if (AppLifecycleService.instance.mediaExtensionAction.action ==
-        IntentAction.pick) {
-      final ioFile = await getFile(file);
-      MediaExtension().setResult("file://${ioFile!.path}");
-    } else {
-      _routeToDetailPage(file, context);
-    }
-  }
-
-  void _routeToDetailPage(File file, BuildContext context) {
-    final page = DetailPage(
-      DetailPageConfiguration(
-        List.unmodifiable(widget.filesInDay),
-        widget.asyncLoader,
-        widget.filesInDay.indexOf(file),
-        widget.tag,
-      ),
-    );
-    routeToPage(context, page, forceCustomPageRoute: true);
-  }
-
-  void _selectedFilesListener() {
-    if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
-      widget.areAllFilesSelected.value = true;
-    } else {
-      widget.areAllFilesSelected.value = false;
-    }
-    bool shouldRefresh = false;
-    for (final file in widget.filesInDay) {
-      if (widget.selectedFiles!.isPartOfLastSelected(file)) {
-        shouldRefresh = true;
-      }
-    }
-    if (shouldRefresh && mounted) {
-      setState(() {});
-    }
-  }
-
-  void _toggleSelectAllFromDayListener() {
-    if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
-      setState(() {
-        widget.selectedFiles!.unSelectAll(widget.filesInDay.toSet());
-      });
-    } else {
-      widget.selectedFiles!.selectAll(widget.filesInDay.toSet());
-    }
-  }
-}

+ 166 - 0
lib/ui/viewer/gallery/component/gallery_file_widget.dart

@@ -0,0 +1,166 @@
+import "package:flutter/material.dart";
+import "package:flutter/services.dart";
+import "package:media_extension/media_extension.dart";
+import "package:media_extension/media_extension_action_types.dart";
+import "package:photos/core/constants.dart";
+import "package:photos/extensions/string_ext.dart";
+import "package:photos/models/file.dart";
+import "package:photos/models/selected_files.dart";
+import "package:photos/services/app_lifecycle_service.dart";
+import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/viewer/file/detail_page.dart";
+import "package:photos/ui/viewer/file/thumbnail_widget.dart";
+import "package:photos/ui/viewer/gallery/gallery.dart";
+import "package:photos/utils/file_util.dart";
+import "package:photos/utils/navigation_util.dart";
+
+class GalleryFileWidget extends StatelessWidget {
+  final File file;
+  final SelectedFiles? selectedFiles;
+  final bool limitSelectionToOne;
+  final String tag;
+  final int photoGridSize;
+  final int? currentUserID;
+  final List<File> filesInDay;
+  final GalleryLoader asyncLoader;
+  const GalleryFileWidget({
+    required this.file,
+    required this.selectedFiles,
+    required this.limitSelectionToOne,
+    required this.tag,
+    required this.photoGridSize,
+    required this.currentUserID,
+    required this.filesInDay,
+    required this.asyncLoader,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final isFileSelected = selectedFiles?.isFileSelected(file) ?? false;
+    Color selectionColor = Colors.white;
+    if (isFileSelected &&
+        file.isUploaded &&
+        (file.ownerID != currentUserID ||
+            file.pubMagicMetadata!.uploaderName != null)) {
+      final avatarColors = getEnteColorScheme(context).avatarColors;
+      final int randomID = file.ownerID != currentUserID
+          ? file.ownerID!
+          : file.pubMagicMetadata!.uploaderName.sumAsciiValues;
+      selectionColor = avatarColors[(randomID).remainder(avatarColors.length)];
+    }
+    return GestureDetector(
+      onTap: () {
+        limitSelectionToOne
+            ? _onTapWithSelectionLimit(file)
+            : _onTapNoSelectionLimit(context, file);
+      },
+      onLongPress: () {
+        limitSelectionToOne
+            ? _onLongPressWithSelectionLimit(context, file)
+            : _onLongPressNoSelectionLimit(context, file);
+      },
+      child: ClipRRect(
+        borderRadius: BorderRadius.circular(1),
+        child: Stack(
+          children: [
+            Hero(
+              tag: tag + file.tag,
+              child: ColorFiltered(
+                colorFilter: ColorFilter.mode(
+                  Colors.black.withOpacity(
+                    isFileSelected ? 0.4 : 0,
+                  ),
+                  BlendMode.darken,
+                ),
+                child: ThumbnailWidget(
+                  file,
+                  diskLoadDeferDuration: thumbnailDiskLoadDeferDuration,
+                  serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
+                  shouldShowLivePhotoOverlay: true,
+                  key: Key(tag + file.tag),
+                  thumbnailSize: photoGridSize < photoGridSizeDefault
+                      ? thumbnailLargeSize
+                      : thumbnailSmallSize,
+                  shouldShowOwnerAvatar: !isFileSelected,
+                ),
+              ),
+            ),
+            Visibility(
+              visible: isFileSelected,
+              child: Positioned(
+                right: 4,
+                top: 4,
+                child: Icon(
+                  Icons.check_circle_rounded,
+                  size: 20,
+                  color: selectionColor, //same for both themes
+                ),
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  void _toggleFileSelection(File file) {
+    selectedFiles!.toggleSelection(file);
+  }
+
+  void _onTapWithSelectionLimit(File file) {
+    if (selectedFiles!.files.isNotEmpty && selectedFiles!.files.first != file) {
+      selectedFiles!.clearAll();
+    }
+    _toggleFileSelection(file);
+  }
+
+  void _onTapNoSelectionLimit(BuildContext context, File file) async {
+    if (selectedFiles?.files.isNotEmpty ?? false) {
+      _toggleFileSelection(file);
+    } else {
+      if (AppLifecycleService.instance.mediaExtensionAction.action ==
+          IntentAction.pick) {
+        final ioFile = await getFile(file);
+        MediaExtension().setResult("file://${ioFile!.path}");
+      } else {
+        _routeToDetailPage(file, context);
+      }
+    }
+  }
+
+  void _onLongPressNoSelectionLimit(BuildContext context, File file) {
+    if (selectedFiles!.files.isNotEmpty) {
+      _routeToDetailPage(file, context);
+    } else if (AppLifecycleService.instance.mediaExtensionAction.action ==
+        IntentAction.main) {
+      HapticFeedback.lightImpact();
+      _toggleFileSelection(file);
+    }
+  }
+
+  Future<void> _onLongPressWithSelectionLimit(
+    BuildContext context,
+    File file,
+  ) async {
+    if (AppLifecycleService.instance.mediaExtensionAction.action ==
+        IntentAction.pick) {
+      final ioFile = await getFile(file);
+      MediaExtension().setResult("file://${ioFile!.path}");
+    } else {
+      _routeToDetailPage(file, context);
+    }
+  }
+
+  void _routeToDetailPage(File file, BuildContext context) {
+    final page = DetailPage(
+      DetailPageConfiguration(
+        List.unmodifiable(filesInDay),
+        asyncLoader,
+        filesInDay.indexOf(file),
+        tag,
+      ),
+    );
+    routeToPage(context, page, forceCustomPageRoute: true);
+  }
+}

+ 137 - 0
lib/ui/viewer/gallery/component/gallery_list_view_widget.dart

@@ -0,0 +1,137 @@
+import "package:flutter/material.dart";
+import "package:logging/logging.dart";
+import "package:photos/core/event_bus.dart";
+import "package:photos/ente_theme_data.dart";
+import "package:photos/events/files_updated_event.dart";
+import "package:photos/models/file.dart";
+import "package:photos/models/selected_files.dart";
+import "package:photos/ui/common/loading_widget.dart";
+import "package:photos/ui/huge_listview/huge_listview.dart";
+import "package:photos/ui/viewer/gallery/component/lazy_loading_gallery.dart";
+import "package:photos/ui/viewer/gallery/gallery.dart";
+import "package:photos/utils/date_time_util.dart";
+import "package:photos/utils/local_settings.dart";
+import "package:scrollable_positioned_list/scrollable_positioned_list.dart";
+
+class GalleryListView extends StatelessWidget {
+  final GlobalKey<HugeListViewState<dynamic>> hugeListViewKey;
+  final ItemScrollController itemScroller;
+  final List<List<File>> collatedFiles;
+  final bool disableScroll;
+  final Widget? header;
+  final Widget? footer;
+  final Widget emptyState;
+  final GalleryLoader asyncLoader;
+  final Stream<FilesUpdatedEvent>? reloadEvent;
+  final Set<EventType> removalEventTypes;
+  final String tagPrefix;
+  final double scrollBottomSafeArea;
+  final bool limitSelectionToOne;
+  final SelectedFiles? selectedFiles;
+  final bool shouldCollateFilesByDay;
+  final String logTag;
+  final Logger logger;
+
+  const GalleryListView({
+    required this.hugeListViewKey,
+    required this.itemScroller,
+    required this.collatedFiles,
+    required this.disableScroll,
+    this.header,
+    this.footer,
+    required this.emptyState,
+    required this.asyncLoader,
+    this.reloadEvent,
+    required this.removalEventTypes,
+    required this.tagPrefix,
+    required this.scrollBottomSafeArea,
+    required this.limitSelectionToOne,
+    this.selectedFiles,
+    required this.shouldCollateFilesByDay,
+    required this.logTag,
+    required this.logger,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return HugeListView<List<File>>(
+      key: hugeListViewKey,
+      controller: itemScroller,
+      startIndex: 0,
+      totalCount: collatedFiles.length,
+      isDraggableScrollbarEnabled: collatedFiles.length > 10,
+      disableScroll: disableScroll,
+      waitBuilder: (_) {
+        return const EnteLoadingWidget();
+      },
+      emptyResultBuilder: (_) {
+        final List<Widget> children = [];
+        if (header != null) {
+          children.add(header!);
+        }
+        children.add(
+          Expanded(
+            child: emptyState,
+          ),
+        );
+        if (footer != null) {
+          children.add(footer!);
+        }
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: children,
+        );
+      },
+      itemBuilder: (context, index) {
+        Widget gallery;
+        gallery = LazyLoadingGallery(
+          collatedFiles[index],
+          index,
+          reloadEvent,
+          removalEventTypes,
+          asyncLoader,
+          selectedFiles,
+          tagPrefix,
+          Bus.instance
+              .on<GalleryIndexUpdatedEvent>()
+              .where((event) => event.tag == tagPrefix)
+              .map((event) => event.index),
+          shouldCollateFilesByDay,
+          logTag: logTag,
+          photoGridSize: LocalSettings.instance.getPhotoGridSize(),
+          limitSelectionToOne: limitSelectionToOne,
+        );
+        if (header != null && index == 0) {
+          gallery = Column(children: [header!, gallery]);
+        }
+        if (footer != null && index == collatedFiles.length - 1) {
+          gallery = Column(children: [gallery, footer!]);
+        }
+        return gallery;
+      },
+      labelTextBuilder: (int index) {
+        try {
+          return getMonthAndYear(
+            DateTime.fromMicrosecondsSinceEpoch(
+              collatedFiles[index][0].creationTime!,
+            ),
+          );
+        } catch (e) {
+          logger.severe("label text builder failed", e);
+          return "";
+        }
+      },
+      thumbBackgroundColor:
+          Theme.of(context).colorScheme.galleryThumbBackgroundColor,
+      thumbDrawColor: Theme.of(context).colorScheme.galleryThumbDrawColor,
+      thumbPadding: header != null
+          ? const EdgeInsets.only(top: 60)
+          : const EdgeInsets.all(0),
+      bottomSafeArea: scrollBottomSafeArea,
+      firstShown: (int firstIndex) {
+        Bus.instance.fire(GalleryIndexUpdatedEvent(tagPrefix, firstIndex));
+      },
+    );
+  }
+}

+ 359 - 0
lib/ui/viewer/gallery/component/lazy_loading_gallery.dart

@@ -0,0 +1,359 @@
+import 'dart:async';
+import 'dart:math';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/events/files_updated_event.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/huge_listview/place_holder_widget.dart';
+import "package:photos/ui/viewer/gallery/component/day_widget.dart";
+import "package:photos/ui/viewer/gallery/component/gallery_file_widget.dart";
+import 'package:photos/ui/viewer/gallery/component/lazy_loading_grid_view.dart';
+import 'package:photos/ui/viewer/gallery/gallery.dart';
+
+class LazyLoadingGallery extends StatefulWidget {
+  final List<File> files;
+  final int index;
+  final Stream<FilesUpdatedEvent>? reloadEvent;
+  final Set<EventType> removalEventTypes;
+  final GalleryLoader asyncLoader;
+  final SelectedFiles? selectedFiles;
+  final String tag;
+  final String? logTag;
+  final Stream<int> currentIndexStream;
+  final int photoGridSize;
+  final bool areFilesCollatedByDay;
+  final bool limitSelectionToOne;
+  LazyLoadingGallery(
+    this.files,
+    this.index,
+    this.reloadEvent,
+    this.removalEventTypes,
+    this.asyncLoader,
+    this.selectedFiles,
+    this.tag,
+    this.currentIndexStream,
+    this.areFilesCollatedByDay, {
+    this.logTag = "",
+    this.photoGridSize = photoGridSizeDefault,
+    this.limitSelectionToOne = false,
+    Key? key,
+  }) : super(key: key ?? UniqueKey());
+
+  @override
+  State<LazyLoadingGallery> createState() => _LazyLoadingGalleryState();
+}
+
+class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
+  static const kNumberOfDaysToRenderBeforeAndAfter = 8;
+
+  late Logger _logger;
+
+  late List<File> _files;
+  late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
+  late StreamSubscription<int> _currentIndexSubscription;
+  bool? _shouldRender;
+  final ValueNotifier<bool> _toggleSelectAllFromDay = ValueNotifier(false);
+  final ValueNotifier<bool> _showSelectAllButton = ValueNotifier(false);
+  final ValueNotifier<bool> _areAllFromDaySelected = ValueNotifier(false);
+
+  @override
+  void initState() {
+    //this is for removing the 'select all from day' icon on unselecting all files with 'cancel'
+    widget.selectedFiles?.addListener(_selectedFilesListener);
+    super.initState();
+    _init();
+  }
+
+  void _init() {
+    _logger = Logger("LazyLoading_${widget.logTag}");
+    _shouldRender = true;
+    _files = widget.files;
+    _reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
+
+    _currentIndexSubscription =
+        widget.currentIndexStream.listen((currentIndex) {
+      final bool shouldRender = (currentIndex - widget.index).abs() <
+          kNumberOfDaysToRenderBeforeAndAfter;
+      if (mounted && shouldRender != _shouldRender) {
+        setState(() {
+          _shouldRender = shouldRender;
+        });
+      }
+    });
+  }
+
+  Future _onReload(FilesUpdatedEvent event) async {
+    final galleryDate =
+        DateTime.fromMicrosecondsSinceEpoch(_files[0].creationTime!);
+    final filesUpdatedThisDay = event.updatedFiles.where((file) {
+      final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
+      return fileDate.year == galleryDate.year &&
+          fileDate.month == galleryDate.month &&
+          fileDate.day == galleryDate.day;
+    });
+    if (filesUpdatedThisDay.isNotEmpty) {
+      if (kDebugMode) {
+        _logger.info(
+          filesUpdatedThisDay.length.toString() +
+              " files were updated due to ${event.reason} on " +
+              DateTime.fromMicrosecondsSinceEpoch(
+                galleryDate.microsecondsSinceEpoch,
+              ).toIso8601String(),
+        );
+      }
+      if (event.type == EventType.addedOrUpdated) {
+        final dayStartTime =
+            DateTime(galleryDate.year, galleryDate.month, galleryDate.day);
+        final result = await widget.asyncLoader(
+          dayStartTime.microsecondsSinceEpoch,
+          dayStartTime.microsecondsSinceEpoch + microSecondsInDay - 1,
+        );
+        if (mounted) {
+          setState(() {
+            _files = result.files;
+          });
+        }
+      } else if (widget.removalEventTypes.contains(event.type)) {
+        // Files were removed
+        final generatedFileIDs = <int?>{};
+        final uploadedFileIds = <int?>{};
+        for (final file in filesUpdatedThisDay) {
+          if (file.generatedID != null) {
+            generatedFileIDs.add(file.generatedID);
+          } else if (file.uploadedFileID != null) {
+            uploadedFileIds.add(file.uploadedFileID);
+          }
+        }
+        final List<File> files = [];
+        files.addAll(_files);
+        files.removeWhere(
+          (file) =>
+              generatedFileIDs.contains(file.generatedID) ||
+              uploadedFileIds.contains(file.uploadedFileID),
+        );
+        if (kDebugMode) {
+          _logger.finest(
+            "removed ${_files.length - files.length} due to ${event.reason}",
+          );
+        }
+        if (mounted) {
+          setState(() {
+            _files = files;
+          });
+        }
+      } else {
+        if (kDebugMode) {
+          debugPrint("Unexpected event ${event.type.name}");
+        }
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    _reloadEventSubscription?.cancel();
+    _currentIndexSubscription.cancel();
+    widget.selectedFiles?.removeListener(_selectedFilesListener);
+    _toggleSelectAllFromDay.dispose();
+    _showSelectAllButton.dispose();
+    _areAllFromDaySelected.dispose();
+    super.dispose();
+  }
+
+  @override
+  void didUpdateWidget(LazyLoadingGallery oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (!listEquals(_files, widget.files)) {
+      _reloadEventSubscription?.cancel();
+      _init();
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_files.isEmpty) {
+      return const SizedBox.shrink();
+    }
+    return Column(
+      children: [
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            if (widget.areFilesCollatedByDay)
+              DayWidget(
+                timestamp: _files[0].creationTime!,
+                gridSize: widget.photoGridSize,
+              ),
+            widget.limitSelectionToOne
+                ? const SizedBox.shrink()
+                : ValueListenableBuilder(
+                    valueListenable: _showSelectAllButton,
+                    builder: (context, dynamic value, _) {
+                      return !value
+                          ? const SizedBox.shrink()
+                          : GestureDetector(
+                              behavior: HitTestBehavior.translucent,
+                              child: SizedBox(
+                                width: 48,
+                                height: 44,
+                                child: ValueListenableBuilder(
+                                  valueListenable: _areAllFromDaySelected,
+                                  builder: (context, dynamic value, _) {
+                                    return value
+                                        ? const Icon(
+                                            Icons.check_circle,
+                                            size: 18,
+                                          )
+                                        : Icon(
+                                            Icons.check_circle_outlined,
+                                            color: getEnteColorScheme(context)
+                                                .strokeMuted,
+                                            size: 18,
+                                          );
+                                  },
+                                ),
+                              ),
+                              onTap: () {
+                                //this value has no significance
+                                //changing only to notify the listeners
+                                _toggleSelectAllFromDay.value =
+                                    !_toggleSelectAllFromDay.value;
+                              },
+                            );
+                    },
+                  )
+          ],
+        ),
+        _shouldRender!
+            ? GetGallery(
+                photoGridSize: widget.photoGridSize,
+                files: _files,
+                tag: widget.tag,
+                asyncLoader: widget.asyncLoader,
+                selectedFiles: widget.selectedFiles,
+                toggleSelectAllFromDay: _toggleSelectAllFromDay,
+                areAllFromDaySelected: _areAllFromDaySelected,
+                limitSelectionToOne: widget.limitSelectionToOne,
+              )
+            : PlaceHolderWidget(
+                _files.length,
+                widget.photoGridSize,
+              ),
+      ],
+    );
+  }
+
+  void _selectedFilesListener() {
+    if (widget.selectedFiles!.files.isEmpty) {
+      _showSelectAllButton.value = false;
+    } else {
+      _showSelectAllButton.value = true;
+    }
+  }
+}
+
+class GetGallery extends StatelessWidget {
+  final int photoGridSize;
+  final List<File> files;
+  final String tag;
+  final GalleryLoader asyncLoader;
+  final SelectedFiles? selectedFiles;
+  final ValueNotifier<bool> toggleSelectAllFromDay;
+  final ValueNotifier<bool> areAllFromDaySelected;
+  final bool limitSelectionToOne;
+  const GetGallery({
+    required this.photoGridSize,
+    required this.files,
+    required this.tag,
+    required this.asyncLoader,
+    required this.selectedFiles,
+    required this.toggleSelectAllFromDay,
+    required this.areAllFromDaySelected,
+    required this.limitSelectionToOne,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    const kRecycleLimit = 400;
+    final List<Widget> childGalleries = [];
+    final subGalleryItemLimit = photoGridSize * subGalleryMultiplier;
+
+    for (int index = 0; index < files.length; index += subGalleryItemLimit) {
+      childGalleries.add(
+        LazyLoadingGridView(
+          tag,
+          files.sublist(
+            index,
+            min(index + subGalleryItemLimit, files.length),
+          ),
+          asyncLoader,
+          selectedFiles,
+          index == 0,
+          files.length > kRecycleLimit,
+          toggleSelectAllFromDay,
+          areAllFromDaySelected,
+          photoGridSize,
+          limitSelectionToOne: limitSelectionToOne,
+        ),
+      );
+    }
+
+    return Column(
+      children: childGalleries,
+    );
+  }
+}
+
+class GalleryGridViewWidget extends StatelessWidget {
+  final List<File> filesInDay;
+  final int photoGridSize;
+  final SelectedFiles? selectedFiles;
+  final bool limitSelectionToOne;
+  final String tag;
+  final int? currentUserID;
+  final GalleryLoader asyncLoader;
+  const GalleryGridViewWidget({
+    required this.filesInDay,
+    required this.photoGridSize,
+    this.selectedFiles,
+    required this.limitSelectionToOne,
+    required this.tag,
+    super.key,
+    this.currentUserID,
+    required this.asyncLoader,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return GridView.builder(
+      shrinkWrap: true,
+      physics: const NeverScrollableScrollPhysics(),
+      // to disable GridView's scrolling
+      itemBuilder: (context, index) {
+        return GalleryFileWidget(
+          file: filesInDay[index],
+          selectedFiles: selectedFiles,
+          limitSelectionToOne: limitSelectionToOne,
+          tag: tag,
+          photoGridSize: photoGridSize,
+          currentUserID: currentUserID,
+          filesInDay: filesInDay,
+          asyncLoader: asyncLoader,
+        );
+      },
+      itemCount: filesInDay.length,
+      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+        crossAxisSpacing: 2,
+        mainAxisSpacing: 2,
+        crossAxisCount: photoGridSize,
+      ),
+      padding: const EdgeInsets.symmetric(vertical: (galleryGridSpacing / 2)),
+    );
+  }
+}

+ 134 - 0
lib/ui/viewer/gallery/component/lazy_loading_grid_view.dart

@@ -0,0 +1,134 @@
+import "dart:async";
+
+import "package:flutter/foundation.dart";
+import "package:flutter/material.dart";
+import "package:photos/core/configuration.dart";
+import "package:photos/core/event_bus.dart";
+import "package:photos/events/clear_selections_event.dart";
+import "package:photos/models/file.dart";
+import "package:photos/models/selected_files.dart";
+import "package:photos/ui/viewer/gallery/component/non_recyclable_view_widget.dart";
+import "package:photos/ui/viewer/gallery/component/recyclable_view_widget.dart";
+import "package:photos/ui/viewer/gallery/gallery.dart";
+
+class LazyLoadingGridView extends StatefulWidget {
+  final String tag;
+  final List<File> filesInDay;
+  final GalleryLoader asyncLoader;
+  final SelectedFiles? selectedFiles;
+  final bool shouldRender;
+  final bool shouldRecycle;
+  final ValueNotifier toggleSelectAllFromDay;
+  final ValueNotifier areAllFilesSelected;
+  final int? photoGridSize;
+  final bool limitSelectionToOne;
+
+  LazyLoadingGridView(
+    this.tag,
+    this.filesInDay,
+    this.asyncLoader,
+    this.selectedFiles,
+    this.shouldRender,
+    this.shouldRecycle,
+    this.toggleSelectAllFromDay,
+    this.areAllFilesSelected,
+    this.photoGridSize, {
+    this.limitSelectionToOne = false,
+    Key? key,
+  }) : super(key: key ?? UniqueKey());
+
+  @override
+  State<LazyLoadingGridView> createState() => _LazyLoadingGridViewState();
+}
+
+class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
+  late bool _shouldRender;
+  int? _currentUserID;
+  late StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
+
+  @override
+  void initState() {
+    _shouldRender = widget.shouldRender;
+    _currentUserID = Configuration.instance.getUserID();
+    widget.selectedFiles?.addListener(_selectedFilesListener);
+    _clearSelectionsEvent =
+        Bus.instance.on<ClearSelectionsEvent>().listen((event) {
+      if (mounted) {
+        setState(() {});
+      }
+    });
+    widget.toggleSelectAllFromDay.addListener(_toggleSelectAllFromDayListener);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    widget.selectedFiles?.removeListener(_selectedFilesListener);
+    _clearSelectionsEvent.cancel();
+    widget.toggleSelectAllFromDay
+        .removeListener(_toggleSelectAllFromDayListener);
+    super.dispose();
+  }
+
+  @override
+  void didUpdateWidget(LazyLoadingGridView oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (!listEquals(widget.filesInDay, oldWidget.filesInDay)) {
+      _shouldRender = widget.shouldRender;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (widget.shouldRecycle) {
+      return RecyclableViewWidget(
+        shouldRender: _shouldRender,
+        filesInDay: widget.filesInDay,
+        photoGridSize: widget.photoGridSize!,
+        limitSelectionToOne: widget.limitSelectionToOne,
+        tag: widget.tag,
+        asyncLoader: widget.asyncLoader,
+        selectedFiles: widget.selectedFiles,
+        currentUserID: _currentUserID,
+      );
+    } else {
+      return NonRecyclableViewWidget(
+        shouldRender: _shouldRender,
+        filesInDay: widget.filesInDay,
+        photoGridSize: widget.photoGridSize!,
+        limitSelectionToOne: widget.limitSelectionToOne,
+        tag: widget.tag,
+        asyncLoader: widget.asyncLoader,
+        selectedFiles: widget.selectedFiles,
+        currentUserID: _currentUserID,
+      );
+    }
+  }
+
+  void _selectedFilesListener() {
+    if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
+      widget.areAllFilesSelected.value = true;
+    } else {
+      widget.areAllFilesSelected.value = false;
+    }
+    bool shouldRefresh = false;
+    for (final file in widget.filesInDay) {
+      if (widget.selectedFiles!.isPartOfLastSelected(file)) {
+        shouldRefresh = true;
+      }
+    }
+    if (shouldRefresh && mounted) {
+      setState(() {});
+    }
+  }
+
+  void _toggleSelectAllFromDayListener() {
+    if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
+      setState(() {
+        widget.selectedFiles!.unSelectAll(widget.filesInDay.toSet());
+      });
+    } else {
+      widget.selectedFiles!.selectAll(widget.filesInDay.toSet());
+    }
+  }
+}

+ 70 - 0
lib/ui/viewer/gallery/component/non_recyclable_view_widget.dart

@@ -0,0 +1,70 @@
+import "package:flutter/material.dart";
+import "package:photos/models/file.dart";
+import "package:photos/models/selected_files.dart";
+import "package:photos/ui/huge_listview/place_holder_widget.dart";
+import "package:photos/ui/viewer/gallery/component/lazy_loading_gallery.dart";
+import "package:photos/ui/viewer/gallery/gallery.dart";
+import "package:visibility_detector/visibility_detector.dart";
+
+class NonRecyclableViewWidget extends StatefulWidget {
+  final bool shouldRender;
+  final List<File> filesInDay;
+  final int photoGridSize;
+  final bool limitSelectionToOne;
+  final String tag;
+  final GalleryLoader asyncLoader;
+  final int? currentUserID;
+  final SelectedFiles? selectedFiles;
+  const NonRecyclableViewWidget({
+    required this.shouldRender,
+    required this.filesInDay,
+    required this.photoGridSize,
+    required this.limitSelectionToOne,
+    required this.tag,
+    required this.asyncLoader,
+    this.currentUserID,
+    this.selectedFiles,
+    super.key,
+  });
+
+  @override
+  State<NonRecyclableViewWidget> createState() =>
+      _NonRecyclableViewWidgetState();
+}
+
+class _NonRecyclableViewWidgetState extends State<NonRecyclableViewWidget> {
+  late bool _shouldRender;
+  @override
+  void initState() {
+    _shouldRender = widget.shouldRender;
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (!_shouldRender!) {
+      return VisibilityDetector(
+        key: Key("gallery" + widget.filesInDay.first.tag),
+        onVisibilityChanged: (visibility) {
+          if (mounted && visibility.visibleFraction > 0 && !_shouldRender) {
+            setState(() {
+              _shouldRender = true;
+            });
+          }
+        },
+        child:
+            PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
+      );
+    } else {
+      return GalleryGridViewWidget(
+        filesInDay: widget.filesInDay,
+        photoGridSize: widget.photoGridSize,
+        limitSelectionToOne: widget.limitSelectionToOne,
+        tag: widget.tag,
+        asyncLoader: widget.asyncLoader,
+        selectedFiles: widget.selectedFiles,
+        currentUserID: widget.currentUserID,
+      );
+    }
+  }
+}

+ 67 - 0
lib/ui/viewer/gallery/component/recyclable_view_widget.dart

@@ -0,0 +1,67 @@
+import "package:flutter/material.dart";
+import "package:photos/models/file.dart";
+import "package:photos/models/selected_files.dart";
+import "package:photos/ui/huge_listview/place_holder_widget.dart";
+import "package:photos/ui/viewer/gallery/component/lazy_loading_gallery.dart";
+import "package:photos/ui/viewer/gallery/gallery.dart";
+import "package:visibility_detector/visibility_detector.dart";
+
+class RecyclableViewWidget extends StatefulWidget {
+  final bool shouldRender;
+  final List<File> filesInDay;
+  final int photoGridSize;
+  final bool limitSelectionToOne;
+  final String tag;
+  final GalleryLoader asyncLoader;
+  final int? currentUserID;
+  final SelectedFiles? selectedFiles;
+  const RecyclableViewWidget({
+    required this.shouldRender,
+    required this.filesInDay,
+    required this.photoGridSize,
+    required this.limitSelectionToOne,
+    required this.tag,
+    required this.asyncLoader,
+    this.currentUserID,
+    this.selectedFiles,
+    super.key,
+  });
+
+  @override
+  State<RecyclableViewWidget> createState() => _RecyclableViewWidgetState();
+}
+
+class _RecyclableViewWidgetState extends State<RecyclableViewWidget> {
+  late bool _shouldRender;
+  @override
+  void initState() {
+    _shouldRender = widget.shouldRender;
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return VisibilityDetector(
+      key: Key("gallery" + widget.filesInDay.first.tag),
+      onVisibilityChanged: (visibility) {
+        final shouldRender = visibility.visibleFraction > 0;
+        if (mounted && shouldRender != _shouldRender) {
+          setState(() {
+            _shouldRender = shouldRender;
+          });
+        }
+      },
+      child: _shouldRender
+          ? GalleryGridViewWidget(
+              filesInDay: widget.filesInDay,
+              photoGridSize: widget.photoGridSize,
+              limitSelectionToOne: widget.limitSelectionToOne,
+              tag: widget.tag,
+              asyncLoader: widget.asyncLoader,
+              selectedFiles: widget.selectedFiles,
+              currentUserID: widget.currentUserID,
+            )
+          : PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
+    );
+  }
+}

+ 22 - 90
lib/ui/viewer/gallery/gallery.dart

@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/event_bus.dart';
-import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/event.dart';
 import 'package:photos/events/files_updated_event.dart';
 import 'package:photos/events/tab_changed_event.dart';
@@ -14,10 +13,9 @@ import 'package:photos/models/file_load_result.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/huge_listview/huge_listview.dart';
-import 'package:photos/ui/huge_listview/lazy_loading_gallery.dart';
+import "package:photos/ui/viewer/gallery/component/gallery_list_view_widget.dart";
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/utils/date_time_util.dart';
-import 'package:photos/utils/local_settings.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 
 typedef GalleryLoader = Future<FileLoadResult> Function(
@@ -79,18 +77,17 @@ class _GalleryState extends State<Gallery> {
   late Logger _logger;
   List<List<File>> _collatedFiles = [];
   bool _hasLoadedFiles = false;
-  ItemScrollController? _itemScroller;
+  late ItemScrollController _itemScroller;
   StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
   StreamSubscription<TabDoubleTapEvent>? _tabDoubleTapEvent;
   final _forceReloadEventSubscriptions = <StreamSubscription<Event>>[];
-  String? _logTag;
-  late int _photoGridSize;
+  late String _logTag;
 
   @override
   void initState() {
     _logTag =
         "Gallery_${widget.tagPrefix}${kDebugMode ? "_" + widget.albumName! : ""}";
-    _logger = Logger(_logTag!);
+    _logger = Logger(_logTag);
     _logger.finest("init Gallery");
     _itemScroller = ItemScrollController();
     if (widget.reloadEvent != null) {
@@ -112,7 +109,7 @@ class _GalleryState extends State<Gallery> {
       // todo: Assign ID to Gallery and fire generic event with ID &
       //  target index/date
       if (mounted && event.selectedIndex == 0) {
-        _itemScroller!.scrollTo(
+        _itemScroller.scrollTo(
           index: 0,
           duration: const Duration(milliseconds: 150),
         );
@@ -209,89 +206,24 @@ class _GalleryState extends State<Gallery> {
     if (!_hasLoadedFiles) {
       return widget.loadingWidget;
     }
-    _photoGridSize = LocalSettings.instance.getPhotoGridSize();
-    return _getListView();
-  }
-
-  Widget _getListView() {
-    return HugeListView<List<File>>(
-      key: _hugeListViewKey,
-      controller: _itemScroller,
-      startIndex: 0,
-      totalCount: _collatedFiles.length,
-      isDraggableScrollbarEnabled: _collatedFiles.length > 10,
+    return GalleryListView(
+      hugeListViewKey: _hugeListViewKey,
+      itemScroller: _itemScroller,
+      collatedFiles: _collatedFiles,
       disableScroll: widget.disableScroll,
-      waitBuilder: (_) {
-        return const EnteLoadingWidget();
-      },
-      emptyResultBuilder: (_) {
-        final List<Widget> 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<GalleryIndexUpdatedEvent>()
-              .where((event) => event.tag == widget.tagPrefix)
-              .map((event) => event.index),
-          widget.shouldCollateFilesByDay,
-          logTag: _logTag,
-          photoGirdSize: _photoGridSize,
-          limitSelectionToOne: widget.limitSelectionToOne,
-        );
-        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) {
-        try {
-          return getMonthAndYear(
-            DateTime.fromMicrosecondsSinceEpoch(
-              _collatedFiles[index][0].creationTime!,
-            ),
-          );
-        } catch (e) {
-          _logger.severe("label text builder failed", e);
-          return "";
-        }
-      },
-      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));
-      },
+      emptyState: widget.emptyState,
+      asyncLoader: widget.asyncLoader,
+      removalEventTypes: widget.removalEventTypes,
+      tagPrefix: widget.tagPrefix,
+      scrollBottomSafeArea: widget.scrollBottomSafeArea,
+      limitSelectionToOne: widget.limitSelectionToOne,
+      shouldCollateFilesByDay: widget.shouldCollateFilesByDay,
+      logTag: _logTag,
+      logger: _logger,
+      reloadEvent: widget.reloadEvent,
+      header: widget.header,
+      footer: widget.footer,
+      selectedFiles: widget.selectedFiles,
     );
   }