redesign for settings page

This commit is contained in:
Arseni
2026-03-13 23:01:57 +03:00
parent 70bd7a6214
commit d601f245d4
36 changed files with 1151 additions and 262 deletions

View File

@@ -0,0 +1,152 @@
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 PasswordUiController extends StatefulWidget {
final TextEditingController controller;
final String? labelText;
final ControlState state;
const PasswordUiController({
required this.controller,
this.labelText,
this.state = ControlState.enabled,
super.key,
});
@override
State<PasswordUiController> createState() => _PasswordUiControllerState();
}
class _PasswordUiControllerState extends State<PasswordUiController> {
@override
void initState() {
super.initState();
widget.controller.addListener(_onPasswordChanged);
}
@override
void dispose() {
widget.controller.removeListener(_onPasswordChanged);
super.dispose();
}
void _onPasswordChanged() => setState(() {});
@override
Widget build(BuildContext context) {
final isEnabled = widget.state == ControlState.enabled;
final specialRule = _SpecialCharacterValidationRule(
customText: AppLocalizations.of(
context,
)!.passwordValidationRuleSpecialCharacter,
);
final value = widget.controller.text;
final missing = _allRules(context, specialRule)
.where((rule) => !rule.validate(value))
.map((rule) => rule.name)
.toList(growable: false);
final hasMissingRules = value.isNotEmpty && missing.isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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(
(ruleText) => Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
'$ruleText',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
),
],
],
);
}
List<ValidationRule> _allRules(
BuildContext context,
ValidationRule specialRule,
) {
final loc = AppLocalizations.of(context)!;
return [
DigitValidationRule(customText: loc.passwordValidationRuleDigit),
UppercaseValidationRule(customText: loc.passwordValidationRuleUpperCase),
LowercaseValidationRule(customText: loc.passwordValidationRuleLowerCase),
MinCharactersValidationRule(
Constants.minPasswordCharacters,
customText: loc.passwordValidationRuleMinCharacters(
Constants.minPasswordCharacters,
),
),
specialRule,
];
}
ThemeData _invalidTheme(BuildContext context) {
final theme = Theme.of(context);
final errorColor = theme.colorScheme.error;
final border = OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: errorColor, width: 1.2),
);
return theme.copyWith(
inputDecorationTheme: theme.inputDecorationTheme.copyWith(
enabledBorder: border,
focusedBorder: border,
errorBorder: border,
focusedErrorBorder: border,
),
);
}
}
class _SpecialCharacterValidationRule extends ValidationRule {
final String customText;
_SpecialCharacterValidationRule({required this.customText});
@override
String get name => customText;
@override
bool get showName => true;
@override
bool validate(String value) => value.runes.any(_isAsciiSpecialCharacter);
bool _isAsciiSpecialCharacter(int code) =>
(code >= 33 && code <= 47) ||
(code >= 58 && code <= 64) ||
(code >= 91 && code <= 96) ||
(code >= 123 && code <= 126);
}

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';
@@ -12,7 +13,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PasswordVeirificationRule extends ValidationRule {
final String ruleName;
final TextEditingController externalPasswordController;
PasswordVeirificationRule({
required this.ruleName,
required this.externalPasswordController,
@@ -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,
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(
context,
controller: widget.controller,
key: widget.key,
labelText: AppLocalizations.of(context)!.confirmPassword,
additionalRules: { rule },
validationRuleBuilder: (rules, value) => rule.validate(value)
? shortValidation(context, rules, value)
: PasswordValidationErrorLabel(labelText: AppLocalizations.of(context)!.passwordsDoNotMatch),
onValid: widget.onValid,
return IgnorePointer(
ignoring: !isEnabled,
child: Opacity(
opacity: isEnabled ? 1 : 0.6,
child: defaulRulesPasswordField(
context,
controller: widget.controller,
key: widget.key,
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,
),
onValid: widget.onValid,
),
),
);
}