Merge gallery_rewrite

This commit is contained in:
ashilkn
2025-07-26 17:33:57 +05:30
12 changed files with 0 additions and 1443 deletions

View File

@@ -1,217 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:photos/ui/huge_listview/scroll_bar_thumb.dart';
class DraggableScrollbar extends StatefulWidget {
final Widget child;
final Color backgroundColor;
final Color drawColor;
final double heightScrollThumb;
final EdgeInsetsGeometry? padding;
final int totalCount;
final int initialScrollIndex;
final double bottomSafeArea;
final int currentFirstIndex;
final ValueChanged<double>? onChange;
final String Function(int) labelTextBuilder;
final bool isEnabled;
const DraggableScrollbar({
super.key,
required this.child,
this.backgroundColor = Colors.white,
this.drawColor = Colors.grey,
this.heightScrollThumb = 80.0,
this.bottomSafeArea = 120,
this.padding,
this.totalCount = 1,
this.initialScrollIndex = 0,
this.currentFirstIndex = 0,
required this.labelTextBuilder,
this.onChange,
this.isEnabled = true,
});
@override
DraggableScrollbarState createState() => DraggableScrollbarState();
}
class DraggableScrollbarState extends State<DraggableScrollbar>
with TickerProviderStateMixin {
static const thumbAnimationDuration = Duration(milliseconds: 1000);
static const labelAnimationDuration = Duration(milliseconds: 1000);
double thumbOffset = 0.0;
bool isDragging = false;
late int currentFirstIndex;
double get thumbMin => 0.0;
double get thumbMax =>
context.size!.height - widget.heightScrollThumb - widget.bottomSafeArea;
late AnimationController _thumbAnimationController;
Animation<double>? _thumbAnimation;
late AnimationController _labelAnimationController;
Animation<double>? _labelAnimation;
Timer? _fadeoutTimer;
@override
void initState() {
super.initState();
currentFirstIndex = widget.currentFirstIndex;
///Where will this be true on init?
if (widget.initialScrollIndex > 0 && widget.totalCount > 1) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(
() => thumbOffset = (widget.initialScrollIndex / widget.totalCount) *
(thumbMax - thumbMin),
);
});
}
_thumbAnimationController = AnimationController(
vsync: this,
duration: thumbAnimationDuration,
animationBehavior: AnimationBehavior.preserve,
);
_thumbAnimation = CurvedAnimation(
parent: _thumbAnimationController,
curve: Curves.fastOutSlowIn,
);
_labelAnimationController = AnimationController(
vsync: this,
duration: labelAnimationDuration,
animationBehavior: AnimationBehavior.preserve,
);
_labelAnimation = CurvedAnimation(
parent: _labelAnimationController,
curve: Curves.fastOutSlowIn,
);
}
@override
void dispose() {
_thumbAnimationController.dispose();
_labelAnimationController.dispose();
_fadeoutTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
RepaintBoundary(child: widget.child),
widget.isEnabled
? RepaintBoundary(child: buildThumb())
: const SizedBox.shrink(),
],
);
}
Widget buildThumb() => Padding(
padding: widget.padding!,
child: Container(
alignment: Alignment.topRight,
margin: EdgeInsets.only(top: thumbOffset),
child: ScrollBarThumb(
widget.backgroundColor,
widget.drawColor,
widget.heightScrollThumb,
widget.labelTextBuilder.call(currentFirstIndex),
_labelAnimation,
_thumbAnimation,
onDragStart,
onDragUpdate,
onDragEnd,
),
),
);
void setPosition(double position, int currentFirstIndex) {
setState(() {
this.currentFirstIndex = currentFirstIndex;
thumbOffset = position * (thumbMax - thumbMin);
if (_thumbAnimationController.status != AnimationStatus.forward) {
_thumbAnimationController.forward();
}
_fadeoutTimer?.cancel();
_fadeoutTimer = Timer(thumbAnimationDuration, () {
_thumbAnimationController.reverse();
_labelAnimationController.reverse();
_fadeoutTimer = null;
});
});
}
void onDragStart(DragStartDetails details) {
setState(() {
isDragging = true;
_labelAnimationController.forward();
_fadeoutTimer?.cancel();
});
}
void onDragUpdate(DragUpdateDetails details) {
setState(() {
if (_thumbAnimationController.status != AnimationStatus.forward) {
_thumbAnimationController.forward();
}
if (isDragging && details.delta.dy != 0) {
thumbOffset += details.delta.dy;
thumbOffset = thumbOffset.clamp(thumbMin, thumbMax);
final double position = thumbOffset / (thumbMax - thumbMin);
widget.onChange?.call(position);
}
});
}
void onDragEnd(DragEndDetails details) {
_fadeoutTimer = Timer(thumbAnimationDuration, () {
_thumbAnimationController.reverse();
_labelAnimationController.reverse();
_fadeoutTimer = null;
});
setState(() => isDragging = false);
}
void keyHandler(KeyEvent value) {
if (value.runtimeType == KeyDownEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowDown) {
onDragUpdate(
DragUpdateDetails(
globalPosition: Offset.zero,
delta: const Offset(0, 2),
),
);
} else if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
onDragUpdate(
DragUpdateDetails(
globalPosition: Offset.zero,
delta: const Offset(0, -2),
),
);
} else if (value.logicalKey == LogicalKeyboardKey.pageDown) {
onDragUpdate(
DragUpdateDetails(
globalPosition: Offset.zero,
delta: const Offset(0, 25),
),
);
} else if (value.logicalKey == LogicalKeyboardKey.pageUp) {
onDragUpdate(
DragUpdateDetails(
globalPosition: Offset.zero,
delta: const Offset(0, -25),
),
);
}
}
}
}

