blob: 160a48577d8976c64d145cc5acd7f83ad29f6d2e [file] [log] [blame]
// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:gallery/data/gallery_options.dart';
import 'package:gallery/deferred_widget.dart';
import 'package:gallery/main.dart';
import 'package:gallery/pages/demo.dart' deferred as demo;
//import 'package:gallery/pages/home.dart';
import 'package:gallery/studies/crane/app.dart' deferred as crane;
import 'package:gallery/studies/fortnightly/app.dart' deferred as fortnightly;
import 'package:gallery/studies/rally/app.dart' deferred as rally;
import 'package:gallery/studies/reply/app.dart' deferred as reply;
import 'package:gallery/studies/shrine/app.dart' deferred as shrine;
import 'package:gallery/studies/starter/app.dart' deferred as starter;
typedef PathWidgetBuilder = Widget Function(BuildContext, String);
class Path {
const Path(this.pattern, this.builder);
/// A RegEx string for route matching.
final String pattern;
/// The builder for the associated pattern route. The first argument is the
/// [BuildContext] and the second argument a RegEx match if that is included
/// in the pattern.
///
/// ```dart
/// Path(
/// 'r'^/demo/([\w-]+)$',
/// (context, matches) => Page(argument: match),
/// )
/// ```
final PathWidgetBuilder builder;
}
class RouteConfiguration {
/// List of [Path] to for route matching. When a named route is pushed with
/// [Navigator.pushNamed], the route name is matched with the [Path.pattern]
/// in the list below. As soon as there is a match, the associated builder
/// will be returned. This means that the paths higher up in the list will
/// take priority.
static List<Path> paths = [
Path(
r'^/demo' + r'/([\w-]+)$',
(context, match) =>
DeferredWidget(demo.loadLibrary, () => demo.DemoPage(slug: match)),
),
Path(
r'^/rally',
(context, match) => DeferredWidget(
rally.loadLibrary, () => StudyWrapper(study: rally.RallyApp())),
),
Path(
r'^/shrine',
(context, match) => DeferredWidget(
shrine.loadLibrary, () => StudyWrapper(study: shrine.ShrineApp())),
),
Path(
r'^/crane',
(context, match) => DeferredWidget(
crane.loadLibrary, () => StudyWrapper(study: crane.CraneApp())),
),
Path(
r'^/fortnightly',
(context, match) => DeferredWidget(fortnightly.loadLibrary,
() => StudyWrapper(study: fortnightly.FortnightlyApp())),
),
Path(
r'^/reply',
(context, match) => DeferredWidget(
reply.loadLibrary,
() => StudyWrapper(
alignment: AlignmentDirectional.topCenter,
study: reply.ReplyApp(),
)),
),
Path(
r'^/starter',
(context, match) => DeferredWidget(
starter.loadLibrary, () => StudyWrapper(study: starter.StarterApp())),
),
Path(
r'^/',
(context, match) => const RootPage(),
),
];
/// The route generator callback used when the app is navigated to a named
/// route. Set it on the [MaterialApp.onGenerateRoute] or
/// [WidgetsApp.onGenerateRoute] to make use of the [paths] for route
/// matching.
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
for (final path in paths) {
final regExpPattern = RegExp(path.pattern);
if (regExpPattern.hasMatch(settings.name)) {
final firstMatch = regExpPattern.firstMatch(settings.name);
final match = (firstMatch.groupCount == 1) ? firstMatch.group(1) : null;
if (kIsWeb) {
return NoAnimationMaterialPageRoute<void>(
builder: (context) => path.builder(context, match),
settings: settings,
);
}
return MaterialPageRoute<void>(
builder: (context) => path.builder(context, match),
settings: settings,
);
}
}
// If no match was found, we let [WidgetsApp.onUnknownRoute] handle it.
return null;
}
}
class NoAnimationMaterialPageRoute<T> extends MaterialPageRoute<T> {
NoAnimationMaterialPageRoute({
@required WidgetBuilder builder,
RouteSettings settings,
}) : super(builder: builder, settings: settings);
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
/// Wrap the studies with this to display a back button and allow the user to
/// exit them at any time.
class StudyWrapper extends StatefulWidget {
const StudyWrapper({
Key key,
this.study,
this.alignment = AlignmentDirectional.bottomStart,
}) : super(key: key);
final Widget study;
final AlignmentDirectional alignment;
@override
_StudyWrapperState createState() => _StudyWrapperState();
}
class _StudyWrapperState extends State<StudyWrapper> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return ApplyTextOptions(
child: Stack(
children: [
Semantics(
sortKey: const OrdinalSortKey(1),
child: widget.study,
),
Align(
alignment: widget.alignment,
child: Padding(
padding: const EdgeInsets.all(16),
child: Semantics(
sortKey: const OrdinalSortKey(0),
//label: GalleryLocalizations.of(context).backToGallery,
button: true,
enabled: true,
excludeSemantics: true,
child: FloatingActionButton.extended(
//heroTag: _BackButtonHeroTag(),
key: const ValueKey('Back'),
onPressed: () {
Navigator.of(context)
.popUntil((route) => route.settings.name == '/');
},
icon: IconTheme(
data: IconThemeData(color: colorScheme.onPrimary),
child: const BackButtonIcon(),
),
label: Text(
MaterialLocalizations.of(context).backButtonTooltip,
style: textTheme.button.apply(color: colorScheme.onPrimary),
),
),
),
),
),
],
),
);
}
}