Updated Settings Page

This commit is contained in:
Arseni
2025-12-18 15:15:33 +03:00
parent d649748f6f
commit 0ecd17d2dc
15 changed files with 679 additions and 110 deletions

View File

@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:pshared/provider/account.dart';
import 'package:pweb/providers/password_form.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PasswordForm extends StatelessWidget {
const PasswordForm({
super.key,
required this.formProvider,
required this.accountProvider,
required this.isBusy,
required this.oldPasswordLabel,
required this.newPasswordLabel,
required this.confirmPasswordLabel,
required this.savePassword,
required this.successText,
required this.errorText,
required this.loc,
});
static const double _fieldWidth = 320;
static const double _gapMedium = 12;
static const double _gapSmall = 8;
final PasswordFormProvider formProvider;
final AccountProvider accountProvider;
final bool isBusy;
final String oldPasswordLabel;
final String newPasswordLabel;
final String confirmPasswordLabel;
final String savePassword;
final String successText;
final String errorText;
final AppLocalizations loc;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isFormBusy = isBusy || formProvider.isSaving;
return Column(
children: [
const SizedBox(height: _gapMedium),
Form(
key: formProvider.formKey,
child: Column(
children: [
SizedBox(
width: _fieldWidth,
child: TextFormField(
controller: formProvider.oldPasswordController,
obscureText: true,
enabled: !isFormBusy,
decoration: InputDecoration(
labelText: oldPasswordLabel,
border: const OutlineInputBorder(),
),
validator: (value) =>
(value == null || value.isEmpty) ? loc.errorPasswordMissing : null,
),
),
const SizedBox(height: _gapSmall),
SizedBox(
width: _fieldWidth,
child: TextFormField(
controller: formProvider.newPasswordController,
obscureText: true,
enabled: !isFormBusy,
decoration: InputDecoration(
labelText: newPasswordLabel,
border: const OutlineInputBorder(),
),
validator: (value) =>
(value == null || value.isEmpty) ? loc.errorPasswordMissing : null,
),
),
const SizedBox(height: _gapSmall),
SizedBox(
width: _fieldWidth,
child: TextFormField(
controller: formProvider.confirmPasswordController,
obscureText: true,
enabled: !isFormBusy,
decoration: InputDecoration(
labelText: confirmPasswordLabel,
border: const OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) return loc.errorPasswordMissing;
if (value != formProvider.newPasswordController.text) {
return loc.passwordsDoNotMatch;
}
return null;
},
),
),
const SizedBox(height: _gapMedium),
ElevatedButton.icon(
onPressed: isFormBusy
? null
: () => formProvider.submit(
context: context,
accountProvider: accountProvider,
successText: successText,
errorText: errorText,
),
icon: const Icon(Icons.save_outlined),
label: Text(savePassword),
),
if (formProvider.errorText.isNotEmpty) ...[
const SizedBox(height: _gapSmall),
Text(
formProvider.errorText,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
],
],
),
),
],
);
}
}

View File

@@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/account.dart';
import 'package:pshared/utils/snackbar.dart';
import 'package:pweb/models/edit_state.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class AccountPassword extends StatefulWidget {
final String title;
final String successText;
final String errorText;
final String oldPasswordLabel;
final String newPasswordLabel;
final String confirmPasswordLabel;
final String savePassword;
const AccountPassword({
super.key,
required this.title,
required this.successText,
required this.errorText,
required this.oldPasswordLabel,
required this.newPasswordLabel,
required this.confirmPasswordLabel,
required this.savePassword,
});
@override
State<AccountPassword> createState() => _AccountPasswordState();
}
class _AccountPasswordState extends State<AccountPassword> {
static const double _fieldWidth = 320;
static const double _gapMedium = 12;
static const double _gapSmall = 8;
final _formKey = GlobalKey<FormState>();
final _oldPasswordController = TextEditingController();
final _newPasswordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
EditState _state = EditState.view;
String _errorText = '';
bool get _isSaving => _state == EditState.saving;
bool get _isExpanded => _state != EditState.view;
@override
void dispose() {
_oldPasswordController.dispose();
_newPasswordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _changePassword(AccountProvider provider) async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_state = EditState.saving;
_errorText = '';
});
try {
await provider.changePassword(_oldPasswordController.text, _newPasswordController.text);
if (!mounted) return;
_oldPasswordController.clear();
_newPasswordController.clear();
_confirmPasswordController.clear();
notifyUser(context, widget.successText);
} catch (e) {
if (!mounted) return;
setState(() => _errorText = widget.errorText);
await postNotifyUserOfErrorX(
context: context,
errorSituation: widget.errorText,
exception: e,
);
} finally {
if (mounted) {
setState(() => _state = EditState.edit);
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
return Consumer<AccountProvider>(
builder: (context, provider, _) {
final isBusy = provider.isLoading || _isSaving;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextButton.icon(
onPressed: isBusy
? null
: () => setState(() {
_state = _isExpanded ? EditState.view : EditState.edit;
_errorText = '';
}),
icon: Icon(Icons.lock_outline, color: theme.colorScheme.primary),
label: Text(widget.title, style: theme.textTheme.bodyMedium),
),
if (_isExpanded) ...[
const SizedBox(height: _gapMedium),
Form(
key: _formKey,
child: Column(
children: [
SizedBox(
width: _fieldWidth,
child: TextFormField(
controller: _oldPasswordController,
obscureText: true,
enabled: !isBusy,
decoration: InputDecoration(
labelText: widget.oldPasswordLabel,
border: const OutlineInputBorder(),
),
validator: (value) => (value == null || value.isEmpty) ? loc.errorPasswordMissing : null,
),
),
const SizedBox(height: _gapSmall),
SizedBox(
width: _fieldWidth,
child: TextFormField(
controller: _newPasswordController,
obscureText: true,
enabled: !isBusy,
decoration: InputDecoration(
labelText: widget.newPasswordLabel,
border: const OutlineInputBorder(),
),
validator: (value) => (value == null || value.isEmpty) ? loc.errorPasswordMissing : null,
),
),
const SizedBox(height: _gapSmall),
SizedBox(
width: _fieldWidth,
child: TextFormField(
controller: _confirmPasswordController,
obscureText: true,
enabled: !isBusy,
decoration: InputDecoration(
labelText: widget.confirmPasswordLabel,
border: const OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) return loc.errorPasswordMissing;
if (value != _newPasswordController.text) return loc.passwordsDoNotMatch;
return null;
},
),
),
const SizedBox(height: _gapMedium),
ElevatedButton.icon(
onPressed: isBusy ? null : () => _changePassword(provider),
icon: const Icon(Icons.save_outlined),
label: Text(widget.savePassword),
),
if (_errorText.isNotEmpty) ...[
const SizedBox(height: _gapSmall),
Text(
_errorText,
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.error),
),
],
],
),
),
],
],
);
},
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class PasswordToggleButton extends StatelessWidget {
const PasswordToggleButton({
super.key,
required this.title,
required this.isExpanded,
required this.isBusy,
required this.onToggle,
});
final String title;
final bool isExpanded;
final bool isBusy;
final VoidCallback onToggle;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconColor = theme.colorScheme.primary;
return TextButton.icon(
onPressed: isBusy
? null
: () {
onToggle();
},
icon: Icon(
isExpanded ? Icons.lock_open : Icons.lock_outline,
color: iconColor,
),
label: Text(title, style: theme.textTheme.bodyMedium),
);
}
}