Line data Source code
1 : import 'package:beamer/beamer.dart';
2 : import 'package:beamer/src/transition_delegates.dart';
3 : import 'package:flutter/foundation.dart';
4 : import 'package:flutter/material.dart';
5 : import 'package:flutter/services.dart';
6 : import 'package:flutter/widgets.dart';
7 :
8 : import 'utils.dart';
9 :
10 : /// A delegate that is used by the [Router] to build the [Navigator].
11 : ///
12 : /// This is "the beamer", the one that does the actual beaming.
13 : class BeamerDelegate extends RouterDelegate<RouteInformation>
14 : with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteInformation> {
15 7 : BeamerDelegate({
16 : required this.locationBuilder,
17 : this.initialPath = '/',
18 : this.routeListener,
19 : this.buildListener,
20 : this.preferUpdate = true,
21 : this.removeDuplicateHistory = true,
22 : this.notFoundPage = const BeamPage(
23 : key: ValueKey('not-found'),
24 : title: 'Not found',
25 : child: Scaffold(body: Center(child: Text('Not found'))),
26 : ),
27 : this.notFoundRedirect,
28 : this.notFoundRedirectNamed,
29 : this.guards = const <BeamGuard>[],
30 : this.navigatorObservers = const <NavigatorObserver>[],
31 : this.transitionDelegate = const DefaultTransitionDelegate(),
32 : this.beamBackTransitionDelegate = const ReverseTransitionDelegate(),
33 : this.onPopPage,
34 : this.setBrowserTabTitle = true,
35 : this.updateFromParent = true,
36 : this.updateParent = true,
37 : this.clearBeamingHistoryOn = const <String>{},
38 : }) {
39 14 : _currentBeamParameters = BeamParameters(
40 7 : transitionDelegate: transitionDelegate,
41 : );
42 :
43 21 : configuration = RouteInformation(location: initialPath);
44 : }
45 :
46 : /// A state of this delegate. This is the `routeInformation` that goes into
47 : /// [locationBuilder] to build an appropriate [BeamLocation].
48 : ///
49 : /// A way to modify this state is via [update].
50 : late RouteInformation configuration;
51 :
52 : BeamerDelegate? _parent;
53 :
54 : /// A delegate of a parent of the [Beamer] that has this delegate.
55 : ///
56 : /// This is not null only if multiple [Beamer]s are used;
57 : /// `*App.router` and at least one more [Beamer] in the Widget tree.
58 12 : BeamerDelegate? get parent => _parent;
59 3 : set parent(BeamerDelegate? parent) {
60 3 : _parent = parent!;
61 3 : _initializeFromParent();
62 3 : if (updateFromParent) {
63 9 : _parent!.addListener(_updateFromParent);
64 : }
65 : }
66 :
67 : /// The top-most [BeamerDelegate], a parent of all.
68 : ///
69 : /// It will return root even when called on root.
70 1 : BeamerDelegate get root {
71 1 : if (_parent == null) {
72 : return this;
73 : }
74 1 : var root = _parent!;
75 1 : while (root._parent != null) {
76 1 : root = root._parent!;
77 : }
78 : return root;
79 : }
80 :
81 : /// A builder for [BeamLocation]s.
82 : ///
83 : /// There are 3 ways of building an appropriate [BeamLocation] which will in
84 : /// turn build a stack of pages that should go into [Navigator.pages].
85 : ///
86 : /// 1. Custom closure
87 : /// ```dart
88 : /// locationBuilder: (state) {
89 : /// if (state.uri.pathSegments.contains('l1')) {
90 : /// return Location1(state);
91 : /// }
92 : /// if (state.uri.pathSegments.contains('l2')) {
93 : /// return Location2(state);
94 : /// }
95 : /// return NotFound(path: state.uri.toString());
96 : /// },
97 : /// ```
98 : ///
99 : /// 2. [BeamerLocationBuilder]; chooses appropriate [BeamLocation] itself
100 : /// ```dart
101 : /// locationBuilder: BeamerLocationBuilder(
102 : /// beamLocations: [
103 : /// Location1(),
104 : /// Location2(),
105 : /// ],
106 : /// ),
107 : /// ```
108 : ///
109 : /// 3. [RoutesLocationBuilder]; a Map of routes
110 : /// ```dart
111 : /// locationBuilder: RoutesLocationBuilder(
112 : /// routes: {
113 : /// '/': (context) => HomeScreen(),
114 : /// '/another': (context) => AnotherScreen(),
115 : /// },
116 : /// ),
117 : /// ```
118 : final LocationBuilder locationBuilder;
119 :
120 : /// The path to replace `/` as default initial route path upon load.
121 : ///
122 : /// Note that (if set to anything other than `/` (default)),
123 : /// you will not be able to navigate to `/` by manually typing
124 : /// it in the URL bar, because it will always be transformed to `initialPath`,
125 : /// but you will be able to get to `/` by popping pages with back button,
126 : /// if there are pages in [BeamLocation.buildPages] that will build
127 : /// when there are no path segments.
128 : final String initialPath;
129 :
130 : /// The routeListener will be called on every navigation event
131 : /// and will recieve the [configuration] and [currentBeamLocation].
132 : final void Function(RouteInformation, BeamLocation)? routeListener;
133 :
134 : /// The buildListener will be called every time after the [currentPages]
135 : /// are updated. it receives a reference to this delegate.
136 : final void Function(BuildContext, BeamerDelegate)? buildListener;
137 :
138 : /// Whether to prefer updating [currentBeamLocation] if it's of the same type
139 : /// as the [BeamLocation] being beamed to,
140 : /// instead of adding it to [beamLocationHistory].
141 : ///
142 : /// See how this is used at [_pushHistory] implementation.
143 : final bool preferUpdate;
144 :
145 : /// Whether to remove [BeamLocation]s from [beamLocationHistory]
146 : /// if they are the same type as the location being beamed to.
147 : ///
148 : /// See how this is used at [_pushHistory] implementation.
149 : final bool removeDuplicateHistory;
150 :
151 : /// Page to show when no [BeamLocation] supports the incoming URI.
152 : late BeamPage notFoundPage;
153 :
154 : /// [BeamLocation] to redirect to when no [BeamLocation] supports the incoming URI.
155 : final BeamLocation? notFoundRedirect;
156 :
157 : /// URI string to redirect to when no [BeamLocation] supports the incoming URI.
158 : final String? notFoundRedirectNamed;
159 :
160 : /// Guards that will be executing [check] on [currentBeamLocation] candidate.
161 : ///
162 : /// Checks will be executed in order; chain of responsibility pattern.
163 : /// When some guard returns `false`, location candidate will not be accepted
164 : /// and stack of pages will be updated as is configured in [BeamGuard].
165 : final List<BeamGuard> guards;
166 :
167 : /// The list of observers for the [Navigator].
168 : final List<NavigatorObserver> navigatorObservers;
169 :
170 : /// A transition delegate to be used by [Navigator].
171 : ///
172 : /// This transition delegate will be overridden by the one in [BeamLocation],
173 : /// if any is set.
174 : ///
175 : /// See [Navigator.transitionDelegate].
176 : final TransitionDelegate transitionDelegate;
177 :
178 : /// A transition delegate to be used by [Navigator] when beaming back.
179 : ///
180 : /// When calling [beamBack], it's useful to animate routes in reverse order;
181 : /// adding the new ones behind and then popping the current ones,
182 : /// therefore, the default is [ReverseTransitionDelegate].
183 : final TransitionDelegate beamBackTransitionDelegate;
184 :
185 : /// Callback when `pop` is requested.
186 : ///
187 : /// Return `true` if pop will be handled entirely by this function.
188 : /// Return `false` if beamer should finish handling the pop.
189 : ///
190 : /// See [build] for details on how beamer handles [Navigator.onPopPage].
191 : bool Function(BuildContext context, Route<dynamic> route, dynamic result)?
192 : onPopPage;
193 :
194 : /// Whether the title attribute of [BeamPage] should
195 : /// be used to set and update the browser tab title.
196 : final bool setBrowserTabTitle;
197 :
198 : /// Whether to call [update] when parent notifies listeners.
199 : ///
200 : /// This means that navigation can be done either on parent or on this
201 : final bool updateFromParent;
202 :
203 : /// Whether to call [update] on [parent] when [state] is updated.
204 : ///
205 : /// This means that parent's [beamStateHistory] will be in sync.
206 : final bool updateParent;
207 :
208 : /// Whether to remove all entries from [routeHistory] when a route
209 : /// belonging to this set is reached, regardless of how it was reached.
210 : ///
211 : /// Note that [popToNamed] will also try to clear as much [routeHistory]
212 : /// as possible, even when this is empty.
213 : final Set<String> clearBeamingHistoryOn;
214 :
215 : final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
216 :
217 : /// {@template beamingHistory}
218 : /// The history of [BeamLocation]s, each holding its [BeamLocation.history].
219 : ///
220 : /// See [_pushHistory].
221 : /// {@endtemplate}
222 : final List<BeamLocation> beamingHistory = [];
223 :
224 5 : int get beamingHistoryCompleteLength {
225 : int length = 0;
226 10 : for (BeamLocation location in beamingHistory) {
227 15 : length += location.history.length;
228 : }
229 : return length;
230 : }
231 :
232 : /// {@template currentBeamLocation}
233 : /// A [BeamLocation] that is currently responsible for providing a page stack
234 : /// via [BeamLocation.buildPages] and holds the current [BeamState].
235 : ///
236 : /// Usually obtained via
237 : /// ```dart
238 : /// Beamer.of(context).currentBeamLocation
239 : /// ```
240 : /// {@endtemplate}
241 6 : BeamLocation get currentBeamLocation =>
242 30 : beamingHistory.isEmpty ? EmptyBeamLocation() : beamingHistory.last;
243 :
244 : List<BeamPage> _currentPages = [];
245 :
246 : /// {@template currentPages}
247 : /// [currentBeamLocation]'s "effective" pages, the ones that were built.
248 : /// {@endtemplate}
249 8 : List<BeamPage> get currentPages => _currentPages;
250 :
251 : /// Describes the current parameters for beaming, such as
252 : /// pop configuration, beam back on pop, etc.
253 : late BeamParameters _currentBeamParameters;
254 :
255 : /// If `false`, does not report the route until next [update].
256 : ///
257 : /// Useful when having sibling beamers that are both build at the same time.
258 : /// Becomes active on next [update].
259 : bool active = true;
260 :
261 : /// The [Navigator] that belongs to this [BeamerDelegate].
262 : ///
263 : /// Useful for popping dialogs without accessing [BuildContext]:
264 : ///
265 : /// ```dart
266 : /// beamerDelegate.navigator.pop();
267 : /// ```
268 9 : NavigatorState get navigator => _navigatorKey.currentState!;
269 :
270 : /// Main method to update the [state] of this; `Beamer.of(context)`,
271 : ///
272 : /// This "top-level" [update] is generally used for navigation
273 : /// _between_ [BeamLocation]s and not _within_ a specific [BeamLocation].
274 : /// For latter purpose, see [BeamLocation.update].
275 : /// Nevertheless, [update] **will** work for navigation within [BeamLocation].
276 : /// Calling [update] will run the [locationBuilder].
277 : ///
278 : /// ```dart
279 : /// Beamer.of(context).update(
280 : /// state: BeamState.fromUriString('/xx'),
281 : /// );
282 : /// ```
283 : ///
284 : /// **[beamTo] and [beamToNamed] call [update] to really do the update.**
285 : ///
286 : /// [transitionDelegate] determines how a new stack of pages replaces current.
287 : /// See [Navigator.transitionDelegate].
288 : ///
289 : /// If [beamBackOnPop] is set to `true`,
290 : /// default pop action will triger [beamBack] instead.
291 : ///
292 : /// [popState] is more general than [beamBackOnPop],
293 : /// and can beam you anywhere; whatever it resolves to during build.
294 : ///
295 : /// If [stacked] is set to `false`,
296 : /// only the location's last page will be shown.
297 : ///
298 : /// If [replaceCurrent] is set to `true`,
299 : /// new location will replace the last one in the stack.
300 : ///
301 : /// If [rebuild] is set to `false`,
302 : /// [build] will not occur, but [state] and browser URL will be updated.
303 6 : void update({
304 : RouteInformation? configuration,
305 : BeamParameters? beamParameters,
306 : Object? data,
307 : bool buildBeamLocation = true,
308 : bool rebuild = true,
309 : bool updateParent = true,
310 : }) {
311 6 : configuration = configuration?.copyWith(
312 12 : location: Utils.trimmed(configuration.location),
313 : );
314 : if (beamParameters != null) {
315 6 : _currentBeamParameters = beamParameters;
316 : }
317 :
318 : // popConfiguration = beamParameters.popConfiguration?.copyWith(
319 : // location: Utils.trimmed(beamParameters.popConfiguration?.location),
320 : // );
321 :
322 18 : if (clearBeamingHistoryOn.contains(configuration?.location)) {
323 2 : for (var beamLocation in beamingHistory) {
324 2 : beamLocation.history.clear();
325 : }
326 2 : beamingHistory.clear();
327 : }
328 :
329 6 : active = true;
330 :
331 6 : this.configuration = configuration ??
332 18 : currentBeamLocation.history.last.state.routeInformation.copyWith();
333 : if (buildBeamLocation) {
334 : final location =
335 18 : locationBuilder(this.configuration, _currentBeamParameters);
336 12 : if (beamingHistory.isEmpty ||
337 30 : location.runtimeType != beamingHistory.last.runtimeType) {
338 6 : _addToBeamingHistory(location);
339 : } else {
340 12 : beamingHistory.last
341 18 : .addToHistory(location.state, location.beamParameters);
342 : }
343 : }
344 : if (data != null) {
345 2 : currentBeamLocation.data = data;
346 : }
347 8 : routeListener?.call(this.configuration, currentBeamLocation);
348 :
349 : bool parentDidUpdate = false;
350 6 : if (parent != null &&
351 3 : this.updateParent &&
352 : updateParent &&
353 5 : (configuration?.location != _parent?.configuration.location ||
354 0 : configuration?.state != _parent?.configuration.state)) {
355 2 : _parent!.update(
356 2 : configuration: this.configuration.copyWith(),
357 : rebuild: false,
358 : );
359 : parentDidUpdate = true;
360 : }
361 :
362 : if (!rebuild && !parentDidUpdate) {
363 6 : updateRouteInformation(this.configuration);
364 : }
365 :
366 : if (rebuild) {
367 6 : notifyListeners();
368 : }
369 : }
370 :
371 : /// {@template beamTo}
372 : /// Beams to a specific, manually configured [BeamLocation].
373 : ///
374 : /// For example
375 : /// ```dart
376 : /// Beamer.of(context).beamTo(
377 : /// Location2(
378 : /// BeamState(
379 : /// pathBlueprintSegments = ['user',':userId','transactions'],
380 : /// pathParameters = {'userId': '1'},
381 : /// queryParameters = {'perPage': '10'},
382 : /// data = {'favoriteUser': true},
383 : /// ),
384 : /// ),
385 : /// );
386 : /// ```
387 : ///
388 : /// See [update] for more details.
389 : /// {@endtemplate}
390 2 : void beamTo(
391 : BeamLocation location, {
392 : Object? data,
393 : BeamLocation? popTo,
394 : TransitionDelegate? transitionDelegate,
395 : bool beamBackOnPop = false,
396 : bool popBeamLocationOnPop = false,
397 : bool stacked = true,
398 : }) {
399 2 : _addToBeamingHistory(location);
400 2 : update(
401 4 : configuration: location.state.routeInformation,
402 4 : beamParameters: _currentBeamParameters.copyWith(
403 0 : popConfiguration: popTo?.state.routeInformation,
404 2 : transitionDelegate: transitionDelegate ?? this.transitionDelegate,
405 : beamBackOnPop: beamBackOnPop,
406 : popBeamLocationOnPop: popBeamLocationOnPop,
407 : stacked: stacked,
408 : ),
409 : data: data,
410 : buildBeamLocation: false,
411 : );
412 : }
413 :
414 : /// The same as [beamTo], but replaces the [currentBeamLocation],
415 : /// i.e. removes it from the [beamingHistory] and then does [beamTo].
416 1 : void beamToReplacement(
417 : BeamLocation location, {
418 : Object? data,
419 : BeamLocation? popTo,
420 : TransitionDelegate? transitionDelegate,
421 : bool beamBackOnPop = false,
422 : bool popBeamLocationOnPop = false,
423 : bool stacked = true,
424 : }) {
425 3 : currentBeamLocation.removeListener(_updateFromLocation);
426 2 : beamingHistory.removeLast();
427 1 : beamTo(
428 : location,
429 : data: data,
430 : popTo: popTo,
431 : transitionDelegate: transitionDelegate,
432 : beamBackOnPop: beamBackOnPop,
433 : popBeamLocationOnPop: popBeamLocationOnPop,
434 : stacked: stacked,
435 : );
436 : }
437 :
438 : /// {@template beamToNamed}
439 : /// Beams to [BeamLocation] that has [uri] contained within its
440 : /// [BeamLocation.pathBlueprintSegments].
441 : ///
442 : /// For example
443 : /// ```dart
444 : /// Beamer.of(context).beamToNamed(
445 : /// '/user/1/transactions?perPage=10',
446 : /// data: {'favoriteUser': true},,
447 : /// );
448 : /// ```
449 : ///
450 : /// See [update] for more details.
451 : /// {@endtemplate}
452 6 : void beamToNamed(
453 : String uri, {
454 : Object? routeState,
455 : Object? data,
456 : String? popToNamed,
457 : TransitionDelegate? transitionDelegate,
458 : bool beamBackOnPop = false,
459 : bool popBeamLocationOnPop = false,
460 : bool stacked = true,
461 : }) {
462 6 : update(
463 6 : configuration: RouteInformation(location: uri, state: routeState),
464 12 : beamParameters: _currentBeamParameters.copyWith(
465 : popConfiguration:
466 1 : popToNamed != null ? RouteInformation(location: popToNamed) : null,
467 6 : transitionDelegate: transitionDelegate ?? this.transitionDelegate,
468 : beamBackOnPop: beamBackOnPop,
469 : popBeamLocationOnPop: popBeamLocationOnPop,
470 : stacked: stacked,
471 : ),
472 : data: data,
473 : );
474 : }
475 :
476 : /// The same as [beamToNamed], but replaces the last state in history,
477 : /// i.e. removes it from the `beamingHistory.last.history` and then does [beamToNamed].
478 1 : void beamToReplacementNamed(
479 : String uri, {
480 : Object? routeState,
481 : Object? data,
482 : String? popToNamed,
483 : TransitionDelegate? transitionDelegate,
484 : bool beamBackOnPop = false,
485 : bool popBeamLocationOnPop = false,
486 : bool stacked = true,
487 : }) {
488 1 : removeLastHistoryElement();
489 1 : beamToNamed(
490 : uri,
491 : routeState: routeState,
492 : data: data,
493 : popToNamed: popToNamed,
494 : transitionDelegate: transitionDelegate,
495 : beamBackOnPop: beamBackOnPop,
496 : popBeamLocationOnPop: popBeamLocationOnPop,
497 : stacked: stacked,
498 : );
499 : }
500 :
501 : /// {@template popToNamed}
502 : /// Calls [beamToNamed] with a [ReverseTransitionDelegate] and tries to
503 : /// remove everything from history after entry corresponding to `uri`, as
504 : /// if doing a pop way back to that state, if it exists in history.
505 : ///
506 : /// See [beamToNamed] for more details.
507 : /// {@endtemplate}
508 3 : void popToNamed(
509 : String uri, {
510 : Object? routeState,
511 : Object? data,
512 : String? popToNamed,
513 : bool beamBackOnPop = false,
514 : bool popBeamLocationOnPop = false,
515 : bool stacked = true,
516 : bool replaceCurrent = false,
517 : }) {
518 6 : while (beamingHistory.isNotEmpty) {
519 12 : final index = beamingHistory.last.history.lastIndexWhere(
520 15 : (element) => element.state.routeInformation.location == uri,
521 : );
522 6 : if (index == -1) {
523 12 : beamingHistory.last.removeListener(_updateFromLocation);
524 6 : beamingHistory.removeLast();
525 : continue;
526 : } else {
527 6 : beamingHistory.last.history
528 10 : .removeRange(index, beamingHistory.last.history.length);
529 : }
530 : }
531 3 : beamToNamed(
532 : uri,
533 : routeState: routeState,
534 : data: data,
535 : popToNamed: popToNamed,
536 : transitionDelegate: const ReverseTransitionDelegate(),
537 : beamBackOnPop: beamBackOnPop,
538 : popBeamLocationOnPop: popBeamLocationOnPop,
539 : stacked: stacked,
540 : );
541 : }
542 :
543 : /// {@template canBeamBack}
544 : /// Whether it is possible to [beamBack],
545 : /// i.e. there is more than 1 state in [beamingHistory].
546 : /// {@endtemplate}
547 4 : bool get canBeamBack =>
548 29 : beamingHistory.last.history.length > 1 || beamingHistory.length > 1;
549 :
550 : /// {@template beamBack}
551 : /// Beams to previous state in [beamingHistory].
552 : /// and **removes** the last state from history.
553 : ///
554 : /// If there is no previous state, does nothing.
555 : ///
556 : /// Returns the success, whether [update] was executed.
557 : /// {@endtemplate}
558 4 : bool beamBack({Object? data}) {
559 4 : if (!canBeamBack) {
560 : return false;
561 : }
562 : late final HistoryElement lastHistoryElement;
563 16 : final lastHistorylength = beamingHistory.last.history.length;
564 : // first we try to beam back within last BeamLocation
565 4 : if (lastHistorylength > 1) {
566 20 : lastHistoryElement = beamingHistory.last.history[lastHistorylength - 2];
567 12 : beamingHistory.last.history
568 8 : .removeRange(lastHistorylength - 2, lastHistorylength);
569 : } else {
570 : // here we know that beamingHistory.length > 1 (because of canBeamBack)
571 : // and that beamingHistory.last.history.length == 1
572 : // so this last (only) entry is removed along with BeamLocation
573 4 : beamingHistory.removeLast();
574 8 : lastHistoryElement = beamingHistory.last.history.last;
575 8 : beamingHistory.last.history.removeLast();
576 : }
577 :
578 4 : update(
579 12 : configuration: lastHistoryElement.state.routeInformation.copyWith(),
580 8 : beamParameters: lastHistoryElement.parameters.copyWith(
581 4 : transitionDelegate: beamBackTransitionDelegate,
582 : ),
583 : data: data,
584 : );
585 : return true;
586 : }
587 :
588 : /// {@template canPopBeamLocation}
589 : /// Whether it is possible to [popBeamLocation],
590 : /// i.e. there is more than 1 location in [beamingHistory].
591 : /// {@endtemplate}
592 12 : bool get canPopBeamLocation => beamingHistory.length > 1;
593 :
594 : /// {@template popBeamLocation}
595 : /// Beams to previous location in [beamingHistory]
596 : /// and **removes** the last location from history.
597 : ///
598 : /// If there is no previous location, does nothing.
599 : ///
600 : /// Returns the success, whether the [currentBeamLocation] was changed.
601 : /// {@endtemplate}
602 3 : bool popBeamLocation({Object? data}) {
603 3 : if (!canPopBeamLocation) {
604 : return false;
605 : }
606 6 : currentBeamLocation.removeListener(_updateFromLocation);
607 4 : beamingHistory.removeLast();
608 2 : update(
609 10 : beamParameters: currentBeamLocation.history.last.parameters.copyWith(
610 2 : transitionDelegate: beamBackTransitionDelegate,
611 : ),
612 : data: data,
613 : buildBeamLocation: false,
614 : );
615 : return true;
616 : }
617 :
618 6 : @override
619 : RouteInformation? get currentConfiguration =>
620 18 : _parent == null ? configuration.copyWith() : null;
621 :
622 6 : @override
623 6 : GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;
624 :
625 6 : @override
626 : Widget build(BuildContext context) {
627 12 : BeamGuard? guard = _checkGuards(context, currentBeamLocation);
628 : if (guard != null) {
629 1 : _applyGuard(guard, context);
630 : }
631 :
632 12 : if (currentBeamLocation is NotFound) {
633 4 : _handleNotFoundRedirect();
634 : }
635 :
636 6 : _setCurrentPages(context, guard);
637 :
638 6 : _setBrowserTitle(context);
639 :
640 6 : buildListener?.call(context, this);
641 :
642 6 : final navigator = Navigator(
643 6 : key: navigatorKey,
644 6 : observers: navigatorObservers,
645 12 : transitionDelegate: currentBeamLocation.transitionDelegate ??
646 12 : _currentBeamParameters.transitionDelegate,
647 6 : pages: _currentPages,
648 6 : onPopPage: (route, result) => _onPopPage(context, route, result),
649 : );
650 :
651 12 : return currentBeamLocation.builder(context, navigator);
652 : }
653 :
654 6 : @override
655 : SynchronousFuture<void> setInitialRoutePath(RouteInformation configuration) {
656 12 : final uri = Uri.parse(configuration.location ?? '/');
657 12 : if (currentBeamLocation is! EmptyBeamLocation) {
658 12 : configuration = currentBeamLocation.state.routeInformation;
659 10 : } else if (uri.path == '/') {
660 5 : configuration = RouteInformation(
661 20 : location: initialPath + (uri.query.isNotEmpty ? '?${uri.query}' : ''),
662 : );
663 : }
664 6 : return setNewRoutePath(configuration);
665 : }
666 :
667 6 : @override
668 : SynchronousFuture<void> setNewRoutePath(RouteInformation configuration) {
669 6 : update(configuration: configuration);
670 6 : return SynchronousFuture(null);
671 : }
672 :
673 : /// Pass this call to [root] which notifies the platform for a [state] change.
674 : ///
675 : /// On Web, creates a new browser history entry and update URL
676 : ///
677 : /// See [SystemNavigator.routeInformationUpdated].
678 3 : void updateRouteInformation(RouteInformation routeInformation) {
679 3 : if (_parent == null) {
680 3 : SystemNavigator.routeInformationUpdated(
681 6 : location: configuration.location ?? '/',
682 6 : state: configuration.state,
683 : );
684 : } else {
685 2 : _parent!.updateRouteInformation(routeInformation);
686 : }
687 : }
688 :
689 6 : BeamGuard? _checkGuards(
690 : BuildContext context,
691 : BeamLocation location,
692 : ) {
693 40 : for (final guard in (parent?.guards ?? []) + guards + location.guards) {
694 2 : if (guard.shouldGuard(location) && !guard.check(context, location)) {
695 1 : guard.onCheckFailed?.call(context, location);
696 : return guard;
697 : }
698 : }
699 : return null;
700 : }
701 :
702 1 : void _applyGuard(BeamGuard guard, BuildContext context) {
703 1 : if (guard.showPage != null) {
704 : return;
705 : }
706 :
707 : late BeamLocation redirectLocation;
708 :
709 2 : if (guard.beamTo == null && guard.beamToNamed == null) {
710 1 : removeLastHistoryElement();
711 1 : return update(
712 : buildBeamLocation: false,
713 : rebuild: false,
714 : );
715 1 : } else if (guard.beamTo != null) {
716 1 : redirectLocation = guard.beamTo!(context);
717 1 : } else if (guard.beamToNamed != null) {
718 1 : redirectLocation = locationBuilder(
719 2 : RouteInformation(location: guard.beamToNamed!),
720 2 : _currentBeamParameters.copyWith(),
721 : );
722 : }
723 :
724 1 : final anotherGuard = _checkGuards(context, redirectLocation);
725 : if (anotherGuard != null) {
726 1 : return _applyGuard(anotherGuard, context);
727 : }
728 :
729 3 : currentBeamLocation.removeListener(_updateFromLocation);
730 1 : if (guard.replaceCurrentStack) {
731 2 : beamingHistory.removeLast();
732 : }
733 1 : _addToBeamingHistory(redirectLocation);
734 1 : _updateFromLocation(rebuild: false);
735 : }
736 :
737 6 : void _addToBeamingHistory(BeamLocation location) {
738 18 : currentBeamLocation.removeListener(_updateFromLocation);
739 6 : if (removeDuplicateHistory) {
740 17 : final index = beamingHistory.indexWhere((historyLocation) =>
741 15 : historyLocation.runtimeType == location.runtimeType);
742 12 : if (index != -1) {
743 16 : beamingHistory[index].removeListener(_updateFromLocation);
744 8 : beamingHistory.removeAt(index);
745 : }
746 : }
747 12 : beamingHistory.add(location);
748 18 : currentBeamLocation.addListener(_updateFromLocation);
749 : }
750 :
751 4 : HistoryElement? removeLastHistoryElement() {
752 8 : if (beamingHistoryCompleteLength == 0) {
753 : return null;
754 : }
755 4 : if (updateParent) {
756 4 : _parent?.removeLastHistoryElement();
757 : }
758 12 : final lastHistoryElement = beamingHistory.last.removeLastFromHistory();
759 16 : if (beamingHistory.last.history.isEmpty) {
760 6 : beamingHistory.removeLast();
761 : }
762 :
763 : return lastHistoryElement;
764 : }
765 :
766 4 : void _handleNotFoundRedirect() {
767 8 : if (notFoundRedirect == null && notFoundRedirectNamed == null) {
768 : // do nothing, pass on NotFound
769 : } else {
770 : late BeamLocation redirectBeamLocation;
771 1 : if (notFoundRedirect != null) {
772 1 : redirectBeamLocation = notFoundRedirect!;
773 1 : } else if (notFoundRedirectNamed != null) {
774 1 : redirectBeamLocation = locationBuilder(
775 2 : RouteInformation(location: notFoundRedirectNamed),
776 2 : _currentBeamParameters.copyWith(),
777 : );
778 : }
779 1 : _addToBeamingHistory(redirectBeamLocation);
780 1 : _updateFromLocation(rebuild: false);
781 : }
782 : }
783 :
784 6 : void _setCurrentPages(BuildContext context, BeamGuard? guard) {
785 12 : if (currentBeamLocation is NotFound) {
786 12 : _currentPages = [notFoundPage];
787 : } else {
788 18 : _currentPages = _currentBeamParameters.stacked
789 24 : ? currentBeamLocation.buildPages(context, currentBeamLocation.state)
790 1 : : [
791 1 : currentBeamLocation
792 3 : .buildPages(context, currentBeamLocation.state)
793 1 : .last
794 : ];
795 : }
796 1 : if (guard != null && guard.showPage != null) {
797 0 : if (guard.replaceCurrentStack) {
798 0 : _currentPages = [guard.showPage!];
799 : } else {
800 0 : _currentPages += [guard.showPage!];
801 : }
802 : }
803 : }
804 :
805 6 : void _setBrowserTitle(BuildContext context) {
806 6 : if (active && kIsWeb && setBrowserTabTitle) {
807 0 : SystemChrome.setApplicationSwitcherDescription(
808 0 : ApplicationSwitcherDescription(
809 0 : label: _currentPages.last.title ??
810 0 : currentBeamLocation.state.routeInformation.location,
811 0 : primaryColor: Theme.of(context).primaryColor.value,
812 : ));
813 : }
814 : }
815 :
816 3 : bool _onPopPage(BuildContext context, Route<dynamic> route, dynamic result) {
817 3 : if (route.willHandlePopInternally) {
818 1 : if (!route.didPop(result)) {
819 : return false;
820 : }
821 : }
822 :
823 6 : if (_currentBeamParameters.popConfiguration != null) {
824 1 : update(
825 2 : configuration: _currentBeamParameters.popConfiguration,
826 2 : beamParameters: _currentBeamParameters.copyWith(
827 1 : transitionDelegate: beamBackTransitionDelegate,
828 : ),
829 : // replaceCurrent: true,
830 : );
831 6 : } else if (_currentBeamParameters.popBeamLocationOnPop) {
832 1 : final didPopBeamLocation = popBeamLocation();
833 : if (!didPopBeamLocation) {
834 : return false;
835 : }
836 6 : } else if (_currentBeamParameters.beamBackOnPop) {
837 1 : final didBeamBack = beamBack();
838 : if (!didBeamBack) {
839 : return false;
840 : }
841 : } else {
842 6 : final lastPage = _currentPages.last;
843 3 : if (lastPage is BeamPage) {
844 3 : if (lastPage.popToNamed != null) {
845 2 : popToNamed(lastPage.popToNamed!);
846 : } else {
847 3 : final shouldPop = lastPage.onPopPage(
848 : context,
849 : this,
850 6 : currentBeamLocation.state,
851 : lastPage,
852 : );
853 : if (!shouldPop) {
854 : return false;
855 : }
856 : }
857 : }
858 : }
859 :
860 3 : return route.didPop(result);
861 : }
862 :
863 3 : void _initializeFromParent() {
864 3 : final parent = _parent;
865 : if (parent == null) {
866 : return;
867 : }
868 9 : configuration = parent.configuration.copyWith();
869 3 : var location = locationBuilder(
870 3 : configuration,
871 6 : _currentBeamParameters.copyWith(),
872 : );
873 3 : if (location is NotFound) {
874 0 : configuration = RouteInformation(location: initialPath);
875 0 : location = locationBuilder(
876 0 : configuration,
877 0 : _currentBeamParameters.copyWith(),
878 : );
879 : }
880 3 : _addToBeamingHistory(location);
881 : }
882 :
883 3 : void _updateFromParent({bool rebuild = true}) {
884 3 : update(
885 9 : configuration: _parent!.configuration.copyWith(),
886 : rebuild: rebuild,
887 : updateParent: false,
888 : );
889 : }
890 :
891 3 : void _updateFromLocation({bool rebuild = true}) {
892 3 : update(
893 9 : configuration: currentBeamLocation.state.routeInformation,
894 : buildBeamLocation: false,
895 : rebuild: rebuild,
896 : );
897 : }
898 :
899 0 : @override
900 : void dispose() {
901 0 : _parent?.removeListener(_updateFromParent);
902 0 : currentBeamLocation.removeListener(_updateFromLocation);
903 0 : super.dispose();
904 : }
905 : }
|