Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed8596a81e | ||
|
|
d601f245d4 |
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/api/responses/error/server.dart';
|
||||
|
||||
import 'package:pweb/models/state/edit_state.dart';
|
||||
import 'package:pweb/models/auth/password_field_type.dart';
|
||||
import 'package:pweb/models/state/controller_lifecycle.dart';
|
||||
import 'package:pweb/models/state/control_state.dart';
|
||||
import 'package:pweb/models/state/visibility.dart';
|
||||
|
||||
|
||||
@@ -14,32 +14,17 @@ class PasswordFormController extends ChangeNotifier {
|
||||
final newPasswordController = TextEditingController();
|
||||
final confirmPasswordController = TextEditingController();
|
||||
|
||||
final Map<PasswordFieldType, VisibilityState> _visibility = {
|
||||
PasswordFieldType.old: VisibilityState.hidden,
|
||||
PasswordFieldType.newPassword: VisibilityState.hidden,
|
||||
PasswordFieldType.confirmPassword: VisibilityState.hidden,
|
||||
};
|
||||
EditState _state = EditState.view;
|
||||
ControlState _formState = ControlState.enabled;
|
||||
String _errorText = '';
|
||||
bool _disposed = false;
|
||||
VisibilityState _oldPasswordVisibility = VisibilityState.hidden;
|
||||
ControllerLifecycleState _lifecycleState = ControllerLifecycleState.active;
|
||||
|
||||
bool get isExpanded => _state != EditState.view;
|
||||
bool get isSaving => _state == EditState.saving;
|
||||
bool get isSaving => _formState == ControlState.loading;
|
||||
String get errorText => _errorText;
|
||||
EditState get state => _state;
|
||||
bool isPasswordVisible(PasswordFieldType type) =>
|
||||
_visibility[type] == VisibilityState.visible;
|
||||
VisibilityState get oldPasswordVisibility => _oldPasswordVisibility;
|
||||
|
||||
void toggleExpanded() {
|
||||
if (_state == EditState.saving) return;
|
||||
_setState(_state == EditState.view ? EditState.edit : EditState.view);
|
||||
_setError('');
|
||||
}
|
||||
|
||||
void togglePasswordVisibility(PasswordFieldType type) {
|
||||
final current = _visibility[type];
|
||||
if (current == null) return;
|
||||
_visibility[type] = current == VisibilityState.hidden
|
||||
void toggleOldPasswordVisibility() {
|
||||
_oldPasswordVisibility = _oldPasswordVisibility == VisibilityState.hidden
|
||||
? VisibilityState.visible
|
||||
: VisibilityState.hidden;
|
||||
notifyListeners();
|
||||
@@ -52,7 +37,7 @@ class PasswordFormController extends ChangeNotifier {
|
||||
final currentForm = formKey.currentState;
|
||||
if (currentForm == null || !currentForm.validate()) return false;
|
||||
|
||||
_setState(EditState.saving);
|
||||
_setState(ControlState.loading);
|
||||
_setError('');
|
||||
|
||||
try {
|
||||
@@ -64,11 +49,11 @@ class PasswordFormController extends ChangeNotifier {
|
||||
oldPasswordController.clear();
|
||||
newPasswordController.clear();
|
||||
confirmPasswordController.clear();
|
||||
_setState(EditState.view);
|
||||
_setState(ControlState.enabled);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_setError(_errorMessageForException(e, errorText));
|
||||
_setState(EditState.edit);
|
||||
_setState(ControlState.enabled);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -80,21 +65,23 @@ class PasswordFormController extends ChangeNotifier {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void _setState(EditState value) {
|
||||
if (_state == value || _disposed) return;
|
||||
_state = value;
|
||||
void _setState(ControlState value) {
|
||||
if (_formState == value || _isDisposed) return;
|
||||
_formState = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String value) {
|
||||
if (_disposed) return;
|
||||
if (_isDisposed) return;
|
||||
_errorText = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get _isDisposed => _lifecycleState == ControllerLifecycleState.disposed;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
_lifecycleState = ControllerLifecycleState.disposed;
|
||||
oldPasswordController.dispose();
|
||||
newPasswordController.dispose();
|
||||
confirmPasswordController.dispose();
|
||||
|
||||
62
frontend/pweb/lib/controllers/settings/profile_actions.dart
Normal file
62
frontend/pweb/lib/controllers/settings/profile_actions.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/controllers/auth/account_name.dart';
|
||||
import 'package:pweb/models/settings/profile_action_section.dart';
|
||||
|
||||
|
||||
class ProfileActionsController extends ChangeNotifier {
|
||||
AccountNameController? _accountNameController;
|
||||
ProfileActionSection? _expandedSection;
|
||||
|
||||
ProfileActionSection? get expandedSection => _expandedSection;
|
||||
bool get isEditingName => _accountNameController?.isEditing ?? false;
|
||||
|
||||
bool isExpanded(ProfileActionSection section) => _expandedSection == section;
|
||||
|
||||
void updateAccountNameController(AccountNameController controller) {
|
||||
if (identical(_accountNameController, controller)) {
|
||||
return;
|
||||
}
|
||||
_accountNameController?.removeListener(_handleAccountNameChanged);
|
||||
_accountNameController = controller;
|
||||
_accountNameController?.addListener(_handleAccountNameChanged);
|
||||
}
|
||||
|
||||
void toggle(ProfileActionSection section) {
|
||||
final isOpeningSection = !isExpanded(section);
|
||||
if (isOpeningSection) {
|
||||
if (_accountNameController?.isBusy ?? false) {
|
||||
return;
|
||||
}
|
||||
_accountNameController?.cancelEditing();
|
||||
}
|
||||
_expandedSection = isExpanded(section) ? null : section;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleNameEditing() {
|
||||
final accountNameController = _accountNameController;
|
||||
if (accountNameController == null || accountNameController.isBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountNameController.isEditing) {
|
||||
accountNameController.cancelEditing();
|
||||
return;
|
||||
}
|
||||
|
||||
_expandedSection = null;
|
||||
accountNameController.startEditing();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleAccountNameChanged() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_accountNameController?.removeListener(_handleAccountNameChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,7 @@
|
||||
"settingsSuccessfullyUpdated": "Settings successfully updated",
|
||||
"language": "Language",
|
||||
"failedToUpdateLanguage": "Failed to update language",
|
||||
"editName": "Edit name",
|
||||
"settingsImageUpdateError": "Couldn't update the image",
|
||||
"settingsImageTitle": "Image",
|
||||
"settingsImageHint": "Tap to change the image",
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"settingsSuccessfullyUpdated": "Настройки успешно обновлены",
|
||||
"language": "Язык",
|
||||
"failedToUpdateLanguage": "Не удалось обновить язык",
|
||||
"editName": "Изменить имя",
|
||||
"settingsImageUpdateError": "Не удалось обновить изображение",
|
||||
"settingsImageTitle": "Изображение",
|
||||
"settingsImageHint": "Нажмите, чтобы изменить изображение",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
enum PasswordFieldType { old, newPassword, confirmPassword }
|
||||
@@ -0,0 +1 @@
|
||||
enum ProfileActionSection { language, password }
|
||||
1
frontend/pweb/lib/models/state/controller_lifecycle.dart
Normal file
1
frontend/pweb/lib/models/state/controller_lifecycle.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum ControllerLifecycleState { active, disposed }
|
||||
@@ -14,6 +14,7 @@ import 'package:pweb/models/account/account_loader.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AccountLoader extends StatefulWidget {
|
||||
final Widget child;
|
||||
const AccountLoader({super.key, required this.child});
|
||||
|
||||
@@ -8,21 +8,12 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class LocalePicker extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const LocalePicker({
|
||||
super.key,
|
||||
required this.title,
|
||||
});
|
||||
const LocalePicker({super.key});
|
||||
|
||||
static const double _pickerWidth = 300;
|
||||
static const double _iconSize = 20;
|
||||
static const double _gapMedium = 6;
|
||||
static const double _gapLarge = 8;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return Consumer<LocaleProvider>(
|
||||
@@ -32,18 +23,7 @@ class LocalePicker extends StatelessWidget {
|
||||
|
||||
return SizedBox(
|
||||
width: _pickerWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.language_outlined, color: theme.colorScheme.primary, size: _iconSize),
|
||||
const SizedBox(width: _gapMedium),
|
||||
Text(title, style: theme.textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: _gapLarge),
|
||||
DropdownButtonFormField<Locale>(
|
||||
child: DropdownButtonFormField<Locale>(
|
||||
initialValue: currentLocale,
|
||||
items: options
|
||||
.map(
|
||||
@@ -58,11 +38,7 @@ class LocalePicker extends StatelessWidget {
|
||||
localeProvider.setLocale(locale);
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ class AccountNameActions extends StatelessWidget {
|
||||
final state = context.watch<AccountNameController>();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (state.isEditing) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -23,10 +22,12 @@ class AccountNameActions extends StatelessWidget {
|
||||
? null
|
||||
: () async {
|
||||
final wasSaved = await state.save();
|
||||
if (!context.mounted || wasSaved || state.errorText.isEmpty) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorText)),
|
||||
);
|
||||
if (!context.mounted || wasSaved || state.errorText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(state.errorText)));
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@@ -36,10 +37,4 @@ class AccountNameActions extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(Icons.edit, color: theme.colorScheme.primary),
|
||||
onPressed: state.isBusy ? null : state.startEditing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
|
||||
import 'package:pweb/controllers/auth/account_name.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/name/actions.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/name/text.dart';
|
||||
@@ -11,52 +9,31 @@ import 'package:pweb/pages/settings/profile/account/name/text.dart';
|
||||
|
||||
class _AccountNameConstants {
|
||||
static const inputWidth = 200.0;
|
||||
static const spacing = 8.0;
|
||||
static const actionsSpacing = 8.0;
|
||||
static const actionsWidth = kMinInteractiveDimension * 2;
|
||||
static const actionsSlotWidth = actionsWidth + actionsSpacing;
|
||||
static const errorSpacing = 4.0;
|
||||
static const borderWidth = 2.0;
|
||||
}
|
||||
|
||||
class AccountName extends StatelessWidget {
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String title;
|
||||
final String hintText;
|
||||
final String lastNameHint;
|
||||
final String errorText;
|
||||
|
||||
const AccountName({
|
||||
super.key,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.title,
|
||||
required this.hintText,
|
||||
required this.lastNameHint,
|
||||
required this.errorText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<AccountProvider, AccountNameController>(
|
||||
create: (_) => AccountNameController(
|
||||
initialFirstName: firstName,
|
||||
initialLastName: lastName,
|
||||
errorMessage: errorText,
|
||||
),
|
||||
update: (_, accountProvider, controller) =>
|
||||
controller!..update(accountProvider),
|
||||
child: _AccountNameBody(
|
||||
hintText: hintText,
|
||||
lastNameHint: lastNameHint,
|
||||
),
|
||||
);
|
||||
return _AccountNameBody(hintText: hintText, lastNameHint: lastNameHint);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountNameBody extends StatelessWidget {
|
||||
const _AccountNameBody({
|
||||
required this.hintText,
|
||||
required this.lastNameHint,
|
||||
});
|
||||
const _AccountNameBody({required this.hintText, required this.lastNameHint});
|
||||
|
||||
final String hintText;
|
||||
final String lastNameHint;
|
||||
@@ -70,16 +47,27 @@ class _AccountNameBody extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(width: _AccountNameConstants.actionsSlotWidth),
|
||||
AccountNameText(
|
||||
hintText: hintText,
|
||||
lastNameHint: lastNameHint,
|
||||
inputWidth: _AccountNameConstants.inputWidth,
|
||||
borderWidth: _AccountNameConstants.borderWidth,
|
||||
),
|
||||
const SizedBox(width: _AccountNameConstants.spacing),
|
||||
const AccountNameActions(),
|
||||
SizedBox(
|
||||
width: _AccountNameConstants.actionsSlotWidth,
|
||||
child: state.isEditing
|
||||
? const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: _AccountNameConstants.actionsSpacing,
|
||||
),
|
||||
child: AccountNameActions(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: _AccountNameConstants.errorSpacing),
|
||||
|
||||
@@ -6,10 +6,12 @@ class AccountNameSingleLineText extends StatelessWidget {
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.style,
|
||||
this.textAlign = TextAlign.start,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final TextStyle? style;
|
||||
final TextAlign textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -18,6 +20,7 @@ class AccountNameSingleLineText extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: textAlign,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class AccountNameViewText extends StatelessWidget {
|
||||
child: AccountNameSingleLineText(
|
||||
text: hintText,
|
||||
style: firstLineStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -49,6 +50,7 @@ class AccountNameViewText extends StatelessWidget {
|
||||
child: AccountNameSingleLineText(
|
||||
text: singleLineName,
|
||||
style: firstLineStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -56,15 +58,17 @@ class AccountNameViewText extends StatelessWidget {
|
||||
return SizedBox(
|
||||
width: inputWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AccountNameSingleLineText(
|
||||
text: trimmedFirstName,
|
||||
style: firstLineStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
AccountNameSingleLineText(
|
||||
text: trimmedLastName,
|
||||
style: secondLineStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
class AccountPasswordContent extends StatelessWidget {
|
||||
const AccountPasswordContent({
|
||||
required this.title,
|
||||
required this.successText,
|
||||
required this.errorText,
|
||||
required this.oldPasswordLabel,
|
||||
@@ -22,7 +21,6 @@ class AccountPasswordContent extends StatelessWidget {
|
||||
required this.loc,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String successText;
|
||||
final String errorText;
|
||||
final String oldPasswordLabel;
|
||||
@@ -33,22 +31,9 @@ class AccountPasswordContent extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Consumer2<AccountProvider, PasswordFormController>(
|
||||
builder: (context, accountProvider, formProvider, _) {
|
||||
final isBusy = accountProvider.isLoading || formProvider.isSaving;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: isBusy ? null : formProvider.toggleExpanded,
|
||||
icon: Icon(Icons.lock_outline, color: theme.colorScheme.primary),
|
||||
label: Text(title, style: theme.textTheme.bodyMedium),
|
||||
),
|
||||
if (formProvider.isExpanded)
|
||||
PasswordForm(
|
||||
return PasswordForm(
|
||||
formProvider: formProvider,
|
||||
accountProvider: accountProvider,
|
||||
isBusy: accountProvider.isLoading,
|
||||
@@ -59,8 +44,6 @@ class AccountPasswordContent extends StatelessWidget {
|
||||
successText: successText,
|
||||
errorText: errorText,
|
||||
loc: loc,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/widgets/password/fields.dart';
|
||||
import 'package:pshared/widgets/password/field.dart';
|
||||
import 'package:pshared/utils/snackbar.dart';
|
||||
|
||||
import 'package:pweb/models/auth/password_field_type.dart';
|
||||
import 'package:pweb/controllers/auth/password_form.dart';
|
||||
import 'package:pweb/models/state/control_state.dart';
|
||||
import 'package:pweb/models/state/visibility.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/password/form/error_text.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/password/form/submit_button.dart';
|
||||
import 'package:pweb/utils/error/snackbar.dart';
|
||||
import 'package:pweb/widgets/password/ui_controller.dart';
|
||||
import 'package:pweb/widgets/password/verify.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -46,6 +49,9 @@ class PasswordForm extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFormBusy = isBusy || formProvider.isSaving;
|
||||
final controlState = isFormBusy
|
||||
? ControlState.disabled
|
||||
: ControlState.enabled;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -54,30 +60,38 @@ class PasswordForm extends StatelessWidget {
|
||||
key: formProvider.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
PasswordFields(
|
||||
oldPasswordController: formProvider.oldPasswordController,
|
||||
newPasswordController: formProvider.newPasswordController,
|
||||
confirmPasswordController: formProvider.confirmPasswordController,
|
||||
oldPasswordLabel: oldPasswordLabel,
|
||||
newPasswordLabel: newPasswordLabel,
|
||||
confirmPasswordLabel: confirmPasswordLabel,
|
||||
missingPasswordError: loc.errorPasswordMissing,
|
||||
passwordsDoNotMatchError: loc.passwordsDoNotMatch,
|
||||
PasswordField(
|
||||
controller: formProvider.oldPasswordController,
|
||||
labelText: oldPasswordLabel,
|
||||
fieldWidth: _fieldWidth,
|
||||
gapSmall: _gapSmall,
|
||||
isEnabled: !isFormBusy,
|
||||
showOldPassword:
|
||||
formProvider.isPasswordVisible(PasswordFieldType.old),
|
||||
showNewPassword:
|
||||
formProvider.isPasswordVisible(PasswordFieldType.newPassword),
|
||||
showConfirmPassword: formProvider
|
||||
.isPasswordVisible(PasswordFieldType.confirmPassword),
|
||||
onToggleOldPassword: () =>
|
||||
formProvider.togglePasswordVisibility(PasswordFieldType.old),
|
||||
onToggleNewPassword: () => formProvider
|
||||
.togglePasswordVisibility(PasswordFieldType.newPassword),
|
||||
onToggleConfirmPassword: () => formProvider
|
||||
.togglePasswordVisibility(PasswordFieldType.confirmPassword),
|
||||
isEnabled: controlState == ControlState.enabled,
|
||||
obscureText:
|
||||
formProvider.oldPasswordVisibility !=
|
||||
VisibilityState.visible,
|
||||
onToggleVisibility: formProvider.toggleOldPasswordVisibility,
|
||||
validator: (value) => (value == null || value.isEmpty)
|
||||
? loc.errorPasswordMissing
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: _gapSmall),
|
||||
SizedBox(
|
||||
width: _fieldWidth,
|
||||
child: PasswordUiController(
|
||||
controller: formProvider.newPasswordController,
|
||||
labelText: newPasswordLabel,
|
||||
state: controlState,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: _gapSmall),
|
||||
SizedBox(
|
||||
width: _fieldWidth,
|
||||
child: VerifyPasswordField(
|
||||
controller: formProvider.confirmPasswordController,
|
||||
externalPasswordController:
|
||||
formProvider.newPasswordController,
|
||||
labelText: confirmPasswordLabel,
|
||||
state: controlState,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: _gapMedium),
|
||||
PasswordSubmitButton(
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AccountPassword extends StatelessWidget {
|
||||
final String title;
|
||||
final String successText;
|
||||
final String errorText;
|
||||
final String oldPasswordLabel;
|
||||
@@ -19,7 +18,6 @@ class AccountPassword extends StatelessWidget {
|
||||
|
||||
const AccountPassword({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.successText,
|
||||
required this.errorText,
|
||||
required this.oldPasswordLabel,
|
||||
@@ -35,7 +33,6 @@ class AccountPassword extends StatelessWidget {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => PasswordFormController(),
|
||||
child: AccountPasswordContent(
|
||||
title: title,
|
||||
successText: successText,
|
||||
errorText: errorText,
|
||||
oldPasswordLabel: oldPasswordLabel,
|
||||
|
||||
64
frontend/pweb/lib/pages/settings/profile/actions/body.dart
Normal file
64
frontend/pweb/lib/pages/settings/profile/actions/body.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/controllers/settings/profile_actions.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/buttons.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/constants.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/content.dart';
|
||||
|
||||
|
||||
class ProfileActionsSectionBody extends StatelessWidget {
|
||||
const ProfileActionsSectionBody({
|
||||
super.key,
|
||||
required this.nameLabel,
|
||||
required this.languageLabel,
|
||||
required this.passwordLabel,
|
||||
required this.passwordSuccessText,
|
||||
required this.passwordErrorText,
|
||||
required this.oldPasswordLabel,
|
||||
required this.newPasswordLabel,
|
||||
required this.confirmPasswordLabel,
|
||||
required this.savePasswordLabel,
|
||||
});
|
||||
|
||||
final String nameLabel;
|
||||
final String languageLabel;
|
||||
final String passwordLabel;
|
||||
final String passwordSuccessText;
|
||||
final String passwordErrorText;
|
||||
final String oldPasswordLabel;
|
||||
final String newPasswordLabel;
|
||||
final String confirmPasswordLabel;
|
||||
final String savePasswordLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = context.watch<ProfileActionsController>();
|
||||
final expandedSection = controller.expandedSection;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ProfileActionButtons(
|
||||
nameLabel: nameLabel,
|
||||
languageLabel: languageLabel,
|
||||
passwordLabel: passwordLabel,
|
||||
),
|
||||
if (expandedSection != null) ...[
|
||||
const SizedBox(height: ProfileActionsLayoutConstants.contentGap),
|
||||
ProfileActionsContent(
|
||||
section: expandedSection,
|
||||
passwordSuccessText: passwordSuccessText,
|
||||
passwordErrorText: passwordErrorText,
|
||||
oldPasswordLabel: oldPasswordLabel,
|
||||
newPasswordLabel: newPasswordLabel,
|
||||
confirmPasswordLabel: confirmPasswordLabel,
|
||||
savePasswordLabel: savePasswordLabel,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
63
frontend/pweb/lib/pages/settings/profile/actions/button.dart
Normal file
63
frontend/pweb/lib/pages/settings/profile/actions/button.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class ProfileActionButton extends StatelessWidget {
|
||||
const ProfileActionButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
static const _buttonPadding = EdgeInsets.symmetric(
|
||||
horizontal: 28,
|
||||
vertical: 24,
|
||||
);
|
||||
static const _iconSize = 28.0;
|
||||
static const _contentGap = 12.0;
|
||||
static const _borderRadius = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final backgroundColor = colorScheme.onSecondary;
|
||||
final borderColor = isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.onPrimary;
|
||||
final textColor = colorScheme.primary;
|
||||
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
style: TextButton.styleFrom(
|
||||
padding: _buttonPadding,
|
||||
backgroundColor: backgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(_borderRadius),
|
||||
side: BorderSide(color: borderColor),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: textColor, size: _iconSize),
|
||||
const SizedBox(height: _contentGap),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/settings/profile/actions/buttons_layout.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/language_button.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/name_button.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/password_button.dart';
|
||||
|
||||
|
||||
class ProfileActionButtons extends StatelessWidget {
|
||||
const ProfileActionButtons({
|
||||
super.key,
|
||||
required this.nameLabel,
|
||||
required this.languageLabel,
|
||||
required this.passwordLabel,
|
||||
});
|
||||
|
||||
final String nameLabel;
|
||||
final String languageLabel;
|
||||
final String passwordLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ProfileActionButtonsLayout(
|
||||
children: [
|
||||
ProfileNameActionButton(label: nameLabel),
|
||||
ProfileLanguageActionButton(label: languageLabel),
|
||||
ProfilePasswordActionButton(label: passwordLabel),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/settings/profile/actions/constants.dart';
|
||||
|
||||
|
||||
class ProfileActionButtonsLayout extends StatelessWidget {
|
||||
const ProfileActionButtonsLayout({super.key, required this.children});
|
||||
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isCompact =
|
||||
constraints.maxWidth <
|
||||
ProfileActionsLayoutConstants.compactBreakpoint;
|
||||
|
||||
if (isCompact) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: ProfileActionsLayoutConstants.buttonWidth,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildChildren(isCompact: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildChildren(isCompact: false),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildChildren({required bool isCompact}) {
|
||||
return [
|
||||
for (var index = 0; index < children.length; index++) ...[
|
||||
SizedBox(
|
||||
width: isCompact
|
||||
? double.infinity
|
||||
: ProfileActionsLayoutConstants.buttonWidth,
|
||||
child: children[index],
|
||||
),
|
||||
if (index != children.length - 1)
|
||||
SizedBox(
|
||||
width: isCompact ? 0 : ProfileActionsLayoutConstants.buttonGap,
|
||||
height: isCompact ? ProfileActionsLayoutConstants.buttonGap : 0,
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
class ProfileActionsLayoutConstants {
|
||||
static const buttonGap = 12.0;
|
||||
static const contentGap = 16.0;
|
||||
static const buttonWidth = 180.0;
|
||||
static const compactBreakpoint = buttonWidth * 3 + buttonGap * 2;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/models/settings/profile_action_section.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/locale.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/password/password.dart';
|
||||
|
||||
|
||||
class ProfileActionsContent extends StatelessWidget {
|
||||
const ProfileActionsContent({
|
||||
super.key,
|
||||
required this.section,
|
||||
required this.passwordSuccessText,
|
||||
required this.passwordErrorText,
|
||||
required this.oldPasswordLabel,
|
||||
required this.newPasswordLabel,
|
||||
required this.confirmPasswordLabel,
|
||||
required this.savePasswordLabel,
|
||||
});
|
||||
|
||||
final ProfileActionSection section;
|
||||
final String passwordSuccessText;
|
||||
final String passwordErrorText;
|
||||
final String oldPasswordLabel;
|
||||
final String newPasswordLabel;
|
||||
final String confirmPasswordLabel;
|
||||
final String savePasswordLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (section) {
|
||||
case ProfileActionSection.language:
|
||||
return const LocalePicker();
|
||||
case ProfileActionSection.password:
|
||||
return AccountPassword(
|
||||
successText: passwordSuccessText,
|
||||
errorText: passwordErrorText,
|
||||
oldPasswordLabel: oldPasswordLabel,
|
||||
newPasswordLabel: newPasswordLabel,
|
||||
confirmPasswordLabel: confirmPasswordLabel,
|
||||
savePassword: savePasswordLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/controllers/settings/profile_actions.dart';
|
||||
import 'package:pweb/models/settings/profile_action_section.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/button.dart';
|
||||
|
||||
|
||||
class ProfileLanguageActionButton extends StatelessWidget {
|
||||
const ProfileLanguageActionButton({super.key, required this.label});
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = context.watch<ProfileActionsController>();
|
||||
|
||||
return ProfileActionButton(
|
||||
icon: Icons.language_outlined,
|
||||
label: label,
|
||||
isSelected: controller.isExpanded(ProfileActionSection.language),
|
||||
onPressed: () => controller.toggle(ProfileActionSection.language),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/controllers/settings/profile_actions.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/button.dart';
|
||||
|
||||
|
||||
class ProfileNameActionButton extends StatelessWidget {
|
||||
const ProfileNameActionButton({super.key, required this.label});
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = context.watch<ProfileActionsController>();
|
||||
|
||||
return ProfileActionButton(
|
||||
icon: Icons.edit_outlined,
|
||||
label: label,
|
||||
isSelected: controller.isEditingName,
|
||||
onPressed: controller.toggleNameEditing,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/controllers/settings/profile_actions.dart';
|
||||
import 'package:pweb/models/settings/profile_action_section.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/button.dart';
|
||||
|
||||
|
||||
class ProfilePasswordActionButton extends StatelessWidget {
|
||||
const ProfilePasswordActionButton({super.key, required this.label});
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = context.watch<ProfileActionsController>();
|
||||
|
||||
return ProfileActionButton(
|
||||
icon: Icons.lock_outline,
|
||||
label: label,
|
||||
isSelected: controller.isExpanded(ProfileActionSection.password),
|
||||
onPressed: () => controller.toggle(ProfileActionSection.password),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/controllers/auth/account_name.dart';
|
||||
import 'package:pweb/controllers/settings/profile_actions.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/body.dart';
|
||||
|
||||
|
||||
class ProfileActionsSection extends StatelessWidget {
|
||||
const ProfileActionsSection({
|
||||
super.key,
|
||||
required this.nameLabel,
|
||||
required this.languageLabel,
|
||||
required this.passwordLabel,
|
||||
required this.passwordSuccessText,
|
||||
required this.passwordErrorText,
|
||||
required this.oldPasswordLabel,
|
||||
required this.newPasswordLabel,
|
||||
required this.confirmPasswordLabel,
|
||||
required this.savePasswordLabel,
|
||||
});
|
||||
|
||||
final String nameLabel;
|
||||
final String languageLabel;
|
||||
final String passwordLabel;
|
||||
final String passwordSuccessText;
|
||||
final String passwordErrorText;
|
||||
final String oldPasswordLabel;
|
||||
final String newPasswordLabel;
|
||||
final String confirmPasswordLabel;
|
||||
final String savePasswordLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<
|
||||
AccountNameController,
|
||||
ProfileActionsController
|
||||
>(
|
||||
create: (_) => ProfileActionsController(),
|
||||
update: (_, accountNameController, controller) =>
|
||||
controller!..updateAccountNameController(accountNameController),
|
||||
child: ProfileActionsSectionBody(
|
||||
nameLabel: nameLabel,
|
||||
languageLabel: languageLabel,
|
||||
passwordLabel: passwordLabel,
|
||||
passwordSuccessText: passwordSuccessText,
|
||||
passwordErrorText: passwordErrorText,
|
||||
oldPasswordLabel: oldPasswordLabel,
|
||||
newPasswordLabel: newPasswordLabel,
|
||||
confirmPasswordLabel: confirmPasswordLabel,
|
||||
savePasswordLabel: savePasswordLabel,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
|
||||
import 'package:pweb/controllers/auth/account_name.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/avatar.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/locale.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/name/name.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/password/password.dart';
|
||||
import 'package:pweb/pages/settings/profile/actions/section.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -15,7 +15,10 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
class ProfileSettingsPage extends StatelessWidget {
|
||||
const ProfileSettingsPage({super.key});
|
||||
|
||||
static const _cardPadding = EdgeInsets.symmetric(vertical: 32, horizontal: 16);
|
||||
static const _cardPadding = EdgeInsets.symmetric(
|
||||
vertical: 32,
|
||||
horizontal: 16,
|
||||
);
|
||||
static const _cardRadius = 16.0;
|
||||
static const _itemSpacing = 12.0;
|
||||
|
||||
@@ -33,7 +36,15 @@ class ProfileSettingsPage extends StatelessWidget {
|
||||
(provider) => provider.account?.avatarUrl,
|
||||
);
|
||||
|
||||
return Align(
|
||||
return ChangeNotifierProxyProvider<AccountProvider, AccountNameController>(
|
||||
create: (_) => AccountNameController(
|
||||
initialFirstName: accountFirstName ?? '',
|
||||
initialLastName: accountLastName ?? '',
|
||||
errorMessage: loc.accountNameUpdateError,
|
||||
),
|
||||
update: (_, accountProvider, controller) =>
|
||||
controller!..update(accountProvider),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
@@ -52,29 +63,26 @@ class ProfileSettingsPage extends StatelessWidget {
|
||||
errorText: loc.avatarUpdateError,
|
||||
),
|
||||
AccountName(
|
||||
firstName: accountFirstName ?? '',
|
||||
lastName: accountLastName ?? '',
|
||||
title: loc.accountName,
|
||||
hintText: loc.accountNameHint,
|
||||
lastNameHint: loc.lastName,
|
||||
errorText: loc.accountNameUpdateError,
|
||||
),
|
||||
AccountPassword(
|
||||
title: loc.changePassword,
|
||||
successText: loc.changePasswordSuccess,
|
||||
errorText: loc.changePasswordError,
|
||||
SizedBox(height: _itemSpacing),
|
||||
ProfileActionsSection(
|
||||
nameLabel: loc.editName,
|
||||
languageLabel: loc.language,
|
||||
passwordLabel: loc.changePassword,
|
||||
passwordSuccessText: loc.changePasswordSuccess,
|
||||
passwordErrorText: loc.changePasswordError,
|
||||
oldPasswordLabel: loc.oldPassword,
|
||||
newPasswordLabel: loc.newPassword,
|
||||
confirmPasswordLabel: loc.confirmPassword,
|
||||
savePassword: loc.savePassword,
|
||||
),
|
||||
LocalePicker(
|
||||
title: loc.language,
|
||||
savePasswordLabel: loc.savePassword,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/signup/form/controllers.dart';
|
||||
import 'package:pweb/pages/signup/form/description.dart';
|
||||
import 'package:pweb/widgets/username.dart';
|
||||
import 'package:pweb/pages/signup/form/password_ui_controller.dart';
|
||||
import 'package:pweb/pages/signup/header.dart';
|
||||
import 'package:pweb/widgets/password/ui_controller.dart';
|
||||
import 'package:pweb/widgets/password/verify.dart';
|
||||
import 'package:pweb/widgets/text_field.dart';
|
||||
import 'package:pweb/widgets/username.dart';
|
||||
import 'package:pweb/widgets/vspacer.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
@@ -47,7 +47,7 @@ class SignUpFormFields extends StatelessWidget {
|
||||
const VSpacer(),
|
||||
UsernameField(controller: controllers.email),
|
||||
const VSpacer(),
|
||||
SignUpPasswordUiController(controller: controllers.password),
|
||||
PasswordUiController(controller: controllers.password),
|
||||
const VSpacer(multiplier: 2.0),
|
||||
VerifyPasswordField(
|
||||
controller: controllers.passwordConfirm,
|
||||
|
||||
@@ -3,23 +3,29 @@ import 'package:flutter/material.dart';
|
||||
import 'package:fancy_password_field/fancy_password_field.dart';
|
||||
|
||||
import 'package:pweb/config/constants.dart';
|
||||
import 'package:pweb/models/state/control_state.dart';
|
||||
import 'package:pweb/widgets/password/password.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class SignUpPasswordUiController extends StatefulWidget {
|
||||
class PasswordUiController extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final String? labelText;
|
||||
final ControlState state;
|
||||
|
||||
const SignUpPasswordUiController({required this.controller, super.key});
|
||||
const PasswordUiController({
|
||||
required this.controller,
|
||||
this.labelText,
|
||||
this.state = ControlState.enabled,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SignUpPasswordUiController> createState() =>
|
||||
_SignUpPasswordUiControllerState();
|
||||
State<PasswordUiController> createState() => _PasswordUiControllerState();
|
||||
}
|
||||
|
||||
class _SignUpPasswordUiControllerState
|
||||
extends State<SignUpPasswordUiController> {
|
||||
class _PasswordUiControllerState extends State<PasswordUiController> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -36,9 +42,11 @@ class _SignUpPasswordUiControllerState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final isEnabled = widget.state == ControlState.enabled;
|
||||
final specialRule = _SpecialCharacterValidationRule(
|
||||
customText: loc.passwordValidationRuleSpecialCharacter,
|
||||
customText: AppLocalizations.of(
|
||||
context,
|
||||
)!.passwordValidationRuleSpecialCharacter,
|
||||
);
|
||||
final value = widget.controller.text;
|
||||
final missing = _allRules(context, specialRule)
|
||||
@@ -50,15 +58,24 @@ class _SignUpPasswordUiControllerState
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Theme(
|
||||
data: hasMissingRules ? _invalidTheme(context) : Theme.of(context),
|
||||
IgnorePointer(
|
||||
ignoring: !isEnabled,
|
||||
child: Opacity(
|
||||
opacity: isEnabled ? 1 : 0.6,
|
||||
child: Theme(
|
||||
data: hasMissingRules
|
||||
? _invalidTheme(context)
|
||||
: Theme.of(context),
|
||||
child: defaulRulesPasswordField(
|
||||
context,
|
||||
controller: widget.controller,
|
||||
labelText: widget.labelText,
|
||||
additionalRules: {specialRule},
|
||||
validationRuleBuilder: (_, _) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasMissingRules) ...[
|
||||
const SizedBox(height: 8),
|
||||
...missing.map(
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:fancy_password_field/fancy_password_field.dart';
|
||||
|
||||
import 'package:pweb/models/state/control_state.dart';
|
||||
import 'package:pweb/widgets/password/hint/error.dart';
|
||||
import 'package:pweb/widgets/password/hint/short.dart';
|
||||
import 'package:pweb/widgets/password/password.dart';
|
||||
@@ -32,12 +33,16 @@ class VerifyPasswordField extends StatefulWidget {
|
||||
final ValueChanged<bool>? onValid;
|
||||
final TextEditingController controller;
|
||||
final TextEditingController externalPasswordController;
|
||||
final String? labelText;
|
||||
final ControlState state;
|
||||
|
||||
const VerifyPasswordField({
|
||||
super.key,
|
||||
this.onValid,
|
||||
required this.controller,
|
||||
required this.externalPasswordController,
|
||||
this.labelText,
|
||||
this.state = ControlState.enabled,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -55,7 +60,8 @@ class _VerifyPasswordFieldState extends State<VerifyPasswordField> {
|
||||
}
|
||||
|
||||
void _validatePassword() {
|
||||
final isValid = widget.controller.text == widget.externalPasswordController.text;
|
||||
final isValid =
|
||||
widget.controller.text == widget.externalPasswordController.text;
|
||||
|
||||
// Only call onValid if the validity state has changed to prevent infinite loops
|
||||
if (isValid != _isCurrentlyValid) {
|
||||
@@ -68,21 +74,31 @@ class _VerifyPasswordFieldState extends State<VerifyPasswordField> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEnabled = widget.state == ControlState.enabled;
|
||||
final rule = PasswordVeirificationRule(
|
||||
ruleName: AppLocalizations.of(context)!.passwordsDoNotMatch,
|
||||
externalPasswordController: widget.externalPasswordController,
|
||||
);
|
||||
|
||||
return defaulRulesPasswordField(
|
||||
return IgnorePointer(
|
||||
ignoring: !isEnabled,
|
||||
child: Opacity(
|
||||
opacity: isEnabled ? 1 : 0.6,
|
||||
child: defaulRulesPasswordField(
|
||||
context,
|
||||
controller: widget.controller,
|
||||
key: widget.key,
|
||||
labelText: AppLocalizations.of(context)!.confirmPassword,
|
||||
additionalRules: { rule },
|
||||
labelText:
|
||||
widget.labelText ?? AppLocalizations.of(context)!.confirmPassword,
|
||||
additionalRules: {rule},
|
||||
validationRuleBuilder: (rules, value) => rule.validate(value)
|
||||
? shortValidation(context, rules, value)
|
||||
: PasswordValidationErrorLabel(labelText: AppLocalizations.of(context)!.passwordsDoNotMatch),
|
||||
: PasswordValidationErrorLabel(
|
||||
labelText: AppLocalizations.of(context)!.passwordsDoNotMatch,
|
||||
),
|
||||
onValid: widget.onValid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user