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