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: