added support for multiple accounts with an account switcher in settings
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../breaker/bloc/breaker_bloc.dart';
|
||||
import '../capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../chat/bloc/chat_bloc.dart';
|
||||
import '../chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart';
|
||||
import '../timetable/bloc/timetable_bloc.dart';
|
||||
import 'bloc/account_bloc.dart';
|
||||
import 'bloc/account_state.dart';
|
||||
|
||||
/// The blocs of one account. Keyed by the account id above, so a switch
|
||||
/// recreates all of them from the new account's storage instead of resetting
|
||||
/// them one by one.
|
||||
class AccountScope extends StatelessWidget {
|
||||
/// Called with a context below the account's blocs once the scope mounted
|
||||
/// for a signed-in account.
|
||||
final void Function(BuildContext scopeContext) onActive;
|
||||
final Widget child;
|
||||
|
||||
const AccountScope({super.key, required this.onActive, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<BreakerBloc>(create: (_) => BreakerBloc()),
|
||||
BlocProvider<CapabilitiesCubit>(create: (_) => CapabilitiesCubit()),
|
||||
BlocProvider<NextcloudCapabilitiesCubit>(
|
||||
create: (_) => NextcloudCapabilitiesCubit(),
|
||||
),
|
||||
BlocProvider<ChatListBloc>(create: (_) => ChatListBloc()),
|
||||
BlocProvider<ChatBloc>(
|
||||
create: (ctx) => ChatBloc(chatListBloc: ctx.read<ChatListBloc>()),
|
||||
),
|
||||
BlocProvider<TimetableBloc>(create: (_) => TimetableBloc()),
|
||||
],
|
||||
child: _Activation(onActive: onActive, child: child),
|
||||
);
|
||||
}
|
||||
|
||||
class _Activation extends StatefulWidget {
|
||||
final void Function(BuildContext scopeContext) onActive;
|
||||
final Widget child;
|
||||
|
||||
const _Activation({required this.onActive, required this.child});
|
||||
|
||||
@override
|
||||
State<_Activation> createState() => _ActivationState();
|
||||
}
|
||||
|
||||
class _ActivationState extends State<_Activation> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (context.read<AccountBloc>().state.status != AccountStatus.loggedIn) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) widget.onActive(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
@@ -4,12 +4,38 @@ import 'account_event.dart';
|
||||
import 'account_state.dart';
|
||||
|
||||
class AccountBloc extends Bloc<AccountEvent, AccountState> {
|
||||
AccountBloc({AccountStatus initialStatus = AccountStatus.undefined})
|
||||
: super(AccountState(status: initialStatus)) {
|
||||
AccountBloc({
|
||||
AccountStatus initialStatus = AccountStatus.undefined,
|
||||
String? accountId,
|
||||
}) : super(AccountState(status: initialStatus, accountId: accountId)) {
|
||||
on<AccountStatusChanged>(
|
||||
(event, emit) => emit(state.copyWith(status: event.status)),
|
||||
(event, emit) => emit(
|
||||
AccountState(
|
||||
status: event.status,
|
||||
accountId: event.status == AccountStatus.loggedOut
|
||||
? null
|
||||
: state.accountId,
|
||||
),
|
||||
),
|
||||
);
|
||||
on<AccountActivated>(
|
||||
(event, emit) => emit(
|
||||
AccountState(
|
||||
status: AccountStatus.loggedIn,
|
||||
accountId: event.accountId,
|
||||
freshLogin: event.freshLogin,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setStatus(AccountStatus status) => add(AccountStatusChanged(status));
|
||||
|
||||
/// [accountId] became the active account (login, switch or takeover after a
|
||||
/// sign-out); null means no account is left.
|
||||
void activated(String? accountId, {bool freshLogin = false}) => add(
|
||||
accountId == null
|
||||
? const AccountStatusChanged(AccountStatus.loggedOut)
|
||||
: AccountActivated(accountId, freshLogin: freshLogin),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,3 +8,9 @@ class AccountStatusChanged extends AccountEvent {
|
||||
final AccountStatus status;
|
||||
const AccountStatusChanged(this.status);
|
||||
}
|
||||
|
||||
class AccountActivated extends AccountEvent {
|
||||
final String accountId;
|
||||
final bool freshLogin;
|
||||
const AccountActivated(this.accountId, {required this.freshLogin});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
enum AccountStatus { undefined, loggedIn, loggedOut }
|
||||
enum AccountStatus { undefined, loggedIn, loggedOut, addingAccount }
|
||||
|
||||
class AccountState {
|
||||
final AccountStatus status;
|
||||
const AccountState({this.status = AccountStatus.undefined});
|
||||
|
||||
AccountState copyWith({AccountStatus? status}) =>
|
||||
AccountState(status: status ?? this.status);
|
||||
/// Active account; the account-scoped part of the app is keyed by it.
|
||||
final String? accountId;
|
||||
|
||||
/// Set when [accountId] came from a login (not a switch), to show the
|
||||
/// post-login splash.
|
||||
final bool freshLogin;
|
||||
|
||||
const AccountState({
|
||||
this.status = AccountStatus.undefined,
|
||||
this.accountId,
|
||||
this.freshLogin = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,8 +87,10 @@ class ChatListBloc
|
||||
|
||||
/// Concurrent callers (tab switch, poll, push, resume) join the running
|
||||
/// refresh instead of each fetching and re-emitting the full room list.
|
||||
Future<void> refresh({bool renew = true, bool silent = false}) =>
|
||||
_refreshFlight.run(() => _refresh(renew: renew, silent: silent));
|
||||
Future<void> refresh({bool renew = true, bool silent = false}) async {
|
||||
if (!ownsSession) return;
|
||||
await _refreshFlight.run(() => _refresh(renew: renew, silent: silent));
|
||||
}
|
||||
|
||||
/// Skips the refresh when the list was fetched within [maxAge].
|
||||
Future<void> refreshIfOlderThan(Duration maxAge) {
|
||||
|
||||
@@ -14,7 +14,12 @@ class SettingsCubit extends HydratedCubit<Settings> {
|
||||
Map<String, dynamic>? _lastEmittedJson;
|
||||
Timer? _silentSave;
|
||||
|
||||
SettingsCubit() : super(DefaultSettings.get());
|
||||
final Storage? _storage;
|
||||
|
||||
// ignore: use_super_parameters
|
||||
SettingsCubit({Storage? storage})
|
||||
: _storage = storage,
|
||||
super(DefaultSettings.get(), storage: storage);
|
||||
|
||||
Settings val({bool write = false}) {
|
||||
if (write) {
|
||||
@@ -50,7 +55,7 @@ class SettingsCubit extends HydratedCubit<Settings> {
|
||||
if (_silentSave == null) return;
|
||||
_silentSave!.cancel();
|
||||
_silentSave = null;
|
||||
HydratedBloc.storage.write(storageToken, state.toJson());
|
||||
(_storage ?? HydratedBloc.storage).write(storageToken, state.toJson());
|
||||
}
|
||||
|
||||
void _emitFreshInstance() {
|
||||
|
||||
Reference in New Issue
Block a user