| // 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/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:gallery/data/gallery_options.dart'; |
| import 'package:gallery/l10n/gallery_localizations.dart'; |
| import 'package:gallery/layout/adaptive.dart'; |
| import 'package:gallery/layout/text_scale.dart'; |
| import 'package:gallery/pages/home.dart'; |
| import 'package:gallery/studies/rally/colors.dart'; |
| import 'package:gallery/layout/focus_traversal_policy.dart'; |
| |
| class LoginPage extends StatefulWidget { |
| @override |
| _LoginPageState createState() => _LoginPageState(); |
| } |
| |
| class _LoginPageState extends State<LoginPage> { |
| final TextEditingController _usernameController = TextEditingController(); |
| final TextEditingController _passwordController = TextEditingController(); |
| |
| @override |
| Widget build(BuildContext context) { |
| final backButtonFocusNode = |
| InheritedFocusNodes.of(context).backButtonFocusNode; |
| |
| return DefaultFocusTraversal( |
| policy: EdgeChildrenFocusTraversalPolicy( |
| firstFocusNodeOutsideScope: backButtonFocusNode, |
| lastFocusNodeOutsideScope: backButtonFocusNode, |
| focusScope: FocusScope.of(context), |
| ), |
| child: ApplyTextOptions( |
| child: Scaffold( |
| body: SafeArea( |
| child: _MainView( |
| usernameController: _usernameController, |
| passwordController: _passwordController, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| @override |
| void dispose() { |
| _usernameController.dispose(); |
| _passwordController.dispose(); |
| super.dispose(); |
| } |
| } |
| |
| class _MainView extends StatelessWidget { |
| const _MainView({ |
| Key key, |
| this.usernameController, |
| this.passwordController, |
| }) : super(key: key); |
| |
| final TextEditingController usernameController; |
| final TextEditingController passwordController; |
| |
| void _login(BuildContext context) { |
| Navigator.pop(context); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final isDesktop = isDisplayDesktop(context); |
| List<Widget> listViewChildren; |
| |
| if (isDesktop) { |
| final desktopMaxWidth = 400.0 + 100.0 * (cappedTextScale(context) - 1); |
| listViewChildren = [ |
| _UsernameInput( |
| maxWidth: desktopMaxWidth, |
| usernameController: usernameController, |
| ), |
| const SizedBox(height: 12), |
| _PasswordInput( |
| maxWidth: desktopMaxWidth, |
| passwordController: passwordController, |
| ), |
| _LoginButton( |
| maxWidth: desktopMaxWidth, |
| onTap: () { |
| _login(context); |
| }, |
| ), |
| ]; |
| } else { |
| listViewChildren = [ |
| _SmallLogo(), |
| _UsernameInput( |
| usernameController: usernameController, |
| ), |
| const SizedBox(height: 12), |
| _PasswordInput( |
| passwordController: passwordController, |
| ), |
| _ThumbButton( |
| onTap: () { |
| _login(context); |
| }, |
| ), |
| ]; |
| } |
| |
| return Column( |
| children: [ |
| if (isDesktop) _TopBar(), |
| Expanded( |
| child: Align( |
| alignment: isDesktop ? Alignment.center : Alignment.topCenter, |
| child: ListView( |
| shrinkWrap: true, |
| padding: const EdgeInsets.symmetric(horizontal: 24), |
| children: listViewChildren, |
| ), |
| ), |
| ), |
| ], |
| ); |
| } |
| } |
| |
| class _TopBar extends StatelessWidget { |
| const _TopBar({ |
| Key key, |
| }) : super(key: key); |
| |
| @override |
| Widget build(BuildContext context) { |
| final spacing = const SizedBox(width: 30); |
| return Container( |
| width: double.infinity, |
| margin: const EdgeInsets.only(top: 8), |
| padding: EdgeInsets.symmetric(horizontal: 30), |
| child: Wrap( |
| alignment: WrapAlignment.spaceBetween, |
| children: [ |
| Row( |
| mainAxisSize: MainAxisSize.min, |
| children: [ |
| ExcludeSemantics( |
| child: SizedBox( |
| height: 80, |
| child: Image.asset( |
| 'logo.png', |
| package: 'rally_assets', |
| ), |
| ), |
| ), |
| spacing, |
| Text( |
| GalleryLocalizations.of(context).rallyLoginLoginToRally, |
| style: Theme.of(context).textTheme.body2.copyWith( |
| fontSize: 35 / reducedTextScale(context), |
| fontWeight: FontWeight.w600, |
| ), |
| ), |
| ], |
| ), |
| Row( |
| mainAxisSize: MainAxisSize.min, |
| children: [ |
| Text( |
| GalleryLocalizations.of(context).rallyLoginNoAccount, |
| style: Theme.of(context).textTheme.subhead, |
| ), |
| spacing, |
| _BorderButton( |
| text: GalleryLocalizations.of(context).rallyLoginSignUp, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| class _SmallLogo extends StatelessWidget { |
| const _SmallLogo({ |
| Key key, |
| }) : super(key: key); |
| |
| @override |
| Widget build(BuildContext context) { |
| return Padding( |
| padding: const EdgeInsets.symmetric(vertical: 64), |
| child: SizedBox( |
| height: 160, |
| child: ExcludeSemantics( |
| child: Image.asset( |
| 'logo.png', |
| package: 'rally_assets', |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _UsernameInput extends StatelessWidget { |
| const _UsernameInput({ |
| Key key, |
| this.maxWidth, |
| this.usernameController, |
| }) : super(key: key); |
| |
| final double maxWidth; |
| final TextEditingController usernameController; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Align( |
| alignment: Alignment.center, |
| child: Container( |
| constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), |
| child: TextField( |
| controller: usernameController, |
| decoration: InputDecoration( |
| labelText: GalleryLocalizations.of(context).rallyLoginUsername, |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _PasswordInput extends StatelessWidget { |
| const _PasswordInput({ |
| Key key, |
| this.maxWidth, |
| this.passwordController, |
| }) : super(key: key); |
| |
| final double maxWidth; |
| final TextEditingController passwordController; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Align( |
| alignment: Alignment.center, |
| child: Container( |
| constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), |
| child: TextField( |
| controller: passwordController, |
| decoration: InputDecoration( |
| labelText: GalleryLocalizations.of(context).rallyLoginPassword, |
| ), |
| obscureText: true, |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _ThumbButton extends StatefulWidget { |
| _ThumbButton({ |
| @required this.onTap, |
| }); |
| |
| final VoidCallback onTap; |
| |
| @override |
| _ThumbButtonState createState() => _ThumbButtonState(); |
| } |
| |
| class _ThumbButtonState extends State<_ThumbButton> { |
| BoxDecoration borderDecoration; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Semantics( |
| button: true, |
| enabled: true, |
| label: GalleryLocalizations.of(context).rallyLoginLabelLogin, |
| child: GestureDetector( |
| onTap: widget.onTap, |
| child: Focus( |
| onKey: (node, event) { |
| if (event is RawKeyDownEvent) { |
| if (event.logicalKey == LogicalKeyboardKey.enter || |
| event.logicalKey == LogicalKeyboardKey.space) { |
| widget.onTap(); |
| return true; |
| } |
| } |
| return false; |
| }, |
| onFocusChange: (hasFocus) { |
| if (hasFocus) { |
| setState(() { |
| borderDecoration = BoxDecoration( |
| border: Border.all( |
| color: Colors.white.withOpacity(0.5), |
| width: 2, |
| ), |
| ); |
| }); |
| } else { |
| setState(() { |
| borderDecoration = null; |
| }); |
| } |
| }, |
| child: Container( |
| decoration: borderDecoration, |
| height: 120, |
| child: ExcludeSemantics( |
| child: Image.asset( |
| 'thumb.png', |
| package: 'rally_assets', |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _LoginButton extends StatelessWidget { |
| const _LoginButton({ |
| Key key, |
| @required this.onTap, |
| this.maxWidth, |
| }) : super(key: key); |
| |
| final double maxWidth; |
| final VoidCallback onTap; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Align( |
| alignment: Alignment.center, |
| child: Container( |
| constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), |
| padding: const EdgeInsets.symmetric(vertical: 30), |
| child: Row( |
| children: [ |
| Icon(Icons.check_circle_outline, color: RallyColors.buttonColor), |
| const SizedBox(width: 12), |
| Text(GalleryLocalizations.of(context).rallyLoginRememberMe), |
| const Expanded(child: SizedBox.shrink()), |
| _FilledButton( |
| text: GalleryLocalizations.of(context).rallyLoginButtonLogin, |
| onTap: onTap, |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _BorderButton extends StatelessWidget { |
| const _BorderButton({Key key, @required this.text}) : super(key: key); |
| |
| final String text; |
| |
| @override |
| Widget build(BuildContext context) { |
| return OutlineButton( |
| borderSide: const BorderSide(color: RallyColors.buttonColor), |
| color: RallyColors.buttonColor, |
| highlightedBorderColor: RallyColors.buttonColor, |
| focusColor: RallyColors.buttonColor.withOpacity(0.8), |
| padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24), |
| shape: RoundedRectangleBorder( |
| borderRadius: BorderRadius.circular(12), |
| ), |
| textColor: Colors.white, |
| onPressed: () { |
| Navigator.pop(context); |
| }, |
| child: Text(text), |
| ); |
| } |
| } |
| |
| class _FilledButton extends StatelessWidget { |
| const _FilledButton({Key key, @required this.text, @required this.onTap}) |
| : super(key: key); |
| |
| final String text; |
| final VoidCallback onTap; |
| |
| @override |
| Widget build(BuildContext context) { |
| return FlatButton( |
| color: RallyColors.buttonColor, |
| padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 24), |
| shape: RoundedRectangleBorder( |
| borderRadius: BorderRadius.circular(12), |
| ), |
| onPressed: onTap, |
| child: Row( |
| children: [ |
| Icon(Icons.lock), |
| const SizedBox(width: 6), |
| Text(text), |
| ], |
| ), |
| ); |
| } |
| } |