design: Improve login design

This commit is contained in:
Krille Fear 2022-10-16 12:37:38 +02:00 committed by Christian Pauly
parent 619a4adacb
commit 9361b3deee
7 changed files with 275 additions and 240 deletions

View File

@ -19,13 +19,11 @@ class ConnectPageView extends StatelessWidget {
final identityProviders = controller.identityProviders; final identityProviders = controller.identityProviders;
return LoginScaffold( return LoginScaffold(
appBar: AppBar( appBar: AppBar(
leading: leading: controller.loading ? null : const BackButton(),
controller.loading ? null : const BackButton(color: Colors.white),
automaticallyImplyLeading: !controller.loading, automaticallyImplyLeading: !controller.loading,
centerTitle: true, centerTitle: true,
title: Text( title: Text(
Matrix.of(context).getLoginClient().homeserver?.host ?? '', Matrix.of(context).getLoginClient().homeserver?.host ?? '',
style: const TextStyle(color: Colors.white),
), ),
), ),
body: ListView( body: ListView(
@ -38,15 +36,19 @@ class ConnectPageView extends StatelessWidget {
children: [ children: [
Material( Material(
borderRadius: BorderRadius.circular(64), borderRadius: BorderRadius.circular(64),
elevation: 10, elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
color: Colors.transparent, color: Colors.transparent,
shadowColor: Theme.of(context).appBarTheme.shadowColor,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: CircleAvatar( child: CircleAvatar(
radius: 64, radius: 64,
backgroundColor: Colors.white.withAlpha(200), backgroundColor: Colors.white,
child: avatar == null child: avatar == null
? const Icon( ? const Icon(
Icons.person_outlined, Icons.person,
color: Colors.black, color: Colors.black,
size: 64, size: 64,
) )
@ -93,10 +95,7 @@ class ConnectPageView extends StatelessWidget {
hintText: L10n.of(context)!.chooseAUsername, hintText: L10n.of(context)!.chooseAUsername,
errorText: controller.signupError, errorText: controller.signupError,
errorStyle: const TextStyle(color: Colors.orange), errorStyle: const TextStyle(color: Colors.orange),
fillColor: Theme.of(context) fillColor: Theme.of(context).colorScheme.background,
.colorScheme
.background
.withOpacity(0.75),
), ),
), ),
), ),
@ -105,6 +104,10 @@ class ConnectPageView extends StatelessWidget {
child: Hero( child: Hero(
tag: 'loginButton', tag: 'loginButton',
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.loading ? () {} : controller.signUp, onPressed: controller.loading ? () {} : controller.signUp,
child: controller.loading child: controller.loading
? const LinearProgressIndicator() ? const LinearProgressIndicator()
@ -112,45 +115,52 @@ class ConnectPageView extends StatelessWidget {
), ),
), ),
), ),
Row( Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16.0),
const Expanded( child: Row(
children: [
Expanded(
child: Divider( child: Divider(
color: Colors.white, thickness: 1,
thickness: 1, color: Theme.of(context).textTheme.subtitle1?.color,
)),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.or,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
), ),
), ),
), Padding(
const Expanded( padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.or,
style: const TextStyle(fontSize: 18),
),
),
Expanded(
child: Divider( child: Divider(
color: Colors.white, thickness: 1,
thickness: 1, color: Theme.of(context).textTheme.subtitle1?.color,
)), ),
], ),
],
),
), ),
], ],
if (controller.supportsSso) if (controller.supportsSso)
identityProviders == null identityProviders == null
? const SizedBox( ? const SizedBox(
height: 74, height: 74,
child: Center( child: Center(child: CircularProgressIndicator.adaptive()),
child: CircularProgressIndicator.adaptive(
backgroundColor: Colors.white,
)),
) )
: Center( : Center(
child: identityProviders.length == 1 child: identityProviders.length == 1
? Padding( ? Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
onPressed: () => controller onPressed: () => controller
.ssoLoginAction(identityProviders.single.id!), .ssoLoginAction(identityProviders.single.id!),
child: Text(identityProviders.single.name ?? child: Text(identityProviders.single.name ??
@ -175,6 +185,12 @@ class ConnectPageView extends StatelessWidget {
child: Hero( child: Hero(
tag: 'signinButton', tag: 'signinButton',
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer,
),
onPressed: controller.loading ? () {} : controller.login, onPressed: controller.loading ? () {} : controller.login,
child: Text(L10n.of(context)!.login), child: Text(L10n.of(context)!.login),
), ),

View File

@ -52,7 +52,6 @@ class SsoButton extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), ),
], ],

View File

@ -180,12 +180,13 @@ class HomeserverPickerController extends State<HomeserverPicker> {
} }
Future<void> restoreBackup() async { Future<void> restoreBackup() async {
final file =
await FilePickerCross.importFromStorage(fileExtension: '.fluffybackup');
if (file.fileName == null) return;
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
try { try {
final file = await FilePickerCross.importFromStorage(
fileExtension: '.fluffybackup');
final client = Matrix.of(context).getLoginClient(); final client = Matrix.of(context).getLoginClient();
await client.importDump(file.toString()); await client.importDump(file.toString());
Matrix.of(context).initMatrix(); Matrix.of(context).initMatrix();

View File

@ -4,7 +4,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
import 'homeserver_picker.dart'; import 'homeserver_picker.dart';
@ -17,147 +16,157 @@ class HomeserverPickerView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final benchmarkResults = controller.benchmarkResults; final benchmarkResults = controller.benchmarkResults;
return LoginScaffold( return LoginScaffold(
appBar: AppBar( body: Column(
actions: [ children: [
IconButton( // display a prominent banner to import session for TOR browser
onPressed: controller.restoreBackup, // users. This feature is just some UX sugar as TOR users are
tooltip: L10n.of(context)!.hydrate, // usually forced to logout as TOR browser is non-persistent
color: Colors.white, AnimatedContainer(
icon: const Icon(Icons.restore_outlined), height: controller.isTorBrowser ? 64 : 0,
), duration: const Duration(milliseconds: 300),
IconButton( clipBehavior: Clip.hardEdge,
tooltip: L10n.of(context)!.privacy, curve: Curves.bounceInOut,
onPressed: () => launch(AppConfig.privacyUrl), decoration: const BoxDecoration(),
color: Colors.white, child: Material(
icon: const Icon(Icons.shield_outlined),
),
IconButton(
tooltip: L10n.of(context)!.about,
onPressed: () => PlatformInfos.showDialog(context),
color: Colors.white,
icon: const Icon(Icons.info_outlined),
),
],
),
body: SafeArea(
child: Column(
children: [
// display a prominent banner to import session for TOR browser
// users. This feature is just some UX sugar as TOR users are
// usually forced to logout as TOR browser is non-persistent
AnimatedContainer(
height: controller.isTorBrowser ? 64 : 0,
duration: const Duration(milliseconds: 300),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
curve: Curves.bounceInOut, borderRadius:
decoration: const BoxDecoration(), const BorderRadius.vertical(bottom: Radius.circular(8)),
child: Material( color: Theme.of(context).colorScheme.surface,
clipBehavior: Clip.hardEdge, child: ListTile(
borderRadius: leading: const Icon(Icons.vpn_key),
const BorderRadius.vertical(bottom: Radius.circular(8)), title: Text(L10n.of(context)!.hydrateTor),
color: Theme.of(context).colorScheme.surface, subtitle: Text(L10n.of(context)!.hydrateTorLong),
child: ListTile( trailing: const Icon(Icons.chevron_right_outlined),
leading: const Icon(Icons.vpn_key), onTap: controller.restoreBackup,
title: Text(L10n.of(context)!.hydrateTor),
subtitle: Text(L10n.of(context)!.hydrateTorLong),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: controller.restoreBackup,
),
), ),
), ),
Expanded( ),
child: ListView( Expanded(
children: [ child: ListView(
Container( children: [
alignment: Alignment.center, Container(
height: 200, alignment: Alignment.center,
child: Image.asset('assets/info-logo.png'), height: 190,
child: Image.asset('assets/info-logo.png'),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
focusNode: controller.homeserverFocusNode,
controller: controller.homeserverController,
onChanged: controller.onChanged,
decoration: InputDecoration(
prefixText: '${L10n.of(context)!.homeserver}: ',
hintText: L10n.of(context)!.enterYourHomeserver,
suffixIcon: const Icon(Icons.search),
errorText: controller.error,
fillColor: Theme.of(context).backgroundColor,
),
readOnly: !AppConfig.allowOtherHomeservers,
onSubmitted: (_) => controller.checkHomeserverAction(),
autocorrect: false,
), ),
),
if (controller.displayServerList)
Padding( Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: TextField( child: Material(
focusNode: controller.homeserverFocusNode, borderRadius:
controller: controller.homeserverController, BorderRadius.circular(AppConfig.borderRadius),
onChanged: controller.onChanged, color: Theme.of(context).colorScheme.onInverseSurface,
decoration: InputDecoration( clipBehavior: Clip.hardEdge,
prefixText: '${L10n.of(context)!.homeserver}: ', child: benchmarkResults == null
hintText: L10n.of(context)!.enterYourHomeserver, ? const Center(
suffixIcon: const Icon(Icons.search), child: Padding(
errorText: controller.error, padding: EdgeInsets.all(12.0),
fillColor: Theme.of(context) child: CircularProgressIndicator.adaptive(),
.colorScheme ))
.background : Column(
.withOpacity(0.75), children: controller.filteredHomeservers
.map(
(server) => ListTile(
trailing: IconButton(
icon: const Icon(
Icons.info_outlined,
color: Colors.black,
),
onPressed: () =>
controller.showServerInfo(server),
),
onTap: () => controller.setServer(
server.homeserver.baseUrl.host),
title: Text(
server.homeserver.baseUrl.host,
style: const TextStyle(
color: Colors.black),
),
subtitle: Text(
server.homeserver.description ?? '',
style: TextStyle(
color: Colors.grey.shade700),
),
),
)
.toList(),
),
),
)
else
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
AppConfig.applicationWelcomeMessage ??
L10n.of(context)!.welcomeText,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20),
), ),
readOnly: !AppConfig.allowOtherHomeservers,
onSubmitted: (_) => controller.checkHomeserverAction(),
autocorrect: false,
), ),
), ),
if (controller.displayServerList) ],
Padding( ),
padding: const EdgeInsets.all(12.0), ),
child: Material( SafeArea(
borderRadius: child: Container(
BorderRadius.circular(AppConfig.borderRadius), padding: const EdgeInsets.all(12),
color: Colors.white.withAlpha(200), width: double.infinity,
clipBehavior: Clip.hardEdge, child: Column(
child: benchmarkResults == null mainAxisSize: MainAxisSize.min,
? const Center( crossAxisAlignment: CrossAxisAlignment.stretch,
child: Padding( children: [
padding: EdgeInsets.all(12.0), TextButton(
child: CircularProgressIndicator.adaptive(), style: TextButton.styleFrom(
)) foregroundColor:
: Column( Theme.of(context).colorScheme.onSurfaceVariant,
children: controller.filteredHomeservers
.map(
(server) => ListTile(
trailing: IconButton(
icon: const Icon(
Icons.info_outlined,
color: Colors.black,
),
onPressed: () =>
controller.showServerInfo(server),
),
onTap: () => controller.setServer(
server.homeserver.baseUrl.host),
title: Text(
server.homeserver.baseUrl.host,
style: const TextStyle(
color: Colors.black),
),
subtitle: Text(
server.homeserver.description ?? '',
style: TextStyle(
color: Colors.grey.shade700),
),
),
)
.toList(),
),
),
), ),
onPressed: controller.restoreBackup,
child: Text(L10n.of(context)!.hydrate),
),
TextButton(
onPressed: () => launch(AppConfig.privacyUrl),
child: Text(L10n.of(context)!.privacy),
),
Hero(
tag: 'loginButton',
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.isLoading
? null
: controller.checkHomeserverAction,
child: controller.isLoading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.connect),
),
),
], ],
), ),
), ),
Container( ),
padding: const EdgeInsets.all(12), ],
width: double.infinity,
child: Hero(
tag: 'loginButton',
child: ElevatedButton(
onPressed: controller.isLoading
? null
: controller.checkHomeserverAction,
child: controller.isLoading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.connect),
),
),
),
],
),
), ),
); );
} }

View File

@ -15,8 +15,7 @@ class LoginView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LoginScaffold( return LoginScaffold(
appBar: AppBar( appBar: AppBar(
leading: leading: controller.loading ? null : const BackButton(),
controller.loading ? null : const BackButton(color: Colors.white),
automaticallyImplyLeading: !controller.loading, automaticallyImplyLeading: !controller.loading,
centerTitle: true, centerTitle: true,
title: Text( title: Text(
@ -25,7 +24,6 @@ class LoginView extends StatelessWidget {
.homeserver .homeserver
.toString() .toString()
.replaceFirst('https://', '')), .replaceFirst('https://', '')),
style: const TextStyle(color: Colors.white),
), ),
), ),
body: Builder(builder: (context) { body: Builder(builder: (context) {
@ -49,10 +47,7 @@ class LoginView extends StatelessWidget {
errorText: controller.usernameError, errorText: controller.usernameError,
errorStyle: const TextStyle(color: Colors.orange), errorStyle: const TextStyle(color: Colors.orange),
hintText: L10n.of(context)!.emailOrUsername, hintText: L10n.of(context)!.emailOrUsername,
fillColor: Theme.of(context) fillColor: Theme.of(context).colorScheme.background,
.colorScheme
.background
.withOpacity(0.75),
), ),
), ),
), ),
@ -82,10 +77,7 @@ class LoginView extends StatelessWidget {
onPressed: controller.toggleShowPassword, onPressed: controller.toggleShowPassword,
), ),
hintText: L10n.of(context)!.password, hintText: L10n.of(context)!.password,
fillColor: Theme.of(context) fillColor: Theme.of(context).colorScheme.background,
.colorScheme
.background
.withOpacity(0.75),
), ),
), ),
), ),
@ -94,6 +86,10 @@ class LoginView extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.loading onPressed: controller.loading
? null ? null
: () => controller.login(context), : () => controller.login(context),
@ -103,36 +99,41 @@ class LoginView extends StatelessWidget {
), ),
), ),
), ),
Row( Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16.0),
const Expanded( child: Row(
children: [
Expanded(
child: Divider( child: Divider(
color: Colors.white, thickness: 1,
thickness: 1, color: Theme.of(context).textTheme.subtitle1?.color,
)),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.or,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
), ),
), ),
), Padding(
const Expanded( padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.or,
style: const TextStyle(fontSize: 18),
),
),
Expanded(
child: Divider( child: Divider(
color: Colors.white, thickness: 1,
thickness: 1, color: Theme.of(context).textTheme.subtitle1?.color,
)), ),
], ),
],
),
), ),
Padding( Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: ElevatedButton( child: ElevatedButton(
onPressed: onPressed:
controller.loading ? () {} : controller.passwordForgotten, controller.loading ? () {} : controller.passwordForgotten,
style: ElevatedButton.styleFrom(foregroundColor: Colors.red), style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
backgroundColor: Theme.of(context).colorScheme.onError,
),
child: Text(L10n.of(context)!.passwordForgotten), child: Text(L10n.of(context)!.passwordForgotten),
), ),
), ),

View File

@ -13,13 +13,9 @@ class SignupPageView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LoginScaffold( return LoginScaffold(
appBar: AppBar( appBar: AppBar(
leading: leading: controller.loading ? null : const BackButton(),
controller.loading ? null : const BackButton(color: Colors.white),
automaticallyImplyLeading: !controller.loading, automaticallyImplyLeading: !controller.loading,
title: Text( title: Text(L10n.of(context)!.signUp),
L10n.of(context)!.signUp,
style: const TextStyle(color: Colors.white),
),
), ),
body: Form( body: Form(
key: controller.formKey, key: controller.formKey,
@ -50,10 +46,7 @@ class SignupPageView extends StatelessWidget {
), ),
errorStyle: const TextStyle(color: Colors.orange), errorStyle: const TextStyle(color: Colors.orange),
hintText: L10n.of(context)!.chooseAStrongPassword, hintText: L10n.of(context)!.chooseAStrongPassword,
fillColor: Theme.of(context) fillColor: Theme.of(context).colorScheme.background,
.colorScheme
.background
.withOpacity(0.75),
), ),
), ),
), ),
@ -72,10 +65,7 @@ class SignupPageView extends StatelessWidget {
prefixIcon: const Icon(Icons.repeat_outlined), prefixIcon: const Icon(Icons.repeat_outlined),
hintText: L10n.of(context)!.repeatPassword, hintText: L10n.of(context)!.repeatPassword,
errorStyle: const TextStyle(color: Colors.orange), errorStyle: const TextStyle(color: Colors.orange),
fillColor: Theme.of(context) fillColor: Theme.of(context).colorScheme.background,
.colorScheme
.background
.withOpacity(0.75),
), ),
), ),
), ),
@ -93,10 +83,8 @@ class SignupPageView extends StatelessWidget {
prefixIcon: const Icon(Icons.mail_outlined), prefixIcon: const Icon(Icons.mail_outlined),
hintText: L10n.of(context)!.enterAnEmailAddress, hintText: L10n.of(context)!.enterAnEmailAddress,
errorText: controller.error, errorText: controller.error,
fillColor: Theme.of(context) fillColor: Theme.of(context).colorScheme.background,
.colorScheme errorMaxLines: 4,
.background
.withOpacity(0.75),
errorStyle: TextStyle( errorStyle: TextStyle(
color: controller.emailController.text.isEmpty color: controller.emailController.text.isEmpty
? Colors.orangeAccent ? Colors.orangeAccent
@ -110,6 +98,10 @@ class SignupPageView extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
onPressed: controller.loading ? () {} : controller.signup, onPressed: controller.loading ? () {} : controller.signup,
child: controller.loading child: controller.loading
? const LinearProgressIndicator() ? const LinearProgressIndicator()

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
class LoginScaffold extends StatelessWidget { class LoginScaffold extends StatelessWidget {
final Widget body; final Widget body;
@ -13,32 +15,47 @@ class LoginScaffold extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final isMobileMode = !FluffyThemes.isColumnMode(context);
appBar: AppBar( return Container(
automaticallyImplyLeading: appBar?.automaticallyImplyLeading ?? true, decoration: const BoxDecoration(
centerTitle: appBar?.centerTitle, image: DecorationImage(
title: appBar?.title, image: AssetImage('assets/login_wallpaper.png'),
leading: appBar?.leading, fit: BoxFit.cover,
actions: appBar?.actions,
iconTheme: const IconThemeData(color: Colors.white),
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: Colors.transparent,
systemOverlayStyle: SystemUiOverlayStyle.light,
),
extendBodyBehindAppBar: true,
extendBody: true,
body: Container(
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage('assets/login_wallpaper.png'),
),
), ),
alignment: Alignment.center, ),
child: ConstrainedBox( child: Center(
constraints: const BoxConstraints(maxWidth: 480), child: Material(
child: body, color: Theme.of(context).brightness == Brightness.light
? Theme.of(context).scaffoldBackgroundColor.withOpacity(0.9)
: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.75),
borderRadius: isMobileMode
? null
: BorderRadius.circular(AppConfig.borderRadius),
elevation: isMobileMode ? 0 : 10,
clipBehavior: Clip.hardEdge,
shadowColor: Colors.black,
child: ConstrainedBox(
constraints: isMobileMode
? const BoxConstraints()
: const BoxConstraints(maxWidth: 480, maxHeight: 640),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: appBar == null
? null
: AppBar(
automaticallyImplyLeading:
appBar?.automaticallyImplyLeading ?? true,
centerTitle: appBar?.centerTitle,
title: appBar?.title,
leading: appBar?.leading,
actions: appBar?.actions,
backgroundColor: Colors.transparent,
),
extendBodyBehindAppBar: true,
extendBody: true,
body: body,
),
),
), ),
), ),
); );