Compare commits
21 Commits
auth_ios_t
...
swipe_to_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bfa5965a2 | ||
|
|
ffaae58abd | ||
|
|
cf56b7e057 | ||
|
|
570871a1f0 | ||
|
|
e71f40098f | ||
|
|
c45bbdda14 | ||
|
|
e0ff71828a | ||
|
|
28d9775203 | ||
|
|
c70c6ac617 | ||
|
|
28107cc7ea | ||
|
|
8711753d6f | ||
|
|
b68d11ffbc | ||
|
|
ac0235a6be | ||
|
|
8116b05a9d | ||
|
|
3f49395ee2 | ||
|
|
2d80ed7332 | ||
|
|
5ca0be9f2b | ||
|
|
e0b2fa5a1b | ||
|
|
a58e9030a0 | ||
|
|
1c7fe80663 | ||
|
|
ce701099f0 |
@@ -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,
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
289
mobile/lib/ui/viewer/gallery/swipe_to_select_helper.dart
Normal file
289
mobile/lib/ui/viewer/gallery/swipe_to_select_helper.dart
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user