import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../api/marianumcloud/cloud_users/cloud_users_actions.dart'; import '../../../../push/push_registration.dart'; import '../../../../routing/app_routes.dart'; import '../../../../session/session.dart'; import '../../../../session/session_lifecycle.dart'; import '../../../../session/session_manager.dart'; import '../../../../state/app/modules/account/bloc/account_bloc.dart'; import '../../../../state/app/modules/account/bloc/account_state.dart'; import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart'; import '../../../../widget/app_progress_indicator.dart'; import '../../../../widget/async_action_button.dart'; import '../../../../widget/avatar_actions_sheet.dart'; import '../../../../widget/centered_leading.dart'; import '../../../../widget/child_switcher.dart'; import '../../../../widget/confirm_dialog.dart'; import '../../../../widget/demo_restricted.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 StatelessWidget { const AccountSection({super.key}); @override Widget build(BuildContext context) => switch (SessionManager().current) { GuardianSession(:final email) => _GuardianAccount(email: email), _ => const _SchoolAccount(), }; } /// Guardians have no Nextcloud profile: show the e-mail and linked children. class _GuardianAccount extends StatelessWidget { final String email; const _GuardianAccount({required this.email}); @override Widget build(BuildContext context) { final children = context.watch().state.children; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ListTile( contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 4), leading: const CenteredLeading(Icon(Icons.family_restroom_outlined)), title: const Text('Elternkonto'), subtitle: Text(email), trailing: TextButton.icon( icon: const Icon(Icons.logout_outlined, size: 18), label: const Text('Abmelden'), onPressed: () => _confirmLogout(context), ), ), for (final child in children) ChildTile(child: child), ], ); } } class _SchoolAccount extends StatefulWidget { const _SchoolAccount(); @override State<_SchoolAccount> createState() => _SchoolAccountState(); } class _SchoolAccountState extends State<_SchoolAccount> { 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 { if (guardDemoAction(context)) return; 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: SessionManager().requireNextcloud().username, isGroup: false, ); setState(() => _avatarVersion++); } @override Widget build(BuildContext context) { final nextcloud = SessionManager().requireNextcloud(); final username = nextcloud.username; 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: () => AppRoutes.openLargeProfilePicture( context, 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: () => _confirmLogout(context), ), ], ), ), // Nur für Login-Flow-Konten (2FA) sichtbar — Passwort-Konten heilen // sich still über das App-Passwort-Minting und sollen von dem ganzen // Flow-Mechanismus nichts mitbekommen. if (!SessionManager().isDemo && nextcloud.usesLoginFlow) AsyncListTile( leading: const Icon(Icons.cloud_sync_outlined), title: const Text('Nextcloud neu verbinden'), subtitle: const Text('Bei Anmeldeproblemen in Talk oder Dateien'), closeOnSuccess: false, onPressed: _reconnectNextcloud, ), ], ); } /// Erneuert die Nextcloud-Zugangsdaten über den Login Flow v2 (inkl. des /// zweiten Talk-Durchlaufs) und bindet die Push-Subscription neu. Future _reconnectNextcloud() async { final ok = await AppRoutes.openNextcloudLoginFlow(context); if (!ok || !mounted) return; // Neues App-Passwort = neue NC-Session: die Push-Subscription neu binden. unawaited(PushRegistration().register()); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Nextcloud-Verbindung erneuert.')), ); } } Future _confirmLogout(BuildContext context) async { // Flip AccountBloc state only after the dialog fully closes: doing it from // inside the sign-out (the previous approach) raced AsyncDialogAction's // pop(true) against the listener's 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); } Future _performLogout() async { await SessionLifecycle.signOut(); _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), ), ), ); } }