Compare commits

...

21 Commits

Author SHA1 Message Date
ashilkn
4bfa5965a2 [mob][photos] Move Swipe to select helper to cover whole gallery for selection across days without lifting finger 2024-07-15 17:01:05 +05:30
ashilkn
ffaae58abd [mob][photos] Refactor swipe to select code 2024-07-15 16:34:44 +05:30
ashilkn
cf56b7e057 [mob][photos] Bug fix on swipe to select multiple photos when vertically swiping 2024-07-15 16:03:35 +05:30
ashilkn
570871a1f0 Merge branch 'main' into swipe_to_select 2024-07-15 15:52:37 +05:30
ashilkn
e71f40098f [mob][photos] Swipe to select refactor 2024-07-05 09:52:45 +05:30
ashilkn
c45bbdda14 [mob][photos] Working, to be refactored and to be optimised version of swipe to select where on dragging horizontally and then dragging vertically, all photos to the left or right depending on on vertical direction are toggled(selection) 2024-07-05 09:34:48 +05:30
ashilkn
e0ff71828a [mob][photos] Add comments 2024-07-04 17:08:13 +05:30
ashilkn
28d9775203 [mob][photos] Fix unexpected selections on vertical drag gestures + remove bugs
Now the feature works without any unexpected behaviour
2024-07-04 16:55:38 +05:30
ashilkn
c70c6ac617 [mob][photos] Handle onTap and onLongPress gestures up the widget tree and not in GalleryFileWidget 2024-07-04 15:07:37 +05:30
ashilkn
28107cc7ea [mob][photos] Swipe to select: Fix bug where on any update to a group (a day), swipe to select stops working 2024-07-03 11:33:37 +05:30
ashilkn
8711753d6f [mob][photos] Swipe to select: Reduce errors by returning from initState if renderBox is null 2024-07-01 16:59:53 +05:30
ashilkn
b68d11ffbc [mob][photos] Swipe to select: Remove flutter errors 2024-07-01 16:57:06 +05:30
ashilkn
ac0235a6be [mob][photos] Swipe to select: remove unnecessary delay 2024-07-01 15:09:45 +05:30
ashilkn
8116b05a9d [mob][photos]Swipe to select clean up + reduce flutter errors 2024-07-01 14:30:14 +05:30
ashilkn
3f49395ee2 [mob][photos] Disable swipe to select if selection is limited to one 2024-07-01 10:06:07 +05:30
ashilkn
2d80ed7332 [mob][photos] Use better names 2024-06-30 14:25:46 +05:30
ashilkn
5ca0be9f2b [mob][photos] Fix issues with single tap and some other issues when dragging and selecting + use better names 2024-06-29 19:46:04 +05:30
ashilkn
e0b2fa5a1b [mob][photos] Improve swipe to select accuracy 2024-06-29 16:45:47 +05:30
ashilkn
a58e9030a0 [mob][photos] Resolve merge conflicts and merge main 2024-06-29 15:36:54 +05:30
ashilkn
1c7fe80663 [mob][photos] Half-working swipe to select 2024-06-29 14:31:05 +05:30
ashilkn
ce701099f0 [mob][photos] Create provider that provides a stream of screen pointer position 2024-06-29 12:11:04 +05:30
8 changed files with 571 additions and 92 deletions

View File

