migrated timetable integration from WebUntis to the MarianumConnect API, implementing a Dio-based client with bearer token authentication, background session validation, and auto-refresh logic.
This commit is contained in:
+87
-15
@@ -6,7 +6,11 @@ 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 '../pages/settings/widgets/endpoint_picker.dart';
|
||||
import 'login_controller.dart';
|
||||
import 'widgets/login_branding.dart';
|
||||
import 'widgets/login_card.dart';
|
||||
@@ -57,21 +61,33 @@ class _LoginState extends State<Login> {
|
||||
minHeight: constraints.maxHeight,
|
||||
maxWidth: 420,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
const LoginHeader(),
|
||||
const SizedBox(height: 28),
|
||||
LoginCard(
|
||||
controller: _controller,
|
||||
onSuccess: _onLoginSuccess,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const LoginDisclaimer(),
|
||||
const Spacer(),
|
||||
const LoginFooter(),
|
||||
],
|
||||
),
|
||||
// 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()],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -80,3 +96,59 @@ class _LoginState extends State<Login> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../api/errors/auth_exception.dart';
|
||||
import '../../api/errors/error_mapper.dart';
|
||||
import '../../api/marianumcloud/talk/room/get_room.dart';
|
||||
import '../../api/marianumcloud/talk/room/get_room_params.dart';
|
||||
import '../../api/marianumconnect/auth/device_token_name.dart';
|
||||
import '../../api/marianumconnect/auth/token_storage.dart';
|
||||
import '../../api/marianumconnect/queries/auth_login/auth_login.dart';
|
||||
import '../../model/account_data.dart';
|
||||
import '../../widget_data/widget_sync.dart';
|
||||
|
||||
@@ -32,22 +33,28 @@ class LoginController extends ChangeNotifier {
|
||||
final user = username.trim().toLowerCase();
|
||||
try {
|
||||
await AccountData().removeData();
|
||||
// Drop any cached widget snapshot from a previous account before the
|
||||
// new credentials populate it — otherwise a re-login with a different
|
||||
// user briefly shows the previous owner's timetable on the home screen.
|
||||
// Vorherigen Token revoken bevor wir einen neuen anfordern — ein altes
|
||||
// Account hätte sonst noch einen aktiven Token in api_tokens.
|
||||
await const MarianumConnectTokenStorage().clear();
|
||||
// Widget-Snapshot löschen, sonst blitzt nach Account-Wechsel kurz der
|
||||
// Stundenplan des vorigen Users auf dem Home-Bildschirm.
|
||||
await WidgetSync.clear();
|
||||
await WidgetSync.triggerUpdate();
|
||||
// AuthLogin = Credential-Probe + Token-Create in einem Call.
|
||||
// 401 hier heißt: falsches Passwort.
|
||||
await AuthLogin().run(
|
||||
username: user,
|
||||
password: password,
|
||||
tokenName: await DeviceTokenName.resolve(),
|
||||
);
|
||||
await AccountData().setData(user, password);
|
||||
await GetRoom(GetRoomParams(includeStatus: false)).run();
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
await AccountData().removeData();
|
||||
// 401 from the probe means the credentials were wrong; everything else
|
||||
// (no network, server down, TLS errors, …) gets the generic mapped
|
||||
// message so the user knows it isn't their typo.
|
||||
await const MarianumConnectTokenStorage().clear();
|
||||
final isWrongCredentials = e is AuthException && e.statusCode == 401;
|
||||
_errorMessage = isWrongCredentials
|
||||
? 'Benutzername oder Passwort falsch.'
|
||||
|
||||
Reference in New Issue
Block a user