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>
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
part of '../settings_modal.dart';
|
||||
|
||||
class _SessionsSection extends ConsumerStatefulWidget {
|
||||
final PyramidTheme pt;
|
||||
const _SessionsSection({required this.pt});
|
||||
|
||||
@override
|
||||
ConsumerState<_SessionsSection> createState() => _SessionsSectionState();
|
||||
}
|
||||
|
||||
class _SessionsSectionState extends ConsumerState<_SessionsSection> {
|
||||
List<DeviceKeys> _devices = [];
|
||||
bool _loading = true;
|
||||
String? _currentDeviceId;
|
||||
final Set<String> _revoking = {};
|
||||
bool _logoutAllLoading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() { _loading = true; _error = null; });
|
||||
try {
|
||||
final client = await ref.read(matrixClientProvider.future);
|
||||
_currentDeviceId = client.deviceID;
|
||||
await client.updateUserDeviceKeys(additionalUsers: {client.userID!});
|
||||
final keys = client.userDeviceKeys[client.userID];
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_devices = keys?.deviceKeys.values.toList() ?? [];
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_error = e.toString().split('\n').first;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _revokeDevice(String deviceId) async {
|
||||
setState(() => _revoking.add(deviceId));
|
||||
try {
|
||||
final client = await ref.read(matrixClientProvider.future);
|
||||
await client.deleteDevice(deviceId);
|
||||
await _load();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_revoking.remove(deviceId);
|
||||
_error = 'Abmelden fehlgeschlagen: ${e.toString().split('\n').first}';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _renameCurrentDevice(BuildContext context) async {
|
||||
final client = await ref.read(matrixClientProvider.future);
|
||||
if (!context.mounted) return;
|
||||
final currentName = _devices
|
||||
.where((d) => d.deviceId == _currentDeviceId)
|
||||
.map((d) => d.unsigned?['device_display_name'] as String? ?? d.deviceId ?? '')
|
||||
.firstOrNull ?? '';
|
||||
|
||||
final ctrl = TextEditingController(text: currentName);
|
||||
final pt = widget.pt;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rXl),
|
||||
side: BorderSide(color: pt.border)),
|
||||
title: Text('Gerät umbenennen',
|
||||
style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
autofocus: true,
|
||||
style: TextStyle(color: pt.fg, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Gerätename',
|
||||
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), borderSide: BorderSide(color: pt.border)),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: pt.border)),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: pt.accent)),
|
||||
),
|
||||
cursorColor: pt.accent,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: pt.accent, foregroundColor: pt.accentFg, elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Umbenennen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
ctrl.dispose();
|
||||
if (confirmed != true || _currentDeviceId == null) return;
|
||||
try {
|
||||
await client.updateDevice(_currentDeviceId!, displayName: ctrl.text.trim());
|
||||
await _load();
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _error = 'Umbenennen fehlgeschlagen: ${e.toString().split('\n').first}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logoutAllOtherDevices(BuildContext context) async {
|
||||
final pt = widget.pt;
|
||||
final otherIds = _devices
|
||||
.where((d) => d.deviceId != _currentDeviceId && d.deviceId != null)
|
||||
.map((d) => d.deviceId!)
|
||||
.toList();
|
||||
if (otherIds.isEmpty) return;
|
||||
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rXl), side: BorderSide(color: pt.border)),
|
||||
title: Text('Alle anderen Sitzungen abmelden?',
|
||||
style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
content: Text(
|
||||
'${otherIds.length} Gerät${otherIds.length == 1 ? '' : 'e'} werden abgemeldet. Nicht gespeicherte Nachrichten auf diesen Geräten können verloren gehen.',
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 13, height: 1.5),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: pt.danger, foregroundColor: Colors.white, elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Alle abmelden'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirm != true || !mounted) return;
|
||||
setState(() { _logoutAllLoading = true; _error = null; });
|
||||
|
||||
final client = await ref.read(matrixClientProvider.future);
|
||||
try {
|
||||
await client.deleteDevices(otherIds);
|
||||
} on MatrixException catch (e) {
|
||||
if (e.requireAdditionalAuthentication && context.mounted) {
|
||||
final password = await _askPassword(context, pt);
|
||||
if (password == null || !mounted) { setState(() => _logoutAllLoading = false); return; }
|
||||
try {
|
||||
await client.deleteDevices(otherIds, auth: AuthenticationData(
|
||||
type: AuthenticationTypes.password,
|
||||
session: e.session,
|
||||
additionalFields: {
|
||||
'identifier': {'type': 'm.id.user', 'user': client.userID},
|
||||
'password': password,
|
||||
},
|
||||
));
|
||||
} catch (e2) {
|
||||
if (mounted) setState(() { _logoutAllLoading = false; _error = 'Abmelden fehlgeschlagen: ${e2.toString().split('\n').first}'; });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (mounted) setState(() { _logoutAllLoading = false; _error = 'Abmelden fehlgeschlagen: ${e.toString().split('\n').first}'; });
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() { _logoutAllLoading = false; _error = 'Abmelden fehlgeschlagen: ${e.toString().split('\n').first}'; });
|
||||
return;
|
||||
}
|
||||
await _load();
|
||||
if (mounted) setState(() => _logoutAllLoading = false);
|
||||
}
|
||||
|
||||
Future<String?> _askPassword(BuildContext context, PyramidTheme pt) {
|
||||
final ctrl = TextEditingController();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rXl), side: BorderSide(color: pt.border)),
|
||||
title: Text('Passwort bestätigen', style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w700)),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
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.accent)),
|
||||
),
|
||||
cursorColor: pt.accent,
|
||||
onSubmitted: (_) => Navigator.pop(ctx, ctrl.text),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, null), child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: pt.accent, foregroundColor: pt.accentFg, elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
onPressed: () => Navigator.pop(ctx, ctrl.text),
|
||||
child: const Text('Bestätigen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((v) { ctrl.dispose(); return v; });
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = widget.pt;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_SectionTitle(
|
||||
title: 'Sitzungen',
|
||||
subtitle: 'Geräte, auf denen du angemeldet bist.',
|
||||
pt: pt),
|
||||
if (_loading)
|
||||
const Center(
|
||||
child: Padding(padding: EdgeInsets.all(32), child: CircularProgressIndicator.adaptive()))
|
||||
else if (_error != null)
|
||||
_StatusCard(
|
||||
pt: pt, color: pt.danger, icon: Icons.error_outline_rounded, text: _error!)
|
||||
else ...[
|
||||
_SettingsGroup(
|
||||
title: 'Aktive Sitzungen (${_devices.length})',
|
||||
pt: pt,
|
||||
children: _devices.isEmpty
|
||||
? [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text('Keine Sitzungen gefunden.',
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 13)),
|
||||
)
|
||||
]
|
||||
: _devices.asMap().entries.map((entry) {
|
||||
final i = entry.key;
|
||||
final device = entry.value;
|
||||
final isCurrent = device.deviceId == _currentDeviceId;
|
||||
final isRevoking = _revoking.contains(device.deviceId);
|
||||
final isVerified = device.verified;
|
||||
final displayName = device.unsigned?['device_display_name'] as String? ??
|
||||
device.deviceId ??
|
||||
'Unbekanntes Gerät';
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrent
|
||||
? pt.accentSoft
|
||||
: pt.bg3,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.devices_rounded,
|
||||
size: 18,
|
||||
color: isCurrent ? pt.accent : pt.fgMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
displayName,
|
||||
style: TextStyle(
|
||||
color: pt.fg,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isCurrent) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: pt.accentSoft,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text('Dieses Gerät',
|
||||
style: TextStyle(
|
||||
color: pt.accent,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600)),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () => _renameCurrentDevice(context),
|
||||
child: Tooltip(
|
||||
message: 'Gerät umbenennen',
|
||||
child: Icon(Icons.edit_rounded, size: 13, color: pt.fgDim),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isVerified == true && !isCurrent)
|
||||
Icon(Icons.verified_rounded,
|
||||
size: 14, color: PyramidColors.success),
|
||||
if (isVerified == false && !isCurrent)
|
||||
Icon(Icons.warning_amber_rounded,
|
||||
size: 14, color: pt.away),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
SelectableText(
|
||||
device.deviceId ?? '',
|
||||
style: TextStyle(
|
||||
color: pt.fgDim,
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isCurrent) ...[
|
||||
const SizedBox(width: 8),
|
||||
if (isVerified == false)
|
||||
GestureDetector(
|
||||
onTap: () => _verifyDevice(context, device, displayName),
|
||||
child: Tooltip(
|
||||
message: 'Verifizieren',
|
||||
child: Icon(Icons.verified_user_outlined,
|
||||
size: 16, color: pt.accent),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if (isRevoking)
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: pt.danger),
|
||||
)
|
||||
else
|
||||
GestureDetector(
|
||||
onTap: () => _confirmRevoke(context, device, displayName),
|
||||
child: Tooltip(
|
||||
message: 'Abmelden',
|
||||
child: Icon(Icons.logout_rounded,
|
||||
size: 16, color: pt.fgDim),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (i < _devices.length - 1) _Divider(pt: pt),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: pt.fgMuted,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
onPressed: _load,
|
||||
icon: const Icon(Icons.refresh_rounded, size: 14),
|
||||
label: const Text('Aktualisieren', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_logoutAllLoading)
|
||||
const SizedBox(width: 16, height: 16, child: CircularProgressIndicator.adaptive(strokeWidth: 2))
|
||||
else if (_devices.where((d) => d.deviceId != _currentDeviceId).isNotEmpty)
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: pt.danger,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
onPressed: () => _logoutAllOtherDevices(context),
|
||||
icon: const Icon(Icons.logout_rounded, size: 14),
|
||||
label: const Text('Alle anderen abmelden', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _verifyDevice(
|
||||
BuildContext context, DeviceKeys device, String displayName) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => _SasVerificationDialog(
|
||||
pt: widget.pt,
|
||||
deviceName: displayName,
|
||||
device: device,
|
||||
),
|
||||
);
|
||||
await _load();
|
||||
}
|
||||
|
||||
Future<void> _confirmRevoke(
|
||||
BuildContext context, DeviceKeys device, String displayName) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
final pt = widget.pt;
|
||||
return AlertDialog(
|
||||
backgroundColor: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rXl),
|
||||
side: BorderSide(color: pt.border)),
|
||||
title: Text('Gerät abmelden?',
|
||||
style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
content: Text(
|
||||
'"$displayName" wird abgemeldet und alle lokalen Schlüssel werden gelöscht.',
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 13),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted)),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: pt.danger,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Abmelden'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirm == true && device.deviceId != null) {
|
||||
await _revokeDevice(device.deviceId!);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user