From ca673787f12b220fe871281ebf0af4f655869b50 Mon Sep 17 00:00:00 2001 From: zaid_daoud Date: Mon, 9 Oct 2023 13:16:59 +0300 Subject: [PATCH] 1 -> New Way of Localizations 2 -> Implementing Login Page 3 -> New way to achieve responsive design --- assets/images/logo.svg | 53 +++++++ assets/translations/ar.json | 9 ++ assets/translations/en.json | 9 ++ .../providers/api/user_provider.dart | 12 +- lib/extensions/context_extension.dart | 10 ++ lib/extensions/int_extensions.dart | 16 ++- lib/main.dart | 8 +- lib/models/enums/translation_keys.dart | 9 ++ lib/models/size_config.dart | 15 ++ .../common_widgets/app_filled_button.dart | 31 +++++ .../common_widgets}/app_lazy_loading.dart | 10 +- .../common_widgets/app_text_form_field.dart | 119 ++++++++++++++++ lib/new_views/pages/login_page.dart | 131 ++++++++++++++++-- lib/new_views/pages/splash_page.dart | 3 + lib/views/pages/login.dart | 1 - lib/views/widgets/app_text_form_field.dart | 1 + pubspec.lock | 8 ++ pubspec.yaml | 4 + 18 files changed, 412 insertions(+), 37 deletions(-) create mode 100644 assets/images/logo.svg create mode 100644 assets/translations/ar.json create mode 100644 assets/translations/en.json create mode 100644 lib/extensions/context_extension.dart create mode 100644 lib/models/enums/translation_keys.dart create mode 100644 lib/models/size_config.dart create mode 100644 lib/new_views/common_widgets/app_filled_button.dart rename lib/{views/widgets/loaders => new_views/common_widgets}/app_lazy_loading.dart (54%) create mode 100644 lib/new_views/common_widgets/app_text_form_field.dart diff --git a/assets/images/logo.svg b/assets/images/logo.svg new file mode 100644 index 00000000..07b0a4ee --- /dev/null +++ b/assets/images/logo.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/translations/ar.json b/assets/translations/ar.json new file mode 100644 index 00000000..00882ed4 --- /dev/null +++ b/assets/translations/ar.json @@ -0,0 +1,9 @@ +{ + "login" : "تسجيل الدخول", + "enterCredsToLogin" : "أدخل بياناتك الخاصة لتسجيل الدخول", + "forgotPassword" : "نسيت كلمة السر", + "password": "كلمة السر", + "username" : "اسم المستخدم", + "requiredField" : "الحقل مطلوب", + "passwordLengthMessage" : "كلمة السر أقل من 6 خانات" +} \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 00000000..9105af01 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,9 @@ +{ + "login" : "Login", + "enterCredsToLogin" : "Enter you credential to login", + "forgotPassword" : "Forgot Password", + "password" : "Password", + "username" : "Username", + "requiredField" : "Required Field", + "passwordLengthMessage" : "Password length is less than 6 characters" +} \ No newline at end of file diff --git a/lib/controllers/providers/api/user_provider.dart b/lib/controllers/providers/api/user_provider.dart index 5f539f05..2a3701ca 100644 --- a/lib/controllers/providers/api/user_provider.dart +++ b/lib/controllers/providers/api/user_provider.dart @@ -39,19 +39,13 @@ class UserProvider extends ChangeNotifier { /// return state code if request complete may be 200, 404 or 403 /// for more details check http state manager /// lib\controllers\http_status_manger\http_status_manger.dart - Future login({ - @required String host, - @required User user, - }) async { + Future login({@required User user}) async { if (_loading == true) return -2; _loading = true; notifyListeners(); Response response; try { - response = await ApiManager.instance.post( - URLs.login, - body: await user.toLoginJson(), - ); + response = await ApiManager.instance.post(URLs.login, body: await user.toLoginJson()); _loading = false; if (response.statusCode >= 200 && response.statusCode < 300) { // client's request was successfully received @@ -63,7 +57,7 @@ class UserProvider extends ChangeNotifier { notifyListeners(); return response.statusCode; } catch (error) { - print(error); + debugPrint(error); _loading = false; notifyListeners(); return -1; diff --git a/lib/extensions/context_extension.dart b/lib/extensions/context_extension.dart new file mode 100644 index 00000000..89e89ab0 --- /dev/null +++ b/lib/extensions/context_extension.dart @@ -0,0 +1,10 @@ +import 'package:flutter/cupertino.dart'; +import 'package:localization/localization.dart'; + +import '../models/enums/translation_keys.dart'; + +extension BuildContextExtension on BuildContext { + String translate(TranslationKeys translationKey) { + return translationKey.name.i18n([Localizations.localeOf(this).toString()]); + } +} diff --git a/lib/extensions/int_extensions.dart b/lib/extensions/int_extensions.dart index d4d867ad..4ca0b964 100644 --- a/lib/extensions/int_extensions.dart +++ b/lib/extensions/int_extensions.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:test_sa/views/app_style/colors.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; + +import '../models/size_config.dart'; extension IntExtensions on int { - Widget get height => SizedBox(height: toDouble()); + Widget get height => SizedBox(height: toScreenHeight); + + Widget get width => SizedBox(width: toScreenWidth); + + Widget get divider => Divider(height: toScreenHeight, thickness: toScreenHeight, color: AppColor.neutral30); - Widget get width => SizedBox(width: toDouble()); + Widget get makeItSquare => SizedBox(width: toScreenWidth, height: toScreenWidth); - Widget get divider => Divider(height: toDouble(), thickness: toDouble(), color: AColors.greyEF); + double get toScreenHeight => (this / 932) * SizeConfig.screenHeight; - Widget get makeItSquare => SizedBox(width: toDouble(), height: toDouble()); + double get toScreenWidth => (this / 430) * SizeConfig.screenWidth; } diff --git a/lib/main.dart b/lib/main.dart index 16bfe7dc..7435cbc6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:localization/localization.dart'; import 'package:provider/provider.dart'; -import 'package:test_sa/controllers/localization/localization.dart'; import 'package:test_sa/new_views/app_style/app_themes.dart'; import 'package:test_sa/new_views/pages/login_page.dart'; import 'package:test_sa/new_views/pages/splash_page.dart'; @@ -35,6 +35,7 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { + LocalJsonLocalization.delegate.directories = ['assets/translations']; final settingProvider = Provider.of(context); return MultiProvider( providers: [ @@ -51,9 +52,8 @@ class MyApp extends StatelessWidget { title: 'ATOMS', debugShowCheckedModeBanner: false, theme: settingProvider.theme ?? AppThemes.lightTheme, - localizationsDelegates: const [ - // ... app-specific localization delegate[s] here - AppLocalization.delegate, + localizationsDelegates: [ + LocalJsonLocalization.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, diff --git a/lib/models/enums/translation_keys.dart b/lib/models/enums/translation_keys.dart new file mode 100644 index 00000000..958ac1fa --- /dev/null +++ b/lib/models/enums/translation_keys.dart @@ -0,0 +1,9 @@ +enum TranslationKeys { + login, + enterCredsToLogin, + forgotPassword, + password, + username, + requiredField, + passwordLengthMessage, +} diff --git a/lib/models/size_config.dart b/lib/models/size_config.dart new file mode 100644 index 00000000..53ed7381 --- /dev/null +++ b/lib/models/size_config.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +abstract class SizeConfig { + static MediaQueryData _mediaQueryData; + static double screenWidth; + static double screenHeight; + static double defaultSize; + + /// Call this method to save the height and width of the available layout + static void init(BuildContext context) { + _mediaQueryData = MediaQuery.of(context); + screenWidth = _mediaQueryData.size.width; + screenHeight = _mediaQueryData.size.height; + } +} diff --git a/lib/new_views/common_widgets/app_filled_button.dart b/lib/new_views/common_widgets/app_filled_button.dart new file mode 100644 index 00000000..cc6df970 --- /dev/null +++ b/lib/new_views/common_widgets/app_filled_button.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/models/enums/translation_keys.dart'; + +class AppFilledButton extends StatelessWidget { + final VoidCallback onPressed; + final TranslationKeys label; + const AppFilledButton({ + @required this.onPressed, + @required this.label, + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + onPressed: onPressed, + child: Text(context.translate(label)), + ), + ); + } +} diff --git a/lib/views/widgets/loaders/app_lazy_loading.dart b/lib/new_views/common_widgets/app_lazy_loading.dart similarity index 54% rename from lib/views/widgets/loaders/app_lazy_loading.dart rename to lib/new_views/common_widgets/app_lazy_loading.dart index a52417e5..f54f4170 100644 --- a/lib/views/widgets/loaders/app_lazy_loading.dart +++ b/lib/new_views/common_widgets/app_lazy_loading.dart @@ -1,18 +1,20 @@ import 'package:flutter/material.dart'; import 'package:test_sa/views/app_style/sizing.dart'; -import 'app_loading.dart'; +import '../../views/widgets/loaders/app_loading.dart'; class ALazyLoading extends StatelessWidget { + const ALazyLoading({Key key}) : super(key: key); + @override Widget build(BuildContext context) { return Center( child: Container( height: 36 * AppStyle.getScaleFactor(context), width: 36 * AppStyle.getScaleFactor(context), - padding: EdgeInsets.all(8), - decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [AppStyle.boxShadow]), - child: ALoading(), + padding: const EdgeInsets.all(8), + decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [AppStyle.boxShadow]), + child: const ALoading(), ), ); } diff --git a/lib/new_views/common_widgets/app_text_form_field.dart b/lib/new_views/common_widgets/app_text_form_field.dart new file mode 100644 index 00000000..450b47e9 --- /dev/null +++ b/lib/new_views/common_widgets/app_text_form_field.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/models/enums/translation_keys.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/views/app_style/sizing.dart'; + +class AppTextFormField extends StatefulWidget { + final Function(String) onSaved; + final Function(String) validator; + final Function(String) onChange; + final bool obscureText; + final VoidCallback showPassword; + final TranslationKeys hintText; + final TranslationKeys labelText; + final TextInputType textInputType; + final String initialValue; + final TextStyle style; + final bool enable; + final TextAlign textAlign; + final FocusNode node; + final Widget suffixIcon; + final IconData prefixIconData; + final double prefixIconSize; + final TextEditingController controller; + final TextInputAction textInputAction; + final VoidCallback onAction; + + const AppTextFormField({ + Key key, + this.onSaved, + this.validator, + this.node, + this.onChange, + this.obscureText, + this.showPassword, + this.hintText, + this.labelText, + this.textInputType = TextInputType.text, + this.initialValue, + this.enable = true, + this.style, + this.textAlign, + this.suffixIcon, + this.prefixIconData, + this.prefixIconSize, + this.controller, + this.textInputAction, + this.onAction, + }) : super(key: key); + + @override + State createState() => _AppTextFormFieldState(); +} + +class _AppTextFormFieldState extends State { + @override + void initState() { + if (widget.controller != null) widget.controller.text = widget.initialValue; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.topCenter, + children: [ + Container( + height: widget.textInputType == TextInputType.multiline ? null : 56.toScreenHeight, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)], + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth), + child: TextFormField( + focusNode: widget.node, + enabled: widget.enable, + onSaved: widget.onSaved, + initialValue: widget.controller != null ? null : widget.initialValue, + validator: widget.validator, + onChanged: widget.onChange, + textAlign: TextAlign.left, + obscureText: widget.obscureText ?? false, + keyboardType: widget.textInputType, + maxLines: widget.textInputType == TextInputType.multiline ? null : 1, + obscuringCharacter: "*", + controller: widget.controller, + textInputAction: widget.textInputType == TextInputType.multiline ? null : widget.textInputAction ?? TextInputAction.next, + onEditingComplete: widget.onAction ?? () => FocusScope.of(context).nextFocus(), + style: Theme.of(context).textTheme.bodyLarge, + decoration: InputDecoration( + border: InputBorder.none, + suffixIconConstraints: const BoxConstraints(minWidth: 0), + disabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + constraints: const BoxConstraints(), + errorStyle: Theme.of(context).textTheme.bodySmall?.copyWith(color: AppColor.red60, height: 0.7), + floatingLabelStyle: Theme.of(context).textTheme.labelLarge?.copyWith(color: AppColor.neutral20, fontWeight: FontWeight.w500), + hintText: widget.hintText != null ? context.translate(widget.hintText) : null, + labelText: widget.labelText != null ? context.translate(widget.labelText) : null, + suffixIcon: widget.prefixIconData == null + ? null + : Icon( + widget.prefixIconData, + size: widget.prefixIconSize == null ? 20 * AppStyle.getScaleFactor(context) : (widget.prefixIconSize - 10) * AppStyle.getScaleFactor(context), + color: const Color(0xff2e303a), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/new_views/pages/login_page.dart b/lib/new_views/pages/login_page.dart index 4a9ae634..fda789c7 100644 --- a/lib/new_views/pages/login_page.dart +++ b/lib/new_views/pages/login_page.dart @@ -1,28 +1,131 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/models/enums/translation_keys.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; -class LoginPage extends StatelessWidget { +import '../../controllers/providers/api/user_provider.dart'; +import '../../controllers/providers/settings/setting_provider.dart'; +import '../../controllers/validator/validator.dart'; +import '../../models/user.dart'; +import '../common_widgets/app_filled_button.dart'; +import '../common_widgets/app_text_form_field.dart'; + +class LoginPage extends StatefulWidget { static const String routeName = "/login_page"; const LoginPage({Key key}) : super(key: key); + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final User _user = User(); + UserProvider _userProvider; + SettingProvider _settingProvider; + final GlobalKey _formKey = GlobalKey(); + @override Widget build(BuildContext context) { - return Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset("assets/images/logo.png", height: 60), - Text( - "Login", - style: Theme.of(context).textTheme.displayMedium?.copyWith(color: AppColor.neutral50), + _userProvider = Provider.of(context); + _settingProvider = Provider.of(context); + + return Form( + key: _formKey, + child: Scaffold( + body: Padding( + padding: EdgeInsets.only( + right: 16.toScreenWidth, + left: 16.toScreenWidth, + bottom: 150.toScreenHeight, + top: MediaQuery.of(context).viewPadding.top, ), - Text( - "Enter you credential to login", - style: Theme.of(context).textTheme.titleLarge?.copyWith(color: AppColor.neutral20), + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: "logo", + child: SvgPicture.asset("assets/images/logo.svg", height: 64.toScreenHeight), + ), + 64.height, + Text( + context.translate(TranslationKeys.login), + style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: AppColor.neutral50, fontWeight: FontWeight.w600), + ), + Text( + context.translate(TranslationKeys.enterCredsToLogin), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: AppColor.neutral20, fontWeight: FontWeight.w500), + ), + 32.height, + AppTextFormField( + initialValue: _user?.userName, + validator: (value) => Validator.hasValue(value) ? null : context.translate(TranslationKeys.requiredField), + labelText: TranslationKeys.username, + textInputType: TextInputType.name, + onSaved: (value) { + _user.userName = value; + }, + ), + 16.height, + AppTextFormField( + initialValue: _user?.password, + labelText: TranslationKeys.password, + obscureText: true, + validator: (value) => Validator.isValidPassword(value) + ? null + : value.isEmpty + ? context.translate(TranslationKeys.requiredField) + : context.translate(TranslationKeys.passwordLengthMessage), + onSaved: (value) { + _user.password = value; + }, + ), + 16.height, + Align( + alignment: AlignmentDirectional.centerEnd, + child: InkWell( + onTap: () { + /// TODO [zaid] : push to another screen + }, + child: Text( + "${context.translate(TranslationKeys.forgotPassword)}?", + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: AppColor.primary50, fontWeight: FontWeight.w500), + ), + ), + ), + ], + ), + ), ), - ], + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + floatingActionButton: SizedBox( + width: double.infinity, + child: AppFilledButton(label: TranslationKeys.login, onPressed: _login), + ), ), ); } + + Future _login() async { + if (!_formKey.currentState.validate()) return; + _formKey.currentState.save(); + showDialog(context: context, barrierDismissible: false, builder: (context) => const ALazyLoading()); + int status = await _userProvider.login(user: _user); + Navigator.pop(context); + if (status >= 200 && status < 300 && _userProvider.user.isAuthenticated ?? false) { + _settingProvider.setUser(_userProvider.user); + + /// TODO [zaid] : push to home page + } else { + Fluttertoast.showToast(msg: _userProvider.user.message); + } + } } diff --git a/lib/new_views/pages/splash_page.dart b/lib/new_views/pages/splash_page.dart index c6a976e4..1ca03e96 100644 --- a/lib/new_views/pages/splash_page.dart +++ b/lib/new_views/pages/splash_page.dart @@ -10,6 +10,8 @@ import 'package:test_sa/controllers/providers/settings/setting_provider.dart'; import 'package:test_sa/models/app_notification.dart'; import 'package:test_sa/new_views/pages/login_page.dart'; +import '../../models/size_config.dart'; + class SplashPage extends StatefulWidget { static const String routeName = '/splash_page'; const SplashPage({Key key}) : super(key: key); @@ -35,6 +37,7 @@ class _SplashPageState extends State { @override Widget build(BuildContext context) { + SizeConfig.init(context); _settingProvider = Provider.of(context, listen: false); _userProvider = Provider.of(context, listen: false); return Scaffold( diff --git a/lib/views/pages/login.dart b/lib/views/pages/login.dart index a6fba23d..cf4bf1df 100644 --- a/lib/views/pages/login.dart +++ b/lib/views/pages/login.dart @@ -112,7 +112,6 @@ class _LoginState extends State { _formKey.currentState.save(); int status = await _userProvider.login( user: _user, - host: _settingProvider.host, ); if (status >= 200 && status < 300) { if (_userProvider.user.isAuthenticated ?? false) { diff --git a/lib/views/widgets/app_text_form_field.dart b/lib/views/widgets/app_text_form_field.dart index 24ea9922..2b8f19f0 100644 --- a/lib/views/widgets/app_text_form_field.dart +++ b/lib/views/widgets/app_text_form_field.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:test_sa/views/app_style/sizing.dart'; +@Deprecated("Use the one inside the [new_views/common_widgets] folder") class ATextFormField extends StatefulWidget { final Function(String) onSaved; final Function(String) validator; diff --git a/pubspec.lock b/pubspec.lock index 87291639..b3a50550 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -645,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + localization: + dependency: "direct main" + description: + name: localization + sha256: "01d892155364dc456e1141dd8003e43c98d457f2b51fe1b8984766bad2a3fd72" + url: "https://pub.dev" + source: hosted + version: "2.1.0" logger: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 031a8b0d..33ea18a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,8 @@ version: 1.0.4+3 environment: sdk: ">=2.7.0 <3.0.0" +localization_dir: assets\translations + # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, @@ -65,6 +67,7 @@ dependencies: record_mp3: ^2.1.0 path_provider: ^2.1.0 open_file: ^3.3.2 + localization: ^2.1.0 dev_dependencies: flutter_test: @@ -95,6 +98,7 @@ flutter: - assets/images/ - assets/subtitles/ - assets/rives/ + - assets/translations/ fonts: - family: Swiss fonts: