This commit is contained in:
toly
2023-09-22 09:15:11 +08:00
parent d456e3c523
commit e95c34018e
132 changed files with 8527 additions and 17 deletions

View File

@@ -0,0 +1,81 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import 'auth.dart';
import 'routing.dart';
import 'screens/navigator.dart';
class Bookstore extends StatefulWidget {
const Bookstore({super.key});
@override
State<Bookstore> createState() => _BookstoreState();
}
class _BookstoreState extends State<Bookstore> {
late final RouteState _routeState;
late final SimpleRouterDelegate _routerDelegate;
late final TemplateRouteParser _routeParser;
@override
void initState() {
/// Configure the parser with all of the app's allowed path templates.
_routeParser = TemplateRouteParser(
allowedPaths: [
'/signin',
'/authors',
'/settings',
'/books/new',
'/books/all',
'/books/popular',
'/book/:bookId',
'/author/:authorId',
],
initialRoute: '/signin',
);
_routeState = RouteState(_routeParser);
_routerDelegate = SimpleRouterDelegate(
routeState: _routeState,
);
super.initState();
}
@override
Widget build(BuildContext context) => RouteStateScope(
notifier: _routeState,
child: MaterialApp.router(
routerDelegate: _routerDelegate,
routeInformationParser: _routeParser,
// Revert back to pre-Flutter-2.5 transition behavior:
// https://github.com/flutter/flutter/issues/82053
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
},
),
),
),
);
@override
void dispose() {
_routeState.dispose();
_routerDelegate.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,47 @@
// // Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// // for details. 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/widgets.dart';
//
// /// A mock authentication service
// class BookstoreAuth extends ChangeNotifier {
// bool _signedIn = false;
//
// bool get signedIn => _signedIn;
//
// Future<void> signOut() async {
// await Future<void>.delayed(const Duration(milliseconds: 200));
// // Sign out.
// _signedIn = false;
// notifyListeners();
// }
//
// Future<bool> signIn(String username, String password) async {
// await Future<void>.delayed(const Duration(milliseconds: 200));
//
// // Sign in. Allow any password.
// _signedIn = true;
// notifyListeners();
// return _signedIn;
// }
//
// @override
// bool operator ==(Object other) =>
// other is BookstoreAuth && other._signedIn == _signedIn;
//
// @override
// int get hashCode => _signedIn.hashCode;
// }
//
// class BookstoreAuthScope extends InheritedNotifier<BookstoreAuth> {
// const BookstoreAuthScope({
// required super.notifier,
// required super.child,
// super.key,
// });
//
// static BookstoreAuth of(BuildContext context) => context
// .dependOnInheritedWidgetOfExactType<BookstoreAuthScope>()!
// .notifier!;
// }

View File

@@ -0,0 +1,7 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
export 'data/author.dart';
export 'data/book.dart';
export 'data/library.dart';

View File

@@ -0,0 +1,13 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'book.dart';
class Author {
final int id;
final String name;
final books = <Book>[];
Author(this.id, this.name);
}

View File

@@ -0,0 +1,15 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'author.dart';
class Book {
final int id;
final String title;
final Author author;
final bool isPopular;
final bool isNew;
Book(this.id, this.title, this.isPopular, this.isNew, this.author);
}

View File

@@ -0,0 +1,61 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'author.dart';
import 'book.dart';
final libraryInstance = Library()
..addBook(
title: 'Left Hand of Darkness',
authorName: 'Ursula K. Le Guin',
isPopular: true,
isNew: true)
..addBook(
title: 'Too Like the Lightning',
authorName: 'Ada Palmer',
isPopular: false,
isNew: true)
..addBook(
title: 'Kindred',
authorName: 'Octavia E. Butler',
isPopular: true,
isNew: false)
..addBook(
title: 'The Lathe of Heaven',
authorName: 'Ursula K. Le Guin',
isPopular: false,
isNew: false);
class Library {
final List<Book> allBooks = [];
final List<Author> allAuthors = [];
void addBook({
required String title,
required String authorName,
required bool isPopular,
required bool isNew,
}) {
var author = allAuthors.firstWhere(
(author) => author.name == authorName,
orElse: () {
final value = Author(allAuthors.length, authorName);
allAuthors.add(value);
return value;
},
);
var book = Book(allBooks.length, title, isPopular, isNew, author);
author.books.add(book);
allBooks.add(book);
}
List<Book> get popularBooks => [
...allBooks.where((book) => book.isPopular),
];
List<Book> get newBooks => [
...allBooks.where((book) => book.isNew),
];
}

View File

@@ -0,0 +1,8 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
export 'routing/delegate.dart';
export 'routing/parsed_route.dart';
export 'routing/parser.dart';
export 'routing/route_state.dart';

View File

@@ -0,0 +1,47 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import '../screens/navigator.dart';
import 'parsed_route.dart';
import 'route_state.dart';
class SimpleRouterDelegate extends RouterDelegate<ParsedRoute>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<ParsedRoute> {
final RouteState routeState;
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
SimpleRouterDelegate({
required this.routeState,
}) {
routeState.addListener(notifyListeners);
}
@override
Widget build(BuildContext context) => BookstoreNavigator(
navigatorKey: navigatorKey,
);
@override
Future<void> setNewRoutePath(ParsedRoute configuration) async {
routeState.route = configuration;
return SynchronousFuture(null);
}
@override
ParsedRoute get currentConfiguration => routeState.route;
@override
void dispose() {
routeState.removeListener(notifyListeners);
routeState.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,51 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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:collection/collection.dart';
import 'package:quiver/core.dart';
import 'parser.dart';
/// A route path that has been parsed by [TemplateRouteParser].
class ParsedRoute {
/// The current path location without query parameters. (/book/123)
final String path;
/// The path template (/book/:id)
final String pathTemplate;
/// The path parameters ({id: 123})
final Map<String, String> parameters;
/// The query parameters ({search: abc})
final Map<String, String> queryParameters;
static const _mapEquality = MapEquality<String, String>();
ParsedRoute(
this.path, this.pathTemplate, this.parameters, this.queryParameters);
@override
bool operator ==(Object other) =>
other is ParsedRoute &&
other.pathTemplate == pathTemplate &&
other.path == path &&
_mapEquality.equals(parameters, other.parameters) &&
_mapEquality.equals(queryParameters, other.queryParameters);
@override
int get hashCode => hash4(
path,
pathTemplate,
_mapEquality.hash(parameters),
_mapEquality.hash(queryParameters),
);
@override
String toString() => '<ParsedRoute '
'template: $pathTemplate '
'path: $path '
'parameters: $parameters '
'query parameters: $queryParameters>';
}

View File

@@ -0,0 +1,62 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/widgets.dart';
import 'package:path_to_regexp/path_to_regexp.dart';
import 'parsed_route.dart';
/// Used by [TemplateRouteParser] to guard access to routes.
typedef RouteGuard<T> = Future<T> Function(T from);
/// Parses the URI path into a [ParsedRoute].
class TemplateRouteParser extends RouteInformationParser<ParsedRoute> {
final List<String> _pathTemplates;
final ParsedRoute initialRoute;
TemplateRouteParser({
/// The list of allowed path templates (['/', '/users/:id'])
required List<String> allowedPaths,
/// The initial route
String initialRoute = '/',
}) : initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {}),
_pathTemplates = [
...allowedPaths,
],
assert(allowedPaths.contains(initialRoute));
@override
Future<ParsedRoute> parseRouteInformation(
RouteInformation routeInformation,
) async {
print("=======parseRouteInformation:${routeInformation.uri.path}===================");
final uri = routeInformation.uri;
final path = uri.toString();
final queryParams = uri.queryParameters;
var parsedRoute = initialRoute;
for (var pathTemplate in _pathTemplates) {
final parameters = <String>[];
var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters);
if (pathRegExp.hasMatch(path)) {
final match = pathRegExp.matchAsPrefix(path);
if (match == null) continue;
final params = extract(parameters, match);
parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams);
}
}
return parsedRoute;
}
@override
RouteInformation restoreRouteInformation(ParsedRoute configuration) {
print("=======restoreRouteInformation:${configuration}===================");
return RouteInformation(uri: Uri.parse(configuration.path));
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/widgets.dart';
import 'parsed_route.dart';
import 'parser.dart';
/// The current route state. To change the current route, call obtain the state
/// using `RouteStateScope.of(context)` and call `go()`:
///
/// ```
/// RouteStateScope.of(context).go('/book/2');
/// ```
class RouteState extends ChangeNotifier {
final TemplateRouteParser _parser;
ParsedRoute _route;
RouteState(this._parser) : _route = _parser.initialRoute;
ParsedRoute get route => _route;
set route(ParsedRoute route) {
// Don't notify listeners if the path hasn't changed.
if (_route == route) return;
_route = route;
notifyListeners();
}
Future<void> go(String route) async {
this.route = await _parser
.parseRouteInformation(RouteInformation(uri: Uri.parse(route)));
}
}
/// Provides the current [RouteState] to descendant widgets in the tree.
class RouteStateScope extends InheritedNotifier<RouteState> {
const RouteStateScope({
required super.notifier,
required super.child,
super.key,
});
static RouteState of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<RouteStateScope>()!.notifier!;
}

View File

@@ -0,0 +1,39 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import '../data.dart';
import '../routing.dart';
import '../widgets/book_list.dart';
class AuthorDetailsScreen extends StatelessWidget {
final Author author;
const AuthorDetailsScreen({
super.key,
required this.author,
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(author.name),
),
body: Center(
child: Column(
children: [
Expanded(
child: BookList(
books: author.books,
onTap: (book) {
RouteStateScope.of(context).go('/book/${book.id}');
},
),
),
],
),
),
);
}

View File

@@ -0,0 +1,28 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import '../data/library.dart';
import '../routing.dart';
import '../widgets/author_list.dart';
class AuthorsScreen extends StatelessWidget {
final String title = 'Authors';
const AuthorsScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(title),
),
body: AuthorList(
authors: libraryInstance.allAuthors,
onTap: (author) {
RouteStateScope.of(context).go('/author/${author.id}');
},
),
);
}

