added support for multiple accounts, guardian login bugfixes, ui changes

This commit is contained in:
2026-09-23 20:50:12 +02:00
parent 630497abdd
commit 84098af7e2
37 changed files with 1759 additions and 376 deletions
@@ -0,0 +1,71 @@
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 '../children/child_selection_cubit.dart';
import '../nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart';
import '../parent_letters/bloc/parent_letters_bloc.dart';
import '../timetable/primary/primary_timetable_scope.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<ChildSelectionCubit>(create: (_) => ChildSelectionCubit()),
BlocProvider<ParentLettersBloc>(create: (_) => ParentLettersBloc()),
],
child: PrimaryTimetableScope(
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,
});
}
@@ -11,7 +11,7 @@ class SettingsCubit extends HydratedCubit<Settings> {
static const _debounceTag = 'settings_persist';
bool _emitScheduled = false;
SettingsCubit() : super(DefaultSettings.get());
SettingsCubit({super.storage}) : super(DefaultSettings.get());
Settings val({bool write = false}) {
if (write) {
@@ -64,7 +64,10 @@ class _PrimaryTimetableScopeState extends State<PrimaryTimetableScope> {
Widget build(BuildContext context) => MultiBlocListener(
listeners: [
BlocListener<AccountBloc, AccountState>(
listenWhen: (a, b) => a.status != b.status,
// While another account is being added, the session may already
// belong to it; this scope is replaced once that account takes over.
listenWhen: (a, b) =>
a.status != b.status && b.status != AccountStatus.addingAccount,
listener: (_, _) => _sync(),
),
BlocListener<CapabilitiesCubit, CapabilitiesState>(
@@ -73,9 +76,6 @@ class _PrimaryTimetableScopeState extends State<PrimaryTimetableScope> {
),
BlocListener<ChildSelectionCubit, String?>(listener: (_, _) => _sync()),
],
child: BlocProvider<TimetableBloc>.value(
value: _bloc,
child: widget.child,
),
child: BlocProvider<TimetableBloc>.value(value: _bloc, child: widget.child),
);
}