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/account_codec.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/capabilities/bloc/capabilities_cubit.dart'; import '../../../../widget/account_switcher_sheet.dart'; import '../../../../widget/app_progress_indicator.dart'; import '../../../../widget/async_action_button.dart'; import '../../../../widget/avatar_actions_sheet.dart'; import '../../../../widget/child_switcher.dart'; import '../../../../widget/confirm_dialog.dart'; import '../../../../widget/demo_restricted.dart'; import '../../../../widget/user_avatar.dart'; // Display-name is stable per account; cache it so every Settings rebuild // doesn't re-issue the OCS request. String? _cachedDisplayName; String? _cachedDisplayNameFor; 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; final colors = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 20, 8, 16), child: Row( children: [ CircleAvatar( radius: 36, backgroundColor: colors.secondaryContainer, foregroundColor: colors.onSecondaryContainer, child: const Icon(Icons.family_restroom_outlined, size: 34), ), const SizedBox(width: 12), Expanded( child: ValueListenableBuilder( valueListenable: SessionManager().accounts, builder: (context, index, _) => _AccountName( name: index.active?.displayName ?? 'Elternkonto', identity: email, ), ), ), const SizedBox(width: 4), const _AccountActions(), ], ), ), if (children.isNotEmpty) Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Text( children.length == 1 ? 'Dein Kind' : 'Deine Kinder', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ), 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; @override void initState() { super.initState(); final username = SessionManager().requireNextcloud().username; if (_cachedDisplayNameFor == username) _displayName = _cachedDisplayName; if (_displayName == null) _loadDisplayName(username); } Future _loadDisplayName(String username) async { try { final info = await GetUserInfo().run(); _cachedDisplayNameFor = username; _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, 8, 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: 12), 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: 4), const _AccountActions(), ], ), ), // 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 { final accountBloc = context.read(); final others = SessionManager().accounts.value.accounts.length - 1; String? nextAccountId; // 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 navigator teardown of the account switch. final confirmed = await showDialog( context: context, builder: (dialogContext) => ConfirmDialog( title: 'Abmelden?', content: others > 0 ? 'Möchtest du dich wirklich abmelden? Die App wechselt danach zu ' 'einem deiner anderen Konten.' : 'Möchtest du dich wirklich abmelden?', confirmButton: 'Abmelden', onConfirmAsync: () async => nextAccountId = await SessionLifecycle.signOut(), ), ); if (confirmed != true) return; accountBloc.activated(nextAccountId); } class _AccountName extends StatelessWidget { final String name; final String identity; const _AccountName({required this.name, required this.identity}); @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( identity, style: TextStyle( fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ); } class _AccountActions extends StatelessWidget { const _AccountActions(); @override Widget build(BuildContext context) => Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextButton.icon( style: compactAccountButtonStyle, icon: const Icon(Icons.logout_outlined, size: 18), label: const Text('Abmelden'), onPressed: () => _confirmLogout(context), ), const AccountSwitchButton(), ], ); } 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), ), ), ); } }