View File

@@ -0,0 +1,66 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import 'package:url_launcher/link.dart';
import '../data.dart';
import 'author_details.dart';
class BookDetailsScreen extends StatelessWidget {
final Book? book;
const BookDetailsScreen({
super.key,
this.book,
});
@override
Widget build(BuildContext context) {
if (book == null) {
return const Scaffold(
body: Center(
child: Text('No book found.'),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text(book!.title),
),
body: Center(
child: Column(
children: [
Text(
book!.title,
style: Theme.of(context).textTheme.headlineMedium,
),
Text(
book!.author.name,
style: Theme.of(context).textTheme.titleMedium,
),
TextButton(
child: const Text('View author (Push)'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (context) =>
AuthorDetailsScreen(author: book!.author),
),
);
},
),
Link(
uri: Uri.parse('/author/${book!.author.id}'),
builder: (context, followLink) => TextButton(
onPressed: followLink,
child: const Text('View author (Link)'),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,111 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import '../data.dart';
import '../routing.dart';
import '../widgets/book_list.dart';
class BooksScreen extends StatefulWidget {
const BooksScreen({
super.key,
});
@override
State<BooksScreen> createState() => _BooksScreenState();
}
class _BooksScreenState extends State<BooksScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this)
..addListener(_handleTabIndexChanged);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final newPath = _routeState.route.pathTemplate;
if (newPath.startsWith('/books/popular')) {
_tabController.index = 0;
} else if (newPath.startsWith('/books/new')) {
_tabController.index = 1;
} else if (newPath == '/books/all') {
_tabController.index = 2;
}
}
@override
void dispose() {
_tabController.removeListener(_handleTabIndexChanged);
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Books'),
elevation: 8,
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(
text: 'Popular',
icon: Icon(Icons.people),
),
Tab(
text: 'New',
icon: Icon(Icons.new_releases),
),
Tab(
text: 'All',
icon: Icon(Icons.list),
),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
BookList(
books: libraryInstance.popularBooks,
onTap: _handleBookTapped,
),
BookList(
books: libraryInstance.newBooks,
onTap: _handleBookTapped,
),
BookList(
books: libraryInstance.allBooks,
onTap: _handleBookTapped,
),
],
),
);
RouteState get _routeState => RouteStateScope.of(context);
void _handleBookTapped(Book book) {
_routeState.go('/book/${book.id}');
}
void _handleTabIndexChanged() {
switch (_tabController.index) {
case 1:
_routeState.go('/books/new');
case 2:
_routeState.go('/books/all');
case 0:
default:
_routeState.go('/books/popular');
break;
}
}
}

View File

@@ -0,0 +1,108 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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:collection/collection.dart';
import 'package:flutter/material.dart';
import '../auth.dart';
import '../data.dart';
import '../routing.dart';
import '../screens/sign_in.dart';
import '../widgets/fade_transition_page.dart';
import 'author_details.dart';
import 'book_details.dart';
import 'scaffold.dart';
/// Builds the top-level navigator for the app. The pages to display are based
/// on the `routeState` that was parsed by the TemplateRouteParser.
class BookstoreNavigator extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey;
const BookstoreNavigator({
required this.navigatorKey,
super.key,
});
@override
State<BookstoreNavigator> createState() => _BookstoreNavigatorState();
}
class _BookstoreNavigatorState extends State<BookstoreNavigator> {
final _signInKey = const ValueKey('Sign in');
final _scaffoldKey = const ValueKey('App scaffold');
final _bookDetailsKey = const ValueKey('Book details screen');
final _authorDetailsKey = const ValueKey('Author details screen');
@override
Widget build(BuildContext context) {
final routeState = RouteStateScope.of(context);
final pathTemplate = routeState.route.pathTemplate;
Book? selectedBook;
if (pathTemplate == '/book/:bookId') {
selectedBook = libraryInstance.allBooks.firstWhereOrNull(
(b) => b.id.toString() == routeState.route.parameters['bookId']);
}
Author? selectedAuthor;
if (pathTemplate == '/author/:authorId') {
selectedAuthor = libraryInstance.allAuthors.firstWhereOrNull(
(b) => b.id.toString() == routeState.route.parameters['authorId']);
}
return Navigator(
key: widget.navigatorKey,
onPopPage: (route, dynamic result) {
// When a page that is stacked on top of the scaffold is popped, display
// the /books or /authors tab in BookstoreScaffold.
if (route.settings is Page &&
(route.settings as Page).key == _bookDetailsKey) {
routeState.go('/books/popular');
}
if (route.settings is Page &&
(route.settings as Page).key == _authorDetailsKey) {
routeState.go('/authors');
}
return route.didPop(result);
},
pages: [
if (routeState.route.pathTemplate == '/signin')
// Display the sign in screen.
FadeTransitionPage<void>(
key: _signInKey,
child: SignInScreen(
onSignIn: (credentials) async {
await routeState.go('/books/popular');
},
),
)
else ...[
// Display the app
FadeTransitionPage<void>(
key: _scaffoldKey,
child: const BookstoreScaffold(),
),
// Add an additional page to the stack if the user is viewing a book
// or an author
if (selectedBook != null)
MaterialPage<void>(
key: _bookDetailsKey,
child: BookDetailsScreen(
book: selectedBook,
),
)
else if (selectedAuthor != null)
MaterialPage<void>(
key: _authorDetailsKey,
child: AuthorDetailsScreen(
author: selectedAuthor,
),
),
],
],
);
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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:adaptive_navigation/adaptive_navigation.dart';
import 'package:flutter/material.dart';
import '../routing.dart';
import 'scaffold_body.dart';
class BookstoreScaffold extends StatelessWidget {
const BookstoreScaffold({
super.key,
});
@override
Widget build(BuildContext context) {
final routeState = RouteStateScope.of(context);
final selectedIndex = _getSelectedIndex(routeState.route.pathTemplate);
return Scaffold(
body: AdaptiveNavigationScaffold(
selectedIndex: selectedIndex,
body: const BookstoreScaffoldBody(),
onDestinationSelected: (idx) {
if (idx == 0) routeState.go('/books/popular');
if (idx == 1) routeState.go('/authors');
if (idx == 2) routeState.go('/settings');
},
destinations: const [
AdaptiveScaffoldDestination(
title: 'Books',
icon: Icons.book,
),
AdaptiveScaffoldDestination(
title: 'Authors',
icon: Icons.person,
),
AdaptiveScaffoldDestination(
title: 'Settings',
icon: Icons.settings,
),
],
),
);
}
int _getSelectedIndex(String pathTemplate) {
if (pathTemplate.startsWith('/books')) return 0;
if (pathTemplate == '/authors') return 1;
if (pathTemplate == '/settings') return 2;
return 0;
}
}

View File

@@ -0,0 +1,63 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import '../routing.dart';
import '../screens/settings.dart';
import '../widgets/fade_transition_page.dart';
import 'authors.dart';
import 'books.dart';
import 'scaffold.dart';
/// Displays the contents of the body of [BookstoreScaffold]
class BookstoreScaffoldBody extends StatelessWidget {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
const BookstoreScaffoldBody({
super.key,
});
@override
Widget build(BuildContext context) {
var currentRoute = RouteStateScope.of(context).route;
// A nested Router isn't necessary because the back button behavior doesn't
// need to be customized.
return Navigator(
key: navigatorKey,
onPopPage: (route, dynamic result) => route.didPop(result),
pages: [
if (currentRoute.pathTemplate.startsWith('/authors'))
const FadeTransitionPage<void>(
key: ValueKey('authors'),
child: AuthorsScreen(),
)
else if (currentRoute.pathTemplate.startsWith('/settings'))
const FadeTransitionPage<void>(
key: ValueKey('settings'),
child: SettingsScreen(),
)
else if (currentRoute.pathTemplate.startsWith('/books') ||
currentRoute.pathTemplate == '/')
const FadeTransitionPage<void>(
key: ValueKey('books'),
child: BooksScreen(),
)
// Avoid building a Navigator with an empty `pages` list when the
// RouteState is set to an unexpected path, such as /signin.
//
// Since RouteStateScope is an InheritedNotifier, any change to the
// route will result in a call to this build method, even though this
// widget isn't built when those routes are active.
else
FadeTransitionPage<void>(
key: const ValueKey('empty'),
child: Container(),
),
],
);
}
}

View File

@@ -0,0 +1,95 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import 'package:url_launcher/link.dart';
import '../auth.dart';
import '../routing.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: const Card(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 12),
child: SettingsContent(),
),
),
),
),
),
),
);
}
class SettingsContent extends StatelessWidget {
const SettingsContent({
super.key,
});
@override
Widget build(BuildContext context) => Column(
children: [
...[
Text(
'Settings',
style: Theme.of(context).textTheme.headlineMedium,
),
FilledButton(
onPressed: () {
// BookstoreAuthScope.of(context).signOut();
},
child: const Text('Sign out'),
),
Link(
uri: Uri.parse('/book/0'),
builder: (context, followLink) => TextButton(
onPressed: followLink,
child: const Text('Go directly to /book/0 (Link)'),
),
),
TextButton(
child: const Text('Go directly to /book/0 (RouteState)'),
onPressed: () {
RouteStateScope.of(context).go('/book/0');
},
),
].map((w) => Padding(padding: const EdgeInsets.all(8), child: w)),
TextButton(
onPressed: () => showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Alert!'),
content: const Text('The alert description goes here.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
),
child: const Text('Show Dialog'),
)
],
);
}

