Line data Source code
1 : part of '../../routemaster.dart';
2 :
3 : class IndexedPage extends StatefulPage<void> with IndexedRouteMixIn {
4 : final Widget child;
5 :
6 : @override
7 : final List<String> paths;
8 :
9 1 : IndexedPage({
10 : required this.child,
11 : required this.paths,
12 : });
13 :
14 1 : @override
15 : PageState createState(Routemaster delegate, RouteInfo routeInfo) {
16 1 : return IndexedPageState(this, delegate, routeInfo);
17 : }
18 :
19 1 : static IndexedPageState of(BuildContext context) {
20 : return context
21 1 : .dependOnInheritedWidgetOfExactType<_IndexedPageStateProvider>()!
22 1 : .pageState;
23 : }
24 : }
25 :
26 : class _IndexedPageStateProvider extends InheritedNotifier {
27 : final IndexedPageState pageState;
28 :
29 1 : _IndexedPageStateProvider({
30 : required Widget child,
31 : required this.pageState,
32 1 : }) : super(
33 : child: child,
34 1 : notifier: pageState._delegate,
35 : );
36 :
37 1 : @override
38 : bool updateShouldNotify(covariant _IndexedPageStateProvider oldWidget) {
39 3 : return pageState != oldWidget.pageState;
40 : }
41 : }
42 :
43 : class IndexedPageState extends PageState
44 : with ChangeNotifier, IndexedPageStateMixIn {
45 : @override
46 : final IndexedPage _page;
47 :
48 : @override
49 : final Routemaster _delegate;
50 :
51 : @override
52 : final RouteInfo routeInfo;
53 :
54 1 : IndexedPageState(
55 : this._page,
56 : this._delegate,
57 : this.routeInfo,
58 : ) {
59 5 : _routes = List.filled(_page.paths.length, null);
60 : }
61 1 : @override
62 : Page createPage() {
63 1 : return MaterialPage<void>(
64 1 : child: _IndexedPageStateProvider(
65 : pageState: this,
66 2 : child: _page.child,
67 : ),
68 2 : key: ValueKey(routeInfo),
69 : );
70 : }
71 : }
72 :
73 : class TabPage extends StatefulPage<void> with IndexedRouteMixIn {
74 : // TODO: This should probably take a page and not a widget
75 : final Widget child;
76 :
77 : @override
78 : final List<String> paths;
79 :
80 2 : TabPage({
81 : required this.child,
82 : required this.paths,
83 : });
84 :
85 2 : @override
86 : PageState createState(Routemaster delegate, RouteInfo routeInfo) {
87 2 : return TabPageState(this, delegate, routeInfo);
88 : }
89 :
90 2 : static TabPageState of(BuildContext context) {
91 : return context
92 2 : .dependOnInheritedWidgetOfExactType<_TabPageStateProvider>()!
93 2 : .pageState;
94 : }
95 : }
96 :
97 : class _TabPageStateProvider extends InheritedNotifier {
98 : final TabPageState pageState;
99 :
100 2 : _TabPageStateProvider({
101 : required Widget child,
102 : required this.pageState,
103 2 : }) : super(
104 : child: child,
105 2 : notifier: pageState._delegate,
106 : );
107 :
108 1 : @override
109 : bool updateShouldNotify(covariant _TabPageStateProvider oldWidget) {
110 3 : return pageState != oldWidget.pageState;
111 : }
112 : }
113 :
114 : class TabPageState extends PageState
115 : with ChangeNotifier, IndexedPageStateMixIn {
116 : @override
117 : final TabPage _page;
118 :
119 : @override
120 : final Routemaster _delegate;
121 :
122 : @override
123 : final RouteInfo routeInfo;
124 :
125 2 : TabPageState(this._page, this._delegate, this.routeInfo) {
126 10 : _routes = List.filled(_page.paths.length, null);
127 : }
128 :
129 2 : @override
130 : set index(int value) {
131 2 : if (_tabController != null) {
132 2 : _tabController!.index = value;
133 : }
134 :
135 2 : super.index = value;
136 : }
137 :
138 2 : @override
139 : Page createPage() {
140 2 : return MaterialPage<void>(
141 4 : key: ValueKey(routeInfo),
142 2 : child: _TabControllerProvider(
143 : pageState: this,
144 2 : child: _TabPageStateProvider(
145 : pageState: this,
146 8 : child: Builder(builder: (_) => _page.child),
147 : ),
148 : ),
149 : );
150 : }
151 :
152 : TabController? _tabController;
153 2 : TabController get tabController => _tabController!;
154 : }
155 :
156 : /// Creates a [TabController] for [TabPageState]
157 : class _TabControllerProvider extends StatefulWidget {
158 : final Widget child;
159 : final TabPageState pageState;
160 :
161 2 : _TabControllerProvider({
162 : required this.child,
163 : required this.pageState,
164 : });
165 :
166 2 : @override
167 2 : _TabControllerProviderState createState() => _TabControllerProviderState();
168 : }
169 :
170 : class _TabControllerProviderState extends State<_TabControllerProvider>
171 : with SingleTickerProviderStateMixin {
172 2 : @override
173 : void initState() {
174 2 : super.initState();
175 :
176 2 : final tabController = TabController(
177 8 : length: widget.pageState._routes.length,
178 6 : initialIndex: widget.pageState.index,
179 : vsync: this,
180 : );
181 :
182 3 : tabController.addListener(() {
183 4 : widget.pageState.index = tabController.index;
184 : });
185 :
186 6 : widget.pageState._tabController = tabController;
187 : }
188 :
189 2 : @override
190 : void dispose() {
191 8 : widget.pageState._tabController?.dispose();
192 6 : widget.pageState._tabController = null;
193 2 : super.dispose();
194 : }
195 :
196 2 : @override
197 4 : Widget build(BuildContext context) => widget.child;
198 : }
199 :
200 : class CupertinoTabPage extends StatefulPage<void> with IndexedRouteMixIn {
201 : final Widget child;
202 :
203 : @override
204 : final List<String> paths;
205 :
206 1 : CupertinoTabPage({
207 : required this.child,
208 : required this.paths,
209 : });
210 :
211 1 : @override
212 : PageState createState(Routemaster delegate, RouteInfo routeInfo) {
213 1 : return CupertinoTabPageState(this, delegate, routeInfo);
214 : }
215 :
216 1 : static CupertinoTabPageState of(BuildContext context) {
217 : return context
218 1 : .dependOnInheritedWidgetOfExactType<_CupertinoTabPageStateProvider>()!
219 1 : .pageState;
220 : }
221 : }
222 :
223 : class _CupertinoTabPageStateProvider extends InheritedNotifier {
224 : final CupertinoTabPageState pageState;
225 :
226 1 : _CupertinoTabPageStateProvider({
227 : required Widget child,
228 : required this.pageState,
229 1 : }) : super(
230 : child: child,
231 1 : notifier: pageState._delegate,
232 : );
233 :
234 1 : @override
235 : bool updateShouldNotify(covariant _CupertinoTabPageStateProvider oldWidget) {
236 3 : return pageState != oldWidget.pageState;
237 : }
238 : }
239 :
240 : class CupertinoTabPageState extends PageState
241 : with ChangeNotifier, IndexedPageStateMixIn {
242 : @override
243 : final CupertinoTabPage _page;
244 :
245 : @override
246 : final Routemaster _delegate;
247 :
248 : @override
249 : final RouteInfo routeInfo;
250 :
251 : final CupertinoTabController tabController = CupertinoTabController();
252 :
253 1 : CupertinoTabPageState(
254 : this._page,
255 : this._delegate,
256 : this.routeInfo,
257 : ) {
258 5 : _routes = List.filled(_page.paths.length, null);
259 :
260 2 : addListener(() {
261 4 : if (index != tabController.index) {
262 3 : tabController.index = index;
263 : }
264 : });
265 :
266 3 : tabController.addListener(() {
267 3 : index = tabController.index;
268 : });
269 : }
270 :
271 1 : @override
272 : Page createPage() {
273 1 : return MaterialPage<void>(
274 1 : child: _CupertinoTabPageStateProvider(
275 : pageState: this,
276 2 : child: _page.child,
277 : ),
278 2 : key: ValueKey(routeInfo),
279 : );
280 : }
281 :
282 1 : Widget tabBuilder(BuildContext context, int index) {
283 1 : final stack = _getStackForIndex(index);
284 1 : final pages = stack.createPages();
285 :
286 1 : assert(pages.isNotEmpty, 'Pages must not be empty');
287 :
288 1 : return Navigator(
289 1 : key: stack.navigatorKey,
290 1 : onPopPage: stack.onPopPage,
291 : pages: pages,
292 : );
293 : }
294 : }
295 :
296 : mixin IndexedRouteMixIn<T> on Page<T> {
297 : List<String> get paths;
298 : }
299 :
300 : mixin IndexedPageStateMixIn on PageWrapper, ChangeNotifier {
301 : late List<StackPageState?> _routes;
302 :
303 : Routemaster get _delegate;
304 :
305 : @override
306 : RouteInfo get routeInfo;
307 :
308 : IndexedRouteMixIn get _page;
309 :
310 : StackList? _stacks;
311 6 : StackList get stacks => _stacks ??= StackList(this);
312 :
313 3 : StackPageState get currentStack => _getStackForIndex(index);
314 :
315 : int _index = 0;
316 8 : int get index => _index;
317 4 : set index(int value) {
318 8 : if (value != _index) {
319 3 : _index = value;
320 :
321 3 : notifyListeners();
322 6 : _delegate._markNeedsUpdate();
323 : }
324 : }
325 :
326 4 : StackPageState _getStackForIndex(int index) {
327 8 : if (_routes[index] == null) {
328 12 : _routes[index] = _createInitialStackState(index);
329 : }
330 :
331 8 : return _routes[index]!;
332 : }
333 :
334 4 : StackPageState? _createInitialStackState(int index) {
335 24 : final path = join(routeInfo.path, _page.paths[index]);
336 8 : final route = _delegate._getPageWrapper(path);
337 12 : return StackPageState(delegate: _delegate, routes: [route]);
338 : }
339 :
340 : /// Attempts to handle a list of child pages.
341 : ///
342 : /// Checks if the first route matches one of the child paths of this tab page.
343 : /// If it does, it sets that stack's pages to the routes, and switches the
344 : /// current index to that tab.
345 4 : @override
346 : bool maybeSetChildPages(Iterable<PageWrapper> pages) {
347 : assert(
348 4 : pages.isNotEmpty,
349 : "Don't call maybeSetPageStates with an empty list",
350 : );
351 :
352 8 : final tabPagePath = routeInfo.path;
353 12 : final subPagePath = pages.first.routeInfo.path;
354 :
355 4 : if (!isWithin(tabPagePath, subPagePath)) {
356 : // subPagePath is not a path beneath the tab page's path.
357 : return false;
358 : }
359 :
360 8 : final index = _getIndexForPath(_stripPath(tabPagePath, subPagePath));
361 : if (index == null) {
362 : // First route didn't match any of our child paths, so this isn't a route
363 : // that we can handle.
364 : return false;
365 : }
366 :
367 : // Handle route
368 12 : _getStackForIndex(index).maybeSetChildPages(pages.toList());
369 4 : this.index = index;
370 : return true;
371 : }
372 :
373 4 : int? _getIndexForPath(String path) {
374 : var i = 0;
375 12 : for (final initialPath in _page.paths) {
376 4 : if (path.startsWith(initialPath)) {
377 : return i;
378 : }
379 1 : i++;
380 : }
381 :
382 : return null;
383 : }
384 :
385 4 : static String _stripPath(String parent, String child) {
386 4 : final splitParent = split(parent);
387 4 : final splitChild = split(child);
388 12 : return joinAll(splitChild.skip(splitParent.length));
389 : }
390 :
391 : // @override
392 : // bool maybePush(PageState route) {
393 : // final index = _getIndexForPath(route.routeInfo.path);
394 : // if (index == null) {
395 : // return false;
396 : // }
397 :
398 : // return _getStackForIndex(index).maybePush(route);
399 : // }
400 :
401 2 : @override
402 : Future<bool> maybePop() {
403 6 : return _getStackForIndex(index).maybePop();
404 : }
405 :
406 : @override
407 : Iterable<PageWrapper> getCurrentPages() sync* {
408 : yield this;
409 : yield* _getStackForIndex(index)._getCurrentPages();
410 : }
411 : }
412 :
413 : class StackList {
414 : final IndexedPageStateMixIn _indexedPageState;
415 :
416 2 : StackList(this._indexedPageState);
417 :
418 2 : StackPageState operator [](int index) =>
419 4 : _indexedPageState._getStackForIndex(index);
420 : }
|