Delete old gallery files

This commit is contained in:
ashilkn
2025-07-26 17:31:09 +05:30
parent 3680ccddfd
commit cdd1353bb2
12 changed files with 0 additions and 1441 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,160 +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;
const MultipleGroupsGalleryView({
required this.itemScroller,
required this.groupedFiles,
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.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) {
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 = 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

@@ -555,29 +555,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,