View File

@@ -0,0 +1,69 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
class Credentials {
final String username;
final String password;
Credentials(this.username, this.password);
}
class SignInScreen extends StatefulWidget {
final ValueChanged<Credentials> onSignIn;
const SignInScreen({
required this.onSignIn,
super.key,
});
@override
State<SignInScreen> createState() => _SignInScreenState();
}
class _SignInScreenState extends State<SignInScreen> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Card(
child: Container(
constraints: BoxConstraints.loose(const Size(600, 600)),
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('Sign in',
style: Theme.of(context).textTheme.headlineMedium),
TextField(
decoration: const InputDecoration(labelText: 'Username'),
controller: _usernameController,
),
TextField(
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
controller: _passwordController,
),
Padding(
padding: const EdgeInsets.all(16),
child: TextButton(
onPressed: () async {
widget.onSignIn(Credentials(
_usernameController.value.text,
_passwordController.value.text));
},
child: const Text('Sign in'),
),
),
],
),
),
),
),
);
}

View File

@@ -0,0 +1,32 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import '../data.dart';
class AuthorList extends StatelessWidget {
final List<Author> authors;
final ValueChanged<Author>? onTap;
const AuthorList({
required this.authors,
this.onTap,
super.key,
});
@override
Widget build(BuildContext context) => ListView.builder(
itemCount: authors.length,
itemBuilder: (context, index) => ListTile(
title: Text(
authors[index].name,
),
subtitle: Text(
'${authors[index].books.length} books',
),
onTap: onTap != null ? () => onTap!(authors[index]) : null,
),
);
}

View File

@@ -0,0 +1,32 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
import '../data.dart';
class BookList extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book>? onTap;
const BookList({
required this.books,
this.onTap,
super.key,
});
@override
Widget build(BuildContext context) => ListView.builder(
itemCount: books.length,
itemBuilder: (context, index) => ListTile(
title: Text(
books[index].title,
),
subtitle: Text(
books[index].author.name,
),
onTap: onTap != null ? () => onTap!(books[index]) : null,
),
);
}

View File

@@ -0,0 +1,53 @@
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
class FadeTransitionPage<T> extends Page<T> {
final Widget child;
final Duration duration;
const FadeTransitionPage({
super.key,
required this.child,
this.duration = const Duration(milliseconds: 300),
});
@override
Route<T> createRoute(BuildContext context) =>
PageBasedFadeTransitionRoute<T>(this);
}
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
final FadeTransitionPage<T> _page;
PageBasedFadeTransitionRoute(this._page) : super(settings: _page);
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
Duration get transitionDuration => _page.duration;
@override
bool get maintainState => true;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
var curveTween = CurveTween(curve: Curves.easeIn);
return FadeTransition(
opacity: animation.drive(curveTween),
child: (settings as FadeTransitionPage).child,
);
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) =>
child;
}