import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../api/marianumcloud/cloud_users/cloud_users_actions.dart'; import '../../../../api/marianumconnect/queries/auth_logout/auth_logout.dart'; import '../../../../model/account_data.dart'; import '../../../../state/app/modules/account/bloc/account_bloc.dart'; import '../../../../state/app/modules/account/bloc/account_state.dart'; import '../../../../widget/app_progress_indicator.dart'; import '../../../../widget/async_action_button.dart'; import '../../../../widget/avatar_actions_sheet.dart'; import '../../../../widget/confirm_dialog.dart'; import '../../../../widget/large_profile_picture_view.dart'; import '../../../../widget/user_avatar.dart'; // Display-name is process-wide stable until the user logs out; cache it so // every Settings rebuild doesn't re-issue the OCS request. String? _cachedDisplayName; class AccountSection extends StatefulWidget { const AccountSection({super.key}); @override State createState() => _AccountSectionState(); } class _AccountSectionState extends State { int _avatarVersion = 0; bool _avatarBusy = false; String? _displayName = _cachedDisplayName; @override void initState() { super.initState(); if (_displayName == null) _loadDisplayName(); } Future _loadDisplayName() async { try { final info = await GetUserInfo().run(); _cachedDisplayName = info.displayName.isEmpty ? null : info.displayName; if (!mounted) return; setState(() => _displayName = _cachedDisplayName); } catch (_) { // Silent fallback to username — surfacing an error dialog over the // settings screen on every open would be noisier than helpful. } } Future _editAvatar() async { final result = await showAvatarActionsSheet(context, allowRemove: true); if (result == null || !mounted) return; if (result is AvatarRemoveResult) { var confirmed = false; await showDialog( context: context, builder: (_) => ConfirmDialog( title: 'Profilbild entfernen', content: 'Möchtest du dein Profilbild wirklich entfernen?', confirmButton: 'Entfernen', onConfirm: () => confirmed = true, ), ); if (!confirmed || !mounted) return; } setState(() => _avatarBusy = true); final ok = await runWithErrorDialog(context, () async { if (result is AvatarUploadResult) { await SetUserAvatar(result.bytes).run(); } else { await DeleteUserAvatar().run(); } }); if (!mounted) return; setState(() => _avatarBusy = false); if (!ok) return; invalidateAvatarCache(id: AccountData().getUsername(), isGroup: false); setState(() => _avatarVersion++); } @override Widget build(BuildContext context) { final username = AccountData().getUsername(); final displayName = _displayName; final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), child: Row( children: [ SizedBox( width: 84, height: 84, child: Stack( clipBehavior: Clip.none, children: [ Center( child: GestureDetector( onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => LargeProfilePictureView(id: username), ), ), child: UserAvatar( key: ValueKey(_avatarVersion), id: username, size: 36, requestSize: 256, ), ), ), Positioned( right: 0, bottom: 0, child: _AvatarEditBadge( busy: _avatarBusy, onTap: _avatarBusy ? null : _editAvatar, ), ), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( displayName ?? username, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w600, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (displayName != null) ...[ const SizedBox(height: 2), Text( username, style: TextStyle( fontSize: 13, color: theme.colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ), const SizedBox(width: 8), TextButton.icon( icon: const Icon(Icons.logout_outlined, size: 18), label: const Text('Abmelden'), onPressed: () => _showLogoutDialog(context), ), ], ), ), ], ); } Future _showLogoutDialog(BuildContext context) async { // Sequential logout flow: dialog wipes secure storage, dialog closes // (single Navigator.pop), then we flip the AccountBloc state. The bloc // listener in main.dart pops the Settings route and runs the in-memory // wipe. Triggering setStatus from inside removeData (the previous // approach) raced AsyncDialogAction's pop(true) against popUntil(isFirst) // and could leave the navigator in an inconsistent state. final confirmed = await showDialog( context: context, builder: (dialogContext) => ConfirmDialog( title: 'Abmelden?', content: 'Möchtest du dich wirklich abmelden?', confirmButton: 'Abmelden', onConfirmAsync: _performLogout, ), ); if (confirmed != true || !context.mounted) return; context.read().setStatus(AccountStatus.loggedOut); } // Best-effort revoke of the MC bearer token before we wipe local credentials. // The token storage itself is cleared inside AuthLogout regardless of network // success, so an offline logout still gets us into a clean local state. Future _performLogout() async { await AuthLogout().run(); await AccountData().removeData(); _cachedDisplayName = null; } } class _AvatarEditBadge extends StatelessWidget { final bool busy; final VoidCallback? onTap; const _AvatarEditBadge({required this.busy, required this.onTap}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Material( color: theme.colorScheme.primary, shape: const CircleBorder(), elevation: 2, child: InkWell( customBorder: const CircleBorder(), onTap: onTap, child: SizedBox( width: 27, height: 27, child: busy ? Padding( padding: const EdgeInsets.all(6), child: AppProgressIndicator.small( color: theme.colorScheme.onPrimary, ), ) : Icon( Icons.edit, size: 14, color: theme.colorScheme.onPrimary, ), ), ), ); } }