View File

@@ -1,201 +0,0 @@
import 'dart:math' show max;
import 'package:flutter/material.dart';
import 'package:photos/ui/huge_listview/draggable_scrollbar.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
typedef HugeListViewItemBuilder<T> = Widget Function(
BuildContext context,
int index,
);
typedef HugeListViewErrorBuilder = Widget Function(
BuildContext context,
dynamic error,
);
class HugeListView<T> extends StatefulWidget {
/// A [ScrollablePositionedList] controller for jumping or scrolling to an item.
final ItemScrollController? controller;
/// Index of an item to initially align within the viewport.
final int startIndex;
/// Total number of items in the list.
final int totalCount;
/// Called to build the thumb. One of [DraggableScrollbarThumbs.RoundedRectThumb], [DraggableScrollbarThumbs.ArrowThumb]
/// or [DraggableScrollbarThumbs.SemicircleThumb], or build your own.
final String Function(int) labelTextBuilder;
/// Background color of scroll thumb, defaults to white.
final Color thumbBackgroundColor;
/// Drawing color of scroll thumb, defaults to gray.
final Color thumbDrawColor;
/// Height of scroll thumb, defaults to 48.
final double thumbHeight;
/// Height of bottomSafeArea so that scroll thumb does not become hidden
/// or un-clickable due to footer elements. Default value is 120
final double bottomSafeArea;
/// Called to build an individual item with the specified [index].
final HugeListViewItemBuilder<T> itemBuilder;
/// Called to build a progress widget while the whole list is initialized.
final WidgetBuilder? waitBuilder;
/// Called to build a widget when the list is empty.
final WidgetBuilder? emptyResultBuilder;
/// Called to build a widget when there is an error.
final HugeListViewErrorBuilder? errorBuilder;
/// Event to call with the index of the topmost visible item in the viewport while scrolling.
/// Can be used to display the current letter of an alphabetically sorted list, for instance.
final ValueChanged<int>? firstShown;
final bool isDraggableScrollbarEnabled;
final EdgeInsetsGeometry? thumbPadding;
final bool disableScroll;
final bool isScrollablePositionedList;
const HugeListView({
super.key,
this.controller,
required this.startIndex,
required this.totalCount,
required this.labelTextBuilder,
required this.itemBuilder,
this.waitBuilder,
this.emptyResultBuilder,
this.errorBuilder,
this.firstShown,
this.thumbBackgroundColor = Colors.red, // Colors.white,
this.thumbDrawColor = Colors.yellow, //Colors.grey,
this.thumbHeight = 48.0,
this.bottomSafeArea = 120.0,
this.isDraggableScrollbarEnabled = true,
this.thumbPadding,
this.disableScroll = false,
this.isScrollablePositionedList = true,
});
@override
HugeListViewState<T> createState() => HugeListViewState<T>();
}
class HugeListViewState<T> extends State<HugeListView<T>> {
final scrollKey = GlobalKey<DraggableScrollbarState>();
final listener = ItemPositionsListener.create();
int lastIndexJump = -1;
dynamic error;
@override
void initState() {
super.initState();
widget.isScrollablePositionedList
? listener.itemPositions.addListener(_sendScroll)
: null;
}
@override
void dispose() {
listener.itemPositions.removeListener(_sendScroll);
super.dispose();
}
void _sendScroll() {
final int current = _currentFirst();
widget.firstShown?.call(current);
scrollKey.currentState?.setPosition(current / widget.totalCount, current);
}
int _currentFirst() {
try {
return listener.itemPositions.value.first.index;
} catch (e) {
return 0;
}
}
@override
Widget build(BuildContext context) {
if (error != null && widget.errorBuilder != null) {
return widget.errorBuilder!(context, error);
}
if (widget.totalCount == -1 && widget.waitBuilder != null) {
return widget.waitBuilder!(context);
}
if (widget.totalCount == 0 && widget.emptyResultBuilder != null) {
return widget.emptyResultBuilder!(context);
}
return widget.isScrollablePositionedList
? DraggableScrollbar(
key: scrollKey,
totalCount: widget.totalCount,
initialScrollIndex: widget.startIndex,
onChange: (position) {
final int currentIndex = _currentFirst();
final int floorIndex = (position * widget.totalCount).floor();
final int cielIndex = (position * widget.totalCount).ceil();
int nextIndexToJump;
if (floorIndex != currentIndex && floorIndex > currentIndex) {
nextIndexToJump = floorIndex;
} else if (cielIndex != currentIndex &&
cielIndex < currentIndex) {
nextIndexToJump = floorIndex;
} else {
return;
}
if (lastIndexJump != nextIndexToJump) {
lastIndexJump = nextIndexToJump;
widget.controller?.jumpTo(index: nextIndexToJump);
}
},
labelTextBuilder: widget.labelTextBuilder,
backgroundColor: widget.thumbBackgroundColor,
drawColor: widget.thumbDrawColor,
heightScrollThumb: widget.thumbHeight,
bottomSafeArea: widget.bottomSafeArea,
currentFirstIndex: _currentFirst(),
isEnabled: widget.isDraggableScrollbarEnabled,
padding: widget.thumbPadding,
child: ScrollablePositionedList.builder(
physics: widget.disableScroll
? const NeverScrollableScrollPhysics()
: const BouncingScrollPhysics(),
itemScrollController: widget.controller,
itemPositionsListener: listener,
initialScrollIndex: widget.startIndex,
itemCount: max(widget.totalCount, 0),
itemBuilder: (context, index) {
return ExcludeSemantics(
child: widget.itemBuilder(context, index),
);
},
),
)
: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: max(widget.totalCount, 0),
itemBuilder: (context, index) {
return ExcludeSemantics(
child: widget.itemBuilder(context, index),
);
},
);
}
/// Jump to the [position] in the list. [position] is between 0.0 (first item) and 1.0 (last item), practically currentIndex / totalCount.
/// To jump to a specific item, use [ItemScrollController.jumpTo] or [ItemScrollController.scrollTo].
void setPosition(double position) {
scrollKey.currentState?.setPosition(position, _currentFirst());
}
}

