Skip to content

Commit d15f67d

Browse files
authored
feat: scroll to top & view in timeline (#20274)
* feat: scroll to top & view in timeline * use EventStream * refactor: event invocation and listerner * fix: correct parent routing
1 parent 6becf40 commit d15f67d

File tree

8 files changed

+114
-25
lines changed

8 files changed

+114
-25
lines changed

mobile/lib/domain/models/timeline.model.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,13 @@ class TimeBucket extends Bucket {
4545
class TimelineReloadEvent extends Event {
4646
const TimelineReloadEvent();
4747
}
48+
49+
class ScrollToTopEvent extends Event {
50+
const ScrollToTopEvent();
51+
}
52+
53+
class ScrollToDateEvent extends Event {
54+
final DateTime date;
55+
56+
const ScrollToDateEvent(this.date);
57+
}

mobile/lib/infrastructure/repositories/log.repository.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ class IsarLogRepository extends IsarDatabaseRepository {
1919

2020
Future<bool> insert(LogMessage log) async {
2121
final logEntity = LoggerMessage.fromDto(log);
22-
await transaction(() async {
23-
await _db.loggerMessages.put(logEntity);
24-
});
22+
23+
try {
24+
await transaction(() => _db.loggerMessages.put(logEntity));
25+
} catch (e) {
26+
return false;
27+
}
28+
2529
return true;
2630
}
2731

mobile/lib/pages/common/tab_shell.page.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import 'package:auto_route/auto_route.dart';
22
import 'package:easy_localization/easy_localization.dart';
33
import 'package:flutter/material.dart';
44
import 'package:hooks_riverpod/hooks_riverpod.dart';
5+
import 'package:immich_mobile/domain/models/timeline.model.dart';
6+
import 'package:immich_mobile/domain/utils/event_stream.dart';
57
import 'package:immich_mobile/extensions/build_context_extensions.dart';
68
import 'package:immich_mobile/providers/app_settings.provider.dart';
7-
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
89
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
910
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
1011
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
@@ -155,7 +156,7 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
155156
void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
156157
// On Photos page menu tapped
157158
if (router.activeIndex == 0 && index == 0) {
158-
scrollToTopNotifierProvider.scrollToTop();
159+
EventStream.shared.emit(const ScrollToTopEvent());
159160
}
160161

161162
// On Search page tapped

mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import 'package:hooks_riverpod/hooks_riverpod.dart';
44
import 'package:immich_mobile/constants/enums.dart';
55
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
6+
import 'package:immich_mobile/domain/models/timeline.model.dart';
67
import 'package:immich_mobile/domain/utils/event_stream.dart';
78
import 'package:immich_mobile/extensions/build_context_extensions.dart';
89
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
@@ -15,6 +16,7 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse
1516
import 'package:immich_mobile/providers/routes.provider.dart';
1617
import 'package:immich_mobile/providers/user.provider.dart';
1718
import 'package:immich_mobile/providers/websocket.provider.dart';
19+
import 'package:immich_mobile/routing/router.dart';
1820

1921
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
2022
const ViewerTopAppBar({super.key});
@@ -30,6 +32,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
3032
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
3133
final isInLockedView = ref.watch(inLockedViewProvider);
3234

35+
final previousRouteName = ref.watch(previousRouteNameProvider);
36+
final showViewInTimelineButton = previousRouteName != TabShellRoute.name && previousRouteName != null;
37+
3338
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
3439
int opacity = ref.watch(
3540
assetViewerProvider.select((state) => state.backgroundOpacity),
@@ -50,6 +55,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
5055
const CastActionButton(
5156
menuItem: true,
5257
),
58+
if (showViewInTimelineButton)
59+
IconButton(
60+
onPressed: () async {
61+
await context.maybePop();
62+
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
63+
EventStream.shared.emit(ScrollToDateEvent(asset.createdAt));
64+
},
65+
icon: const Icon(Icons.image_search),
66+
tooltip: 'view_in_timeline',
67+
),
5368
if (asset.hasRemote && isOwner && !asset.isFavorite)
5469
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
5570
if (asset.hasRemote && isOwner && asset.isFavorite)

mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import 'package:auto_route/auto_route.dart';
44
import 'package:easy_localization/easy_localization.dart';
55
import 'package:flutter/material.dart';
66
import 'package:immich_mobile/domain/models/memory.model.dart';
7-
8-
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
7+
import 'package:immich_mobile/domain/models/timeline.model.dart';
8+
import 'package:immich_mobile/domain/utils/event_stream.dart';
9+
import 'package:immich_mobile/routing/router.dart';
910

1011
class DriftMemoryBottomInfo extends StatelessWidget {
1112
final DriftMemory memory;
@@ -44,18 +45,22 @@ class DriftMemoryBottomInfo extends StatelessWidget {
4445
),
4546
],
4647
),
47-
MaterialButton(
48-
minWidth: 0,
49-
onPressed: () {
50-
context.maybePop();
51-
scrollToDateNotifierProvider.scrollToDate(fileCreatedDate);
52-
},
53-
shape: const CircleBorder(),
54-
color: Colors.white.withValues(alpha: 0.2),
55-
elevation: 0,
56-
child: const Icon(
57-
Icons.open_in_new,
58-
color: Colors.white,
48+
Tooltip(
49+
message: 'view_in_timeline'.tr(),
50+
child: MaterialButton(
51+
minWidth: 0,
52+
onPressed: () {
53+
context.maybePop();
54+
context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
55+
EventStream.shared.emit(ScrollToDateEvent(fileCreatedDate));
56+
},
57+
shape: const CircleBorder(),
58+
color: Colors.white.withValues(alpha: 0.2),
59+
elevation: 0,
60+
child: const Icon(
61+
Icons.open_in_new,
62+
color: Colors.white,
63+
),
5964
),
6065
),
6166
]),

mobile/lib/presentation/widgets/timeline/timeline.widget.dart

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,73 @@ class _SliverTimeline extends ConsumerStatefulWidget {
9797

9898
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
9999
final _scrollController = ScrollController();
100-
StreamSubscription? _reloadSubscription;
100+
StreamSubscription? _eventSubscription;
101101

102102
@override
103103
void initState() {
104104
super.initState();
105-
_reloadSubscription = EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
105+
_eventSubscription = EventStream.shared.listen(_onEvent);
106+
}
107+
108+
void _onEvent(Event event) {
109+
switch (event) {
110+
case ScrollToTopEvent():
111+
_scrollController.animateTo(
112+
0,
113+
duration: const Duration(milliseconds: 250),
114+
curve: Curves.easeInOut,
115+
);
116+
case ScrollToDateEvent scrollToDateEvent:
117+
_scrollToDate(scrollToDateEvent.date);
118+
case TimelineReloadEvent():
119+
setState(() {});
120+
default:
121+
break;
122+
}
106123
}
107124

108125
@override
109126
void dispose() {
110127
_scrollController.dispose();
111-
_reloadSubscription?.cancel();
128+
_eventSubscription?.cancel();
112129
super.dispose();
113130
}
114131

132+
void _scrollToDate(DateTime date) {
133+
final asyncSegments = ref.read(timelineSegmentProvider);
134+
asyncSegments.whenData((segments) {
135+
// Find the segment that contains assets from the target date
136+
final targetSegment = segments.firstWhereOrNull((segment) {
137+
if (segment.bucket is TimeBucket) {
138+
final segmentDate = (segment.bucket as TimeBucket).date;
139+
// Check if the segment date matches the target date (year, month, day)
140+
return segmentDate.year == date.year && segmentDate.month == date.month && segmentDate.day == date.day;
141+
}
142+
return false;
143+
});
144+
145+
// If exact date not found, try to find the closest month
146+
final fallbackSegment = targetSegment ??
147+
segments.firstWhereOrNull((segment) {
148+
if (segment.bucket is TimeBucket) {
149+
final segmentDate = (segment.bucket as TimeBucket).date;
150+
return segmentDate.year == date.year && segmentDate.month == date.month;
151+
}
152+
return false;
153+
});
154+
155+
if (fallbackSegment != null) {
156+
// Scroll to the segment with a small offset to show the header
157+
final targetOffset = fallbackSegment.startOffset - 50;
158+
_scrollController.animateTo(
159+
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
160+
duration: const Duration(milliseconds: 500),
161+
curve: Curves.easeInOut,
162+
);
163+
}
164+
});
165+
}
166+
115167
@override
116168
Widget build(BuildContext _) {
117169
final asyncSegments = ref.watch(timelineSegmentProvider);

mobile/lib/providers/routes.provider.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
22

33
final inLockedViewProvider = StateProvider<bool>((ref) => false);
44
final currentRouteNameProvider = StateProvider<String?>((ref) => null);
5+
final previousRouteNameProvider = StateProvider<String?>((ref) => null);

mobile/lib/routing/app_navigation_observer.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ class AppNavigationObserver extends AutoRouterObserver {
2626
void didPush(Route route, Route? previousRoute) {
2727
_handleLockedViewState(route, previousRoute);
2828
_handleDriftLockedFolderState(route, previousRoute);
29-
Future(
30-
() => ref.read(currentRouteNameProvider.notifier).state = route.settings.name,
31-
);
29+
Future(() {
30+
ref.read(currentRouteNameProvider.notifier).state = route.settings.name;
31+
ref.read(previousRouteNameProvider.notifier).state = previousRoute?.settings.name;
32+
});
3233
}
3334

3435
_handleLockedViewState(Route route, Route? previousRoute) {

0 commit comments

Comments
 (0)