2 Commits

Author SHA1 Message Date
Arseni
ed8596a81e deleted unnecessaryfiles 2026-03-13 23:06:23 +03:00
Arseni
d601f245d4 redesign for settings page 2026-03-13 23:01:57 +03:00
30 changed files with 715 additions and 262 deletions

View File

@@ -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();

View 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();
}
}

View File

@@ -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",

View File

@@ -225,6 +225,7 @@
"settingsSuccessfullyUpdated": "Настройки успешно обновлены",
"language": "Язык",
"failedToUpdateLanguage": "Не удалось обновить язык",
"editName": "Изменить имя",
"settingsImageUpdateError": "Не удалось обновить изображение",
"settingsImageTitle": "Изображение",
"settingsImageHint": "Нажмите, чтобы изменить изображение",

View File

@@ -1 +0,0 @@
enum PasswordFieldType { old, newPassword, confirmPassword }

View File

@@ -0,0 +1 @@
enum ProfileActionSection { language, password }

View File

@@ -0,0 +1 @@
enum ControllerLifecycleState { active, disposed }

View File

@@ -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});

View File

@@ -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()),
),
);
},

View File

@@ -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,
);
}
}

View File

@@ -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),

View File

@@ -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,
);
}

View File

@@ -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,
),
],
),

View File

@@ -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,
),
],
);
},
);

View File

@@ -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(

View File

@@ -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,

View 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,
),
],
],
);
}
}

View 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,
),
),
],
),
);
}
}

View File

@@ -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),
],
);
}
}

View File

@@ -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,
),
],
];
}
}

View File

@@ -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;
}

View File

@@ -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,
);
}
}
}

View File

@@ -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),
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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),
);
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -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,
),
],
),
),
),
),
);
}
}

View File

@@ -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,

View File

@@ -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(

View File

@@ -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,
),
),
);
}