TF-518 Apply new UI for login in mobile
This commit is contained in:
committed by
Dat H. Pham
parent
0932cf0185
commit
cc66d141d4
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12C10.5304 12 11.0391 11.7893 11.4142 11.4142C11.7893 11.0391 12 10.5304 12 10C12 9.46957 11.7893 8.96086 11.4142 8.58579C11.0391 8.21071 10.5304 8 10 8C9.46957 8 8.96086 8.21071 8.58579 8.58579C8.21071 8.96086 8 9.46957 8 10C8 10.5304 8.21071 11.0391 8.58579 11.4142C8.96086 11.7893 9.46957 12 10 12Z" fill="#818C99"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.458008 10C1.73201 5.943 5.52201 3 10 3C14.478 3 18.268 5.943 19.542 10C18.268 14.057 14.478 17 10 17C5.52201 17 1.73201 14.057 0.458008 10ZM14 10C14 11.0609 13.5786 12.0783 12.8284 12.8284C12.0783 13.5786 11.0609 14 10 14C8.93914 14 7.92173 13.5786 7.17158 12.8284C6.42143 12.0783 6.00001 11.0609 6.00001 10C6.00001 8.93913 6.42143 7.92172 7.17158 7.17157C7.92173 6.42143 8.93914 6 10 6C11.0609 6 12.0783 6.42143 12.8284 7.17157C13.5786 7.92172 14 8.93913 14 10Z" fill="#818C99"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 969 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.70741 2.29292C3.5188 2.11076 3.2662 2.00997 3.00401 2.01224C2.74181 2.01452 2.491 2.11969 2.30559 2.3051C2.12018 2.49051 2.01501 2.74132 2.01273 3.00352C2.01045 3.26571 2.11125 3.51832 2.29341 3.70692L16.2934 17.7069C16.482 17.8891 16.7346 17.9899 16.9968 17.9876C17.259 17.9853 17.5098 17.8801 17.6952 17.6947C17.8806 17.5093 17.9858 17.2585 17.9881 16.9963C17.9904 16.7341 17.8896 16.4815 17.7074 16.2929L16.2344 14.8199C17.7919 13.578 18.9437 11.8998 19.5424 9.99992C18.2684 5.94292 14.4784 2.99992 10.0004 2.99992C8.43284 2.99779 6.88691 3.36577 5.48841 4.07392L3.70841 2.29292H3.70741ZM7.96841 6.55292L9.48241 8.06792C9.8215 7.97787 10.1783 7.97847 10.5171 8.06965C10.8559 8.16083 11.1648 8.3394 11.4128 8.58748C11.6609 8.83557 11.8395 9.14446 11.9307 9.48325C12.0219 9.82204 12.0225 10.1788 11.9324 10.5179L13.4464 12.0319C13.8974 11.2679 14.0816 10.3757 13.9701 9.49559C13.8585 8.61548 13.4575 7.79741 12.8302 7.1701C12.2029 6.54278 11.3848 6.14181 10.5047 6.03027C9.62461 5.91872 8.73239 6.10293 7.96841 6.55392V6.55292Z" fill="#818C99"/>
|
||||
<path d="M12.454 16.6969L9.75001 13.9919C8.77769 13.931 7.86103 13.5173 7.17206 12.8285C6.4831 12.1397 6.06918 11.2232 6.00801 10.2509L2.33501 6.57788C1.49022 7.5839 0.852357 8.7468 0.458008 9.99988C1.73201 14.0569 5.52301 16.9999 10 16.9999C10.847 16.9999 11.669 16.8949 12.454 16.6969Z" fill="#818C99"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -10,9 +10,15 @@ extension AppColor on Color {
|
||||
static const textFieldTextColor = Color(0xFF7E869B);
|
||||
static const textFieldLabelColor = Color(0xFF7E869B);
|
||||
static const textFieldHintColor = Color(0xFF757575);
|
||||
static const textFieldBorderColor = Color(0xfff2f1fd);
|
||||
static const textFieldFocusedBorderColor = Color(0xFF837DFF);
|
||||
static const textFieldErrorBorderColor = Color(0xffFF5858);
|
||||
static const textFieldBorderColor = Color(0xfff2f3f5);
|
||||
static const textFieldFocusedBorderColor = Color(0xFF007AFF);
|
||||
static const loginTextFieldBorderColor = Color(0xFFF2F3F5);
|
||||
static const textFieldErrorBorderColor = Color(0xffE64646);
|
||||
static const loginTextFieldErrorBorder = Color(0xffE64646);
|
||||
static const loginTextFieldFocusedBorder = Color(0xFF007AFF);
|
||||
static const loginTextFieldHintColor = Color(0xff818C99);
|
||||
static const loginTextFieldBackgroundColor = Color(0xFFF2F3F5);
|
||||
static const loginTextFieldBackgroundErrorColor = Color(0xFFFAEBEB);
|
||||
static const buttonColor = Color(0xFF837DFF);
|
||||
static const appColor = Color(0xFF3840F7);
|
||||
static const nameUserColor = Color(0xFF182952);
|
||||
|
||||
@@ -104,6 +104,8 @@ class ImagePaths {
|
||||
String get icDeleteDialogIdentity => _getImagePath('ic_delete_dialog_identity.svg');
|
||||
String get icDeleteDialogFailed => _getImagePath('ic_delete_dialog_failed.svg');
|
||||
String get icEdit => _getImagePath('ic_edit.svg');
|
||||
String get icEye => _getImagePath('ic_eye.svg');
|
||||
String get icEyeOff => _getImagePath('ic_eye_off.svg');
|
||||
|
||||
String _getImagePath(String imageName) {
|
||||
return AssetsPaths.images + imageName;
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:get/get.dart';
|
||||
|
||||
class ResponsiveUtils {
|
||||
|
||||
final int heightShortest = 600;
|
||||
|
||||
final int minDesktopWidth = 1200;
|
||||
final int minTabletWidth = 600;
|
||||
final int minTabletLargeWidth = 900;
|
||||
@@ -51,4 +53,8 @@ class ResponsiveUtils {
|
||||
isMobile(context) ? _loginTextFieldWidthSmallScreen : _loginTextFieldWidthLargeScreen;
|
||||
|
||||
double getWidthLoginButton() => _loginButtonWidth;
|
||||
|
||||
bool isHeightShortest(BuildContext context) {
|
||||
return MediaQuery.of(context).size.shortestSide < 600;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
abstract class InputDecorationBuilder {
|
||||
String? prefixText;
|
||||
TextStyle? prefixStyle;
|
||||
String? labelText;
|
||||
TextStyle? labelStyle;
|
||||
String? hintText;
|
||||
@@ -9,38 +10,43 @@ abstract class InputDecorationBuilder {
|
||||
EdgeInsets? contentPadding;
|
||||
OutlineInputBorder? enabledBorder;
|
||||
OutlineInputBorder? errorBorder;
|
||||
OutlineInputBorder? focusBorder;
|
||||
String? errorText;
|
||||
TextStyle? errorTextStyle;
|
||||
|
||||
void setPrefixText(String newPrefixText) {
|
||||
void setPrefixText(String? newPrefixText) {
|
||||
prefixText = newPrefixText;
|
||||
}
|
||||
|
||||
void setLabelText(String newLabelText) {
|
||||
void setPrefixStyle(TextStyle? newPrefixStyle) {
|
||||
prefixStyle = newPrefixStyle;
|
||||
}
|
||||
|
||||
void setLabelText(String? newLabelText) {
|
||||
labelText = newLabelText;
|
||||
}
|
||||
|
||||
void setLabelStyle(TextStyle newLabelStyle) {
|
||||
void setLabelStyle(TextStyle? newLabelStyle) {
|
||||
labelStyle = newLabelStyle;
|
||||
}
|
||||
|
||||
void setHintText(String newHintText) {
|
||||
void setHintText(String? newHintText) {
|
||||
hintText = newHintText;
|
||||
}
|
||||
|
||||
void setHintStyle(TextStyle newHintStyle) {
|
||||
void setHintStyle(TextStyle? newHintStyle) {
|
||||
hintStyle = newHintStyle;
|
||||
}
|
||||
|
||||
void setContentPadding(EdgeInsets newContentPadding) {
|
||||
void setContentPadding(EdgeInsets? newContentPadding) {
|
||||
contentPadding = newContentPadding;
|
||||
}
|
||||
|
||||
void setEnabledBorder(OutlineInputBorder newEnabledBorder) {
|
||||
void setEnabledBorder(OutlineInputBorder? newEnabledBorder) {
|
||||
enabledBorder = newEnabledBorder;
|
||||
}
|
||||
|
||||
void setErrorBorder(OutlineInputBorder newErrorBorder) {
|
||||
void setErrorBorder(OutlineInputBorder? newErrorBorder) {
|
||||
errorBorder = newErrorBorder;
|
||||
}
|
||||
|
||||
@@ -48,10 +54,14 @@ abstract class InputDecorationBuilder {
|
||||
errorText = newText;
|
||||
}
|
||||
|
||||
void setErrorTextStyle(TextStyle newStyle) {
|
||||
void setErrorTextStyle(TextStyle? newStyle) {
|
||||
errorTextStyle = newStyle;
|
||||
}
|
||||
|
||||
void setFocusBorder(OutlineInputBorder focusBorder) {
|
||||
focusBorder = focusBorder;
|
||||
}
|
||||
|
||||
InputDecoration build() {
|
||||
return InputDecoration(
|
||||
prefixText: prefixText,
|
||||
@@ -63,6 +73,8 @@ abstract class InputDecorationBuilder {
|
||||
errorBorder: errorBorder,
|
||||
errorText: errorText,
|
||||
errorStyle: errorTextStyle,
|
||||
enabledBorder: enabledBorder);
|
||||
enabledBorder: enabledBorder,
|
||||
focusedBorder: focusBorder
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:get/get.dart';
|
||||
import 'package:model/model.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart';
|
||||
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
@@ -26,6 +27,7 @@ class LoginController extends GetxController {
|
||||
);
|
||||
|
||||
var loginState = LoginState(Right(LoginInitAction())).obs;
|
||||
final loginFormType = LoginFormType.baseUrlForm.obs;
|
||||
|
||||
String? _urlText;
|
||||
String? _userNameText;
|
||||
@@ -43,6 +45,20 @@ class LoginController extends GetxController {
|
||||
|
||||
Password? _parsePassword(String? password) => password != null ? Password(password.trim()) : null;
|
||||
|
||||
void handleNextInUrlInputFormPress() {
|
||||
completeInputUrlAction();
|
||||
}
|
||||
|
||||
void handleBackInCredentialForm() {
|
||||
loginState.value = LoginState(Right(LoginInitAction()));
|
||||
loginFormType.value = LoginFormType.baseUrlForm;
|
||||
}
|
||||
|
||||
void completeInputUrlAction() {
|
||||
loginState.value = LoginState(Right(InputUrlCompletion()));
|
||||
loginFormType.value = LoginFormType.credentialForm;
|
||||
}
|
||||
|
||||
void handleLoginPressed() {
|
||||
final baseUri = kIsWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText);
|
||||
final userName = _parseUserName(_userNameText);
|
||||
@@ -82,7 +98,7 @@ class LoginController extends GetxController {
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
urlInputController.dispose();
|
||||
urlInputController.clear();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
enum LoginFormType {
|
||||
none, baseUrlForm, credentialForm
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/widgets/login_input_decoration_builder.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/widgets/login_text_input_builder.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
|
||||
class LoginView extends GetWidget<LoginController> {
|
||||
|
||||
final loginController = Get.find<LoginController>();
|
||||
final imagePaths = Get.find<ImagePaths>();
|
||||
final responsiveUtils = Get.find<ResponsiveUtils>();
|
||||
final _responsiveUtils = Get.find<ResponsiveUtils>();
|
||||
final keyboardUtils = Get.find<KeyboardUtils>();
|
||||
|
||||
LoginView({Key? key}) : super(key: key);
|
||||
@@ -20,97 +22,161 @@ class LoginView extends GetWidget<LoginController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
resizeToAvoidBottomInset: true,
|
||||
backgroundColor: AppColor.primaryLightColor,
|
||||
body: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
reverse: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.only(top: 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildSlogan(context),
|
||||
Obx(() => _buildLoginMessage(context, loginController.loginState.value)),
|
||||
if (!kIsWeb) _buildUrlInput(context),
|
||||
_buildUserNameInput(context),
|
||||
_buildPasswordInput(context),
|
||||
Obx(() => loginController.loginState.value.viewState.fold(
|
||||
(failure) => _buildLoginButton(context),
|
||||
(success) => success is LoginLoadingAction ? _buildLoadingCircularProgress() : _buildLoginButton(context))),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: _supportScrollForm(context)
|
||||
? Stack(
|
||||
children: [
|
||||
Center(child: SingleChildScrollView(child: _buildCenterForm(context), scrollDirection: Axis.vertical)),
|
||||
Obx(() {
|
||||
return loginController.loginFormType.value == LoginFormType.credentialForm
|
||||
? _buildBackButton(context)
|
||||
: const SizedBox.shrink();
|
||||
})
|
||||
]
|
||||
)
|
||||
: Stack(
|
||||
children: [
|
||||
_buildCenterForm(context),
|
||||
Obx(() {
|
||||
return loginController.loginFormType.value == LoginFormType.credentialForm
|
||||
? _buildBackButton(context)
|
||||
: const SizedBox.shrink();
|
||||
})
|
||||
]
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildCenterForm(BuildContext context) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: _supportScrollForm(context)
|
||||
? const BoxConstraints(minWidth: 240, maxWidth: 400)
|
||||
: const BoxConstraints(minWidth: double.infinity),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: _responsiveUtils.isHeightShortest(context) ? 64 : 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).login,
|
||||
style: const TextStyle(fontSize: 32, color: AppColor.colorNameEmail, fontWeight: FontWeight.w900)
|
||||
)
|
||||
),
|
||||
),
|
||||
Obx(() => _buildLoginMessage(context, loginController.loginState.value)),
|
||||
Obx(() {
|
||||
switch (controller.loginFormType.value) {
|
||||
case LoginFormType.baseUrlForm:
|
||||
return _buildUrlInput(context);
|
||||
case LoginFormType.credentialForm:
|
||||
return _buildInputCredentialForm(context);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
Obx(() {
|
||||
switch (controller.loginFormType.value) {
|
||||
case LoginFormType.baseUrlForm:
|
||||
return _supportScrollForm(context)
|
||||
? _buildNextButton(context)
|
||||
: _buildExpandedButton(context, _buildNextButton(context));
|
||||
case LoginFormType.credentialForm:
|
||||
return Obx(() => loginController.loginState.value.viewState.fold(
|
||||
(failure) => _buildLoginButtonInContext(context),
|
||||
(success) => success is LoginLoadingAction
|
||||
? _buildLoadingCircularProgress()
|
||||
: _buildLoginButtonInContext(context)));
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
})
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlogan(BuildContext context) {
|
||||
return (SloganBuilder()
|
||||
..setSloganText(AppLocalizations.of(context).login_text_slogan)
|
||||
..setSloganTextAlign(TextAlign.center)
|
||||
..setSloganTextStyle(const TextStyle(color: AppColor.primaryColor, fontSize: 16, fontWeight: FontWeight.w700))
|
||||
..setLogo(imagePaths.icLogoTMail))
|
||||
.build();
|
||||
Widget _buildBackButton(BuildContext context) {
|
||||
return Positioned(
|
||||
left: 12,
|
||||
top: 12,
|
||||
child: IconButton(
|
||||
key: const Key('login_arrow_back_button'),
|
||||
onPressed: () => controller.handleBackInCredentialForm(),
|
||||
icon: SvgPicture.asset(
|
||||
imagePaths.icBack,
|
||||
alignment: Alignment.center,
|
||||
color: AppColor.primaryColor
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoginMessage(BuildContext context, LoginState loginState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24, top: 60),
|
||||
padding: const EdgeInsets.only(top: 11, bottom: 36, left: 58, right: 58),
|
||||
child: SizedBox(
|
||||
width: responsiveUtils.getWidthLoginTextField(context),
|
||||
width: _responsiveUtils.getWidthLoginTextField(context),
|
||||
child: CenterTextBuilder()
|
||||
.key(const Key('login_message'))
|
||||
.text(loginState.viewState.fold(
|
||||
(failure) => failure is AuthenticationUserFailure
|
||||
? AppLocalizations.of(context).unknown_error_login_message
|
||||
: AppLocalizations.of(context).login_text_login_to_continue,
|
||||
(success) => AppLocalizations.of(context).login_text_login_to_continue))
|
||||
: AppLocalizations.of(context).loginInputUrlMessage,
|
||||
(success) => AppLocalizations.of(context).loginInputUrlMessage))
|
||||
.textStyle(TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: loginState.viewState.fold(
|
||||
(failure) => failure is AuthenticationUserFailure
|
||||
? AppColor.textFieldErrorBorderColor
|
||||
: AppColor.appColor,
|
||||
(success) => AppColor.appColor)))
|
||||
: AppColor.colorNameEmail,
|
||||
(success) => AppColor.colorNameEmail)))
|
||||
.build()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputCredentialForm(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildUserNameInput(context),
|
||||
_buildPasswordInput(context)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUrlInput(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: SizedBox(
|
||||
width: responsiveUtils.getWidthLoginTextField(context),
|
||||
padding: const EdgeInsets.only(right: 24, left: 24, bottom: 24),
|
||||
child: Container(
|
||||
child: (TextFieldBuilder()
|
||||
..key(const Key('login_url_input'))
|
||||
..onChange((value) => loginController.formatUrl(value))
|
||||
..textInputAction(TextInputAction.next)
|
||||
..addController(loginController.urlInputController)
|
||||
..keyboardType(TextInputType.url)
|
||||
..onSubmitted((value) {
|
||||
controller.handleNextInUrlInputFormPress();
|
||||
})
|
||||
..textDecoration((LoginInputDecorationBuilder()
|
||||
..setLabelText(AppLocalizations.of(context).prefix_https)
|
||||
..setPrefixText(AppLocalizations.of(context).prefix_https))
|
||||
.build()))
|
||||
..setLabelText(AppLocalizations.of(context).prefix_https)
|
||||
..setPrefixText(AppLocalizations.of(context).prefix_https))
|
||||
.build()))
|
||||
.build()));
|
||||
}
|
||||
|
||||
Widget _buildUserNameInput(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: SizedBox(
|
||||
width: responsiveUtils.getWidthLoginTextField(context),
|
||||
padding: const EdgeInsets.only(bottom: 16, right: 24, left: 24),
|
||||
child: Container(
|
||||
child: (TextFieldBuilder()
|
||||
..key(const Key('login_username_input'))
|
||||
..onChange((value) => loginController.setUserNameText(value))
|
||||
@@ -125,40 +191,78 @@ class LoginView extends GetWidget<LoginController> {
|
||||
|
||||
Widget _buildPasswordInput(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: SizedBox(
|
||||
width: responsiveUtils.getWidthLoginTextField(context),
|
||||
child: (TextFieldBuilder()
|
||||
padding: const EdgeInsets.only(bottom: 40, right: 24, left: 24),
|
||||
child: Container(
|
||||
child: (LoginTextInputBuilder(context, imagePaths)
|
||||
..setOnSubmitted((value) => loginController.handleLoginPressed())
|
||||
..passwordInput(true)
|
||||
..key(const Key('login_password_input'))
|
||||
..obscureText(true)
|
||||
..onChange((value) => loginController.setPasswordText(value))
|
||||
..textInputAction(TextInputAction.done)
|
||||
..textDecoration((LoginInputDecorationBuilder()
|
||||
..setLabelText(AppLocalizations.of(context).password)
|
||||
..setHintText(AppLocalizations.of(context).password))
|
||||
.build()))
|
||||
..hintText(AppLocalizations.of(context).password))
|
||||
.build()));
|
||||
}
|
||||
|
||||
Widget _buildExpandedButton(BuildContext context, Widget child) {
|
||||
return Expanded(
|
||||
child: Align(
|
||||
alignment: FractionalOffset.bottomCenter,
|
||||
child: child
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNextButton(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24),
|
||||
width: _responsiveUtils.getDeviceWidth(context),height: 48,
|
||||
child: ElevatedButton(
|
||||
key: const Key('nextToCredentialForm'),
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) => Colors.white),
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) => AppColor.primaryColor),
|
||||
shape: MaterialStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(width: 0, color: AppColor.primaryColor)
|
||||
))
|
||||
),
|
||||
child: Text(AppLocalizations.of(context).next,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white)
|
||||
),
|
||||
onPressed: () {
|
||||
loginController.handleNextInUrlInputFormPress();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoginButtonInContext(BuildContext context) {
|
||||
return _supportScrollForm(context)
|
||||
? _buildLoginButton(context)
|
||||
: _buildExpandedButton(context, _buildLoginButton(context));
|
||||
}
|
||||
|
||||
Widget _buildLoginButton(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: SizedBox(
|
||||
key: const Key('login_confirm_button'),
|
||||
width: responsiveUtils.getWidthLoginButton(),
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) => Colors.white),
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) => AppColor.buttonColor),
|
||||
shape: MaterialStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
side: const BorderSide(width: 0, color: AppColor.buttonColor)))),
|
||||
child: Text(AppLocalizations.of(context).login, style: const TextStyle(fontSize: 16, color: Colors.white)),
|
||||
onPressed: () {
|
||||
keyboardUtils.hideKeyboard(context);
|
||||
loginController.handleLoginPressed();
|
||||
}),
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24),
|
||||
width: _responsiveUtils.getDeviceWidth(context),height: 48,
|
||||
child: ElevatedButton(
|
||||
key: const Key('loginSubmitForm'),
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) => Colors.white),
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) => AppColor.primaryColor),
|
||||
shape: MaterialStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(width: 0, color: AppColor.primaryColor)
|
||||
))
|
||||
),
|
||||
child: Text(AppLocalizations.of(context).login,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white)
|
||||
),
|
||||
onPressed: () {
|
||||
loginController.handleLoginPressed();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -170,4 +274,8 @@ class LoginView extends GetWidget<LoginController> {
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(color: AppColor.primaryColor));
|
||||
}
|
||||
|
||||
bool _supportScrollForm(BuildContext context) {
|
||||
return !(responsiveUtils.isMobile(context) && responsiveUtils.isPortrait(context));
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,12 @@ class LoginState extends AppState {
|
||||
LoginState(Either<Failure, Success> viewState) : super(viewState);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class InputUrlCompletion extends ViewState {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
@immutable
|
||||
class LoginLoadingAction extends ViewState {
|
||||
@override
|
||||
|
||||
@@ -8,13 +8,13 @@ class LoginInputDecorationBuilder extends InputDecorationBuilder {
|
||||
InputDecoration build() {
|
||||
return InputDecoration(
|
||||
enabledBorder: enabledBorder ?? const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(29)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
borderSide: BorderSide(width: 1, color: AppColor.textFieldBorderColor)),
|
||||
focusedBorder: enabledBorder ?? const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(29)),
|
||||
focusedBorder: focusBorder ?? const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
borderSide: BorderSide(width: 2, color: AppColor.textFieldFocusedBorderColor)),
|
||||
errorBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(29)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
borderSide: BorderSide(width: 1, color: AppColor.textFieldErrorBorderColor)),
|
||||
prefixText: prefixText,
|
||||
labelText: labelText,
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// LinShare is an open source filesharing software, part of the LinPKI software
|
||||
// suite, developed by Linagora.
|
||||
//
|
||||
// Copyright (C) 2020 LINAGORA
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it under the
|
||||
// terms of the GNU Affero General Public License as published by the Free Software
|
||||
// Foundation, either version 3 of the License, or (at your option) any later version,
|
||||
// provided you comply with the Additional Terms applicable for LinShare software by
|
||||
// Linagora pursuant to Section 7 of the GNU Affero General Public License,
|
||||
// subsections (b), (c), and (e), pursuant to which you must notably (i) retain the
|
||||
// display in the interface of the “LinShare™” trademark/logo, the "Libre & Free" mention,
|
||||
// the words “You are using the Free and Open Source version of LinShare™, powered by
|
||||
// Linagora © 2009–2020. Contribute to Linshare R&D by subscribing to an Enterprise
|
||||
// offer!”. You must also retain the latter notice in all asynchronous messages such as
|
||||
// e-mails sent with the Program, (ii) retain all hypertext links between LinShare and
|
||||
// http://www.linshare.org, between linagora.com and Linagora, and (iii) refrain from
|
||||
// infringing Linagora intellectual property rights over its trademarks and commercial
|
||||
// brands. Other Additional Terms apply, see
|
||||
// <http://www.linshare.org/licenses/LinShare-License_AfferoGPL-v3.pdf
|
||||
//
|
||||
// for more details.
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
// more details.
|
||||
// You should have received a copy of the GNU Affero General Public License and its
|
||||
// applicable Additional Terms for LinShare along with this program. If not, see
|
||||
// <http://www.gnu.org/licenses
|
||||
// for the GNU Affero General Public License version
|
||||
//
|
||||
// 3 and <http://www.linshare.org/licenses/LinShare-License_AfferoGPL-v3.pdf
|
||||
// for
|
||||
//
|
||||
// the Additional Terms applicable to LinShare software.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import 'login_input_decoration_builder.dart';
|
||||
|
||||
typedef SetErrorString = String? Function(String);
|
||||
typedef OnSubmitted = void Function(String);
|
||||
|
||||
class LoginTextInputBuilder {
|
||||
Key? _key;
|
||||
String? _title;
|
||||
String? _hintText;
|
||||
String? _labelText;
|
||||
String? _prefixText;
|
||||
SetErrorString? _setErrorString;
|
||||
String? _errorText;
|
||||
Timer? _debounce;
|
||||
bool? _obscureText;
|
||||
ValueChanged<String>? _onTextChange;
|
||||
TextInputAction? _textInputAction;
|
||||
TextEditingController? _textEditingController;
|
||||
bool? _passwordInput;
|
||||
OnSubmitted? _onSubmitted;
|
||||
|
||||
final BuildContext context;
|
||||
final ImagePaths imagePaths;
|
||||
|
||||
LoginTextInputBuilder(this.context, this.imagePaths);
|
||||
|
||||
void key(Key key) {
|
||||
_key = key;
|
||||
}
|
||||
|
||||
void title(String? title) {
|
||||
_title = title;
|
||||
}
|
||||
|
||||
void hintText(String? hintText) {
|
||||
_hintText = hintText;
|
||||
}
|
||||
|
||||
void labelText(String? labelText) {
|
||||
_labelText = labelText;
|
||||
}
|
||||
|
||||
void prefixText(String? prefixText) {
|
||||
_prefixText = prefixText;
|
||||
}
|
||||
|
||||
void textInputAction(TextInputAction inputAction) {
|
||||
_textInputAction = inputAction;
|
||||
}
|
||||
|
||||
void obscureText(bool obscureText) {
|
||||
_obscureText = obscureText;
|
||||
}
|
||||
|
||||
void onChange(ValueChanged<String> onChange) {
|
||||
_onTextChange = onChange;
|
||||
}
|
||||
|
||||
void setErrorString(SetErrorString setErrorString) {
|
||||
_setErrorString = setErrorString;
|
||||
}
|
||||
|
||||
void passwordInput(bool? passwordInput) {
|
||||
_passwordInput = passwordInput;
|
||||
}
|
||||
|
||||
void setTextEditingController(TextEditingController textEditingController) {
|
||||
_textEditingController = textEditingController;
|
||||
}
|
||||
|
||||
void _onTextChanged(String name, StateSetter setState) {
|
||||
if (_onTextChange != null) {
|
||||
_onTextChange!(name);
|
||||
}
|
||||
if (_debounce?.isActive ?? false) _debounce?.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||
setState(() {
|
||||
_errorText = (_setErrorString != null) ? _setErrorString!(name) : '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void errorText(String? errorText) {
|
||||
_errorText = errorText;
|
||||
}
|
||||
|
||||
void _onObscureTextChanged(bool? value, StateSetter setState) {
|
||||
setState(() {
|
||||
_obscureText = value == true ? false : true;
|
||||
});
|
||||
}
|
||||
|
||||
void setOnSubmitted(OnSubmitted onSubmitted) {
|
||||
_onSubmitted = onSubmitted;
|
||||
}
|
||||
|
||||
Widget build() {
|
||||
return StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Wrap(
|
||||
key: _key,
|
||||
children: <Widget>[
|
||||
Text(_title ?? '', style: const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 14, fontWeight: FontWeight.normal)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
children: [
|
||||
TextFormField(
|
||||
onFieldSubmitted: _onSubmitted,
|
||||
onChanged: (value) => _onTextChanged(value, setState),
|
||||
obscureText: _obscureText ?? false,
|
||||
textInputAction: _textInputAction,
|
||||
controller: _textEditingController,
|
||||
cursorColor: AppColor.primaryColor,
|
||||
style: const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal),
|
||||
decoration: (LoginInputDecorationBuilder()
|
||||
..setHintText(_hintText)
|
||||
..setPrefixText(_prefixText)
|
||||
..setErrorText(_errorText)
|
||||
..setHintStyle(const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal))
|
||||
..setPrefixStyle(const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal))
|
||||
..setErrorTextStyle(const TextStyle(color: AppColor.loginTextFieldErrorBorder, fontSize: 13, fontWeight: FontWeight.normal))
|
||||
..setFocusBorder(const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
borderSide: BorderSide(width: 1, color: AppColor.loginTextFieldFocusedBorder)))
|
||||
..setEnabledBorder(const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
borderSide: BorderSide(width: 1, color: AppColor.loginTextFieldBorderColor)))
|
||||
..setErrorBorder(const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
borderSide: BorderSide(width: 1, color: AppColor.loginTextFieldErrorBorder))))
|
||||
.build(),
|
||||
),
|
||||
if (_passwordInput == true)
|
||||
Transform(
|
||||
transform: Matrix4.translationValues(
|
||||
0.0,
|
||||
(_errorText != null && _errorText!.isNotEmpty) ? -10.0 : 0.0,
|
||||
0.0),
|
||||
child: IconButton(
|
||||
onPressed: () => _onObscureTextChanged(_obscureText, setState),
|
||||
icon: SvgPicture.asset(
|
||||
_obscureText == true ? imagePaths.icEye : imagePaths.icEyeOff,
|
||||
width: 18,
|
||||
height: 18,
|
||||
fit: BoxFit.fill)))
|
||||
]
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,14 @@ class AppLocalizations {
|
||||
name: 'login_text_slogan');
|
||||
}
|
||||
|
||||
String get loginInputUrlMessage {
|
||||
return Intl.message('To login and access your message please connect to your JMAP server', name: 'loginInputUrlMessage');
|
||||
}
|
||||
|
||||
String get next {
|
||||
return Intl.message('Next', name: 'next');
|
||||
}
|
||||
|
||||
String get prefix_https {
|
||||
return Intl.message('https://',
|
||||
name: 'prefix_https');
|
||||
@@ -49,11 +57,6 @@ class AppLocalizations {
|
||||
name: 'login');
|
||||
}
|
||||
|
||||
String get login_text_login_to_continue {
|
||||
return Intl.message('Please login to continue',
|
||||
name: 'login_text_login_to_continue');
|
||||
}
|
||||
|
||||
String get unknown_error_login_message {
|
||||
return Intl.message('Unknown error occurred, please try again',
|
||||
name: 'unknown_error_login_message');
|
||||
|
||||
Reference in New Issue
Block a user