From dc9b51ad02150b13514733c6396d44ab8237b679 Mon Sep 17 00:00:00 2001 From: Arno Wiest <46051866+arnolicious@users.noreply.github.com> Date: Wed, 24 Apr 2024 22:02:03 +0200 Subject: [PATCH] feat(mobile): Added "jump to date" functionality to the memory view (#7323) * implemented jump to date from memory * Changed implementation to a ValueNotifier & fixes * remove debug code * feat(mobile): - Added index bound checks - Handled edge cases when scrolling to the very bottom of the grid-view - removing the listener on dispose * feat(mobile): fixed debug index offset & added debug toast for scroll errors * feat(mobile): added more debug toasts... * feat(mobile): scroll to month, if timeline is not grouped by days --------- Co-authored-by: Alex Tran --- .../scroll_to_date_notifier.provider.dart | 14 ++++ .../ui/asset_grid/immich_asset_grid_view.dart | 65 +++++++++++++++++++ .../memories/ui/memory_bottom_info.dart | 65 ++++++++++++------- 3 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 mobile/lib/modules/asset_viewer/providers/scroll_to_date_notifier.provider.dart diff --git a/mobile/lib/modules/asset_viewer/providers/scroll_to_date_notifier.provider.dart b/mobile/lib/modules/asset_viewer/providers/scroll_to_date_notifier.provider.dart new file mode 100644 index 0000000000..69e598c436 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/providers/scroll_to_date_notifier.provider.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +final scrollToDateNotifierProvider = ScrollToDateNotifier(null); + +class ScrollToDateNotifier extends ValueNotifier { + ScrollToDateNotifier(super.value); + + void scrollToDate(DateTime date) { + value = date; + + // Manually notify listeners to trigger the scroll, even if the value hasn't changed + notifyListeners(); + } +} diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart index 5ece42d5cf..2ab9db56be 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart @@ -13,8 +13,11 @@ import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.pro import 'package:immich_mobile/modules/home/ui/asset_grid/asset_drag_region.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/scroll_to_date_notifier.provider.dart'; import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -150,6 +153,23 @@ class ImmichAssetGridViewState extends ConsumerState { assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null; } + Future _scrollToIndex(int index) async { + // if the index is so far down, that the end of the list is reached on the screen + // the scroll_position widget crashes. This is a workaround to prevent this. + // If the index is within the last 10 elements, we jump instead of scrolling. + if (widget.renderList.elements.length <= index + 10) { + _itemScrollController.jumpTo( + index: index, + ); + return; + } + await _itemScrollController.scrollTo( + index: index, + alignment: 0, + duration: const Duration(milliseconds: 500), + ); + } + Widget _itemBuilder(BuildContext c, int position) { int index = position; if (widget.topWidget != null) { @@ -247,6 +267,48 @@ class ImmichAssetGridViewState extends ConsumerState { : RefreshIndicator(onRefresh: widget.onRefresh!, child: child); } + void _scrollToDate() { + final date = scrollToDateNotifierProvider.value; + if (date == null) { + ImmichToast.show( + context: context, + msg: "Scroll To Date failed, date is null.", + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + return; + } + + // Search for the index of the exact date in the list + var index = widget.renderList.elements.indexWhere( + (e) => + e.date.year == date.year && + e.date.month == date.month && + e.date.day == date.day, + ); + + // If the exact date is not found, the timeline is grouped by month, + // thus we search for the month + if (index == -1) { + index = widget.renderList.elements.indexWhere( + (e) => e.date.year == date.year && e.date.month == date.month, + ); + } + + if (index != -1 && index < widget.renderList.elements.length) { + // Not sure why the index is shifted, but it works. :3 + _scrollToIndex(index + 1); + } else { + ImmichToast.show( + context: context, + msg: + "The date (${DateFormat.yMd().format(date)}) could not be found in the timeline.", + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } + } + @override void didUpdateWidget(ImmichAssetGridView oldWidget) { super.didUpdateWidget(oldWidget); @@ -261,6 +323,8 @@ class ImmichAssetGridViewState extends ConsumerState { void initState() { super.initState(); scrollToTopNotifierProvider.addListener(_scrollToTop); + scrollToDateNotifierProvider.addListener(_scrollToDate); + if (widget.visibleItemsListener != null) { _itemPositionsListener.itemPositions.addListener(_positionListener); } @@ -274,6 +338,7 @@ class ImmichAssetGridViewState extends ConsumerState { @override void dispose() { scrollToTopNotifierProvider.removeListener(_scrollToTop); + scrollToDateNotifierProvider.removeListener(_scrollToDate); if (widget.visibleItemsListener != null) { _itemPositionsListener.itemPositions.removeListener(_positionListener); } diff --git a/mobile/lib/modules/memories/ui/memory_bottom_info.dart b/mobile/lib/modules/memories/ui/memory_bottom_info.dart index b09d83c9f9..54086291fa 100644 --- a/mobile/lib/modules/memories/ui/memory_bottom_info.dart +++ b/mobile/lib/modules/memories/ui/memory_bottom_info.dart @@ -1,6 +1,10 @@ +// ignore_for_file: require_trailing_commas + +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/modules/memories/models/memory.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/scroll_to_date_notifier.provider.dart'; class MemoryBottomInfo extends StatelessWidget { final Memory memory; @@ -12,33 +16,46 @@ class MemoryBottomInfo extends StatelessWidget { final df = DateFormat.yMMMMd(); return Padding( padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - memory.title, - style: TextStyle( - color: Colors.grey[400], - fontSize: 13.0, - fontWeight: FontWeight.w500, - ), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + memory.title, + style: TextStyle( + color: Colors.grey[400], + fontSize: 13.0, + fontWeight: FontWeight.w500, ), - Text( - df.format( - memory.assets[0].fileCreatedAt, - ), - style: const TextStyle( - color: Colors.white, - fontSize: 15.0, - fontWeight: FontWeight.w500, - ), + ), + Text( + df.format( + memory.assets[0].fileCreatedAt, ), - ], + style: const TextStyle( + color: Colors.white, + fontSize: 15.0, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + MaterialButton( + minWidth: 0, + onPressed: () { + context.popRoute(); + scrollToDateNotifierProvider + .scrollToDate(memory.assets[0].fileCreatedAt); + }, + shape: const CircleBorder(), + color: Colors.white.withOpacity(0.2), + elevation: 0, + child: const Icon( + Icons.open_in_new, + color: Colors.white, ), - ], - ), + ), + ]), ); } }