import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../session/account_codec.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 '../utils/haptics.dart'; import 'app_progress_indicator.dart'; import 'async_action_button.dart'; import 'centered_leading.dart'; import 'confirm_dialog.dart'; import 'details_bottom_sheet.dart'; import 'user_avatar.dart'; /// Lists the signed-in accounts: tap one to switch, add another, or sign out /// of an inactive one. Future showAccountSwitcherSheet(BuildContext context) { final accountBloc = context.read(); return showDetailsBottomSheet( context, header: Builder( builder: (headerContext) => ListTile( title: const Text('Konten'), trailing: TextButton.icon( icon: const Icon(Icons.person_add_alt_outlined, size: 18), label: const Text('Hinzufügen'), onPressed: () { Navigator.pop(headerContext); unawaited(startAddAccount(accountBloc)); }, ), ), ), children: (sheetContext) => [ ValueListenableBuilder( valueListenable: SessionManager().accounts, builder: (context, index, _) => Column( mainAxisSize: MainAxisSize.min, children: [ for (final account in index.accounts) _AccountTile( account: account, active: account.id == index.activeId, accountBloc: accountBloc, ), ], ), ), ], ); } /// Opens the login for another account; the active one is parked meanwhile. Future startAddAccount(AccountBloc accountBloc) async { await SessionLifecycle.beginAddAccount(); accountBloc.setStatus(AccountStatus.addingAccount); } /// Stacked next to the account name, which needs the width more. /// Only the horizontal padding is trimmed; height stays at a full tap target. final ButtonStyle compactAccountButtonStyle = TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8), ); /// Below the sign-out button: adds an account, or opens the switcher once /// there is more than one. class AccountSwitchButton extends StatelessWidget { const AccountSwitchButton({super.key}); @override Widget build(BuildContext context) => ValueListenableBuilder( valueListenable: SessionManager().accounts, builder: (context, index, _) => index.accounts.length > 1 ? TextButton.icon( style: compactAccountButtonStyle, icon: const Icon(Icons.switch_account_outlined, size: 18), label: const Text('Wechseln'), onPressed: () => showAccountSwitcherSheet(context), ) : TextButton.icon( style: compactAccountButtonStyle, icon: const Icon(Icons.person_add_alt_outlined, size: 18), label: const Text('Hinzufügen'), onPressed: () => startAddAccount(context.read()), ), ); } class _AccountTile extends StatefulWidget { final AccountEntry account; final bool active; final AccountBloc accountBloc; const _AccountTile({ required this.account, required this.active, required this.accountBloc, }); @override State<_AccountTile> createState() => _AccountTileState(); } class _AccountTileState extends State<_AccountTile> { bool _busy = false; Future _switch() async { Haptics.selection(); setState(() => _busy = true); final ok = await runWithErrorDialog( context, () => SessionLifecycle.switchTo(widget.account.id), ); if (!mounted) return; setState(() => _busy = false); if (!ok) return; Navigator.pop(context); widget.accountBloc.activated(SessionManager().activeAccount?.id); } void _confirmRemove() => ConfirmDialog( title: 'Abmelden?', content: '${widget.account.displayName ?? widget.account.label} wird von diesem Gerät abgemeldet und seine ' 'lokal gespeicherten Daten werden gelöscht.', confirmButton: 'Abmelden', onConfirmAsync: () => SessionLifecycle.removeInactive(widget.account.id), ).asDialog(context); @override Widget build(BuildContext context) { final account = widget.account; return ListTile( leading: CenteredLeading( _busy ? const SizedBox.square( dimension: 24, child: AppProgressIndicator.small(), ) : _AccountAvatar(account: account), ), title: Text( account.displayName ?? account.label, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text( account.isDemo ? '${account.label} (Demo)' : account.label, maxLines: 1, overflow: TextOverflow.ellipsis, ), // Sized like the IconButton so both line up. trailing: widget.active ? const SizedBox.square( dimension: kMinInteractiveDimension, child: Icon(Icons.check), ) : IconButton( icon: const Icon(Icons.logout_outlined), tooltip: 'Abmelden', onPressed: _busy ? null : _confirmRemove, ), onTap: widget.active || _busy ? null : _switch, ); } } class _AccountAvatar extends StatelessWidget { final AccountEntry account; const _AccountAvatar({required this.account}); @override Widget build(BuildContext context) { // Demo accounts have no real Nextcloud user behind them. if (!account.isGuardian && !account.isDemo) { return UserAvatar(id: account.label, size: 16); } final colors = Theme.of(context).colorScheme; return CircleAvatar( radius: 16, backgroundColor: colors.secondaryContainer, foregroundColor: colors.onSecondaryContainer, child: account.isGuardian ? const Icon(Icons.family_restroom_outlined, size: 18) : Text(account.label.characters.first.toUpperCase()), ); } }