View File

@@ -1,159 +0,0 @@
import 'package:flutter/material.dart';
class ScrollBarThumb extends StatelessWidget {
final Color backgroundColor;
final Color drawColor;
final double height;
final String title;
final Animation? labelAnimation;
final Animation? thumbAnimation;
final Function(DragStartDetails details) onDragStart;
final Function(DragUpdateDetails details) onDragUpdate;
final Function(DragEndDetails details) onDragEnd;
const ScrollBarThumb(
this.backgroundColor,
this.drawColor,
this.height,
this.title,
this.labelAnimation,
this.thumbAnimation,
this.onDragStart,
this.onDragUpdate,
this.onDragEnd, {
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IgnorePointer(
child: FadeTransition(
opacity: labelAnimation as Animation<double>,
child: Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: backgroundColor,
),
child: Text(
title,
style: TextStyle(
color: drawColor,
fontWeight: FontWeight.bold,
backgroundColor: Colors.transparent,
fontSize: 14,
),
),
),
),
),
const Padding(
padding: EdgeInsets.all(12),
),
GestureDetector(
onVerticalDragStart: onDragStart,
onVerticalDragUpdate: onDragUpdate,
onVerticalDragEnd: onDragEnd,
child: SlideFadeTransition(
animation: thumbAnimation as Animation<double>?,
child: CustomPaint(
foregroundPainter: _ArrowCustomPainter(drawColor),
child: Material(
elevation: 4.0,
color: backgroundColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(height),
bottomLeft: Radius.circular(height),
topRight: const Radius.circular(4.0),
bottomRight: const Radius.circular(4.0),
),
child: Container(
constraints: BoxConstraints.tight(Size(height * 0.6, height)),
),
),
),
),
),
],
);
}
}
class _ArrowCustomPainter extends CustomPainter {
final Color drawColor;
_ArrowCustomPainter(this.drawColor);
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.fill
..color = drawColor;
const width = 10.0;
const height = 8.0;
final baseX = size.width / 2;
final baseY = size.height / 2;
canvas.drawPath(
trianglePath(Offset(baseX - 2.0, baseY - 2.0), width, height, true),
paint,
);
canvas.drawPath(
trianglePath(Offset(baseX - 2.0, baseY + 2.0), width, height, false),
paint,
);
}
static Path trianglePath(
Offset offset,
double width,
double height,
bool isUp,
) {
return Path()
..moveTo(offset.dx, offset.dy)
..lineTo(offset.dx + width, offset.dy)
..lineTo(
offset.dx + (width / 2),
isUp ? offset.dy - height : offset.dy + height,
)
..close();
}
}
class SlideFadeTransition extends StatelessWidget {
final Animation<double>? animation;
final Widget child;
const SlideFadeTransition({
super.key,
required this.animation,
required this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation!,
builder: (context, child) =>
animation!.value == 0.0 ? const SizedBox.shrink() : child!,
child: SlideTransition(
position: Tween(
begin: const Offset(0.3, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation!),
child: FadeTransition(
opacity: animation!,
child: child,
),
),
);
}
}

View File

@@ -1,52 +0,0 @@
import "package:flutter/widgets.dart";
import "package:photos/core/constants.dart";
import 'package:photos/models/file/file.dart';
import "package:photos/models/selected_files.dart";
import "package:photos/ui/viewer/gallery/component/gallery_file_widget.dart";
import "package:photos/ui/viewer/gallery/gallery.dart";
class GalleryGridViewWidget extends StatelessWidget {
final List<EnteFile> filesInGroup;
final int photoGridSize;
final SelectedFiles? selectedFiles;
final bool limitSelectionToOne;
final String tag;
final int? currentUserID;
final GalleryLoader asyncLoader;
const GalleryGridViewWidget({
required this.filesInGroup,
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: filesInGroup[index],
selectedFiles: selectedFiles,
limitSelectionToOne: limitSelectionToOne,
tag: tag,
photoGridSize: photoGridSize,
currentUserID: currentUserID,
);
},
itemCount: filesInGroup.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 2,
mainAxisSpacing: 2,
crossAxisCount: photoGridSize,
),
padding: const EdgeInsets.symmetric(vertical: (galleryGridSpacing / 2)),
);
}
}

