mirror of
https://github.com/immich-app/immich.git
synced 2025-01-17 01:06:46 +01:00
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 <alex.tran1502@gmail.com>
This commit is contained in:
parent
0dbe44cb78
commit
dc9b51ad02
3 changed files with 120 additions and 24 deletions
|
@ -0,0 +1,14 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final scrollToDateNotifierProvider = ScrollToDateNotifier(null);
|
||||||
|
|
||||||
|
class ScrollToDateNotifier extends ValueNotifier<DateTime?> {
|
||||||
|
ScrollToDateNotifier(super.value);
|
||||||
|
|
||||||
|
void scrollToDate(DateTime date) {
|
||||||
|
value = date;
|
||||||
|
|
||||||
|
// Manually notify listeners to trigger the scroll, even if the value hasn't changed
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/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_image.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.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/modules/home/ui/control_bottom_app_bar.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.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:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
|
@ -150,6 +153,23 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||||
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null;
|
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _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) {
|
Widget _itemBuilder(BuildContext c, int position) {
|
||||||
int index = position;
|
int index = position;
|
||||||
if (widget.topWidget != null) {
|
if (widget.topWidget != null) {
|
||||||
|
@ -247,6 +267,48 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||||
: RefreshIndicator(onRefresh: widget.onRefresh!, child: child);
|
: 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
|
@override
|
||||||
void didUpdateWidget(ImmichAssetGridView oldWidget) {
|
void didUpdateWidget(ImmichAssetGridView oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
@ -261,6 +323,8 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
scrollToTopNotifierProvider.addListener(_scrollToTop);
|
scrollToTopNotifierProvider.addListener(_scrollToTop);
|
||||||
|
scrollToDateNotifierProvider.addListener(_scrollToDate);
|
||||||
|
|
||||||
if (widget.visibleItemsListener != null) {
|
if (widget.visibleItemsListener != null) {
|
||||||
_itemPositionsListener.itemPositions.addListener(_positionListener);
|
_itemPositionsListener.itemPositions.addListener(_positionListener);
|
||||||
}
|
}
|
||||||
|
@ -274,6 +338,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
scrollToTopNotifierProvider.removeListener(_scrollToTop);
|
scrollToTopNotifierProvider.removeListener(_scrollToTop);
|
||||||
|
scrollToDateNotifierProvider.removeListener(_scrollToDate);
|
||||||
if (widget.visibleItemsListener != null) {
|
if (widget.visibleItemsListener != null) {
|
||||||
_itemPositionsListener.itemPositions.removeListener(_positionListener);
|
_itemPositionsListener.itemPositions.removeListener(_positionListener);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/memories/models/memory.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 {
|
class MemoryBottomInfo extends StatelessWidget {
|
||||||
final Memory memory;
|
final Memory memory;
|
||||||
|
@ -12,8 +16,7 @@ class MemoryBottomInfo extends StatelessWidget {
|
||||||
final df = DateFormat.yMMMMd();
|
final df = DateFormat.yMMMMd();
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Row(
|
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
children: [
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -37,8 +40,22 @@ class MemoryBottomInfo extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
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,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue