added support for multiple accounts with an account switcher in settings
This commit is contained in:
@@ -9,7 +9,7 @@ import '../../../../routing/app_routes.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 '../../../../widget/account_switcher_sheet.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/avatar_actions_sheet.dart';
|
||||
@@ -17,34 +17,43 @@ 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.
|
||||
// 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 StatefulWidget {
|
||||
class AccountSection extends StatelessWidget {
|
||||
const AccountSection({super.key});
|
||||
|
||||
@override
|
||||
State<AccountSection> createState() => _AccountSectionState();
|
||||
Widget build(BuildContext context) => const _SchoolAccount();
|
||||
}
|
||||
|
||||
class _AccountSectionState extends State<AccountSection> {
|
||||
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;
|
||||
String? _displayName;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_displayName == null) _loadDisplayName();
|
||||
final username = SessionManager().requireNextcloud().username;
|
||||
if (_cachedDisplayNameFor == username) _displayName = _cachedDisplayName;
|
||||
if (_displayName == null) _loadDisplayName(username);
|
||||
}
|
||||
|
||||
Future<void> _loadDisplayName() async {
|
||||
Future<void> _loadDisplayName(String username) async {
|
||||
try {
|
||||
final info = await GetUserInfo().run();
|
||||
_cachedDisplayName = info.displayName.isEmpty
|
||||
? null
|
||||
: info.displayName;
|
||||
_cachedDisplayNameFor = username;
|
||||
_cachedDisplayName = info.displayName.isEmpty ? null : info.displayName;
|
||||
if (!mounted) return;
|
||||
setState(() => _displayName = _cachedDisplayName);
|
||||
} catch (_) {
|
||||
@@ -102,7 +111,7 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 8, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -113,8 +122,10 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
children: [
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
AppRoutes.openLargeProfilePicture(context, username),
|
||||
onTap: () => AppRoutes.openLargeProfilePicture(
|
||||
context,
|
||||
username,
|
||||
),
|
||||
child: UserAvatar(
|
||||
key: ValueKey(_avatarVersion),
|
||||
id: username,
|
||||
@@ -134,7 +145,7 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -167,12 +178,8 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.logout_outlined, size: 18),
|
||||
label: const Text('Abmelden'),
|
||||
onPressed: () => _showLogoutDialog(context),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const _AccountActions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -183,9 +190,7 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.cloud_sync_outlined),
|
||||
title: const Text('Nextcloud neu verbinden'),
|
||||
subtitle: const Text(
|
||||
'Bei Anmeldeproblemen in Talk oder Dateien',
|
||||
),
|
||||
subtitle: const Text('Bei Anmeldeproblemen in Talk oder Dateien'),
|
||||
closeOnSuccess: false,
|
||||
onPressed: _reconnectNextcloud,
|
||||
),
|
||||
@@ -204,29 +209,49 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
const SnackBar(content: Text('Nextcloud-Verbindung erneuert.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showLogoutDialog(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<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => ConfirmDialog(
|
||||
title: 'Abmelden?',
|
||||
content: 'Möchtest du dich wirklich abmelden?',
|
||||
confirmButton: 'Abmelden',
|
||||
onConfirmAsync: _performLogout,
|
||||
Future<void> _confirmLogout(BuildContext context) async {
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
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<bool>(
|
||||
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 _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),
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
context.read<AccountBloc>().setStatus(AccountStatus.loggedOut);
|
||||
}
|
||||
|
||||
Future<void> _performLogout() async {
|
||||
await SessionLifecycle.signOut();
|
||||
_cachedDisplayName = null;
|
||||
}
|
||||
const AccountSwitchButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _AvatarEditBadge extends StatelessWidget {
|
||||
@@ -254,11 +279,7 @@ class _AvatarEditBadge extends StatelessWidget {
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.edit,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
: Icon(Icons.edit, size: 14, color: theme.colorScheme.onPrimary),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user