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 _devices = []; bool _loading = true; String? _currentDeviceId; final Set _revoking = {}; bool _logoutAllLoading = false; String? _error; @override void initState() { super.initState(); _load(); } Future _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 _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 _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( 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 _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( 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 _askPassword(BuildContext context, PyramidTheme pt) { final ctrl = TextEditingController(); return showDialog( 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 _verifyDevice( BuildContext context, DeviceKeys device, String displayName) async { await showDialog( context: context, barrierDismissible: false, builder: (ctx) => _SasVerificationDialog( pt: widget.pt, deviceName: displayName, device: device, ), ); await _load(); } Future _confirmRevoke( BuildContext context, DeviceKeys device, String displayName) async { final confirm = await showDialog( 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!); } } }