Compare commits

..

1 Commits

5 changed files with 304 additions and 187 deletions

View File

@@ -1,7 +1,10 @@
import "dart:async";
import 'dart:math' show max;
import 'package:flutter/material.dart';
import 'package:photos/ui/huge_listview/draggable_scrollbar.dart';
import "package:photos/ui/viewer/gallery/component/multiple_groups_gallery_view.dart";
import "package:photos/utils/debouncer.dart";
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
typedef HugeListViewItemBuilder<T> = Widget Function(
@@ -94,6 +97,14 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
final listener = ItemPositionsListener.create();
int lastIndexJump = -1;
dynamic error;
final scrollOffsetController = ScrollOffsetController();
final scrollController = ScrollController();
double currentScrollOffset = 0.0;
StreamSubscription<double>? shouldScrollGalleryEventSubscription;
final debouncer = Debouncer(
const Duration(milliseconds: 100),
executionInterval: const Duration(milliseconds: 100),
);
@override
void initState() {
@@ -104,9 +115,32 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
: null;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (shouldScrollGalleryEventSubscription != null) {
shouldScrollGalleryEventSubscription!.cancel();
}
shouldScrollGalleryEventSubscription =
SwipeToSelectGalleryScroll.of(context)
.streamController
.stream
.listen((factor) {
debouncer.run(() async {
final double newOffset = (factor * 70);
await scrollOffsetController.animateScroll(
offset: newOffset,
duration: const Duration(milliseconds: 100),
);
});
});
}
@override
void dispose() {
listener.itemPositions.removeListener(_sendScroll);
shouldScrollGalleryEventSubscription?.cancel();
SwipeToSelectGalleryScroll.of(context).streamController.close();
super.dispose();
}
@@ -136,61 +170,69 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
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,
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
currentScrollOffset = notification.metrics.pixels;
return false;
},
child: 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(),
scrollOffsetController: scrollOffsetController,
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),
controller: scrollController,
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.

View File

@@ -14,6 +14,7 @@ import "package:photos/ui/viewer/gallery/component/group/group_header_widget.dar
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/ui/viewer/gallery/swipe_to_select_helper.dart";
class LazyGroupGallery extends StatefulWidget {
final List<EnteFile> files;
@@ -233,21 +234,41 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
),
],
),
_shouldRender!
? GroupGallery(
photoGridSize: widget.photoGridSize,
widget.selectedFiles != null
? SwipeToSelectHelper(
files: _filesInGroup,
tag: widget.tag,
asyncLoader: widget.asyncLoader,
selectedFiles: widget.selectedFiles,
limitSelectionToOne: widget.limitSelectionToOne,
selectedFiles: widget.selectedFiles!,
child: _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,
),
)
// todo: perf eval should we have separate PlaceHolder for Groups
// instead of creating a large cached view
: PlaceHolderGridViewWidget(
_filesInGroup.length,
widget.photoGridSize,
),
: _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,
),
],
);
}

View File

@@ -1,3 +1,5 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:intl/intl.dart";
import "package:logging/logging.dart";
@@ -69,92 +71,119 @@ class MultipleGroupsGalleryView extends StatelessWidget {
@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.instance.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 SwipeToSelectGalleryScroll(
child: 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!);
}
return DateFormat.yMMM(Localizations.localeOf(context).languageCode)
.format(
DateTime.fromMicrosecondsSinceEpoch(
file.creationTime!,
children.add(
Expanded(
child: emptyState,
),
);
} 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));
},
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.instance.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));
},
),
);
}
}
class SwipeToSelectGalleryScroll extends InheritedWidget {
final StreamController<double> streamController = StreamController();
SwipeToSelectGalleryScroll({
required Widget child,
Key? key,
}) : super(key: key, child: child);
static SwipeToSelectGalleryScroll? maybeOf(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<SwipeToSelectGalleryScroll>();
}
static SwipeToSelectGalleryScroll of(BuildContext context) {
final SwipeToSelectGalleryScroll? result = maybeOf(context);
assert(result != null, 'No SwipeToSelectGalleryScroll found in context');
return result!;
}
@override
bool updateShouldNotify(SwipeToSelectGalleryScroll oldWidget) {
return streamController != oldWidget.streamController;
}
}

View File

@@ -17,7 +17,6 @@ import "package:photos/ui/viewer/gallery/component/multiple_groups_gallery_view.
import 'package:photos/ui/viewer/gallery/empty_state.dart';
import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart";
import "package:photos/ui/viewer/gallery/state/selection_state.dart";
import "package:photos/ui/viewer/gallery/swipe_to_select_helper.dart";
import "package:photos/utils/debouncer.dart";
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@@ -259,31 +258,27 @@ class GalleryState extends State<Gallery> {
sortOrderAsc: _sortOrderAsc,
inSelectionMode: widget.inSelectionMode,
type: widget.groupType,
child: SwipeToSelectHelper(
files: currentGroupedFiles.expand((element) => element).toList(),
child: MultipleGroupsGalleryView(
itemScroller: _itemScroller,
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,
child: MultipleGroupsGalleryView(
itemScroller: _itemScroller,
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,
),
showSelectAllByDefault:
widget.showSelectAllByDefault && widget.groupType.showGroupHeader(),
isScrollablePositionedList: widget.isScrollablePositionedList,
),
);
}

