Path:
lib/application/view_models/navigation_notifier.dart
Lines:
406
Non-empty lines:
334
Non-empty lines covered with requirements:
334 / 334 (100.0%)
Functions:
0
Functions covered by requirements:
0 / 0 (0.0%)
1
import 'package:bioflow_pro/application/providers/ui/ui_providers.dart';
2
import 'package:bioflow_pro/domain/entities/ui/navigation_state.dart';
3
import 'package:bioflow_pro/domain/entities/ui/overlay_state.dart';
4
import 'package:flutter_riverpod/flutter_riverpod.dart';
5
6
// @relation(ARCH-010, scope=file)7
/// Notifier for managing navigation state with route history tracking8
class NavigationNotifier extends Notifier<NavigationState> {
9
@override
10
NavigationState build() {
11
return NavigationState.initial();
12
}
13
14
/// Navigate to a new route with optional parameters
15
Future<void> navigateTo(
16
String newRoute, {
17
Map<String, dynamic>? params,
18
}) async {
19
state = state.navigateTo(newRoute, params: params);
20
}
21
22
/// Navigate back to the previous route
23
Future<void> goBack() async {
24
if (state.canGoBack) {
25
state = state.goBack();
26
}
27
}
28
29
/// Replace current route without adding to history
30
Future<void> replaceCurrent(
31
String newRoute, {
32
Map<String, dynamic>? params,
33
}) async {
34
state = state.replaceCurrent(newRoute, params: params);
35
}
36
37
/// Reset navigation to root route
38
Future<void> resetToRoot() async {
39
state = state.resetToRoot();
40
}
41
42
/// Clear route history but keep current route
43
Future<void> clearHistory() async {
44
state = state.clearHistory();
45
}
46
47
/// Update route parameters without changing route
48
Future<void> updateParams(Map<String, dynamic> newParams) async {
49
state = state.updateParams(newParams);
50
}
51
52
/// Add or update a single route parameter
53
Future<void> setParam(String key, dynamic value) async {
54
state = state.setParam(key, value);
55
}
56
57
/// Remove a route parameter
58
Future<void> removeParam(String key) async {
59
state = state.removeParam(key);
60
}
61
62
/// Navigate to patients overlay
63
Future<void> navigateToPatients({Map<String, dynamic>? params}) async {
64
await navigateTo('/patients', params: params);
65
}
66
67
/// Navigate to montages overlay
68
Future<void> navigateToMontages({Map<String, dynamic>? params}) async {
69
await navigateTo('/montages', params: params);
70
}
71
72
/// Navigate to settings overlay
73
Future<void> navigateToSettings({Map<String, dynamic>? params}) async {
74
await navigateTo('/settings', params: params);
75
}
76
77
/// Navigate to activities overlay
78
Future<void> navigateToActivities({Map<String, dynamic>? params}) async {
79
await navigateTo('/activities', params: params);
80
}
81
82
/// Navigate to home/root
83
Future<void> navigateToHome() async {
84
await navigateTo('/');
85
}
86
87
/// Get current route
88
String get currentRoute => state.currentRoute;
89
90
/// Get previous route
91
String? get previousRoute => state.previousRoute;
92
93
/// Get route history
94
List<String> get routeHistory => state.routeHistory;
95
96
/// Get route parameters
97
Map<String, dynamic> get routeParams => state.routeParams;
98
99
/// Check if at root route
100
bool get isAtRoot => state.isAtRoot;
101
102
/// Check if at overlay route
103
bool get isAtOverlay => state.isAtOverlay;
104
105
/// Get current overlay type
106
String? get currentOverlayType => state.currentOverlayType;
107
108
/// Check if can navigate back
109
bool get canGoBack => state.canGoBack;
110
111
/// Get route depth
112
int get routeDepth => state.routeDepth;
113
114
/// Check if route has parameters
115
bool get hasParams => state.hasParams;
116
117
/// Get typed parameter value
118
T? getParam<T>(String key) {
119
return state.getParam<T>(key);
120
}
121
122
/// Check if specific parameter exists
123
bool hasParam(String key) => state.hasParam(key);
124
125
/// Get time since last navigation
126
Duration? get timeSinceLastNavigation => state.timeSinceLastNavigation;
127
128
/// Check if navigation was recent
129
bool get wasRecentNavigation => state.wasRecentNavigation;
130
131
/// Get formatted history
132
String get formattedHistory => state.formattedHistory;
133
134
/// Get current route segments
135
List<String> get currentRouteSegments => state.currentRouteSegments;
136
137
/// Check if current route matches pattern
138
bool matchesRoute(String pattern) {
139
return state.matchesRoute(pattern);
140
}
141
142
/// Get breadcrumb trail
143
List<String> get breadcrumbTrail => state.breadcrumbTrail;
144
145
/// Get current route name
146
String get currentRouteName => state.currentRouteName;
147
148
/// Check if navigation state is valid
149
bool get isStateValid => state.isValid;
150
151
/// Navigate to route with validation
152
Future<bool> tryNavigateTo(
153
String newRoute, {
154
Map<String, dynamic>? params,
155
}) async {
156
try {
157
await navigateTo(newRoute, params: params);
158
return true;
159
} catch (e) {
160
// Handle navigation errors
161
return false;
162
}
163
}
164
165
/// Navigate to specific overlay by type
166
Future<void> navigateToOverlay(String overlayType, {Map<String, dynamic>? params}) async {
167
final route = '/$overlayType';
168
await navigateTo(route, params: params);
169
}
170
171
/// Check if currently at specific overlay
172
bool isAtSpecificOverlay(String overlayType) {
173
return state.currentOverlayType == overlayType;
174
}
175
176
/// Go back with safety check
177
Future<bool> safeGoBack() async {
178
if (state.canGoBack) {
179
await goBack();
180
return true;
181
}
182
return false;
183
}
184
185
/// Navigate with route validation
186
Future<bool> navigateWithValidation(String route) async {
187
// Basic route validation
188
if (route.isEmpty || !route.startsWith('/')) {
189
return false;
190
}
191
192
await navigateTo(route);
193
return true;
194
}
195
196
/// Push new route onto stack
197
Future<void> pushRoute(String route, {Map<String, dynamic>? params}) async {
198
await navigateTo(route, params: params);
199
}
200
201
/// Pop current route from stack
202
Future<void> popRoute() async {
203
await goBack();
204
}
205
206
/// Replace entire route stack
207
Future<void> replaceStack(List<String> routes) async {
208
if (routes.isEmpty) {
209
await resetToRoot();
210
return;
211
}
212
213
// Clear current history and build new stack
214
state = state.clearHistory();
215
216
// Navigate through each route except the last
217
for (int i = 0; i < routes.length - 1; i++) {
218
state = state.navigateTo(routes[i]);
219
}
220
221
// Navigate to final route
222
if (routes.isNotEmpty) {
223
await navigateTo(routes.last);
224
}
225
}
226
227
/// Get navigation statistics
228
Map<String, dynamic> getNavigationStats() {
229
return {
230
'current_route': state.currentRoute,
231
'previous_route': state.previousRoute,
232
'route_depth': state.routeDepth,
233
'history_length': state.routeHistory.length,
234
'is_at_root': state.isAtRoot,
235
'is_at_overlay': state.isAtOverlay,
236
'current_overlay_type': state.currentOverlayType,
237
'can_go_back': state.canGoBack,
238
'has_params': state.hasParams,
239
'param_count': state.routeParams.length,
240
'last_navigation_at': state.lastNavigationAt?.toIso8601String(),
241
'time_since_last_navigation_ms': state.timeSinceLastNavigation?.inMilliseconds,
242
'was_recent_navigation': state.wasRecentNavigation,
243
'is_valid_state': state.isValid,
244
'route_name': state.currentRouteName,
245
'breadcrumb_count': state.breadcrumbTrail.length,
246
};
247
}
248
249
/// Handle deep link navigation
250
Future<void> handleDeepLink(String deepLink) async {
251
// Parse deep link and extract route and parameters
252
final uri = Uri.tryParse(deepLink);
253
if (uri != null) {
254
final route = uri.path.isEmpty ? '/' : uri.path;
255
final params = uri.queryParameters;
256
257
await navigateTo(route, params: params);
258
}
259
}
260
261
/// Generate deep link for current state
262
String generateDeepLink() {
263
final uri = Uri(
264
path: state.currentRoute,
265
queryParameters: state.routeParams.map(
266
(key, value) => MapEntry(key, value.toString()),
267
),
268
);
269
return uri.toString();
270
}
271
272
/// Check if navigation to route is allowed
273
bool isNavigationAllowed(String route) {
274
// Define navigation rules
275
const restrictedRoutes = <String>[];
276
const allowedRoutes = ['/', '/patients', '/montages', '/settings', '/activities'];
277
278
if (restrictedRoutes.contains(route)) {
279
return false;
280
}
281
282
return allowedRoutes.contains(route) || allowedRoutes.any(route.startsWith);
283
}
284
285
/// Navigate with permission check
286
Future<bool> navigateWithPermission(String route) async {
287
if (isNavigationAllowed(route)) {
288
await navigateTo(route);
289
return true;
290
}
291
return false;
292
}
293
294
/// Get route hierarchy
295
List<String> getRouteHierarchy() {
296
return state.breadcrumbTrail;
297
}
298
299
/// Check if route is in history
300
bool isRouteInHistory(String route) {
301
return state.routeHistory.contains(route);
302
}
303
304
/// Get last occurrence of route in history
305
int? getLastRouteIndex(String route) {
306
final lastIndex = state.routeHistory.lastIndexOf(route);
307
return lastIndex >= 0 ? lastIndex : null;
308
}
309
310
/// Navigate to route by history index
311
Future<void> navigateToHistoryIndex(int index) async {
312
if (index >= 0 && index < state.routeHistory.length) {
313
final targetRoute = state.routeHistory[index];
314
315
// Trim history to target index + 1
316
final newHistory = state.routeHistory.sublist(0, index + 1);
317
318
state = state.copyWith(
319
currentRoute: targetRoute,
320
routeHistory: newHistory,
321
previousRoute: index > 0 ? newHistory[index - 1] : null,
322
routeParams: {}, // Clear params when jumping to history
323
lastNavigationAt: DateTime.now().toUtc(),
324
);
325
}
326
}
327
328
/// Clear parameter by key if exists
329
Future<void> clearParamIfExists(String key) async {
330
if (state.hasParam(key)) {
331
await removeParam(key);
332
}
333
}
334
335
/// Update multiple parameters at once
336
Future<void> updateMultipleParams(Map<String, dynamic> newParams) async {
337
final updatedParams = Map<String, dynamic>.from(state.routeParams)
338
..addAll(newParams);
339
await updateParams(updatedParams);
340
}
341
342
/// Clear all parameters
343
Future<void> clearAllParams() async {
344
await updateParams({});
345
}
346
347
// Overlay navigation methods
348
349
/// Select an overlay by index (0-3) or null to close
350
void selectItem(int? index) {
351
final overlayNotifier = ref.read(overlayProvider.notifier);
352
353
if (state.selectedIndex == index) {
354
// Same panel already open — do nothing. Only the central toggle
355
// button should dismiss panels, not re-clicking an active nav icon.
356
return;
357
} else {
358
// Switch to new overlay or open if none selected
359
if (index != null) {
360
final overlayType = _getOverlayTypeForIndex(index);
361
if (overlayType != null) {
362
overlayNotifier.show(overlayType);
363
}
364
}
365
state = state.copyWith(selectedIndex: index);
366
}
367
}
368
369
/// Clear the current selection (close overlay) and switch to signal mode
370
void clearSelection() {
371
// Save current overlay index before clearing (for restoration later)
372
final currentIndex = state.selectedIndex;
373
if (currentIndex != null) {
374
ref.read(bottomBarProvider.notifier).saveOverlayIndex(currentIndex);
375
}
376
377
// Dismiss overlay and clear selection
378
ref.read(overlayProvider.notifier).dismiss();
379
state = state.copyWith(selectedIndex: null);
380
381
// Switch bottom bar to signal mode
382
ref.read(bottomBarProvider.notifier).setSignalMode();
383
}
384
385
/// Helper method to map index to OverlayType
386
OverlayType? _getOverlayTypeForIndex(int index) {
387
switch (index) {
388
case 0:
389
return OverlayType.patients;
390
case 1:
391
return OverlayType.montages;
392
case 2:
393
return OverlayType.settings;
394
case 3:
395
return OverlayType.activities;
396
default:
397
return null;
398
}
399
}
400
401
/// Check if any overlay is open
402
bool get isOverlayOpen => state.selectedIndex != null;
403
404
/// Get the current selected overlay index
405
int? get selectedIndex => state.selectedIndex;
406
}