157 lines
5.4 KiB
Dart
157 lines
5.4 KiB
Dart
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<Login> createState() => _LoginState();
|
|
}
|
|
|
|
class _LoginState extends State<Login> {
|
|
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<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());
|
|
}
|
|
|
|
@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<SettingsCubit, model.Settings>(
|
|
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<SettingsCubit>(),
|
|
),
|
|
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)';
|
|
}
|
|
}
|
|
}
|