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/provider/account.dart';
|
||||||
import 'package:pshared/api/responses/error/server.dart';
|
import 'package:pshared/api/responses/error/server.dart';
|
||||||
|
|
||||||
import 'package:pweb/models/state/edit_state.dart';
|
import 'package:pweb/models/state/controller_lifecycle.dart';
|
||||||
import 'package:pweb/models/auth/password_field_type.dart';
|
import 'package:pweb/models/state/control_state.dart';
|
||||||
import 'package:pweb/models/state/visibility.dart';
|
import 'package:pweb/models/state/visibility.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -14,32 +14,17 @@ class PasswordFormController extends ChangeNotifier {
|
|||||||
final newPasswordController = TextEditingController();
|
final newPasswordController = TextEditingController();
|
||||||
final confirmPasswordController = TextEditingController();
|
final confirmPasswordController = TextEditingController();
|
||||||
|
|
||||||
final Map<PasswordFieldType, VisibilityState> _visibility = {
|
ControlState _formState = ControlState.enabled;
|
||||||
PasswordFieldType.old: VisibilityState.hidden,
|
|
||||||
PasswordFieldType.newPassword: VisibilityState.hidden,
|
|
||||||
PasswordFieldType.confirmPassword: VisibilityState.hidden,
|
|
||||||
};
|
|
||||||
EditState _state = EditState.view;
|
|
||||||
String _errorText = '';
|
String _errorText = '';
|
||||||
bool _disposed = false;
|
VisibilityState _oldPasswordVisibility = VisibilityState.hidden;
|
||||||
|
ControllerLifecycleState _lifecycleState = ControllerLifecycleState.active;
|
||||||
|
|
||||||
bool get isExpanded => _state != EditState.view;
|
bool get isSaving => _formState == ControlState.loading;
|
||||||
bool get isSaving => _state == EditState.saving;
|
|
||||||
String get errorText => _errorText;
|
String get errorText => _errorText;
|
||||||
EditState get state => _state;
|
VisibilityState get oldPasswordVisibility => _oldPasswordVisibility;
|
||||||
bool isPasswordVisible(PasswordFieldType type) =>
|
|
||||||
_visibility[type] == VisibilityState.visible;
|
|
||||||
|
|
||||||
void toggleExpanded() {
|
void toggleOldPasswordVisibility() {
|
||||||
if (_state == EditState.saving) return;
|
_oldPasswordVisibility = _oldPasswordVisibility == VisibilityState.hidden
|
||||||
_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
|
|
||||||
? VisibilityState.visible
|
? VisibilityState.visible
|
||||||
: VisibilityState.hidden;
|
: VisibilityState.hidden;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -52,7 +37,7 @@ class PasswordFormController extends ChangeNotifier {
|
|||||||
final currentForm = formKey.currentState;
|
final currentForm = formKey.currentState;
|
||||||
if (currentForm == null || !currentForm.validate()) return false;
|
if (currentForm == null || !currentForm.validate()) return false;
|
||||||
|
|
||||||
_setState(EditState.saving);
|
_setState(ControlState.loading);
|
||||||
_setError('');
|
_setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -64,11 +49,11 @@ class PasswordFormController extends ChangeNotifier {
|
|||||||
oldPasswordController.clear();
|
oldPasswordController.clear();
|
||||||
newPasswordController.clear();
|
newPasswordController.clear();
|
||||||
confirmPasswordController.clear();
|
confirmPasswordController.clear();
|
||||||
_setState(EditState.view);
|
_setState(ControlState.enabled);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_setError(_errorMessageForException(e, errorText));
|
_setError(_errorMessageForException(e, errorText));
|
||||||
_setState(EditState.edit);
|
_setState(ControlState.enabled);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,21 +65,23 @@ class PasswordFormController extends ChangeNotifier {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setState(EditState value) {
|
void _setState(ControlState value) {
|
||||||
if (_state == value || _disposed) return;
|
if (_formState == value || _isDisposed) return;
|
||||||
_state = value;
|
_formState = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setError(String value) {
|
void _setError(String value) {
|
||||||
if (_disposed) return;
|
if (_isDisposed) return;
|
||||||
_errorText = value;
|
_errorText = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _isDisposed => _lifecycleState == ControllerLifecycleState.disposed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_disposed = true;
|
_lifecycleState = ControllerLifecycleState.disposed;
|
||||||
oldPasswordController.dispose();
|
oldPasswordController.dispose();
|
||||||
newPasswordController.dispose();
|
newPasswordController.dispose();
|
||||||
confirmPasswordController.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",
|
"settingsSuccessfullyUpdated": "Settings successfully updated",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"failedToUpdateLanguage": "Failed to update language",
|
"failedToUpdateLanguage": "Failed to update language",
|
||||||
|
"editName": "Edit name",
|
||||||
"settingsImageUpdateError": "Couldn't update the image",
|
"settingsImageUpdateError": "Couldn't update the image",
|
||||||
"settingsImageTitle": "Image",
|
"settingsImageTitle": "Image",
|
||||||
"settingsImageHint": "Tap to change the image",
|
"settingsImageHint": "Tap to change the image",
|
||||||
|
|||||||
@@ -225,6 +225,7 @@
|
|||||||
"settingsSuccessfullyUpdated": "Настройки успешно обновлены",
|
"settingsSuccessfullyUpdated": "Настройки успешно обновлены",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"failedToUpdateLanguage": "Не удалось обновить язык",
|
"failedToUpdateLanguage": "Не удалось обновить язык",
|
||||||
|
"editName": "Изменить имя",
|
||||||
"settingsImageUpdateError": "Не удалось обновить изображение",
|
"settingsImageUpdateError": "Не удалось обновить изображение",
|
||||||
"settingsImageTitle": "Изображение",
|
"settingsImageTitle": "Изображение",
|
||||||
"settingsImageHint": "Нажмите, чтобы изменить изображение",
|
"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';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
class AccountLoader extends StatefulWidget {
|
class AccountLoader extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
const AccountLoader({super.key, required this.child});
|
const AccountLoader({super.key, required this.child});
|
||||||
|
|||||||
@@ -8,21 +8,12 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
|
|
||||||
class LocalePicker extends StatelessWidget {
|
class LocalePicker extends StatelessWidget {
|
||||||
final String title;
|
const LocalePicker({super.key});
|
||||||
|
|
||||||
const LocalePicker({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const double _pickerWidth = 300;
|
static const double _pickerWidth = 300;
|
||||||
static const double _iconSize = 20;
|
|
||||||
static const double _gapMedium = 6;
|
|
||||||
static const double _gapLarge = 8;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
return Consumer<LocaleProvider>(
|
return Consumer<LocaleProvider>(
|
||||||
@@ -32,18 +23,7 @@ class LocalePicker extends StatelessWidget {
|
|||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _pickerWidth,
|
width: _pickerWidth,
|
||||||
child: Column(
|
child: DropdownButtonFormField<Locale>(
|
||||||
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>(
|
|
||||||
initialValue: currentLocale,
|
initialValue: currentLocale,
|
||||||
items: options
|
items: options
|
||||||
.map(
|
.map(
|
||||||
@@ -58,11 +38,7 @@ class LocalePicker extends StatelessWidget {
|
|||||||
localeProvider.setLocale(locale);
|
localeProvider.setLocale(locale);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ class AccountNameActions extends StatelessWidget {
|
|||||||
final state = context.watch<AccountNameController>();
|
final state = context.watch<AccountNameController>();
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
if (state.isEditing) {
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -23,10 +22,12 @@ class AccountNameActions extends StatelessWidget {
|
|||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
final wasSaved = await state.save();
|
final wasSaved = await state.save();
|
||||||
if (!context.mounted || wasSaved || state.errorText.isEmpty) return;
|
if (!context.mounted || wasSaved || state.errorText.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
return;
|
||||||
SnackBar(content: Text(state.errorText)),
|
}
|
||||||
);
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(state.errorText)));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
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:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/provider/account.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/controllers/auth/account_name.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/actions.dart';
|
||||||
import 'package:pweb/pages/settings/profile/account/name/text.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 {
|
class _AccountNameConstants {
|
||||||
static const inputWidth = 200.0;
|
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 errorSpacing = 4.0;
|
||||||
static const borderWidth = 2.0;
|
static const borderWidth = 2.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountName extends StatelessWidget {
|
class AccountName extends StatelessWidget {
|
||||||
final String firstName;
|
|
||||||
final String lastName;
|
|
||||||
final String title;
|
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final String lastNameHint;
|
final String lastNameHint;
|
||||||
final String errorText;
|
|
||||||
|
|
||||||
const AccountName({
|
const AccountName({
|
||||||
super.key,
|
super.key,
|
||||||
required this.firstName,
|
|
||||||
required this.lastName,
|
|
||||||
required this.title,
|
|
||||||
required this.hintText,
|
required this.hintText,
|
||||||
required this.lastNameHint,
|
required this.lastNameHint,
|
||||||
required this.errorText,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProxyProvider<AccountProvider, AccountNameController>(
|
return _AccountNameBody(hintText: hintText, lastNameHint: lastNameHint);
|
||||||
create: (_) => AccountNameController(
|
|
||||||
initialFirstName: firstName,
|
|
||||||
initialLastName: lastName,
|
|
||||||
errorMessage: errorText,
|
|
||||||
),
|
|
||||||
update: (_, accountProvider, controller) =>
|
|
||||||
controller!..update(accountProvider),
|
|
||||||
child: _AccountNameBody(
|
|
||||||
hintText: hintText,
|
|
||||||
lastNameHint: lastNameHint,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccountNameBody extends StatelessWidget {
|
class _AccountNameBody extends StatelessWidget {
|
||||||
const _AccountNameBody({
|
const _AccountNameBody({required this.hintText, required this.lastNameHint});
|
||||||
required this.hintText,
|
|
||||||
required this.lastNameHint,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final String lastNameHint;
|
final String lastNameHint;
|
||||||
@@ -70,16 +47,27 @@ class _AccountNameBody extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(width: _AccountNameConstants.actionsSlotWidth),
|
||||||
AccountNameText(
|
AccountNameText(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
lastNameHint: lastNameHint,
|
lastNameHint: lastNameHint,
|
||||||
inputWidth: _AccountNameConstants.inputWidth,
|
inputWidth: _AccountNameConstants.inputWidth,
|
||||||
borderWidth: _AccountNameConstants.borderWidth,
|
borderWidth: _AccountNameConstants.borderWidth,
|
||||||
),
|
),
|
||||||
const SizedBox(width: _AccountNameConstants.spacing),
|
SizedBox(
|
||||||
const AccountNameActions(),
|
width: _AccountNameConstants.actionsSlotWidth,
|
||||||
|
child: state.isEditing
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: _AccountNameConstants.actionsSpacing,
|
||||||
|
),
|
||||||
|
child: AccountNameActions(),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: _AccountNameConstants.errorSpacing),
|
const SizedBox(height: _AccountNameConstants.errorSpacing),
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ class AccountNameSingleLineText extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.style,
|
required this.style,
|
||||||
|
this.textAlign = TextAlign.start,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
|
final TextAlign textAlign;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -18,6 +20,7 @@ class AccountNameSingleLineText extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: textAlign,
|
||||||
style: style,
|
style: style,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class AccountNameViewText extends StatelessWidget {
|
|||||||
child: AccountNameSingleLineText(
|
child: AccountNameSingleLineText(
|
||||||
text: hintText,
|
text: hintText,
|
||||||
style: firstLineStyle,
|
style: firstLineStyle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,7 @@ class AccountNameViewText extends StatelessWidget {
|
|||||||
child: AccountNameSingleLineText(
|
child: AccountNameSingleLineText(
|
||||||
text: singleLineName,
|
text: singleLineName,
|
||||||
style: firstLineStyle,
|
style: firstLineStyle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,15 +58,17 @@ class AccountNameViewText extends StatelessWidget {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: inputWidth,
|
width: inputWidth,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
AccountNameSingleLineText(
|
AccountNameSingleLineText(
|
||||||
text: trimmedFirstName,
|
text: trimmedFirstName,
|
||||||
style: firstLineStyle,
|
style: firstLineStyle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
AccountNameSingleLineText(
|
AccountNameSingleLineText(
|
||||||
text: trimmedLastName,
|
text: trimmedLastName,
|
||||||
style: secondLineStyle,
|
style: secondLineStyle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
class AccountPasswordContent extends StatelessWidget {
|
class AccountPasswordContent extends StatelessWidget {
|
||||||
const AccountPasswordContent({
|
const AccountPasswordContent({
|
||||||
required this.title,
|
|
||||||
required this.successText,
|
required this.successText,
|
||||||
required this.errorText,
|
required this.errorText,
|
||||||
required this.oldPasswordLabel,
|
required this.oldPasswordLabel,
|
||||||
@@ -22,7 +21,6 @@ class AccountPasswordContent extends StatelessWidget {
|
|||||||
required this.loc,
|
required this.loc,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
|
||||||
final String successText;
|
final String successText;
|
||||||
final String errorText;
|
final String errorText;
|
||||||
final String oldPasswordLabel;
|
final String oldPasswordLabel;
|
||||||
@@ -33,22 +31,9 @@ class AccountPasswordContent extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Consumer2<AccountProvider, PasswordFormController>(
|
return Consumer2<AccountProvider, PasswordFormController>(
|
||||||
builder: (context, accountProvider, formProvider, _) {
|
builder: (context, accountProvider, formProvider, _) {
|
||||||
final isBusy = accountProvider.isLoading || formProvider.isSaving;
|
return PasswordForm(
|
||||||
|
|
||||||
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(
|
|
||||||
formProvider: formProvider,
|
formProvider: formProvider,
|
||||||
accountProvider: accountProvider,
|
accountProvider: accountProvider,
|
||||||
isBusy: accountProvider.isLoading,
|
isBusy: accountProvider.isLoading,
|
||||||
@@ -59,8 +44,6 @@ class AccountPasswordContent extends StatelessWidget {
|
|||||||
successText: successText,
|
successText: successText,
|
||||||
errorText: errorText,
|
errorText: errorText,
|
||||||
loc: loc,
|
loc: loc,
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/provider/account.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:pshared/utils/snackbar.dart';
|
||||||
|
|
||||||
import 'package:pweb/models/auth/password_field_type.dart';
|
|
||||||
import 'package:pweb/controllers/auth/password_form.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/error_text.dart';
|
||||||
import 'package:pweb/pages/settings/profile/account/password/form/submit_button.dart';
|
import 'package:pweb/pages/settings/profile/account/password/form/submit_button.dart';
|
||||||
import 'package:pweb/utils/error/snackbar.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';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -46,6 +49,9 @@ class PasswordForm extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isFormBusy = isBusy || formProvider.isSaving;
|
final isFormBusy = isBusy || formProvider.isSaving;
|
||||||
|
final controlState = isFormBusy
|
||||||
|
? ControlState.disabled
|
||||||
|
: ControlState.enabled;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -54,30 +60,38 @@ class PasswordForm extends StatelessWidget {
|
|||||||
key: formProvider.formKey,
|
key: formProvider.formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
PasswordFields(
|
PasswordField(
|
||||||
oldPasswordController: formProvider.oldPasswordController,
|
controller: formProvider.oldPasswordController,
|
||||||
newPasswordController: formProvider.newPasswordController,
|
labelText: oldPasswordLabel,
|
||||||
confirmPasswordController: formProvider.confirmPasswordController,
|
|
||||||
oldPasswordLabel: oldPasswordLabel,
|
|
||||||
newPasswordLabel: newPasswordLabel,
|
|
||||||
confirmPasswordLabel: confirmPasswordLabel,
|
|
||||||
missingPasswordError: loc.errorPasswordMissing,
|
|
||||||
passwordsDoNotMatchError: loc.passwordsDoNotMatch,
|
|
||||||
fieldWidth: _fieldWidth,
|
fieldWidth: _fieldWidth,
|
||||||
gapSmall: _gapSmall,
|
isEnabled: controlState == ControlState.enabled,
|
||||||
isEnabled: !isFormBusy,
|
obscureText:
|
||||||
showOldPassword:
|
formProvider.oldPasswordVisibility !=
|
||||||
formProvider.isPasswordVisible(PasswordFieldType.old),
|
VisibilityState.visible,
|
||||||
showNewPassword:
|
onToggleVisibility: formProvider.toggleOldPasswordVisibility,
|
||||||
formProvider.isPasswordVisible(PasswordFieldType.newPassword),
|
validator: (value) => (value == null || value.isEmpty)
|
||||||
showConfirmPassword: formProvider
|
? loc.errorPasswordMissing
|
||||||
.isPasswordVisible(PasswordFieldType.confirmPassword),
|
: null,
|
||||||
onToggleOldPassword: () =>
|
),
|
||||||
formProvider.togglePasswordVisibility(PasswordFieldType.old),
|
const SizedBox(height: _gapSmall),
|
||||||
onToggleNewPassword: () => formProvider
|
SizedBox(
|
||||||
.togglePasswordVisibility(PasswordFieldType.newPassword),
|
width: _fieldWidth,
|
||||||
onToggleConfirmPassword: () => formProvider
|
child: PasswordUiController(
|
||||||
.togglePasswordVisibility(PasswordFieldType.confirmPassword),
|
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),
|
const SizedBox(height: _gapMedium),
|
||||||
PasswordSubmitButton(
|
PasswordSubmitButton(
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
|
|
||||||
class AccountPassword extends StatelessWidget {
|
class AccountPassword extends StatelessWidget {
|
||||||
final String title;
|
|
||||||
final String successText;
|
final String successText;
|
||||||
final String errorText;
|
final String errorText;
|
||||||
final String oldPasswordLabel;
|
final String oldPasswordLabel;
|
||||||
@@ -19,7 +18,6 @@ class AccountPassword extends StatelessWidget {
|
|||||||
|
|
||||||
const AccountPassword({
|
const AccountPassword({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
|
||||||
required this.successText,
|
required this.successText,
|
||||||
required this.errorText,
|
required this.errorText,
|
||||||
required this.oldPasswordLabel,
|
required this.oldPasswordLabel,
|
||||||
@@ -35,7 +33,6 @@ class AccountPassword extends StatelessWidget {
|
|||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (_) => PasswordFormController(),
|
create: (_) => PasswordFormController(),
|
||||||
child: AccountPasswordContent(
|
child: AccountPasswordContent(
|
||||||
title: title,
|
|
||||||
successText: successText,
|
successText: successText,
|
||||||
errorText: errorText,
|
errorText: errorText,
|
||||||
oldPasswordLabel: oldPasswordLabel,
|
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: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/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/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';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -15,7 +15,10 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
class ProfileSettingsPage extends StatelessWidget {
|
class ProfileSettingsPage extends StatelessWidget {
|
||||||
const ProfileSettingsPage({super.key});
|
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 _cardRadius = 16.0;
|
||||||
static const _itemSpacing = 12.0;
|
static const _itemSpacing = 12.0;
|
||||||
|
|
||||||
@@ -33,7 +36,15 @@ class ProfileSettingsPage extends StatelessWidget {
|
|||||||
(provider) => provider.account?.avatarUrl,
|
(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,
|
alignment: Alignment.topCenter,
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
@@ -52,29 +63,26 @@ class ProfileSettingsPage extends StatelessWidget {
|
|||||||
errorText: loc.avatarUpdateError,
|
errorText: loc.avatarUpdateError,
|
||||||
),
|
),
|
||||||
AccountName(
|
AccountName(
|
||||||
firstName: accountFirstName ?? '',
|
|
||||||
lastName: accountLastName ?? '',
|
|
||||||
title: loc.accountName,
|
|
||||||
hintText: loc.accountNameHint,
|
hintText: loc.accountNameHint,
|
||||||
lastNameHint: loc.lastName,
|
lastNameHint: loc.lastName,
|
||||||
errorText: loc.accountNameUpdateError,
|
|
||||||
),
|
),
|
||||||
AccountPassword(
|
SizedBox(height: _itemSpacing),
|
||||||
title: loc.changePassword,
|
ProfileActionsSection(
|
||||||
successText: loc.changePasswordSuccess,
|
nameLabel: loc.editName,
|
||||||
errorText: loc.changePasswordError,
|
languageLabel: loc.language,
|
||||||
|
passwordLabel: loc.changePassword,
|
||||||
|
passwordSuccessText: loc.changePasswordSuccess,
|
||||||
|
passwordErrorText: loc.changePasswordError,
|
||||||
oldPasswordLabel: loc.oldPassword,
|
oldPasswordLabel: loc.oldPassword,
|
||||||
newPasswordLabel: loc.newPassword,
|
newPasswordLabel: loc.newPassword,
|
||||||
confirmPasswordLabel: loc.confirmPassword,
|
confirmPasswordLabel: loc.confirmPassword,
|
||||||
savePassword: loc.savePassword,
|
savePasswordLabel: loc.savePassword,
|
||||||
),
|
|
||||||
LocalePicker(
|
|
||||||
title: loc.language,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:pweb/pages/signup/form/controllers.dart';
|
import 'package:pweb/pages/signup/form/controllers.dart';
|
||||||
import 'package:pweb/pages/signup/form/description.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/pages/signup/header.dart';
|
||||||
|
import 'package:pweb/widgets/password/ui_controller.dart';
|
||||||
import 'package:pweb/widgets/password/verify.dart';
|
import 'package:pweb/widgets/password/verify.dart';
|
||||||
import 'package:pweb/widgets/text_field.dart';
|
import 'package:pweb/widgets/text_field.dart';
|
||||||
|
import 'package:pweb/widgets/username.dart';
|
||||||
import 'package:pweb/widgets/vspacer.dart';
|
import 'package:pweb/widgets/vspacer.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
@@ -47,7 +47,7 @@ class SignUpFormFields extends StatelessWidget {
|
|||||||
const VSpacer(),
|
const VSpacer(),
|
||||||
UsernameField(controller: controllers.email),
|
UsernameField(controller: controllers.email),
|
||||||
const VSpacer(),
|
const VSpacer(),
|
||||||
SignUpPasswordUiController(controller: controllers.password),
|
PasswordUiController(controller: controllers.password),
|
||||||
const VSpacer(multiplier: 2.0),
|
const VSpacer(multiplier: 2.0),
|
||||||
VerifyPasswordField(
|
VerifyPasswordField(
|
||||||
controller: controllers.passwordConfirm,
|
controller: controllers.passwordConfirm,
|
||||||
|
|||||||
@@ -3,23 +3,29 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fancy_password_field/fancy_password_field.dart';
|
import 'package:fancy_password_field/fancy_password_field.dart';
|
||||||
|
|
||||||
import 'package:pweb/config/constants.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/widgets/password/password.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
class SignUpPasswordUiController extends StatefulWidget {
|
class PasswordUiController extends StatefulWidget {
|
||||||
final TextEditingController controller;
|
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
|
@override
|
||||||
State<SignUpPasswordUiController> createState() =>
|
State<PasswordUiController> createState() => _PasswordUiControllerState();
|
||||||
_SignUpPasswordUiControllerState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SignUpPasswordUiControllerState
|
class _PasswordUiControllerState extends State<PasswordUiController> {
|
||||||
extends State<SignUpPasswordUiController> {
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -36,9 +42,11 @@ class _SignUpPasswordUiControllerState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final loc = AppLocalizations.of(context)!;
|
final isEnabled = widget.state == ControlState.enabled;
|
||||||
final specialRule = _SpecialCharacterValidationRule(
|
final specialRule = _SpecialCharacterValidationRule(
|
||||||
customText: loc.passwordValidationRuleSpecialCharacter,
|
customText: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.passwordValidationRuleSpecialCharacter,
|
||||||
);
|
);
|
||||||
final value = widget.controller.text;
|
final value = widget.controller.text;
|
||||||
final missing = _allRules(context, specialRule)
|
final missing = _allRules(context, specialRule)
|
||||||
@@ -50,15 +58,24 @@ class _SignUpPasswordUiControllerState
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Theme(
|
IgnorePointer(
|
||||||
data: hasMissingRules ? _invalidTheme(context) : Theme.of(context),
|
ignoring: !isEnabled,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: isEnabled ? 1 : 0.6,
|
||||||
|
child: Theme(
|
||||||
|
data: hasMissingRules
|
||||||
|
? _invalidTheme(context)
|
||||||
|
: Theme.of(context),
|
||||||
child: defaulRulesPasswordField(
|
child: defaulRulesPasswordField(
|
||||||
context,
|
context,
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
|
labelText: widget.labelText,
|
||||||
additionalRules: {specialRule},
|
additionalRules: {specialRule},
|
||||||
validationRuleBuilder: (_, _) => const SizedBox.shrink(),
|
validationRuleBuilder: (_, _) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (hasMissingRules) ...[
|
if (hasMissingRules) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
...missing.map(
|
...missing.map(
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'package:fancy_password_field/fancy_password_field.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/error.dart';
|
||||||
import 'package:pweb/widgets/password/hint/short.dart';
|
import 'package:pweb/widgets/password/hint/short.dart';
|
||||||
import 'package:pweb/widgets/password/password.dart';
|
import 'package:pweb/widgets/password/password.dart';
|
||||||
@@ -32,12 +33,16 @@ class VerifyPasswordField extends StatefulWidget {
|
|||||||
final ValueChanged<bool>? onValid;
|
final ValueChanged<bool>? onValid;
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final TextEditingController externalPasswordController;
|
final TextEditingController externalPasswordController;
|
||||||
|
final String? labelText;
|
||||||
|
final ControlState state;
|
||||||
|
|
||||||
const VerifyPasswordField({
|
const VerifyPasswordField({
|
||||||
super.key,
|
super.key,
|
||||||
this.onValid,
|
this.onValid,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.externalPasswordController,
|
required this.externalPasswordController,
|
||||||
|
this.labelText,
|
||||||
|
this.state = ControlState.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -55,7 +60,8 @@ class _VerifyPasswordFieldState extends State<VerifyPasswordField> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _validatePassword() {
|
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
|
// Only call onValid if the validity state has changed to prevent infinite loops
|
||||||
if (isValid != _isCurrentlyValid) {
|
if (isValid != _isCurrentlyValid) {
|
||||||
@@ -68,21 +74,31 @@ class _VerifyPasswordFieldState extends State<VerifyPasswordField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isEnabled = widget.state == ControlState.enabled;
|
||||||
final rule = PasswordVeirificationRule(
|
final rule = PasswordVeirificationRule(
|
||||||
ruleName: AppLocalizations.of(context)!.passwordsDoNotMatch,
|
ruleName: AppLocalizations.of(context)!.passwordsDoNotMatch,
|
||||||
externalPasswordController: widget.externalPasswordController,
|
externalPasswordController: widget.externalPasswordController,
|
||||||
);
|
);
|
||||||
|
|
||||||
return defaulRulesPasswordField(
|
return IgnorePointer(
|
||||||
|
ignoring: !isEnabled,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: isEnabled ? 1 : 0.6,
|
||||||
|
child: defaulRulesPasswordField(
|
||||||
context,
|
context,
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
key: widget.key,
|
key: widget.key,
|
||||||
labelText: AppLocalizations.of(context)!.confirmPassword,
|
labelText:
|
||||||
|
widget.labelText ?? AppLocalizations.of(context)!.confirmPassword,
|
||||||
additionalRules: {rule},
|
additionalRules: {rule},
|
||||||
validationRuleBuilder: (rules, value) => rule.validate(value)
|
validationRuleBuilder: (rules, value) => rule.validate(value)
|
||||||
? shortValidation(context, rules, value)
|
? shortValidation(context, rules, value)
|
||||||
: PasswordValidationErrorLabel(labelText: AppLocalizations.of(context)!.passwordsDoNotMatch),
|
: PasswordValidationErrorLabel(
|
||||||
|
labelText: AppLocalizations.of(context)!.passwordsDoNotMatch,
|
||||||
|
),
|
||||||
onValid: widget.onValid,
|
onValid: widget.onValid,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user