View File

@@ -1,113 +0,0 @@
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/file.dart';
import "package:photos/models/selected_files.dart";
import "package:photos/ui/viewer/gallery/component/grid/non_recyclable_grid_view_widget.dart";
import "package:photos/ui/viewer/gallery/component/grid/recyclable_grid_view_widget.dart";
import "package:photos/ui/viewer/gallery/gallery.dart";
class LazyGridView extends StatefulWidget {
final String tag;
final List<EnteFile> filesInGroup;
final GalleryLoader asyncLoader;
final SelectedFiles? selectedFiles;
final bool shouldRender;
final bool shouldRecycle;
final int? photoGridSize;
final bool limitSelectionToOne;
const LazyGridView(
this.tag,
this.filesInGroup,
this.asyncLoader,
this.selectedFiles,
this.shouldRender,
this.shouldRecycle,
this.photoGridSize, {
this.limitSelectionToOne = false,
super.key,
});
@override
State<LazyGridView> createState() => _LazyGridViewState();
}
class _LazyGridViewState extends State<LazyGridView> {
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(() {});
}
});
super.initState();
}
@override
void dispose() {
widget.selectedFiles?.removeListener(_selectedFilesListener);
_clearSelectionsEvent.cancel();
super.dispose();
}
@override
void didUpdateWidget(LazyGridView oldWidget) {
super.didUpdateWidget(oldWidget);
if (!listEquals(widget.filesInGroup, oldWidget.filesInGroup)) {
_shouldRender = widget.shouldRender;
}
}
@override
Widget build(BuildContext context) {
if (widget.shouldRecycle) {
return RecyclableGridViewWidget(
shouldRender: _shouldRender,
filesInGroup: widget.filesInGroup,
photoGridSize: widget.photoGridSize!,
limitSelectionToOne: widget.limitSelectionToOne,
tag: widget.tag,
asyncLoader: widget.asyncLoader,
selectedFiles: widget.selectedFiles,
currentUserID: _currentUserID,
);
} else {
return NonRecyclableGridViewWidget(
shouldRender: _shouldRender,
filesInGroup: widget.filesInGroup,
photoGridSize: widget.photoGridSize!,
limitSelectionToOne: widget.limitSelectionToOne,
tag: widget.tag,
asyncLoader: widget.asyncLoader,
selectedFiles: widget.selectedFiles,
currentUserID: _currentUserID,
);
}
}
void _selectedFilesListener() {
bool shouldRefresh = false;
for (final file in widget.filesInGroup) {
if (widget.selectedFiles!.isPartOfLastSelected(file)) {
shouldRefresh = true;
}
}
if (shouldRefresh && mounted) {
setState(() {});
}
}
}

View File

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

View File

@@ -1,49 +0,0 @@
import "dart:math";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import "package:photos/theme/ente_theme.dart";
class PlaceHolderGridViewWidget extends StatelessWidget {
const PlaceHolderGridViewWidget(
this.count,
this.columns, {
super.key,
});
final int count, columns;
static Widget? _placeHolderCache;
static final _gridViewCache = <String, GridView>{};
static const crossAxisSpacing = 2.0; // as per your code
static const mainAxisSpacing = 2.0; // as per your code
@override
Widget build(BuildContext context) {
final Color faintColor = getEnteColorScheme(context).fillFaint;
int limitCount = count;
if (kDebugMode) {
limitCount = min(count, columns * 5);
}
final key = '$limitCount:$columns';
if (!_gridViewCache.containsKey(key)) {
_gridViewCache[key] = GridView.builder(
padding: const EdgeInsets.only(top: 2),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return PlaceHolderGridViewWidget._placeHolderCache ??=
Container(color: faintColor);
},
itemCount: limitCount,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
crossAxisSpacing: crossAxisSpacing,
mainAxisSpacing: mainAxisSpacing,
),
);
}
return _gridViewCache[key]!;
}
}

View File

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

View File

@@ -1,56 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/selected_files.dart';
import "package:photos/ui/viewer/gallery/component/grid/lazy_grid_view.dart";
import 'package:photos/ui/viewer/gallery/gallery.dart';
class GroupGallery extends StatelessWidget {
final int photoGridSize;
final List<EnteFile> files;
final String tag;
final GalleryLoader asyncLoader;
final SelectedFiles? selectedFiles;
final bool limitSelectionToOne;
const GroupGallery({
required this.photoGridSize,
required this.files,
required this.tag,
required this.asyncLoader,
required this.selectedFiles,
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(
LazyGridView(
tag,
files.sublist(
index,
min(index + subGalleryItemLimit, files.length),
),
asyncLoader,
selectedFiles,
index == 0,
files.length > kRecycleLimit,
photoGridSize,
limitSelectionToOne: limitSelectionToOne,
),
);
}
return Column(
children: childGalleries,
);
}
}

View File

@@ -1,267 +0,0 @@
import 'dart:async';
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/file.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/theme/ente_theme.dart';
import "package:photos/ui/viewer/gallery/component/grid/place_holder_grid_view_widget.dart";
import "package:photos/ui/viewer/gallery/component/group/group_gallery.dart";
import "package:photos/ui/viewer/gallery/component/group/group_header_widget.dart";
import "package:photos/ui/viewer/gallery/component/group/type.dart";
import 'package:photos/ui/viewer/gallery/gallery.dart';
import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart";
class LazyGroupGallery extends StatefulWidget {
final List<EnteFile> 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 enableFileGrouping;
final bool limitSelectionToOne;
final bool showSelectAllByDefault;
const LazyGroupGallery(
this.files,
this.index,
this.reloadEvent,
this.removalEventTypes,
this.asyncLoader,
this.selectedFiles,
this.tag,
this.currentIndexStream,
this.enableFileGrouping,
this.showSelectAllByDefault, {
this.logTag = "",
this.photoGridSize = photoGridSizeDefault,
this.limitSelectionToOne = false,
super.key,
});
@override
State<LazyGroupGallery> createState() => _LazyGroupGalleryState();
}
class _LazyGroupGalleryState extends State<LazyGroupGallery> {
static const numberOfGroupsToRenderBeforeAndAfter = 8;
late final ValueNotifier<bool> _showSelectAllButtonNotifier;
late final ValueNotifier<bool> _areAllFromGroupSelectedNotifier;
late Logger _logger;
late List<EnteFile> _filesInGroup;
late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
late StreamSubscription<int> _currentIndexSubscription;
bool? _shouldRender;
@override
void initState() {
super.initState();
_areAllFromGroupSelectedNotifier =
ValueNotifier(_areAllFromGroupSelected());
widget.selectedFiles?.addListener(_selectedFilesListener);
_showSelectAllButtonNotifier = ValueNotifier(widget.showSelectAllByDefault);
_init();
}
void _init() {
_logger = Logger("LazyLoading_${widget.logTag}");
_shouldRender = true;
_filesInGroup = widget.files;
_areAllFromGroupSelectedNotifier.value = _areAllFromGroupSelected();
_reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
_currentIndexSubscription =
widget.currentIndexStream.listen((currentIndex) {
final bool shouldRender = (currentIndex - widget.index).abs() <
numberOfGroupsToRenderBeforeAndAfter;
if (mounted && shouldRender != _shouldRender) {
setState(() {
_shouldRender = shouldRender;
});
}
});
}
bool _areAllFromGroupSelected() {
if (widget.selectedFiles != null &&
widget.selectedFiles!.files.length >= widget.files.length) {
return widget.selectedFiles!.files.containsAll(widget.files);
} else {
return false;
}
}
Future _onReload(FilesUpdatedEvent event) async {
if (_filesInGroup.isEmpty) {
return;
}
final galleryState = context.findAncestorStateOfType<GalleryState>();
final groupType = GalleryContextState.of(context)!.type;
// iterate over files and check if any of the belongs to this group
final anyCandidateForGroup = groupType.areModifiedFilesPartOfGroup(
event.updatedFiles,
_filesInGroup[0],
lastFile: _filesInGroup.last,
);
if (anyCandidateForGroup) {
late int startRange, endRange;
(startRange, endRange) = groupType.getGroupRange(_filesInGroup[0]);
if (kDebugMode) {
_logger.info(
" files were updated due to ${event.reason} on type ${groupType.name} from ${DateTime.fromMicrosecondsSinceEpoch(startRange).toIso8601String()}"
" to ${DateTime.fromMicrosecondsSinceEpoch(endRange).toIso8601String()}",
);
}
if (event.type == EventType.addedOrUpdated ||
widget.removalEventTypes.contains(event.type)) {
// We are reloading the whole group
final result = await widget.asyncLoader(
startRange,
endRange,
asc: GalleryContextState.of(context)!.sortOrderAsc,
);
//When items are updated in a LazyGroupGallery, only it rebuilds with the
//new state of _files which is a state variable in it's state object.
//widget.files is not updated. Calling setState from it's ancestor
//state object 'Gallery' creates a new LazyLoadingGallery widget with
//updated widget.files
//If widget.files is kept in it's old state, the old state will come
//up when scrolled down and back up to the group.
//[galleryState] will never be null except when LazyLoadingGallery is
//used without Gallery as an ancestor.
if (galleryState?.mounted ?? false) {
galleryState!.setState(() {});
_filesInGroup = result.files;
}
} else if (kDebugMode) {
debugPrint("Unexpected event ${event.type.name}");
}
}
}
@override
void dispose() {
_reloadEventSubscription?.cancel();
_currentIndexSubscription.cancel();
_areAllFromGroupSelectedNotifier.dispose();
widget.selectedFiles?.removeListener(_selectedFilesListener);
super.dispose();
}
@override
void didUpdateWidget(LazyGroupGallery oldWidget) {
super.didUpdateWidget(oldWidget);
if (!listEquals(_filesInGroup, widget.files)) {
_reloadEventSubscription?.cancel();
_init();
}
}
@override
Widget build(BuildContext context) {
if (_filesInGroup.isEmpty) {
return const SizedBox.shrink();
}
final groupType = GalleryContextState.of(context)!.type;
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.enableFileGrouping)
GroupHeaderWidget(
title: groupType.getTitle(
context,
_filesInGroup[0],
lastFile: _filesInGroup.last,
),
gridSize: widget.photoGridSize,
),
Expanded(child: Container()),
widget.limitSelectionToOne
? const SizedBox.shrink()
: ValueListenableBuilder(
valueListenable: _showSelectAllButtonNotifier,
builder: (context, dynamic value, _) {
return !value
? const SizedBox.shrink()
: GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 48,
height: 44,
child: ValueListenableBuilder(
valueListenable:
_areAllFromGroupSelectedNotifier,
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: () {
widget.selectedFiles?.toggleGroupSelection(
_filesInGroup.toSet(),
);
},
);
},
),
],
),
_shouldRender!
? GroupGallery(
photoGridSize: widget.photoGridSize,
files: _filesInGroup,
tag: widget.tag,
asyncLoader: widget.asyncLoader,
selectedFiles: widget.selectedFiles,
limitSelectionToOne: widget.limitSelectionToOne,
)
// todo: perf eval should we have separate PlaceHolder for Groups
// instead of creating a large cached view
: PlaceHolderGridViewWidget(
_filesInGroup.length,
widget.photoGridSize,
),
],
);
}
void _selectedFilesListener() {
if (widget.selectedFiles == null) return;
_areAllFromGroupSelectedNotifier.value =
widget.selectedFiles!.files.containsAll(_filesInGroup.toSet());
//Can remove this if we decide to show select all by default for all galleries
if (widget.selectedFiles!.files.isEmpty && !widget.showSelectAllByDefault) {
_showSelectAllButtonNotifier.value = false;
} else {
_showSelectAllButtonNotifier.value = true;
}
}
}

View File

@@ -1,162 +0,0 @@
import "package:flutter/material.dart";
import "package:intl/intl.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/file.dart';
import "package:photos/models/selected_files.dart";
import "package:photos/service_locator.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/group/lazy_group_gallery.dart';
import "package:photos/ui/viewer/gallery/component/group/type.dart";
import "package:photos/ui/viewer/gallery/gallery.dart";
import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart";
import "package:photos/utils/standalone/data.dart";
import "package:scrollable_positioned_list/scrollable_positioned_list.dart";
/*
MultipleGroupsGalleryView is a widget that displays a list of grouped/collated
files when grouping is enabled.
For each group, it displays a header and use LazyGroupGallery to display a
particular group of files.
If a group has more than 400 files, LazyGroupGallery internally divides the
group into multiple grid views during rendering.
*/
class MultipleGroupsGalleryView extends StatelessWidget {
final ItemScrollController itemScroller;
final List<List<EnteFile>> groupedFiles;
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 enableFileGrouping;
final String logTag;
final Logger logger;
final bool showSelectAllByDefault;
final bool isScrollablePositionedList;
final bool addHeaderOrFooterEmptyState;
const MultipleGroupsGalleryView({
required this.itemScroller,
required this.groupedFiles,
required this.disableScroll,
this.header,
this.footer,
this.addHeaderOrFooterEmptyState = true,
required this.emptyState,
required this.asyncLoader,
this.reloadEvent,
required this.removalEventTypes,
required this.tagPrefix,
required this.scrollBottomSafeArea,
required this.limitSelectionToOne,
this.selectedFiles,
required this.enableFileGrouping,
required this.logTag,
required this.logger,
required this.showSelectAllByDefault,
required this.isScrollablePositionedList,
super.key,
});
@override
Widget build(BuildContext context) {
final gType = GalleryContextState.of(context)!.type;
return HugeListView<List<EnteFile>>(
controller: itemScroller,
startIndex: 0,
totalCount: groupedFiles.length,
isDraggableScrollbarEnabled: groupedFiles.length > 10,
disableScroll: disableScroll,
isScrollablePositionedList: isScrollablePositionedList,
waitBuilder: (_) {
return const EnteLoadingWidget();
},
emptyResultBuilder: (_) {
final List<Widget> children = [];
if (header != null && addHeaderOrFooterEmptyState) {
children.add(header!);
}
children.add(
Expanded(
child: emptyState,
),
);
if (footer != null && addHeaderOrFooterEmptyState) {
children.add(footer!);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
);
},
itemBuilder: (context, index) {
Widget gallery;
gallery = LazyGroupGallery(
groupedFiles[index],
index,
reloadEvent,
removalEventTypes,
asyncLoader,
selectedFiles,
tagPrefix,
Bus.instance
.on<GalleryIndexUpdatedEvent>()
.where((event) => event.tag == tagPrefix)
.map((event) => event.index),
enableFileGrouping,
showSelectAllByDefault,
logTag: logTag,
photoGridSize: localSettings.getPhotoGridSize(),
limitSelectionToOne: limitSelectionToOne,
);
if (header != null && index == 0) {
gallery = Column(children: [header!, gallery]);
}
if (footer != null && index == groupedFiles.length - 1) {
gallery = Column(children: [gallery, footer!]);
}
return gallery;
},
labelTextBuilder: (int index) {
try {
final EnteFile file = groupedFiles[index][0];
if (gType == GroupType.size) {
return file.fileSize != null
? convertBytesToReadableFormat(file.fileSize!)
: "";
}
return DateFormat.yMMM(Localizations.localeOf(context).languageCode)
.format(
DateTime.fromMicrosecondsSinceEpoch(
file.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));
},
);
}
}

View File

@@ -558,29 +558,6 @@ class GalleryState extends State<Gallery> {
sortOrderAsc: _sortOrderAsc,
inSelectionMode: widget.inSelectionMode,
type: _groupType,
// Replace this with the new gallery and use `_allGalleryFiles`
// child: MultipleGroupsGalleryView(
// groupedFiles: currentGroupedFiles,
// disableScroll: widget.disableScroll,
// emptyState: widget.emptyState,
// asyncLoader: widget.asyncLoader,
// removalEventTypes: widget.removalEventTypes,
// tagPrefix: widget.tagPrefix,
// scrollBottomSafeArea: widget.scrollBottomSafeArea,
// limitSelectionToOne: widget.limitSelectionToOne,
// enableFileGrouping:
// widget.enableFileGrouping && widget.groupType.showGroupHeader(),
// logTag: _logTag,
// logger: _logger,
// reloadEvent: widget.reloadEvent,
// header: widget.header,
// footer: widget.footer,
// selectedFiles: widget.selectedFiles,
// showSelectAllByDefault:
// widget.showSelectAllByDefault && widget.groupType.showGroupHeader(),
// isScrollablePositionedList: widget.isScrollablePositionedList,
// ),
child: _allGalleryFiles.isEmpty
? Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,