View File

@@ -5,10 +5,11 @@ import "package:logging/logging.dart";
import "package:photos/models/file/file.dart";
import "package:photos/models/selected_files.dart";
import "package:photos/ui/viewer/gallery/component/group/lazy_group_gallery.dart";
import "package:photos/ui/viewer/gallery/component/multiple_groups_gallery_view.dart";
class SwipeToSelectHelper extends StatefulWidget {
final List<EnteFile> files;
final SelectedFiles? selectedFiles;
final SelectedFiles selectedFiles;
final Widget child;
const SwipeToSelectHelper({
required this.files,
@@ -26,26 +27,24 @@ class _SwipeToSelectHelperState extends State<SwipeToSelectHelper> {
@override
Widget build(BuildContext context) {
return widget.selectedFiles == null
? widget.child
: LastSelectedFileByDragging(
filesInGroup: widget.files,
child: Builder(
builder: (context) {
return SelectionGesturesEventProvider(
selectedFiles: widget.selectedFiles!,
files: widget.files,
child: GroupGalleryGlobalKey(
globalKey: _groupGalleryGlobalKey,
child: SizedBox(
key: _groupGalleryGlobalKey,
child: widget.child,
),
),
);
},
return LastSelectedFileByDragging(
filesInGroup: widget.files,
child: Builder(
builder: (context) {
return SelectionGesturesEventProvider(
selectedFiles: widget.selectedFiles,
files: widget.files,
child: GroupGalleryGlobalKey(
globalKey: _groupGalleryGlobalKey,
child: SizedBox(
key: _groupGalleryGlobalKey,
child: widget.child,
),
),
);
},
),
);
}
}
@@ -104,11 +103,15 @@ class SelectionGesturesEventProvider extends StatefulWidget {
class _SelectionGesturesEventProviderState
extends State<SelectionGesturesEventProvider> {
late SelectionGesturesEvent selectionGesturesEvent;
late SwipeToSelectGalleryScroll swipeToSelectGalleryScroll;
bool _isFingerOnScreenSinceLongPress = false;
bool _isDragging = false;
int prevSelectedFileIndex = -1;
int currentSelectedFileIndex = -1;
final _logger = Logger("PointerProvider");
static const kUpThreshold = 180.0;
static const kDownThreshold = 240.0;
static const kSelectionSheetBuffer = 120.0;
@override
void initState() {
@@ -167,10 +170,13 @@ class _SelectionGesturesEventProviderState
@override
Widget build(BuildContext context) {
final heightOfScreen = MediaQuery.sizeOf(context).height;
return SelectionGesturesEvent(
child: Builder(
builder: (context) {
selectionGesturesEvent = SelectionGesturesEvent.of(context);
swipeToSelectGalleryScroll = SwipeToSelectGalleryScroll.of(context);
return GestureDetector(
onTap: () {
selectionGesturesEvent.onTapStreamController
@@ -195,6 +201,8 @@ class _SelectionGesturesEventProviderState
(event.localDelta.dx.abs() > 0 &&
event.localDelta.dy.abs() > 0)) {
onDragToSelect(event.localPosition);
sinkScrollEvent(event.position.dy, heightOfScreen);
}
},
onPointerDown: (event) {
@@ -221,6 +229,28 @@ class _SelectionGesturesEventProviderState
selectionGesturesEvent.moveOffsetStreamController.add(offset);
_isDragging = true;
}
void sinkScrollEvent(double yGlobalPos, double heightOfScreen) {
final pixelsBeyondThresholdDown =
yGlobalPos - (heightOfScreen - kDownThreshold);
final pixelsBeyondThresholdUp = yGlobalPos - kUpThreshold;
if (pixelsBeyondThresholdUp < 0) {
print(
"up with strength: ${pixelsBeyondThresholdUp / kUpThreshold}",
);
swipeToSelectGalleryScroll.streamController.sink
.add(pixelsBeyondThresholdUp / kUpThreshold);
}
if (pixelsBeyondThresholdDown > 0) {
print(
"down with strength: ${(pixelsBeyondThresholdDown + kSelectionSheetBuffer) / kDownThreshold}",
);
swipeToSelectGalleryScroll.streamController.sink.add(
(pixelsBeyondThresholdDown + kSelectionSheetBuffer) / kDownThreshold,
);
}
}
}
class SelectionGesturesEvent extends InheritedWidget {