Files
pyramid/lib/widgets/settings/settings_account.dart
Bernd Steckmeister 5d772389d6 refactor: Gott-Datei settings_modal.dart in 12 Dateien aufgeteilt
Reine Verschiebung per Dart part/part-of (keine Umbenennung, keine
Logikänderung) - settings_modal.dart schrumpft von 5541 auf 270 Zeilen.
Verifiziert per automatisiertem Blockvergleich gegen das Original und
flutter analyze (Baseline unverändert: 1 bekannter Hinweis).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 10:56:48 +02:00

365 lines
14 KiB
Dart

part of '../settings_modal.dart';
class _ChangePasswordDialog extends ConsumerStatefulWidget {
final PyramidTheme pt;
const _ChangePasswordDialog({required this.pt});
@override
ConsumerState<_ChangePasswordDialog> createState() => _ChangePasswordDialogState();
}
class _ChangePasswordDialogState extends ConsumerState<_ChangePasswordDialog> {
final _oldCtrl = TextEditingController();
final _newCtrl = TextEditingController();
final _confirmCtrl = TextEditingController();
bool _oldVisible = false;
bool _newVisible = false;
bool _loading = false;
String? _error;
bool _done = false;
@override
void dispose() {
_oldCtrl.dispose();
_newCtrl.dispose();
_confirmCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
final oldPw = _oldCtrl.text;
final newPw = _newCtrl.text;
final confirm = _confirmCtrl.text;
if (oldPw.isEmpty || newPw.isEmpty) {
setState(() => _error = 'Bitte alle Felder ausfüllen.');
return;
}
if (newPw != confirm) {
setState(() => _error = 'Die neuen Passwörter stimmen nicht überein.');
return;
}
if (newPw.length < 8) {
setState(() => _error = 'Das neue Passwort muss mindestens 8 Zeichen lang sein.');
return;
}
setState(() { _loading = true; _error = null; });
try {
final client = await ref.read(matrixClientProvider.future);
await client.changePassword(newPw, oldPassword: oldPw);
if (mounted) setState(() { _loading = false; _done = true; });
} on MatrixException catch (e) {
if (mounted) setState(() { _loading = false; _error = e.errorMessage; });
} catch (e) {
if (mounted) setState(() { _loading = false; _error = e.toString().split('\n').first; });
}
}
@override
Widget build(BuildContext context) {
final pt = widget.pt;
return Dialog(
backgroundColor: pt.bg2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(pt.rXl), side: BorderSide(color: pt.border)),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420, minWidth: 300),
child: Padding(
padding: const EdgeInsets.all(24),
child: _done ? _buildDone(pt) : _buildForm(pt),
),
),
);
}
Widget _buildDone(PyramidTheme pt) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle_outline_rounded, color: PyramidColors.success, size: 52),
const SizedBox(height: 12),
Text('Passwort geändert', style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
Text('Dein Passwort wurde erfolgreich aktualisiert.',
textAlign: TextAlign.center, style: TextStyle(color: pt.fgMuted, fontSize: 13)),
const SizedBox(height: 20),
SizedBox(width: double.infinity, child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: pt.accent, foregroundColor: pt.accentFg,
elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: () => Navigator.of(context).pop(),
child: const Text('Schließen'),
)),
],
);
Widget _buildForm(PyramidTheme pt) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(Icons.lock_outline_rounded, size: 18, color: pt.accent),
const SizedBox(width: 8),
Text('Passwort ändern', style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)),
const Spacer(),
IconButton(
icon: Icon(Icons.close_rounded, color: pt.fgDim, size: 16),
onPressed: () => Navigator.of(context).pop(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(maxWidth: 28, maxHeight: 28),
),
]),
const SizedBox(height: 20),
_PwField(ctrl: _oldCtrl, label: 'Aktuelles Passwort', pt: pt,
visible: _oldVisible, onToggle: () => setState(() => _oldVisible = !_oldVisible)),
const SizedBox(height: 12),
_PwField(ctrl: _newCtrl, label: 'Neues Passwort', pt: pt,
visible: _newVisible, onToggle: () => setState(() => _newVisible = !_newVisible)),
const SizedBox(height: 12),
_PwField(ctrl: _confirmCtrl, label: 'Neues Passwort bestätigen', pt: pt,
visible: _newVisible, onToggle: null,
onSubmitted: (_) => _submit()),
if (_error != null) ...[
const SizedBox(height: 10),
_StatusCard(pt: pt, color: pt.danger, icon: Icons.error_outline_rounded, text: _error!),
],
const SizedBox(height: 20),
SizedBox(width: double.infinity, child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: pt.accent, foregroundColor: pt.accentFg,
elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator.adaptive(strokeWidth: 2))
: const Text('Passwort ändern'),
)),
],
);
}
class _DeleteAccountDialog extends ConsumerStatefulWidget {
final PyramidTheme pt;
const _DeleteAccountDialog({required this.pt});
@override
ConsumerState<_DeleteAccountDialog> createState() => _DeleteAccountDialogState();
}
enum _DeleteStep { confirm, auth, deleting, error }
class _DeleteAccountDialogState extends ConsumerState<_DeleteAccountDialog> {
_DeleteStep _step = _DeleteStep.confirm;
final _pwCtrl = TextEditingController();
bool _pwVisible = false;
bool _eraseData = true;
String? _error;
@override
void dispose() { _pwCtrl.dispose(); super.dispose(); }
Future<void> _proceed() async {
setState(() { _step = _DeleteStep.deleting; _error = null; });
final client = await ref.read(matrixClientProvider.future);
try {
await client.deactivateAccount(erase: _eraseData);
} on MatrixException catch (e) {
if (e.requireAdditionalAuthentication && mounted) {
// UIA required — try with password
try {
await client.deactivateAccount(
erase: _eraseData,
auth: AuthenticationData(
type: AuthenticationTypes.password,
session: e.session,
additionalFields: {
'identifier': {'type': 'm.id.user', 'user': client.userID},
'password': _pwCtrl.text,
},
),
);
} on MatrixException catch (e2) {
if (mounted) setState(() { _step = _DeleteStep.error; _error = e2.errorMessage; });
return;
} catch (e2) {
if (mounted) setState(() { _step = _DeleteStep.error; _error = e2.toString().split('\n').first; });
return;
}
} else {
if (mounted) setState(() { _step = _DeleteStep.error; _error = e.errorMessage; });
return;
}
} catch (e) {
if (mounted) setState(() { _step = _DeleteStep.error; _error = e.toString().split('\n').first; });
return;
}
// Konto wurde gelöscht — ausloggen
unawaited(AuthLog.write('User-initiated logout (Konto gelöscht)'));
try { await client.logout(); } catch (_) {}
}
@override
Widget build(BuildContext context) {
final pt = widget.pt;
return Dialog(
backgroundColor: pt.bg2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(pt.rXl), side: BorderSide(color: pt.border)),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 440, minWidth: 300),
child: Padding(
padding: const EdgeInsets.all(24),
child: switch (_step) {
_DeleteStep.confirm => _buildConfirm(pt),
_DeleteStep.auth => _buildAuth(pt),
_DeleteStep.deleting => const Center(
child: Padding(padding: EdgeInsets.all(32), child: CircularProgressIndicator.adaptive()),
),
_DeleteStep.error => _buildError(pt),
},
),
),
);
}
Widget _buildConfirm(PyramidTheme pt) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(Icons.warning_rounded, size: 20, color: pt.danger),
const SizedBox(width: 8),
Text('Konto löschen', style: TextStyle(color: pt.danger, fontSize: 15, fontWeight: FontWeight.w700)),
const Spacer(),
IconButton(
icon: Icon(Icons.close_rounded, color: pt.fgDim, size: 16),
onPressed: () => Navigator.of(context).pop(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(maxWidth: 28, maxHeight: 28),
),
]),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: pt.danger.withAlpha(20),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: pt.danger.withAlpha(60)),
),
child: Text(
'Diese Aktion ist unwiderruflich. Dein Konto, alle Sitzungen und '
'deine Mitgliedschaft in Räumen werden dauerhaft gelöscht.',
style: TextStyle(color: pt.fg, fontSize: 13, height: 1.5),
),
),
const SizedBox(height: 16),
GestureDetector(
onTap: () => setState(() => _eraseData = !_eraseData),
child: Row(children: [
SizedBox(
width: 18, height: 18,
child: Checkbox(
value: _eraseData,
onChanged: (v) => setState(() => _eraseData = v ?? true),
activeColor: pt.accent,
side: BorderSide(color: pt.border),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
const SizedBox(width: 8),
Expanded(child: Text('Nachrichten und Daten vom Server löschen',
style: TextStyle(color: pt.fg, fontSize: 13))),
]),
),
const SizedBox(height: 20),
Row(children: [
Expanded(child: OutlinedButton(
style: OutlinedButton.styleFrom(foregroundColor: pt.fg, side: BorderSide(color: pt.border),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: () => Navigator.of(context).pop(),
child: const Text('Abbrechen'),
)),
const SizedBox(width: 12),
Expanded(child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: pt.danger, foregroundColor: Colors.white,
elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: () => setState(() => _step = _DeleteStep.auth),
child: const Text('Weiter'),
)),
]),
],
);
Widget _buildAuth(PyramidTheme pt) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(Icons.lock_outline_rounded, size: 18, color: pt.danger),
const SizedBox(width: 8),
Text('Konto löschen bestätigen', style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)),
]),
const SizedBox(height: 16),
Text('Gib dein Passwort ein, um die Löschung zu bestätigen.',
style: TextStyle(color: pt.fgMuted, fontSize: 13)),
const SizedBox(height: 16),
TextField(
controller: _pwCtrl,
autofocus: true,
obscureText: !_pwVisible,
style: TextStyle(color: pt.fg, fontSize: 13),
decoration: InputDecoration(
labelText: 'Passwort',
labelStyle: TextStyle(color: pt.fgMuted, fontSize: 13),
filled: true, fillColor: pt.bg3,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: pt.border)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: pt.danger)),
suffixIcon: IconButton(
icon: Icon(_pwVisible ? Icons.visibility_off_rounded : Icons.visibility_rounded, size: 16, color: pt.fgDim),
onPressed: () => setState(() => _pwVisible = !_pwVisible),
),
),
cursorColor: pt.danger,
onSubmitted: (_) => _proceed(),
),
const SizedBox(height: 20),
Row(children: [
Expanded(child: OutlinedButton(
style: OutlinedButton.styleFrom(foregroundColor: pt.fg, side: BorderSide(color: pt.border),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: () => setState(() => _step = _DeleteStep.confirm),
child: const Text('Zurück'),
)),
const SizedBox(width: 12),
Expanded(child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: pt.danger, foregroundColor: Colors.white,
elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: _proceed,
child: const Text('Konto endgültig löschen'),
)),
]),
],
);
Widget _buildError(PyramidTheme pt) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline_rounded, color: pt.danger, size: 48),
const SizedBox(height: 12),
Text(_error ?? 'Ein Fehler ist aufgetreten.',
textAlign: TextAlign.center, style: TextStyle(color: pt.fgMuted, fontSize: 13)),
const SizedBox(height: 20),
SizedBox(width: double.infinity, child: OutlinedButton(
style: OutlinedButton.styleFrom(foregroundColor: pt.fg, side: BorderSide(color: pt.border),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: () => Navigator.of(context).pop(),
child: const Text('Schließen'),
)),
],
);
}