Line data Source code
1 : import 'package:flutter/cupertino.dart';
2 : import 'package:flutter/material.dart';
3 :
4 : import '../beamer.dart';
5 :
6 : /// Types for how to route should be built.
7 12 : enum BeamPageType {
8 : material,
9 : cupertino,
10 : fadeTransition,
11 : slideTransition,
12 : scaleTransition,
13 : noTransition,
14 : }
15 :
16 : /// A wrapper for screens in a navigation stack.
17 : class BeamPage extends Page {
18 19 : const BeamPage({
19 : LocalKey? key,
20 : String? name,
21 : required this.child,
22 : this.title,
23 : this.onPopPage = pathSegmentPop,
24 : this.popToNamed,
25 : this.type = BeamPageType.material,
26 : this.routeBuilder,
27 : this.fullScreenDialog = false,
28 : this.keepQueryOnPop = false,
29 6 : }) : super(key: key, name: name);
30 :
31 : /// The default pop behavior for [BeamPage].
32 : ///
33 : /// Pops the last path segment from URI and calls [BeamerDelegate.update].
34 3 : static bool pathSegmentPop(
35 : BuildContext context,
36 : BeamerDelegate delegate,
37 : RouteInformationSerializable state,
38 : BeamPage poppedPage,
39 : ) {
40 6 : if (!delegate.navigator.canPop() ||
41 6 : delegate.beamingHistoryCompleteLength < 2) {
42 : return false;
43 : }
44 :
45 : // take the data in case we remove the BeamLocation from history
46 : // and generate a new one (but the same).
47 6 : final data = delegate.currentBeamLocation.data;
48 :
49 : // Take the history element that is being popped and the one before
50 : // as they will be compared later on to fine-tune the pop experience.
51 3 : final poppedHistoryElement = delegate.removeLastHistoryElement()!;
52 12 : final previousHistoryElement = delegate.beamingHistory.last.history.last;
53 :
54 : // Convert both to Uri as their path and query will be compared.
55 3 : final poppedUri = Uri.parse(
56 9 : poppedHistoryElement.state.routeInformation.location ?? '/',
57 : );
58 3 : final previousUri = Uri.parse(
59 9 : previousHistoryElement.state.routeInformation.location ?? '/',
60 : );
61 :
62 3 : final poppedPathSegments = poppedUri.pathSegments;
63 3 : final poppedQueryParameters = poppedUri.queryParameters;
64 :
65 : // Pop path is obtained via removing the last path segment from path
66 : // that is beaing popped.
67 6 : final popPathSegments = List.from(poppedPathSegments)..removeLast();
68 6 : final popPath = '/' + popPathSegments.join('/');
69 3 : var popUri = Uri(
70 : path: popPath,
71 3 : queryParameters: poppedPage.keepQueryOnPop
72 : ? poppedQueryParameters
73 6 : : (popPath == previousUri.path)
74 2 : ? previousUri.queryParameters
75 : : null,
76 : );
77 :
78 : // We need the routeState from the route we are trying to pop to.
79 : //
80 : // Remove the last history element if it's the same as the path
81 : // we're trying to pop to, because `update` will add it to history.
82 : // This is `false` in case we deep-linked.
83 : //
84 : // Otherwise, find the state with popPath in history.
85 : RouteInformationSerializable? lastState;
86 6 : if (popPath == previousUri.path) {
87 4 : lastState = delegate.removeLastHistoryElement()?.state;
88 : } else {
89 : // find the last
90 : bool found = false;
91 6 : for (var beamLocation in delegate.beamingHistory.reversed) {
92 : if (found) {
93 : break;
94 : }
95 6 : for (var historyElement in beamLocation.history.reversed) {
96 : final uri =
97 8 : Uri.parse(historyElement.state.routeInformation.location ?? '/');
98 4 : if (uri.path == popPath) {
99 1 : lastState = historyElement.state;
100 : found = true;
101 : break;
102 : }
103 : }
104 : }
105 : }
106 :
107 3 : delegate.update(
108 6 : configuration: delegate.configuration.copyWith(
109 3 : location: popUri.toString(),
110 4 : state: lastState?.routeInformation.state,
111 : ),
112 : data: data,
113 : );
114 :
115 : return true;
116 : }
117 :
118 : /// Pops the last route from history and calls [BeamerDelegate.update].
119 1 : static bool routePop(
120 : BuildContext context,
121 : BeamerDelegate delegate,
122 : RouteInformationSerializable state,
123 : BeamPage poppedPage,
124 : ) {
125 2 : if (delegate.beamingHistoryCompleteLength < 2) {
126 : return false;
127 : }
128 :
129 1 : delegate.removeLastHistoryElement();
130 1 : final previousHistoryElement = delegate.removeLastHistoryElement()!;
131 :
132 1 : delegate.update(
133 3 : configuration: previousHistoryElement.state.routeInformation.copyWith(),
134 : );
135 :
136 : return true;
137 : }
138 :
139 : /// The concrete Widget representing app's screen.
140 : final Widget child;
141 :
142 : /// The BeamPage's title. On the web, this is used for the browser tab title.
143 : final String? title;
144 :
145 : /// Overrides the default pop by executing an arbitrary closure.
146 : /// Mainly used to manually update the [delegate.currentBeamLocation] state.
147 : ///
148 : /// [poppedPage] is this [BeamPage].
149 : ///
150 : /// Return `false` (rarely used) to prevent **any** navigation from happening,
151 : /// otherwise return `true`.
152 : ///
153 : /// More powerful than [popToNamed].
154 : final bool Function(
155 : BuildContext context,
156 : BeamerDelegate delegate,
157 : RouteInformationSerializable state,
158 : BeamPage poppedPage,
159 : ) onPopPage;
160 :
161 : /// Overrides the default pop by beaming to specified URI string.
162 : ///
163 : /// Less powerful than [onPopPage].
164 : final String? popToNamed;
165 :
166 : /// The type to determine how a route should be built.
167 : ///
168 : /// See [BeamPageType] for available types.
169 : final BeamPageType type;
170 :
171 : /// A builder for custom [Route] to use in [createRoute].
172 : ///
173 : /// [settings] must be passed to [PageRoute.settings].
174 : /// [child] is the child of this [BeamPage]
175 : final Route Function(RouteSettings settings, Widget child)? routeBuilder;
176 :
177 : /// Whether to present current [BeamPage] as a fullscreen dialog
178 : ///
179 : /// On iOS, dialog transitions animate differently and are also not closeable with the back swipe gesture
180 : final bool fullScreenDialog;
181 :
182 : /// When this [BeamPage] pops from [Navigator] stack, whether to keep the
183 : /// query parameters within current [BeamLocation].
184 : ///
185 : /// Defaults to `false`.
186 : final bool keepQueryOnPop;
187 :
188 6 : @override
189 : Route createRoute(BuildContext context) {
190 6 : if (routeBuilder != null) {
191 2 : return routeBuilder!(this, child);
192 : }
193 6 : switch (type) {
194 6 : case BeamPageType.cupertino:
195 1 : return CupertinoPageRoute(
196 1 : title: title,
197 1 : fullscreenDialog: fullScreenDialog,
198 : settings: this,
199 2 : builder: (context) => child,
200 : );
201 6 : case BeamPageType.fadeTransition:
202 1 : return PageRouteBuilder(
203 1 : fullscreenDialog: fullScreenDialog,
204 : settings: this,
205 2 : pageBuilder: (_, __, ___) => child,
206 2 : transitionsBuilder: (_, animation, __, child) => FadeTransition(
207 : opacity: animation,
208 : child: child,
209 : ),
210 : );
211 6 : case BeamPageType.slideTransition:
212 1 : return PageRouteBuilder(
213 1 : fullscreenDialog: fullScreenDialog,
214 : settings: this,
215 2 : pageBuilder: (_, __, ___) => child,
216 2 : transitionsBuilder: (_, animation, __, child) => SlideTransition(
217 1 : position: animation.drive(
218 1 : Tween(begin: const Offset(0, 1), end: const Offset(0, 0))
219 2 : .chain(CurveTween(curve: Curves.ease))),
220 : child: child,
221 : ),
222 : );
223 6 : case BeamPageType.scaleTransition:
224 1 : return PageRouteBuilder(
225 1 : fullscreenDialog: fullScreenDialog,
226 : settings: this,
227 2 : pageBuilder: (_, __, ___) => child,
228 2 : transitionsBuilder: (_, animation, __, child) => ScaleTransition(
229 : scale: animation,
230 : child: child,
231 : ),
232 : );
233 6 : case BeamPageType.noTransition:
234 1 : return PageRouteBuilder(
235 1 : fullscreenDialog: fullScreenDialog,
236 : settings: this,
237 2 : pageBuilder: (context, animation, secondaryAnimation) => child,
238 : );
239 : default:
240 6 : return MaterialPageRoute(
241 6 : fullscreenDialog: fullScreenDialog,
242 : settings: this,
243 12 : builder: (context) => child,
244 : );
245 : }
246 : }
247 : }
|