import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../background/widget_background_task.dart'; import '../../state/app/modules/account/bloc/account_bloc.dart'; import '../../state/app/modules/account/bloc/account_state.dart'; import '../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../storage/dev_tools_settings.dart'; import '../../storage/settings.dart' as model; import '../../theming/light_app_theme.dart'; import '../../utils/haptics.dart'; import '../pages/settings/widgets/endpoint_picker.dart'; import 'login_controller.dart'; import 'widgets/login_branding.dart'; import 'widgets/login_card.dart'; class Login extends StatefulWidget { const Login({super.key}); @override State createState() => _LoginState(); } class _LoginState extends State { static const _marianumRed = LightAppTheme.marianumRed; final LoginController _controller = LoginController(); @override void didChangeDependencies() { super.didChangeDependencies(); precacheImage(const AssetImage('assets/logo/icon.png'), context); } @override void dispose() { _controller.dispose(); super.dispose(); } void _onLoginSuccess() { Haptics.heavyAccent(); context.read().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()); } @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()], ), ], ), ), ), ), ), ), ); } /// Subtle text link above the footer that surfaces the currently selected /// Marianum-Connect endpoint and opens the picker on tap. Always visible so /// devs can switch the endpoint before the first login without hunting for a /// long-press easter egg, but understated enough not to draw regular users /// into the dev menu. class _EndpointLink extends StatelessWidget { const _EndpointLink(); @override Widget build(BuildContext context) => BlocBuilder( builder: (context, settings) { final dev = settings.devToolsSettings; final label = _label(dev); return Padding( padding: const EdgeInsets.only(bottom: 4), child: TextButton( style: TextButton.styleFrom( foregroundColor: Colors.white.withValues(alpha: 0.85), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), minimumSize: const Size(0, 28), tapTargetSize: MaterialTapTargetSize.shrinkWrap, textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, decoration: TextDecoration.underline, ), ), onPressed: () => MarianumConnectEndpointPicker.show( context, context.read(), ), child: Text('Server: $label'), ), ); }, ); static String _label(DevToolsSettings dev) { switch (dev.marianumConnectEndpoint) { case MarianumConnectEndpoint.live: return 'Normal'; case MarianumConnectEndpoint.beta: return 'Beta'; case MarianumConnectEndpoint.custom: final url = DevToolsSettings.sanitizeCustomUrl( dev.marianumConnectCustomUrl, ); return url ?? 'Eigener Server (ungültig)'; } } }