blob: 790c39c02eb8cdbee54678aab8ac8daf3e78518e [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/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),
],
),
);
}
}