@@ -93,8 +93,12 @@ Future<void> _runInForeground(AdaptiveThemeMode? savedThemeMode) async {
runApp(
AppLock(
builder: (args) =>
EnteApp(_runBackgroundTask, _killBGTask, locale, savedThemeMode),
builder: (args) => EnteApp(
_runBackgroundTask,
_killBGTask,
locale,
savedThemeMode,
),
lockScreen: const LockScreen(),
enabled: await Configuration.instance.shouldShowLockScreen(),
locale: locale,

View File

@@ -30,6 +30,18 @@ class SelectedFiles extends ChangeNotifier {
notifyListeners();
}
void toggleFilesSelection(Set<EnteFile> filesToToggle) {
final filesToUnselect = files.intersection(filesToToggle);
final filesToSelect = filesToToggle.difference(filesToUnselect);
//remove the files that are already selected
files.removeAll(filesToToggle);
files.addAll(filesToSelect);
lastSelectionOperationFiles.clear();
lastSelectionOperationFiles.addAll(filesToSelect..addAll(filesToUnselect));
notifyListeners();
}
void toggleGroupSelection(Set<EnteFile> filesToToggle) {
if (files.containsAll(filesToToggle)) {
unSelectAll(filesToToggle);
@@ -38,11 +50,13 @@ class SelectedFiles extends ChangeNotifier {
}
}
void selectAll(Set<EnteFile> filesToSelect) {
void selectAll(Set<EnteFile> filesToSelect, {bool skipNotify = false}) {
files.addAll(filesToSelect);
lastSelectionOperationFiles.clear();
lastSelectionOperationFiles.addAll(filesToSelect);
notifyListeners();
if (!skipNotify) {
notifyListeners();
}
}
void unSelectAll(Set<EnteFile> filesToUnselect, {bool skipNotify = false}) {

View File

@@ -1,5 +1,8 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:logging/logging.dart";
import "package:media_extension/media_extension.dart";
import "package:media_extension/media_extension_action_types.dart";
import "package:photos/core/constants.dart";
@@ -9,12 +12,14 @@ import "package:photos/services/app_lifecycle_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/file/detail_page.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/gallery/component/group/lazy_group_gallery.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";
import "package:photos/utils/file_util.dart";
import "package:photos/utils/navigation_util.dart";
class GalleryFileWidget extends StatelessWidget {
class GalleryFileWidget extends StatefulWidget {
final EnteFile file;
final SelectedFiles? selectedFiles;
final bool limitSelectionToOne;
@@ -35,98 +40,232 @@ class GalleryFileWidget extends StatelessWidget {
super.key,
});
@override
State<GalleryFileWidget> createState() => _GalleryFileWidgetState();
}
class _GalleryFileWidgetState extends State<GalleryFileWidget> {
final _globalKey = GlobalKey();
/// This does not always hold the correct value. This is used to unselect/select
/// photos in swipe selection. It hold the right values during swipe selection
/// so, it works fine for what it is used for.
/// This can hold incorrect values when during onTap and certain cases of onLongPress.
/// Too get a better idea, make this a ValueNotfier and update the UI when this changes.
bool _pointerInsideBbox = false;
bool _pointerInsideBboxPrevValue = false;
late StreamSubscription<Offset> _pointerPositionStreamSubscription;
late StreamSubscription<Offset> _pointerUpEventStreamSubscription;
late StreamSubscription<Offset> _onTapEventStreamSubscription;
late StreamSubscription<Offset> _onLongPressEventStreamSubscription;
final _logger = Logger("GalleryFileWidget");
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!widget.limitSelectionToOne) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
try {
final RenderBox? renderBox =
_globalKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) {
_logger.info("RenderBox is null. Returning.");
return;
}
final groupGalleryGlobalKey =
GroupGalleryGlobalKey.of(context).globalKey;
final RenderBox? groupGalleryRenderBox =
groupGalleryGlobalKey.currentContext?.findRenderObject()
as RenderBox?;
if (groupGalleryRenderBox == null) {
_logger.info("GroupGalleryRenderBox is null. Returning.");
return;
}
final position = renderBox.localToGlobal(
Offset.zero,
ancestor: groupGalleryRenderBox,
);
final size = renderBox.size;
final bbox = Rect.fromLTWH(
position.dx,
position.dy,
size.width,
size.height,
);
_onTapEventStreamSubscription = SelectionGesturesEvent.of(context)
.onTapStreamController
.stream
.listen((offset) {
if (bbox.contains(offset)) {
_pointerInsideBbox = true;
widget.limitSelectionToOne
? _onTapWithSelectionLimit(widget.file)
: _onTapNoSelectionLimit(context, widget.file);
}
});
_onLongPressEventStreamSubscription =
SelectionGesturesEvent.of(context)
.onLongPressStreamController
.stream
.listen((offset) {
if (bbox.contains(offset)) {
_pointerInsideBbox = true;
widget.limitSelectionToOne
? _onLongPressWithSelectionLimit(context, widget.file)
: _onLongPressNoSelectionLimit(context, widget.file);
}
});
_pointerUpEventStreamSubscription =
SelectionGesturesEvent.of(context)
.upOffsetStreamController
.stream
.listen((event) {
if (bbox.contains(event)) {
if (_pointerInsideBbox) _pointerInsideBbox = false;
}
});
_pointerPositionStreamSubscription =
SelectionGesturesEvent.of(context)
.moveOffsetStreamController
.stream
.listen(
(event) {
if (widget.selectedFiles?.files.isEmpty ?? true) return;
_pointerInsideBboxPrevValue = _pointerInsideBbox;
if (bbox.contains(event)) {
_pointerInsideBbox = true;
} else {
_pointerInsideBbox = false;
}
if (_pointerInsideBbox == true &&
_pointerInsideBboxPrevValue == false) {
widget.selectedFiles!.toggleSelection(widget.file);
LastSelectedFileByDragging.of(context)
.updateLastSelectedFile(widget.file);
}
},
onError: (e) {
_logger.warning("Error in pointer position subscription", e);
},
onDone: () {
_logger.info("Pointer position subscription done");
},
);
} catch (e) {
_logger.warning("Error in pointer subscription", e);
}
}
});
}
}
@override
void dispose() {
_onTapEventStreamSubscription.cancel();
_pointerPositionStreamSubscription.cancel();
_pointerUpEventStreamSubscription.cancel();
_onLongPressEventStreamSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isFileSelected = selectedFiles?.isFileSelected(file) ?? false;
final isFileSelected =
widget.selectedFiles?.isFileSelected(widget.file) ?? false;
Color selectionColor = Colors.white;
if (isFileSelected && file.isUploaded && file.ownerID != currentUserID) {
if (isFileSelected &&
widget.file.isUploaded &&
widget.file.ownerID != widget.currentUserID) {
final avatarColors = getEnteColorScheme(context).avatarColors;
selectionColor =
avatarColors[(file.ownerID!).remainder(avatarColors.length)];
avatarColors[(widget.file.ownerID!).remainder(avatarColors.length)];
}
final String heroTag = tag + file.tag;
final String heroTag = widget.tag + widget.file.tag;
final Widget thumbnailWidget = ThumbnailWidget(
file,
widget.file,
diskLoadDeferDuration: thumbnailDiskLoadDeferDuration,
serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
shouldShowLivePhotoOverlay: true,
key: Key(heroTag),
thumbnailSize: photoGridSize < photoGridSizeDefault
thumbnailSize: widget.photoGridSize < photoGridSizeDefault
? thumbnailLargeSize
: thumbnailSmallSize,
shouldShowOwnerAvatar: !isFileSelected,
shouldShowVideoDuration: true,
);
return GestureDetector(
onTap: () {
limitSelectionToOne
? _onTapWithSelectionLimit(file)
: _onTapNoSelectionLimit(context, file);
},
onLongPress: () {
limitSelectionToOne
? _onLongPressWithSelectionLimit(context, file)
: _onLongPressNoSelectionLimit(context, file);
},
child: Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(1),
child: Hero(
tag: heroTag,
flightShuttleBuilder: (
flightContext,
animation,
flightDirection,
fromHeroContext,
toHeroContext,
) =>
thumbnailWidget,
transitionOnUserGestures: true,
child: isFileSelected
? ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(
0.4,
),
BlendMode.darken,
return Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
key: _globalKey,
borderRadius: BorderRadius.circular(1),
child: Hero(
tag: heroTag,
flightShuttleBuilder: (
flightContext,
animation,
flightDirection,
fromHeroContext,
toHeroContext,
) =>
thumbnailWidget,
transitionOnUserGestures: true,
child: isFileSelected
? ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(
0.4,
),
child: thumbnailWidget,
)
: thumbnailWidget,
),
BlendMode.darken,
),
child: thumbnailWidget,
)
: thumbnailWidget,
),
isFileSelected
? Positioned(
right: 4,
top: 4,
child: Icon(
Icons.check_circle_rounded,
size: 20,
color: selectionColor, //same for both themes
),
)
: const SizedBox.shrink(),
],
),
),
isFileSelected
? Positioned(
right: 4,
top: 4,
child: Icon(
Icons.check_circle_rounded,
size: 20,
color: selectionColor, //same for both themes
),
)
: const SizedBox.shrink(),
],
);
}
void _toggleFileSelection(EnteFile file) {
selectedFiles!.toggleSelection(file);
widget.selectedFiles!.toggleSelection(file);
}
void _onTapWithSelectionLimit(EnteFile file) {
if (selectedFiles!.files.isNotEmpty && selectedFiles!.files.first != file) {
selectedFiles!.clearAll();
if (widget.selectedFiles!.files.isNotEmpty &&
widget.selectedFiles!.files.first != file) {
widget.selectedFiles!.clearAll();
}
_toggleFileSelection(file);
}
void _onTapNoSelectionLimit(BuildContext context, EnteFile file) async {
final bool shouldToggleSelection =
(selectedFiles?.files.isNotEmpty ?? false) ||
(widget.selectedFiles?.files.isNotEmpty ?? false) ||
GalleryContextState.of(context)!.inSelectionMode;
if (shouldToggleSelection) {
_toggleFileSelection(file);
@@ -142,11 +281,14 @@ class GalleryFileWidget extends StatelessWidget {
}
void _onLongPressNoSelectionLimit(BuildContext context, EnteFile file) {
if (selectedFiles!.files.isNotEmpty) {
if (widget.selectedFiles!.files.isNotEmpty) {
_routeToDetailPage(file, context);
} else if (AppLifecycleService.instance.mediaExtensionAction.action ==
IntentAction.main) {
HapticFeedback.lightImpact();
LastSelectedFileByDragging.of(context).updateLastSelectedFile(
widget.file,
); // This is used to show the selection box
_toggleFileSelection(file);
}
}
@@ -167,10 +309,10 @@ class GalleryFileWidget extends StatelessWidget {
void _routeToDetailPage(EnteFile file, BuildContext context) {
final page = DetailPage(
DetailPageConfiguration(
List.unmodifiable(filesInGroup),
asyncLoader,
filesInGroup.indexOf(file),
tag,
List.unmodifiable(widget.filesInGroup),
widget.asyncLoader,
widget.filesInGroup.indexOf(file),
widget.tag,
sortOrderAsc: GalleryContextState.of(context)!.sortOrderAsc,
),
);

View File

@@ -44,6 +44,7 @@ class _LazyGridViewState extends State<LazyGridView> {
@override
void initState() {
super.initState();
_shouldRender = widget.shouldRender;
_currentUserID = Configuration.instance.getUserID();
widget.selectedFiles?.addListener(_selectedFilesListener);
@@ -53,7 +54,6 @@ class _LazyGridViewState extends State<LazyGridView> {
setState(() {});
}
});
super.initState();
}
@override

View File

@@ -265,3 +265,27 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
}
}
}
class GroupGalleryGlobalKey extends InheritedWidget {
const GroupGalleryGlobalKey({
super.key,
required this.globalKey,
required super.child,
});
final GlobalKey globalKey;
static GroupGalleryGlobalKey? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<GroupGalleryGlobalKey>();
}
static GroupGalleryGlobalKey of(BuildContext context) {
final GroupGalleryGlobalKey? result = maybeOf(context);
assert(result != null, 'No GroupGalleryGlobalKey found in context');
return result!;
}
@override
bool updateShouldNotify(GroupGalleryGlobalKey oldWidget) =>
globalKey != oldWidget.globalKey;
}

View File

@@ -17,6 +17,7 @@ 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';
@@ -258,27 +259,31 @@ class GalleryState extends State<Gallery> {
sortOrderAsc: _sortOrderAsc,
inSelectionMode: widget.inSelectionMode,
type: widget.groupType,
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,
child: SwipeToSelectHelper(
files: currentGroupedFiles.expand((element) => element).toList(),
selectedFiles: widget.selectedFiles,
showSelectAllByDefault:
widget.showSelectAllByDefault && widget.groupType.showGroupHeader(),
isScrollablePositionedList: widget.isScrollablePositionedList,
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,
),
),
);
}

View File

@@ -15,6 +15,7 @@ class GalleryContextState extends InheritedWidget {
Key? key,
}) : super(key: key, child: child);
//TODO: throw error with message if no GalleryContextState found
static GalleryContextState? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<GalleryContextState>();
}

View File

@@ -0,0 +1,289 @@
import "dart:async";
import "package:flutter/material.dart";
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";
class SwipeToSelectHelper extends StatefulWidget {
final List<EnteFile> files;
final SelectedFiles? selectedFiles;
final Widget child;
const SwipeToSelectHelper({
required this.files,
required this.selectedFiles,
required this.child,
super.key,
});
@override
State<SwipeToSelectHelper> createState() => _SwipeToSelectHelperState();
}
class _SwipeToSelectHelperState extends State<SwipeToSelectHelper> {
final _groupGalleryGlobalKey = GlobalKey();
@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,
),
),
);
},
),
);
}
}
class LastSelectedFileByDragging extends InheritedWidget {
///Check if this should updates on didUpdateWidget. If so, use a state varaible
///and update it there on didUpdateWidget.
final List<EnteFile> filesInGroup;
LastSelectedFileByDragging({
super.key,
required this.filesInGroup,
required super.child,
});
final _indexInGroup = ValueNotifier<int>(-1);
void updateLastSelectedFile(EnteFile file) {
_indexInGroup.value = filesInGroup.indexOf(file);
}
ValueNotifier<int> get index => _indexInGroup;
static LastSelectedFileByDragging? maybeOf(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<LastSelectedFileByDragging>();
}
static LastSelectedFileByDragging of(BuildContext context) {
final LastSelectedFileByDragging? result = maybeOf(context);
assert(result != null, 'No LastSelectedFileByDragging found in context');
return result!;
}
@override
bool updateShouldNotify(LastSelectedFileByDragging oldWidget) =>
_indexInGroup != oldWidget._indexInGroup ||
filesInGroup != oldWidget.filesInGroup;
}
class SelectionGesturesEventProvider extends StatefulWidget {
final Widget child;
final List<EnteFile> files;
final SelectedFiles selectedFiles;
const SelectionGesturesEventProvider({
super.key,
required this.selectedFiles,
required this.files,
required this.child,
});
@override
State<SelectionGesturesEventProvider> createState() =>
_SelectionGesturesEventProviderState();
}
class _SelectionGesturesEventProviderState
extends State<SelectionGesturesEventProvider> {
late SelectionGesturesEvent selectionGesturesEvent;
bool _isFingerOnScreenSinceLongPress = false;
bool _isDragging = false;
int prevSelectedFileIndex = -1;
int currentSelectedFileIndex = -1;
final _logger = Logger("PointerProvider");
@override
void initState() {
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
LastSelectedFileByDragging.of(context)
.index
.removeListener(swipingToSelectListener);
LastSelectedFileByDragging.of(context)._indexInGroup.addListener(
swipingToSelectListener,
);
}
@override
void dispose() {
selectionGesturesEvent.closeMoveOffsetController();
selectionGesturesEvent.closeUpOffsetStreamController();
selectionGesturesEvent.closeOnTapStreamController();
selectionGesturesEvent.closeOnLongPressStreamController();
widget.selectedFiles.removeListener(
swipingToSelectListener,
);
super.dispose();
}
void swipingToSelectListener() {
prevSelectedFileIndex = currentSelectedFileIndex;
currentSelectedFileIndex =
LastSelectedFileByDragging.of(context).index.value;
if (prevSelectedFileIndex != -1 && currentSelectedFileIndex != -1) {
if ((currentSelectedFileIndex - prevSelectedFileIndex).abs() > 1) {
late final int startIndex;
late final int endIndex;
if (currentSelectedFileIndex > prevSelectedFileIndex) {
startIndex = prevSelectedFileIndex;
endIndex = currentSelectedFileIndex;
} else {
startIndex = currentSelectedFileIndex;
endIndex = prevSelectedFileIndex;
}
widget.selectedFiles.toggleFilesSelection(
widget.files
.sublist(
startIndex + 1,
endIndex,
)
.toSet(),
);
}
}
}
@override
Widget build(BuildContext context) {
return SelectionGesturesEvent(
child: Builder(
builder: (context) {
selectionGesturesEvent = SelectionGesturesEvent.of(context);
return GestureDetector(
onTap: () {
selectionGesturesEvent.onTapStreamController
.add(selectionGesturesEvent.pointerPosition);
},
onLongPress: () {
_isFingerOnScreenSinceLongPress = true;
selectionGesturesEvent.onLongPressStreamController
.add(selectionGesturesEvent.pointerPosition);
},
onHorizontalDragUpdate: (details) {
onDragToSelect(details.localPosition);
},
child: Listener(
onPointerMove: (event) {
selectionGesturesEvent.pointerPosition = event.localPosition;
//onHorizontalDragUpdate is not called when dragging after
//long press without lifting finger. This is for handling only
//this case.
if (_isFingerOnScreenSinceLongPress &&
(event.localDelta.dx.abs() > 0 &&
event.localDelta.dy.abs() > 0)) {
onDragToSelect(event.localPosition);
}
},
onPointerDown: (event) {
selectionGesturesEvent.pointerPosition = event.localPosition;
},
onPointerUp: (event) {
_isFingerOnScreenSinceLongPress = false;
_isDragging = false;
selectionGesturesEvent.upOffsetStreamController
.add(event.localPosition);
LastSelectedFileByDragging.of(context).index.value = -1;
currentSelectedFileIndex = -1;
},
child: widget.child,
),
);
},
),
);
}
void onDragToSelect(Offset offset) {
selectionGesturesEvent.moveOffsetStreamController.add(offset);
_isDragging = true;
}
}
class SelectionGesturesEvent extends InheritedWidget {
SelectionGesturesEvent({super.key, required super.child});
//This is a List<Offset> instead of just and Offset is so that it can be final
//and still be mutable. Need to have this as final to keep Pointer immutable
//which is recommended for inherited widgets.
final _pointerPosition =
List.generate(1, (_) => Offset.zero, growable: false);
Offset get pointerPosition => _pointerPosition[0];
set pointerPosition(Offset offset) {
_pointerPosition[0] = offset;
}
final StreamController<Offset> onTapStreamController =
StreamController.broadcast();
final StreamController<Offset> onLongPressStreamController =
StreamController.broadcast();
final StreamController<Offset> moveOffsetStreamController =
StreamController.broadcast();
final StreamController<Offset> upOffsetStreamController =
StreamController.broadcast();
Future<dynamic> closeOnTapStreamController() {
debugPrint("dragToSelect: Closing onTapStreamController");
return onTapStreamController.close();
}
Future<dynamic> closeOnLongPressStreamController() {
debugPrint("dragToSelect: Closing onLongPressStreamController");
return onLongPressStreamController.close();
}
Future<dynamic> closeMoveOffsetController() {
debugPrint("dragToSelect: Closing moveOffsetStreamController");
return moveOffsetStreamController.close();
}
Future<dynamic> closeUpOffsetStreamController() {
debugPrint("dragToSelect: Closing upOffsetStreamController");
return upOffsetStreamController.close();
}
static SelectionGesturesEvent? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<SelectionGesturesEvent>();
}
static SelectionGesturesEvent of(BuildContext context) {
final SelectionGesturesEvent? result = maybeOf(context);
assert(result != null, 'No SelectionGesturesEvent found in context');
return result!;
}
@override
bool updateShouldNotify(SelectionGesturesEvent oldWidget) =>
moveOffsetStreamController != oldWidget.moveOffsetStreamController ||
upOffsetStreamController != oldWidget.upOffsetStreamController ||
onTapStreamController != oldWidget.onTapStreamController ||
onLongPressStreamController != oldWidget.onLongPressStreamController;
}