progress in login page
This commit is contained in:
parent
f00d2eca6d
commit
5af2bf0460
|
|
@ -29,6 +29,10 @@ class Settings(BaseSettings):
|
||||||
captcha_secret: str = "change_me_captcha"
|
captcha_secret: str = "change_me_captcha"
|
||||||
reset_password_ttl_seconds: int = 3600
|
reset_password_ttl_seconds: int = 3600
|
||||||
|
|
||||||
|
# Phone normalization
|
||||||
|
# Used as default region when parsing phone numbers without a country code
|
||||||
|
default_phone_region: str = "IR"
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
cors_allowed_origins: list[str] = ["*"]
|
cors_allowed_origins: list[str] = ["*"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,14 @@ def _normalize_email(email: str | None) -> str | None:
|
||||||
def _normalize_mobile(mobile: str | None) -> str | None:
|
def _normalize_mobile(mobile: str | None) -> str | None:
|
||||||
if not mobile:
|
if not mobile:
|
||||||
return None
|
return None
|
||||||
# Try parse as international; fallback no region
|
# Clean input: keep digits and leading plus
|
||||||
|
raw = mobile.strip()
|
||||||
|
raw = ''.join(ch for ch in raw if ch.isdigit() or ch == '+')
|
||||||
try:
|
try:
|
||||||
num = phonenumbers.parse(mobile, None)
|
from app.core.settings import get_settings
|
||||||
|
settings = get_settings()
|
||||||
|
region = None if raw.startswith('+') else settings.default_phone_region
|
||||||
|
num = phonenumbers.parse(raw, region)
|
||||||
if not phonenumbers.is_valid_number(num):
|
if not phonenumbers.is_valid_number(num):
|
||||||
return None
|
return None
|
||||||
return phonenumbers.format_number(num, phonenumbers.PhoneNumberFormat.E164)
|
return phonenumbers.format_number(num, phonenumbers.PhoneNumberFormat.E164)
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,12 @@
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
,
|
,
|
||||||
"captchaRequired": "Captcha is required."
|
"captchaRequired": "Captcha is required."
|
||||||
|
,
|
||||||
|
"sendReset": "Send reset code"
|
||||||
|
,
|
||||||
|
"registerFailed": "Registration failed. Please try again.",
|
||||||
|
"resetFailed": "Request failed. Please try again."
|
||||||
|
,
|
||||||
|
"fixFormErrors": "Please fix the form errors."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,12 @@
|
||||||
"refresh": "تازهسازی"
|
"refresh": "تازهسازی"
|
||||||
,
|
,
|
||||||
"captchaRequired": "کد امنیتی الزامی است."
|
"captchaRequired": "کد امنیتی الزامی است."
|
||||||
|
,
|
||||||
|
"sendReset": "ارسال کد بازیابی"
|
||||||
|
,
|
||||||
|
"registerFailed": "عضویت ناموفق بود. لطفاً دوباره تلاش کنید.",
|
||||||
|
"resetFailed": "ارسال کد بازیابی ناموفق بود. لطفاً دوباره تلاش کنید."
|
||||||
|
,
|
||||||
|
"fixFormErrors": "لطفاً خطاهای فرم را برطرف کنید."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,30 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Captcha is required.'**
|
/// **'Captcha is required.'**
|
||||||
String get captchaRequired;
|
String get captchaRequired;
|
||||||
|
|
||||||
|
/// No description provided for @sendReset.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Send reset code'**
|
||||||
|
String get sendReset;
|
||||||
|
|
||||||
|
/// No description provided for @registerFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Registration failed. Please try again.'**
|
||||||
|
String get registerFailed;
|
||||||
|
|
||||||
|
/// No description provided for @resetFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Request failed. Please try again.'**
|
||||||
|
String get resetFailed;
|
||||||
|
|
||||||
|
/// No description provided for @fixFormErrors.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Please fix the form errors.'**
|
||||||
|
String get fixFormErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -93,4 +93,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get captchaRequired => 'Captcha is required.';
|
String get captchaRequired => 'Captcha is required.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sendReset => 'Send reset code';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registerFailed => 'Registration failed. Please try again.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get resetFailed => 'Request failed. Please try again.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fixFormErrors => 'Please fix the form errors.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,4 +92,16 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get captchaRequired => 'کد امنیتی الزامی است.';
|
String get captchaRequired => 'کد امنیتی الزامی است.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sendReset => 'ارسال کد بازیابی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registerFailed => 'عضویت ناموفق بود. لطفاً دوباره تلاش کنید.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get resetFailed => 'ارسال کد بازیابی ناموفق بود. لطفاً دوباره تلاش کنید.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fixFormErrors => 'لطفاً خطاهای فرم را برطرف کنید.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import '../core/locale_controller.dart';
|
||||||
import '../theme/theme_controller.dart';
|
import '../theme/theme_controller.dart';
|
||||||
import '../widgets/auth_footer.dart';
|
import '../widgets/auth_footer.dart';
|
||||||
import '../core/auth_store.dart';
|
import '../core/auth_store.dart';
|
||||||
import '../widgets/error_notice.dart';
|
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
final LocaleController localeController;
|
final LocaleController localeController;
|
||||||
|
|
@ -34,7 +33,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
Uint8List? _loginCaptchaImage;
|
Uint8List? _loginCaptchaImage;
|
||||||
Timer? _loginCaptchaTimer;
|
Timer? _loginCaptchaTimer;
|
||||||
bool _loadingLogin = false;
|
bool _loadingLogin = false;
|
||||||
String? _errorText;
|
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
final _registerKey = GlobalKey<FormState>();
|
final _registerKey = GlobalKey<FormState>();
|
||||||
|
|
@ -48,7 +46,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
Uint8List? _registerCaptchaImage;
|
Uint8List? _registerCaptchaImage;
|
||||||
bool _loadingRegister = false;
|
bool _loadingRegister = false;
|
||||||
Timer? _registerCaptchaTimer;
|
Timer? _registerCaptchaTimer;
|
||||||
String? _registerErrorText;
|
|
||||||
|
|
||||||
// Forgot password
|
// Forgot password
|
||||||
final _forgotKey = GlobalKey<FormState>();
|
final _forgotKey = GlobalKey<FormState>();
|
||||||
|
|
@ -58,7 +55,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
Uint8List? _forgotCaptchaImage;
|
Uint8List? _forgotCaptchaImage;
|
||||||
bool _loadingForgot = false;
|
bool _loadingForgot = false;
|
||||||
Timer? _forgotCaptchaTimer;
|
Timer? _forgotCaptchaTimer;
|
||||||
String? _forgotErrorText;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|
@ -155,7 +151,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_loadingLogin = true;
|
_loadingLogin = true;
|
||||||
_errorText = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -183,8 +178,9 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
||||||
_showSnack(msg);
|
_showSnack(msg);
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorText = msg;
|
_loginCaptchaCtrl.clear();
|
||||||
});
|
});
|
||||||
|
// فقط اسنکبار نمایش داده میشود؛ وضعیت داخلی خطا ذخیره نمیشود
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -196,19 +192,33 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onRegister() async {
|
Future<void> _onRegister() async {
|
||||||
final form = _registerKey.currentState;
|
|
||||||
final t = AppLocalizations.of(context);
|
final t = AppLocalizations.of(context);
|
||||||
if (form == null || !form.validate()) return;
|
// اعتبارسنجی دستی و نمایش فقط Snackbar
|
||||||
|
if (_firstNameCtrl.text.trim().isEmpty) {
|
||||||
|
_showSnack('${t.firstName} ${t.requiredField}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_lastNameCtrl.text.trim().isEmpty) {
|
||||||
|
_showSnack('${t.lastName} ${t.requiredField}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_emailCtrl.text.trim().isEmpty && _mobileCtrl.text.trim().isEmpty) {
|
||||||
|
final msg = '${t.email} / ${t.mobile} ${t.requiredField}';
|
||||||
|
_showSnack(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_registerPasswordCtrl.text.isEmpty) {
|
||||||
|
_showSnack('${t.password} ${t.requiredField}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_registerCaptchaId == null || _registerCaptchaCtrl.text.trim().isEmpty) {
|
||||||
|
_showSnack(t.captchaRequired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _loadingRegister = true);
|
setState(() => _loadingRegister = true);
|
||||||
try {
|
try {
|
||||||
final api = ApiClient();
|
final api = ApiClient();
|
||||||
if (_emailCtrl.text.trim().isEmpty && _mobileCtrl.text.trim().isEmpty) {
|
|
||||||
final msg = '${t.email} / ${t.mobile} ${t.requiredField}';
|
|
||||||
setState(() { _registerErrorText = msg; });
|
|
||||||
_showSnack(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await api.post<Map<String, dynamic>>(
|
await api.post<Map<String, dynamic>>(
|
||||||
'/api/v1/auth/register',
|
'/api/v1/auth/register',
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -223,14 +233,15 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() { _registerErrorText = null; });
|
|
||||||
_showSnack(t.registerSuccess);
|
_showSnack(t.registerSuccess);
|
||||||
DefaultTabController.of(context).animateTo(0);
|
DefaultTabController.of(context).animateTo(0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
||||||
setState(() { _registerErrorText = msg; });
|
_showSnack(msg.isEmpty ? t.registerFailed : msg);
|
||||||
_showSnack(msg);
|
setState(() {
|
||||||
|
_registerCaptchaCtrl.clear();
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _loadingRegister = false);
|
if (mounted) setState(() => _loadingRegister = false);
|
||||||
_refreshCaptcha('register');
|
_refreshCaptcha('register');
|
||||||
|
|
@ -238,9 +249,16 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onForgot() async {
|
Future<void> _onForgot() async {
|
||||||
final form = _forgotKey.currentState;
|
|
||||||
final t = AppLocalizations.of(context);
|
final t = AppLocalizations.of(context);
|
||||||
if (form == null || !form.validate()) return;
|
// اعتبارسنجی دستی و نمایش فقط Snackbar
|
||||||
|
if (_forgotIdentifierCtrl.text.trim().isEmpty) {
|
||||||
|
_showSnack('${t.identifier} ${t.requiredField}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_forgotCaptchaId == null || _forgotCaptchaCtrl.text.trim().isEmpty) {
|
||||||
|
_showSnack(t.captchaRequired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _loadingForgot = true);
|
setState(() => _loadingForgot = true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -259,8 +277,10 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
final msg = _extractErrorMessage(e, AppLocalizations.of(context));
|
||||||
setState(() { _forgotErrorText = msg; });
|
|
||||||
_showSnack(msg);
|
_showSnack(msg);
|
||||||
|
setState(() {
|
||||||
|
_forgotCaptchaCtrl.clear();
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _loadingForgot = false);
|
if (mounted) setState(() => _loadingForgot = false);
|
||||||
_refreshCaptcha('forgot');
|
_refreshCaptcha('forgot');
|
||||||
|
|
@ -277,9 +297,19 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 3,
|
length: 3,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Center(
|
resizeToAvoidBottomInset: true,
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.only(bottom: bottomInset + 16),
|
||||||
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 520),
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: 520,
|
||||||
|
minHeight: constraints.maxHeight - 32, // to keep card vertically centered when possible
|
||||||
|
),
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
|
|
@ -314,6 +344,10 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
if (idx == 0) {
|
if (idx == 0) {
|
||||||
body = Padding(
|
body = Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
AbsorbPointer(
|
||||||
|
absorbing: _loadingLogin,
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -360,29 +394,43 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
const SizedBox(height: 40, width: 120),
|
const SizedBox(height: 40, width: 120),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _refreshCaptcha('login'),
|
onPressed: _loadingLogin ? null : () => _refreshCaptcha('login'),
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
tooltip: t.refresh,
|
tooltip: t.refresh,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (_errorText != null)
|
// در تب ورود، فقط Snackbar نمایش داده میشود (بدون ویجت خطا)
|
||||||
ErrorNotice(message: _errorText!, onClose: () => setState(() => _errorText = null)),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _loadingLogin ? null : _onSubmit,
|
onPressed: _loadingLogin ? null : _onSubmit,
|
||||||
child: _loadingLogin
|
child: _loadingLogin
|
||||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
: Text(t.submit),
|
: Text(t.login),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (_loadingLogin)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black26,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else if (idx == 1) {
|
} else if (idx == 1) {
|
||||||
body = Padding(
|
body = Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
AbsorbPointer(
|
||||||
|
absorbing: _loadingRegister,
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _registerKey,
|
key: _registerKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -452,29 +500,41 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
const SizedBox(height: 40, width: 120),
|
const SizedBox(height: 40, width: 120),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _refreshCaptcha('register'),
|
onPressed: _loadingRegister ? null : () => _refreshCaptcha('register'),
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
tooltip: t.refresh,
|
tooltip: t.refresh,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (_registerErrorText != null)
|
|
||||||
ErrorNotice(message: _registerErrorText!, onClose: () => setState(() => _registerErrorText = null)),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _loadingRegister ? null : _onRegister,
|
onPressed: _loadingRegister ? null : _onRegister,
|
||||||
child: _loadingRegister
|
child: _loadingRegister
|
||||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
: Text(t.submit),
|
: Text(t.register),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (_loadingRegister)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black26,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
body = Padding(
|
body = Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
AbsorbPointer(
|
||||||
|
absorbing: _loadingForgot,
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _forgotKey,
|
key: _forgotKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -513,25 +573,33 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
const SizedBox(height: 40, width: 120),
|
const SizedBox(height: 40, width: 120),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _refreshCaptcha('forgot'),
|
onPressed: _loadingForgot ? null : () => _refreshCaptcha('forgot'),
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
tooltip: t.refresh,
|
tooltip: t.refresh,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (_forgotErrorText != null)
|
|
||||||
ErrorNotice(message: _forgotErrorText!, onClose: () => setState(() => _forgotErrorText = null)),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _loadingForgot ? null : _onForgot,
|
onPressed: _loadingForgot ? null : _onForgot,
|
||||||
child: _loadingForgot
|
child: _loadingForgot
|
||||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
: Text(t.submit),
|
: Text(t.sendReset),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (_loadingForgot)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black26,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return AnimatedSize(
|
return AnimatedSize(
|
||||||
|
|
@ -552,6 +620,10 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue