implemented post-login splash screen with Lottie animations and integrated background data prefetching during the login transition

This commit is contained in:
2026-06-29 12:00:26 +02:00
parent 13f4f79829
commit 4bc7ffd37a
6 changed files with 256 additions and 40 deletions
+56 -39
View File
@@ -13,6 +13,7 @@ import '../../theming/light_app_theme.dart';
import '../../utils/haptics.dart';
import '../pages/settings/widgets/endpoint_picker.dart';
import 'login_controller.dart';
import 'post_login_splash.dart';
import 'widgets/login_branding.dart';
import 'widgets/login_card.dart';
@@ -23,73 +24,89 @@ class Login extends StatefulWidget {
State<Login> createState() => _LoginState();
}
class _LoginState extends State<Login> {
class _LoginState extends State<Login> with SingleTickerProviderStateMixin {
static const _marianumRed = LightAppTheme.marianumRed;
final LoginController _controller = LoginController();
late final AnimationController _fade = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 450),
value: 1,
);
@override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(const AssetImage('assets/logo/icon.png'), context);
unawaited(PostLoginSplash.precache());
}
@override
void dispose() {
_fade.dispose();
_controller.dispose();
super.dispose();
}
void _onLoginSuccess() {
Haptics.heavyAccent();
context.read<AccountBloc>().setStatus(AccountStatus.loggedIn);
// Re-register the periodic refresh (cancelAll runs on logout) and kick
// off an immediate one-off so the widget populates within seconds
// instead of waiting up to 30 minutes for the next periodic slot.
unawaited(WidgetBackgroundTask.initialize());
unawaited(WidgetBackgroundTask.requestImmediateRefresh());
// Fade the login content out before handing over to the post-login splash.
// Both share the red backdrop, so this reads as one continuous transition
// instead of an abrupt swap.
_fade.reverse().whenComplete(() {
if (!mounted) return;
context.read<AccountBloc>().setStatus(AccountStatus.loggedIn);
});
}
@override
Widget build(BuildContext context) => Scaffold(
backgroundColor: _marianumRed,
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
maxWidth: 420,
),
// spaceBetween statt Spacer-in-IntrinsicHeight: bei jeder
// Inhaltsänderung im unteren Block (z.B. EndpointLink mit
// dynamischem Label) würde IntrinsicHeight sonst die Column
// an die intrinsic-Höhe pinnen und ein paar Pixel Overflow
// produzieren. spaceBetween fügt nur den verbleibenden Gap
// ein und schrumpft sauber auf 0, wenn der Inhalt zu hoch
// wird — dann übernimmt der äußere ScrollView.
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
const LoginHeader(),
const SizedBox(height: 28),
LoginCard(
controller: _controller,
onSuccess: _onLoginSuccess,
),
const SizedBox(height: 18),
const LoginDisclaimer(),
],
),
const Column(
mainAxisSize: MainAxisSize.min,
children: [_EndpointLink(), LoginFooter()],
),
],
body: FadeTransition(
opacity: _fade,
child: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
maxWidth: 420,
),
// spaceBetween statt Spacer-in-IntrinsicHeight: bei jeder
// Inhaltsänderung im unteren Block (z.B. EndpointLink mit
// dynamischem Label) würde IntrinsicHeight sonst die Column
// an die intrinsic-Höhe pinnen und ein paar Pixel Overflow
// produzieren. spaceBetween fügt nur den verbleibenden Gap
// ein und schrumpft sauber auf 0, wenn der Inhalt zu hoch
// wird — dann übernimmt der äußere ScrollView.
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
const LoginHeader(),
const SizedBox(height: 28),
LoginCard(
controller: _controller,
onSuccess: _onLoginSuccess,
),
const SizedBox(height: 18),
const LoginDisclaimer(),
],
),
const Column(
mainAxisSize: MainAxisSize.min,
children: [_EndpointLink(), LoginFooter()],
),
],
),
),
),
),