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:
2026-05-23 17:32:42 +02:00
parent 2858f910c9
commit 93b9929f8f
106 changed files with 2739 additions and 2624 deletions
-4
View File
@@ -6,13 +6,11 @@ import 'package:http/http.dart' as http;
import '../api_error.dart';
import '../marianumcloud/talk/talk_error.dart';
import '../webuntis/webuntis_error.dart';
import 'app_exception.dart';
import 'network_exception.dart';
import 'parse_exception.dart';
import 'server_exception.dart';
import 'talk_exception.dart';
import 'webuntis_exception.dart';
const String _defaultFallback =
'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
@@ -57,7 +55,6 @@ String errorToUserMessage(Object? error, {String fallback = _defaultFallback}) {
if (error is AppException) return error.userMessage;
if (error is TalkError) return TalkException(error).userMessage;
if (error is WebuntisError) return WebuntisException(error).userMessage;
if (error is DioException) {
final mapped = _dioToAppException(error);
@@ -90,7 +87,6 @@ String? errorToTechnicalDetails(Object? error) {
if (error == null) return null;
if (error is AppException) return error.technicalDetails ?? error.toString();
if (error is TalkError) return TalkException(error).technicalDetails;
if (error is WebuntisError) return WebuntisException(error).technicalDetails;
if (error is DioException) {
final mapped = _dioToAppException(error);
if (mapped != null) return mapped.technicalDetails ?? mapped.toString();
-31
View File
@@ -1,31 +0,0 @@
import '../webuntis/webuntis_error.dart';
import 'app_exception.dart';
class WebuntisException extends AppException {
final WebuntisError source;
WebuntisException(this.source)
: super(
userMessage: _mapMessage(source),
technicalDetails: 'WebUntis (${source.code}): ${source.message}',
allowRetry: true,
);
static String _mapMessage(WebuntisError e) {
switch (e.code) {
case -8504:
case -8502:
return 'WebUntis-Anmeldung abgelaufen. Bitte erneut anmelden.';
case -8520:
return 'Bitte melde dich erneut an.';
case -7004:
return 'Für diesen Zeitraum sind keine Stundenplandaten verfügbar.';
case -32601:
return 'WebUntis kennt diese Anfrage nicht. Bitte App aktualisieren.';
default:
return e.message.isNotEmpty
? 'WebUntis: ${e.message}'
: 'WebUntis konnte die Anfrage nicht bearbeiten (Code ${e.code}).';
}
}
}
@@ -0,0 +1,110 @@
import 'package:dio/dio.dart';
import '../../../model/account_data.dart';
import '../queries/auth_login/auth_login.dart';
import 'device_token_name.dart';
import 'token_storage.dart';
/// Adds the bearer token to outgoing Marianum-Connect requests and, on 401,
/// re-logs in once with the credentials in [AccountData] before retrying.
class MarianumConnectAuthInterceptor extends Interceptor {
static const _retriedKey = 'mc_auth_retried';
final MarianumConnectTokenStorage _tokenStorage;
final Dio _retryDio;
final AuthLogin _loginClient;
// Single-flight lock: parallel 401s share the same login Future instead of
// each spawning a fresh row in api_tokens.
Future<bool>? _pendingReLogin;
MarianumConnectAuthInterceptor({
MarianumConnectTokenStorage tokenStorage =
const MarianumConnectTokenStorage(),
Dio? retryDio,
AuthLogin? loginClient,
}) : _tokenStorage = tokenStorage,
_retryDio = retryDio ?? Dio(),
_loginClient = loginClient ?? AuthLogin();
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
// Wait for an in-flight re-login so nachrückende Requests den frischen
// Token mitschicken statt ein eigenes 401 einzufangen.
final pending = _pendingReLogin;
if (pending != null) await pending;
final token = await _tokenStorage.readToken();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
Future<void> onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
final response = err.response;
if (response?.statusCode != 401 ||
err.requestOptions.extra[_retriedKey] == true) {
handler.next(err);
return;
}
final refreshed = await _attemptReLogin();
if (!refreshed) {
handler.next(err);
return;
}
try {
final retried = await _retryWithFreshToken(err.requestOptions);
handler.resolve(retried);
} on DioException catch (retryError) {
handler.next(retryError);
}
}
Future<bool> _attemptReLogin() {
final inFlight = _pendingReLogin;
if (inFlight != null) return inFlight;
final fresh = _performReLogin();
_pendingReLogin = fresh;
fresh.whenComplete(() {
if (identical(_pendingReLogin, fresh)) _pendingReLogin = null;
});
return fresh;
}
Future<bool> _performReLogin() async {
if (!AccountData().isPopulated()) return false;
try {
await _loginClient.run(
username: AccountData().getUsername(),
password: AccountData().getPassword(),
tokenName: await DeviceTokenName.resolve(),
);
return true;
} catch (_) {
await _tokenStorage.clear();
return false;
}
}
Future<Response<dynamic>> _retryWithFreshToken(
RequestOptions originalOptions,
) async {
final freshToken = await _tokenStorage.readToken();
final headers = Map<String, dynamic>.of(originalOptions.headers);
if (freshToken != null && freshToken.isNotEmpty) {
headers['Authorization'] = 'Bearer $freshToken';
}
final clone = originalOptions.copyWith(
headers: headers,
extra: {...originalOptions.extra, _retriedKey: true},
);
return _retryDio.fetch<dynamic>(clone);
}
}
@@ -0,0 +1,41 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
/// Bearer-token display name shown in the dashboard token list, in the form
/// `"Marianum Fulda App (Pixel 10)"`. Cached because device-info never
/// changes at runtime.
class DeviceTokenName {
static const String _appName = 'Marianum Fulda App';
static String? _cached;
static Future<String> resolve() async {
if (_cached != null) return _cached!;
final device = await _deviceLabel();
_cached = device.isEmpty ? _appName : '$_appName ($device)';
return _cached!;
}
static Future<String> _deviceLabel() async {
try {
final info = DeviceInfoPlugin();
if (Platform.isAndroid) {
final android = await info.androidInfo;
final model = android.model.trim();
return model.isNotEmpty ? model : android.device.trim();
}
if (Platform.isIOS) {
final ios = await info.iosInfo;
// utsname.machine bleibt auch ohne user-zugewiesenen Gerätenamen
// verfügbar; ios.name liefert auf iOS 16+ nur noch Generika.
final machine = ios.utsname.machine.trim();
if (machine.isNotEmpty) return machine;
return ios.name.trim();
}
} catch (_) {
// Device-Plugin nicht verfügbar (z.B. Tests).
}
return '';
}
}
@@ -0,0 +1,32 @@
import 'dart:developer';
import '../../../model/account_data.dart';
import '../../errors/auth_exception.dart';
import '../queries/auth_logout/auth_logout.dart';
import '../queries/auth_verify/auth_verify.dart';
import 'token_storage.dart';
/// Background credential probe — a server-side password rotation forces a
/// re-login on the next cold start even when the bearer token would still
/// be accepted.
class SessionValidator {
static Future<void> probeStored({
required Future<void> Function() onInvalidated,
}) async {
if (!AccountData().isPopulated()) return;
final username = AccountData().getUsername();
final password = AccountData().getPassword();
try {
await AuthVerify().run(username: username, password: password);
} on AuthException catch (e) {
if (e.statusCode != 401) return;
log('MC: stored credentials rejected — forcing re-login');
await AuthLogout().run();
await const MarianumConnectTokenStorage().clear();
await AccountData().removeData();
await onInvalidated();
} catch (e) {
log('MC: background credential check failed (transient): $e');
}
}
}
@@ -0,0 +1,45 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// Persists the Marianum-Connect bearer token in the platform keystore. Kept
/// separate from `AccountData` because the username/password live on (Nextcloud
/// + MHSL still need them) while the MC token is short-lived and per-endpoint.
class MarianumConnectTokenStorage {
static const _tokenKey = 'mc_bearer_token';
static const _tokenIdKey = 'mc_token_id';
static const _expiresAtKey = 'mc_token_expires_at';
final FlutterSecureStorage _storage;
const MarianumConnectTokenStorage([
this._storage = const FlutterSecureStorage(),
]);
Future<String?> readToken() => _storage.read(key: _tokenKey);
Future<String?> readTokenId() => _storage.read(key: _tokenIdKey);
Future<DateTime?> readExpiresAt() async {
final raw = await _storage.read(key: _expiresAtKey);
if (raw == null || raw.isEmpty) return null;
return DateTime.tryParse(raw);
}
Future<void> write({
required String token,
required String tokenId,
required DateTime? expiresAt,
}) async {
await _storage.write(key: _tokenKey, value: token);
await _storage.write(key: _tokenIdKey, value: tokenId);
await _storage.write(
key: _expiresAtKey,
value: expiresAt?.toIso8601String() ?? '',
);
}
Future<void> clear() async {
await _storage.delete(key: _tokenKey);
await _storage.delete(key: _tokenIdKey);
await _storage.delete(key: _expiresAtKey);
}
}
@@ -0,0 +1,49 @@
import 'package:dio/dio.dart';
import '../../errors/app_exception.dart';
import '../../errors/auth_exception.dart';
import '../../errors/network_exception.dart';
import '../../errors/parse_exception.dart';
import '../../errors/server_exception.dart';
/// Converts a DioException raised against the Marianum-Connect API into one of
/// the app's typed AppExceptions. Keeps the dio dependency out of call sites
/// that just want to render an error message.
AppException mapMarianumConnectError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return NetworkException.timeout(technicalDetails: error.message);
case DioExceptionType.connectionError:
return NetworkException(technicalDetails: error.message);
case DioExceptionType.badCertificate:
return const NetworkException(
userMessage:
'Die sichere Verbindung zum Marianum-Connect-Server wurde abgelehnt.',
);
case DioExceptionType.badResponse:
final status = error.response?.statusCode ?? -1;
if (status == 401) {
return AuthException.unauthorized(
technicalDetails: 'MC 401: ${error.response?.data}',
);
}
if (status == 403) {
return AuthException.forbidden(
technicalDetails: 'MC 403: ${error.response?.data}',
);
}
return ServerException(
statusCode: status,
technicalDetails: 'MC HTTP $status: ${error.response?.data}',
);
case DioExceptionType.cancel:
case DioExceptionType.unknown:
final inner = error.error;
if (inner is FormatException) {
return ParseException(technicalDetails: inner.message);
}
return NetworkException(technicalDetails: error.message);
}
}
@@ -0,0 +1,30 @@
import 'package:dio/dio.dart';
import 'auth/auth_interceptor.dart';
/// Singleton dio instance for the Marianum-Connect mobile API. Wired with the
/// bearer auth interceptor at startup; the base URL is resolved per request
/// through [MarianumConnectEndpoint] so settings changes take effect without
/// recreating the client.
class MarianumConnectApi {
static const Duration _connectTimeout = Duration(seconds: 10);
static const Duration _receiveTimeout = Duration(seconds: 20);
static final Dio _instance = _build();
static Dio dio() => _instance;
static Dio _build() {
final dio = Dio(
BaseOptions(
connectTimeout: _connectTimeout,
sendTimeout: _connectTimeout,
receiveTimeout: _receiveTimeout,
responseType: ResponseType.json,
contentType: 'application/json',
),
);
dio.interceptors.add(MarianumConnectAuthInterceptor());
return dio;
}
}
@@ -0,0 +1,22 @@
import '../../storage/dev_tools_settings.dart';
/// Singleton holding the currently active Marianum-Connect base URL. Fed by a
/// SettingsCubit listener in app.dart so every dio call picks up endpoint
/// changes without holding a reference to the cubit.
class MarianumConnectEndpoint {
static String _baseUrl = DevToolsSettings.liveUrl;
static String current() => _baseUrl;
static void update(String baseUrl) {
_baseUrl = baseUrl;
}
/// Joins the base URL with the mobile API prefix and the given path.
static String resolve(String relativePath) {
final path = relativePath.startsWith('/')
? relativePath.substring(1)
: relativePath;
return '$_baseUrl/api/mobile/v1/$path';
}
}
@@ -0,0 +1,61 @@
import 'package:dio/dio.dart';
import '../../auth/token_storage.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_endpoint.dart';
import 'auth_login_response.dart';
/// Performs the Marianum-Connect bearer login. Used both by the foreground
/// login flow and by the auth interceptor's silent re-auth on 401. Does *not*
/// run through the shared dio instance — that one has the interceptor, which
/// would attempt to re-auth us into a loop if our credentials are wrong.
class AuthLogin {
static const Duration _connectTimeout = Duration(seconds: 10);
static const Duration _receiveTimeout = Duration(seconds: 15);
final MarianumConnectTokenStorage _tokenStorage;
final Dio _dio;
AuthLogin({
MarianumConnectTokenStorage tokenStorage =
const MarianumConnectTokenStorage(),
Dio? dio,
}) : _tokenStorage = tokenStorage,
_dio =
dio ??
Dio(
BaseOptions(
connectTimeout: _connectTimeout,
receiveTimeout: _receiveTimeout,
sendTimeout: _connectTimeout,
responseType: ResponseType.json,
contentType: 'application/json',
),
);
Future<AuthLoginResponse> run({
required String username,
required String password,
required String tokenName,
}) async {
try {
final response = await _dio.post<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('auth/login'),
data: {
'username': username,
'password': password,
'tokenName': tokenName,
},
);
final payload = AuthLoginResponse.fromJson(response.data!);
await _tokenStorage.write(
token: payload.token,
tokenId: payload.tokenId,
expiresAt: payload.expiresAt,
);
return payload;
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
@@ -0,0 +1,54 @@
import 'package:json_annotation/json_annotation.dart';
part 'auth_login_response.g.dart';
@JsonSerializable()
class AuthLoginUser {
final String id;
final String username;
final String firstName;
final String lastName;
final String? userType;
final String? className;
AuthLoginUser({
required this.id,
required this.username,
required this.firstName,
required this.lastName,
required this.userType,
required this.className,
});
factory AuthLoginUser.fromJson(Map<String, dynamic> json) =>
_$AuthLoginUserFromJson(json);
Map<String, dynamic> toJson() => _$AuthLoginUserToJson(this);
}
@JsonSerializable()
class AuthLoginResponse {
final String token;
final String tokenId;
@JsonKey(fromJson: _expiresFromJson)
final DateTime? expiresAt;
final AuthLoginUser user;
AuthLoginResponse({
required this.token,
required this.tokenId,
required this.expiresAt,
required this.user,
});
factory AuthLoginResponse.fromJson(Map<String, dynamic> json) =>
_$AuthLoginResponseFromJson(json);
Map<String, dynamic> toJson() => _$AuthLoginResponseToJson(this);
static DateTime? _expiresFromJson(Object? value) {
if (value == null) return null;
if (value is String) return DateTime.tryParse(value);
return null;
}
}
@@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'auth_login_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AuthLoginUser _$AuthLoginUserFromJson(Map<String, dynamic> json) =>
AuthLoginUser(
id: json['id'] as String,
username: json['username'] as String,
firstName: json['firstName'] as String,
lastName: json['lastName'] as String,
userType: json['userType'] as String?,
className: json['className'] as String?,
);
Map<String, dynamic> _$AuthLoginUserToJson(AuthLoginUser instance) =>
<String, dynamic>{
'id': instance.id,
'username': instance.username,
'firstName': instance.firstName,
'lastName': instance.lastName,
'userType': instance.userType,
'className': instance.className,
};
AuthLoginResponse _$AuthLoginResponseFromJson(Map<String, dynamic> json) =>
AuthLoginResponse(
token: json['token'] as String,
tokenId: json['tokenId'] as String,
expiresAt: AuthLoginResponse._expiresFromJson(json['expiresAt']),
user: AuthLoginUser.fromJson(json['user'] as Map<String, dynamic>),
);
Map<String, dynamic> _$AuthLoginResponseToJson(AuthLoginResponse instance) =>
<String, dynamic>{
'token': instance.token,
'tokenId': instance.tokenId,
'expiresAt': instance.expiresAt?.toIso8601String(),
'user': instance.user,
};
@@ -0,0 +1,30 @@
import 'package:dio/dio.dart';
import '../../auth/token_storage.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Revokes the stored MC bearer token both server-side and locally. Best-effort
/// — a network error still clears the local token so the user isn't stuck with
/// an unusable session.
class AuthLogout {
final MarianumConnectTokenStorage _tokenStorage;
final Dio _dio;
AuthLogout({
MarianumConnectTokenStorage tokenStorage =
const MarianumConnectTokenStorage(),
Dio? dio,
}) : _tokenStorage = tokenStorage,
_dio = dio ?? MarianumConnectApi.dio();
Future<void> run() async {
try {
await _dio.post<void>(MarianumConnectEndpoint.resolve('auth/logout'));
} on DioException catch (_) {
// ignore — local clear below still happens
} finally {
await _tokenStorage.clear();
}
}
}
@@ -0,0 +1,62 @@
import 'package:dio/dio.dart';
import '../../../errors/auth_exception.dart';
import '../../auth/token_storage.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_endpoint.dart';
/// Probes that the stored bearer token still maps to the given credentials.
/// Server returns 200 only when the credentials belong to the user that the
/// token was issued for — a password rotation on that user's account flips
/// it to 401 even if the token itself would still be accepted.
///
/// Bypasses the shared dio singleton so the auth interceptor doesn't kick in
/// and obscure a real 401 with a silent re-login.
class AuthVerify {
static const Duration _connectTimeout = Duration(seconds: 10);
static const Duration _receiveTimeout = Duration(seconds: 15);
final MarianumConnectTokenStorage _tokenStorage;
final Dio _dio;
AuthVerify({
MarianumConnectTokenStorage tokenStorage =
const MarianumConnectTokenStorage(),
Dio? dio,
}) : _tokenStorage = tokenStorage,
_dio =
dio ??
Dio(
BaseOptions(
connectTimeout: _connectTimeout,
sendTimeout: _connectTimeout,
receiveTimeout: _receiveTimeout,
responseType: ResponseType.json,
contentType: 'application/json',
),
);
/// Throws [AuthException] on 401 (credentials no longer match the token's
/// user, token missing, or token rejected), other [AppException]s on
/// network/server errors. Completes silently on success.
Future<void> run({
required String username,
required String password,
}) async {
final token = await _tokenStorage.readToken();
if (token == null || token.isEmpty) {
throw AuthException.unauthorized(
technicalDetails: 'AuthVerify: no bearer token in storage',
);
}
try {
await _dio.post<void>(
MarianumConnectEndpoint.resolve('auth/verify'),
data: {'username': username, 'password': password},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
@@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'timetable_get_holidays_response.dart';
class TimetableGetHolidays {
final Dio _dio;
TimetableGetHolidays({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<TimetableGetHolidaysResponse> run() async {
try {
final response = await _dio.get<List<dynamic>>(
MarianumConnectEndpoint.resolve('timetable/holidays'),
);
final list = response.data!
.map((e) => McHoliday.fromJson(e as Map<String, dynamic>))
.toList();
return TimetableGetHolidaysResponse(result: list);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
@@ -0,0 +1,43 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'timetable_get_holidays_response.g.dart';
@JsonSerializable(explicitToJson: true)
class McHoliday {
final String shortName;
final String longName;
@JsonKey(fromJson: _dateFromJson, toJson: _dateToJson)
final DateTime startDate;
@JsonKey(fromJson: _dateFromJson, toJson: _dateToJson)
final DateTime endDate;
McHoliday({
required this.shortName,
required this.longName,
required this.startDate,
required this.endDate,
});
factory McHoliday.fromJson(Map<String, dynamic> json) =>
_$McHolidayFromJson(json);
Map<String, dynamic> toJson() => _$McHolidayToJson(this);
static DateTime _dateFromJson(String raw) => DateTime.parse(raw);
static String _dateToJson(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
}
@JsonSerializable(explicitToJson: true)
class TimetableGetHolidaysResponse extends ApiResponse {
final List<McHoliday> result;
TimetableGetHolidaysResponse({required this.result});
factory TimetableGetHolidaysResponse.fromJson(Map<String, dynamic> json) =>
_$TimetableGetHolidaysResponseFromJson(json);
Map<String, dynamic> toJson() => _$TimetableGetHolidaysResponseToJson(this);
}
@@ -0,0 +1,40 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'timetable_get_holidays_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
McHoliday _$McHolidayFromJson(Map<String, dynamic> json) => McHoliday(
shortName: json['shortName'] as String,
longName: json['longName'] as String,
startDate: McHoliday._dateFromJson(json['startDate'] as String),
endDate: McHoliday._dateFromJson(json['endDate'] as String),
);
Map<String, dynamic> _$McHolidayToJson(McHoliday instance) => <String, dynamic>{
'shortName': instance.shortName,
'longName': instance.longName,
'startDate': McHoliday._dateToJson(instance.startDate),
'endDate': McHoliday._dateToJson(instance.endDate),
};
TimetableGetHolidaysResponse _$TimetableGetHolidaysResponseFromJson(
Map<String, dynamic> json,
) =>
TimetableGetHolidaysResponse(
result: (json['result'] as List<dynamic>)
.map((e) => McHoliday.fromJson(e as Map<String, dynamic>))
.toList(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$TimetableGetHolidaysResponseToJson(
TimetableGetHolidaysResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
@@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'timetable_get_rooms_response.dart';
class TimetableGetRooms {
final Dio _dio;
TimetableGetRooms({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<TimetableGetRoomsResponse> run() async {
try {
final response = await _dio.get<List<dynamic>>(
MarianumConnectEndpoint.resolve('timetable/rooms'),
);
final list = response.data!
.map((e) => McRoom.fromJson(e as Map<String, dynamic>))
.toList();
return TimetableGetRoomsResponse(result: list);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
@@ -0,0 +1,28 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'timetable_get_rooms_response.g.dart';
@JsonSerializable(explicitToJson: true)
class McRoom {
final int id;
final String shortName;
final String longName;
McRoom({required this.id, required this.shortName, required this.longName});
factory McRoom.fromJson(Map<String, dynamic> json) => _$McRoomFromJson(json);
Map<String, dynamic> toJson() => _$McRoomToJson(this);
}
@JsonSerializable(explicitToJson: true)
class TimetableGetRoomsResponse extends ApiResponse {
final List<McRoom> result;
TimetableGetRoomsResponse({required this.result});
factory TimetableGetRoomsResponse.fromJson(Map<String, dynamic> json) =>
_$TimetableGetRoomsResponseFromJson(json);
Map<String, dynamic> toJson() => _$TimetableGetRoomsResponseToJson(this);
}
@@ -0,0 +1,38 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'timetable_get_rooms_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
McRoom _$McRoomFromJson(Map<String, dynamic> json) => McRoom(
id: (json['id'] as num).toInt(),
shortName: json['shortName'] as String,
longName: json['longName'] as String,
);
Map<String, dynamic> _$McRoomToJson(McRoom instance) => <String, dynamic>{
'id': instance.id,
'shortName': instance.shortName,
'longName': instance.longName,
};
TimetableGetRoomsResponse _$TimetableGetRoomsResponseFromJson(
Map<String, dynamic> json,
) =>
TimetableGetRoomsResponse(
result: (json['result'] as List<dynamic>)
.map((e) => McRoom.fromJson(e as Map<String, dynamic>))
.toList(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$TimetableGetRoomsResponseToJson(
TimetableGetRoomsResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
@@ -0,0 +1,23 @@
import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'timetable_get_schoolyear_response.dart';
class TimetableGetSchoolyear {
final Dio _dio;
TimetableGetSchoolyear({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<TimetableGetSchoolyearResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('timetable/schoolyear'),
);
return TimetableGetSchoolyearResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
@@ -0,0 +1,33 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'timetable_get_schoolyear_response.g.dart';
@JsonSerializable(explicitToJson: true)
class TimetableGetSchoolyearResponse extends ApiResponse {
final int id;
final String name;
@JsonKey(fromJson: _dateFromJson, toJson: _dateToJson)
final DateTime startDate;
@JsonKey(fromJson: _dateFromJson, toJson: _dateToJson)
final DateTime endDate;
TimetableGetSchoolyearResponse({
required this.id,
required this.name,
required this.startDate,
required this.endDate,
});
factory TimetableGetSchoolyearResponse.fromJson(Map<String, dynamic> json) =>
_$TimetableGetSchoolyearResponseFromJson(json);
Map<String, dynamic> toJson() =>
_$TimetableGetSchoolyearResponseToJson(this);
static DateTime _dateFromJson(String raw) => DateTime.parse(raw);
static String _dateToJson(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
}
@@ -0,0 +1,34 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'timetable_get_schoolyear_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
TimetableGetSchoolyearResponse _$TimetableGetSchoolyearResponseFromJson(
Map<String, dynamic> json,
) =>
TimetableGetSchoolyearResponse(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
startDate: TimetableGetSchoolyearResponse._dateFromJson(
json['startDate'] as String,
),
endDate: TimetableGetSchoolyearResponse._dateFromJson(
json['endDate'] as String,
),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$TimetableGetSchoolyearResponseToJson(
TimetableGetSchoolyearResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'id': instance.id,
'name': instance.name,
'startDate': TimetableGetSchoolyearResponse._dateToJson(instance.startDate),
'endDate': TimetableGetSchoolyearResponse._dateToJson(instance.endDate),
};
@@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'timetable_get_subjects_response.dart';
class TimetableGetSubjects {
final Dio _dio;
TimetableGetSubjects({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<TimetableGetSubjectsResponse> run() async {
try {
final response = await _dio.get<List<dynamic>>(
MarianumConnectEndpoint.resolve('timetable/subjects'),
);
final list = response.data!
.map((e) => McSubject.fromJson(e as Map<String, dynamic>))
.toList();
return TimetableGetSubjectsResponse(result: list);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
@@ -0,0 +1,33 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'timetable_get_subjects_response.g.dart';
@JsonSerializable(explicitToJson: true)
class McSubject {
final int id;
final String shortName;
final String longName;
McSubject({
required this.id,
required this.shortName,
required this.longName,
});
factory McSubject.fromJson(Map<String, dynamic> json) =>
_$McSubjectFromJson(json);
Map<String, dynamic> toJson() => _$McSubjectToJson(this);
}
@JsonSerializable(explicitToJson: true)
class TimetableGetSubjectsResponse extends ApiResponse {
final List<McSubject> result;
TimetableGetSubjectsResponse({required this.result});
factory TimetableGetSubjectsResponse.fromJson(Map<String, dynamic> json) =>
_$TimetableGetSubjectsResponseFromJson(json);
Map<String, dynamic> toJson() => _$TimetableGetSubjectsResponseToJson(this);
}
@@ -0,0 +1,38 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'timetable_get_subjects_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
McSubject _$McSubjectFromJson(Map<String, dynamic> json) => McSubject(
id: (json['id'] as num).toInt(),
shortName: json['shortName'] as String,
longName: json['longName'] as String,
);
Map<String, dynamic> _$McSubjectToJson(McSubject instance) => <String, dynamic>{
'id': instance.id,
'shortName': instance.shortName,
'longName': instance.longName,
};
TimetableGetSubjectsResponse _$TimetableGetSubjectsResponseFromJson(
Map<String, dynamic> json,
) =>
TimetableGetSubjectsResponse(
result: (json['result'] as List<dynamic>)
.map((e) => McSubject.fromJson(e as Map<String, dynamic>))
.toList(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$TimetableGetSubjectsResponseToJson(
TimetableGetSubjectsResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
@@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'timetable_get_timegrid_response.dart';
class TimetableGetTimegrid {
final Dio _dio;
TimetableGetTimegrid({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<TimetableGetTimegridResponse> run() async {
try {
final response = await _dio.get<List<dynamic>>(
MarianumConnectEndpoint.resolve('timetable/timegrid'),
);
final list = response.data!
.map((e) => McTimegridUnit.fromJson(e as Map<String, dynamic>))
.toList();
return TimetableGetTimegridResponse(result: list);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
@@ -0,0 +1,98 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'timetable_get_timegrid_response.g.dart';
/// Java DayOfWeek serializes as the enum name (MONDAY, TUESDAY, …).
enum McDayOfWeek {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday,
}
McDayOfWeek _dayFromJson(String raw) {
switch (raw.toUpperCase()) {
case 'MONDAY':
return McDayOfWeek.monday;
case 'TUESDAY':
return McDayOfWeek.tuesday;
case 'WEDNESDAY':
return McDayOfWeek.wednesday;
case 'THURSDAY':
return McDayOfWeek.thursday;
case 'FRIDAY':
return McDayOfWeek.friday;
case 'SATURDAY':
return McDayOfWeek.saturday;
case 'SUNDAY':
return McDayOfWeek.sunday;
default:
// Unknown values keep the timetable rendering from crashing; the UI
// falls back to its hardcoded grid in that case.
return McDayOfWeek.monday;
}
}
String _dayToJson(McDayOfWeek d) {
switch (d) {
case McDayOfWeek.monday:
return 'MONDAY';
case McDayOfWeek.tuesday:
return 'TUESDAY';
case McDayOfWeek.wednesday:
return 'WEDNESDAY';
case McDayOfWeek.thursday:
return 'THURSDAY';
case McDayOfWeek.friday:
return 'FRIDAY';
case McDayOfWeek.saturday:
return 'SATURDAY';
case McDayOfWeek.sunday:
return 'SUNDAY';
}
}
@JsonSerializable(explicitToJson: true)
class McTimegridUnit {
@JsonKey(fromJson: _dayFromJson, toJson: _dayToJson)
final McDayOfWeek dayOfWeek;
final String label;
@JsonKey(fromJson: _timeFromJson, toJson: _timeToJson)
final DateTime startTime;
@JsonKey(fromJson: _timeFromJson, toJson: _timeToJson)
final DateTime endTime;
McTimegridUnit({
required this.dayOfWeek,
required this.label,
required this.startTime,
required this.endTime,
});
factory McTimegridUnit.fromJson(Map<String, dynamic> json) =>
_$McTimegridUnitFromJson(json);
Map<String, dynamic> toJson() => _$McTimegridUnitToJson(this);
static DateTime _timeFromJson(String raw) => DateTime.parse('1970-01-01T$raw');
static String _timeToJson(DateTime t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}';
}
@JsonSerializable(explicitToJson: true)
class TimetableGetTimegridResponse extends ApiResponse {
final List<McTimegridUnit> result;
TimetableGetTimegridResponse({required this.result});
factory TimetableGetTimegridResponse.fromJson(Map<String, dynamic> json) =>
_$TimetableGetTimegridResponseFromJson(json);
Map<String, dynamic> toJson() => _$TimetableGetTimegridResponseToJson(this);
}
@@ -0,0 +1,42 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'timetable_get_timegrid_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
McTimegridUnit _$McTimegridUnitFromJson(Map<String, dynamic> json) =>
McTimegridUnit(
dayOfWeek: _dayFromJson(json['dayOfWeek'] as String),
label: json['label'] as String,
startTime: McTimegridUnit._timeFromJson(json['startTime'] as String),
endTime: McTimegridUnit._timeFromJson(json['endTime'] as String),
);
Map<String, dynamic> _$McTimegridUnitToJson(McTimegridUnit instance) =>
<String, dynamic>{
'dayOfWeek': _dayToJson(instance.dayOfWeek),
'label': instance.label,
'startTime': McTimegridUnit._timeToJson(instance.startTime),
'endTime': McTimegridUnit._timeToJson(instance.endTime),
};
TimetableGetTimegridResponse _$TimetableGetTimegridResponseFromJson(
Map<String, dynamic> json,
) =>
TimetableGetTimegridResponse(
result: (json['result'] as List<dynamic>)
.map((e) => McTimegridUnit.fromJson(e as Map<String, dynamic>))
.toList(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$TimetableGetTimegridResponseToJson(
TimetableGetTimegridResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
@@ -0,0 +1,33 @@
import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'timetable_get_week_response.dart';
class TimetableGetWeek {
final Dio _dio;
TimetableGetWeek({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<TimetableGetWeekResponse> run({
required DateTime from,
required DateTime until,
}) async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('timetable/me'),
queryParameters: {
'from': _format(from),
'until': _format(until),
},
);
return TimetableGetWeekResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
String _format(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
}
@@ -0,0 +1,108 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'timetable_get_week_response.g.dart';
@JsonSerializable(explicitToJson: true)
class McTimetableTeacher {
final String shortName;
final String displayName;
final String? originalShortName;
final String? originalDisplayName;
McTimetableTeacher({
required this.shortName,
required this.displayName,
this.originalShortName,
this.originalDisplayName,
});
factory McTimetableTeacher.fromJson(Map<String, dynamic> json) =>
_$McTimetableTeacherFromJson(json);
Map<String, dynamic> toJson() => _$McTimetableTeacherToJson(this);
}
@JsonSerializable(explicitToJson: true)
class McTimetableEntry {
final int id;
@JsonKey(fromJson: _dateFromJson, toJson: _dateToJson)
final DateTime date;
@JsonKey(fromJson: _timeFromJson, toJson: _timeToJson)
final DateTime startTime;
@JsonKey(fromJson: _timeFromJson, toJson: _timeToJson)
final DateTime endTime;
final List<String> subjects;
final List<McTimetableTeacher> teachers;
final List<String> rooms;
final List<String> classNames;
final String lessonType;
final String status;
final String? substitutionText;
final String? lessonText;
final String? infoText;
McTimetableEntry({
required this.id,
required this.date,
required this.startTime,
required this.endTime,
required this.subjects,
required this.teachers,
required this.rooms,
required this.classNames,
required this.lessonType,
required this.status,
required this.substitutionText,
required this.lessonText,
required this.infoText,
});
factory McTimetableEntry.fromJson(Map<String, dynamic> json) =>
_$McTimetableEntryFromJson(json);
Map<String, dynamic> toJson() => _$McTimetableEntryToJson(this);
/// Combines the calendar date with the hour/minute portion of [startTime]
/// (which carries a 1970 placeholder date) into a real DateTime.
DateTime get startDateTime =>
DateTime(date.year, date.month, date.day, startTime.hour, startTime.minute);
DateTime get endDateTime =>
DateTime(date.year, date.month, date.day, endTime.hour, endTime.minute);
static DateTime _dateFromJson(String raw) => DateTime.parse(raw);
static String _dateToJson(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
// Backend sends ISO_LOCAL_TIME (e.g. "08:00:00" or "08:00"). Parsed via a
// fixed-date prefix so we get a real DateTime out of it; only hour/minute
// are meaningful for rendering.
static DateTime _timeFromJson(String raw) => DateTime.parse('1970-01-01T$raw');
static String _timeToJson(DateTime t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}';
}
@JsonSerializable(explicitToJson: true)
class TimetableGetWeekResponse extends ApiResponse {
@JsonKey(fromJson: McTimetableEntry._dateFromJson, toJson: McTimetableEntry._dateToJson)
final DateTime from;
@JsonKey(fromJson: McTimetableEntry._dateFromJson, toJson: McTimetableEntry._dateToJson)
final DateTime until;
final List<McTimetableEntry> entries;
TimetableGetWeekResponse({
required this.from,
required this.until,
required this.entries,
});
factory TimetableGetWeekResponse.fromJson(Map<String, dynamic> json) =>
_$TimetableGetWeekResponseFromJson(json);
Map<String, dynamic> toJson() => _$TimetableGetWeekResponseToJson(this);
}
@@ -0,0 +1,86 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'timetable_get_week_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
McTimetableTeacher _$McTimetableTeacherFromJson(Map<String, dynamic> json) =>
McTimetableTeacher(
shortName: json['shortName'] as String,
displayName: json['displayName'] as String,
originalShortName: json['originalShortName'] as String?,
originalDisplayName: json['originalDisplayName'] as String?,
);
Map<String, dynamic> _$McTimetableTeacherToJson(McTimetableTeacher instance) =>
<String, dynamic>{
'shortName': instance.shortName,
'displayName': instance.displayName,
'originalShortName': instance.originalShortName,
'originalDisplayName': instance.originalDisplayName,
};
McTimetableEntry _$McTimetableEntryFromJson(Map<String, dynamic> json) =>
McTimetableEntry(
id: (json['id'] as num).toInt(),
date: McTimetableEntry._dateFromJson(json['date'] as String),
startTime: McTimetableEntry._timeFromJson(json['startTime'] as String),
endTime: McTimetableEntry._timeFromJson(json['endTime'] as String),
subjects: (json['subjects'] as List<dynamic>)
.map((e) => e as String)
.toList(),
teachers: (json['teachers'] as List<dynamic>)
.map((e) => McTimetableTeacher.fromJson(e as Map<String, dynamic>))
.toList(),
rooms: (json['rooms'] as List<dynamic>).map((e) => e as String).toList(),
classNames: (json['classNames'] as List<dynamic>)
.map((e) => e as String)
.toList(),
lessonType: json['lessonType'] as String,
status: json['status'] as String,
substitutionText: json['substitutionText'] as String?,
lessonText: json['lessonText'] as String?,
infoText: json['infoText'] as String?,
);
Map<String, dynamic> _$McTimetableEntryToJson(McTimetableEntry instance) =>
<String, dynamic>{
'id': instance.id,
'date': McTimetableEntry._dateToJson(instance.date),
'startTime': McTimetableEntry._timeToJson(instance.startTime),
'endTime': McTimetableEntry._timeToJson(instance.endTime),
'subjects': instance.subjects,
'teachers': instance.teachers.map((e) => e.toJson()).toList(),
'rooms': instance.rooms,
'classNames': instance.classNames,
'lessonType': instance.lessonType,
'status': instance.status,
'substitutionText': instance.substitutionText,
'lessonText': instance.lessonText,
'infoText': instance.infoText,
};
TimetableGetWeekResponse _$TimetableGetWeekResponseFromJson(
Map<String, dynamic> json,
) =>
TimetableGetWeekResponse(
from: McTimetableEntry._dateFromJson(json['from'] as String),
until: McTimetableEntry._dateFromJson(json['until'] as String),
entries: (json['entries'] as List<dynamic>)
.map((e) => McTimetableEntry.fromJson(e as Map<String, dynamic>))
.toList(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$TimetableGetWeekResponseToJson(
TimetableGetWeekResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'from': McTimetableEntry._dateToJson(instance.from),
'until': McTimetableEntry._dateToJson(instance.until),
'entries': instance.entries.map((e) => e.toJson()).toList(),
};
@@ -1,66 +0,0 @@
import 'dart:async';
import 'dart:convert';
import '../../../../model/account_data.dart';
import '../../webuntis_api.dart';
import 'authenticate_params.dart';
import 'authenticate_response.dart';
class Authenticate extends WebuntisApi {
AuthenticateParams param;
Authenticate(this.param)
: super('authenticate', param, authenticatedResponse: false);
@override
Future<AuthenticateResponse> run() async {
awaitingResponse = true;
try {
final rawAnswer = await query(this);
final decoded = jsonDecode(rawAnswer) as Map<String, dynamic>;
final response = finalize(
AuthenticateResponse.fromJson(
decoded['result'] as Map<String, dynamic>,
),
);
_lastResponse = response;
if (!awaitedResponse.isCompleted) awaitedResponse.complete();
return response;
} catch (e) {
// Surface the error to anyone waiting on the current completer, then
// install a fresh one so a future attempt can succeed. Without this,
// any later call to getSession() would hang forever on a completer
// that is already settled with no listeners (or never settles at all).
if (!awaitedResponse.isCompleted) awaitedResponse.completeError(e);
awaitedResponse = Completer<void>();
rethrow;
} finally {
awaitingResponse = false;
}
}
static bool awaitingResponse = false;
static Completer<void> awaitedResponse = Completer<void>();
static AuthenticateResponse? _lastResponse;
static Future<void> createSession() async {
_lastResponse = await Authenticate(
AuthenticateParams(
user: AccountData().getUsername(),
password: AccountData().getPassword(),
),
).run();
}
static Future<AuthenticateResponse> getSession() async {
if (awaitingResponse) {
await awaitedResponse.future;
}
if (_lastResponse == null) {
awaitingResponse = true;
await createSession();
}
return _lastResponse!;
}
}
@@ -1,17 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_params.dart';
part 'authenticate_params.g.dart';
@JsonSerializable()
class AuthenticateParams extends ApiParams {
String user;
String password;
AuthenticateParams({required this.user, required this.password});
factory AuthenticateParams.fromJson(Map<String, dynamic> json) =>
_$AuthenticateParamsFromJson(json);
Map<String, dynamic> toJson() => _$AuthenticateParamsToJson(this);
}
@@ -1,16 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authenticate_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AuthenticateParams _$AuthenticateParamsFromJson(Map<String, dynamic> json) =>
AuthenticateParams(
user: json['user'] as String,
password: json['password'] as String,
);
Map<String, dynamic> _$AuthenticateParamsToJson(AuthenticateParams instance) =>
<String, dynamic>{'user': instance.user, 'password': instance.password};
@@ -1,24 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'authenticate_response.g.dart';
@JsonSerializable()
class AuthenticateResponse extends ApiResponse {
String sessionId;
int personType;
int personId;
int klasseId;
AuthenticateResponse(
this.sessionId,
this.personType,
this.personId,
this.klasseId,
);
factory AuthenticateResponse.fromJson(Map<String, dynamic> json) =>
_$AuthenticateResponseFromJson(json);
Map<String, dynamic> toJson() => _$AuthenticateResponseToJson(this);
}
@@ -1,30 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authenticate_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AuthenticateResponse _$AuthenticateResponseFromJson(
Map<String, dynamic> json,
) =>
AuthenticateResponse(
json['sessionId'] as String,
(json['personType'] as num).toInt(),
(json['personId'] as num).toInt(),
(json['klasseId'] as num).toInt(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$AuthenticateResponseToJson(
AuthenticateResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'sessionId': instance.sessionId,
'personType': instance.personType,
'personId': instance.personId,
'klasseId': instance.klasseId,
};
@@ -1,18 +0,0 @@
import 'dart:convert';
import '../../webuntis_api.dart';
import 'get_current_schoolyear_response.dart';
class GetCurrentSchoolyear extends WebuntisApi {
GetCurrentSchoolyear() : super('getCurrentSchoolyear', null);
@override
Future<GetCurrentSchoolyearResponse> run() async {
final rawAnswer = await query(this);
return finalize(
GetCurrentSchoolyearResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
}
}
@@ -1,15 +0,0 @@
import '../../../request_cache.dart';
import 'get_current_schoolyear.dart';
import 'get_current_schoolyear_response.dart';
class GetCurrentSchoolyearCache
extends SimpleCache<GetCurrentSchoolyearResponse> {
GetCurrentSchoolyearCache({super.onUpdate, super.onError, super.renew})
: super(
cacheTime: RequestCache.cacheDay,
loader: () => GetCurrentSchoolyear().run(),
fromJson: GetCurrentSchoolyearResponse.fromJson,
) {
start('wu-current-schoolyear');
}
}
@@ -1,39 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'get_current_schoolyear_response.g.dart';
/// Wraps Webuntis' `getCurrentSchoolyear` payload. The server returns a
/// single object with the current school year's bounds (yyyyMMdd integers).
@JsonSerializable(explicitToJson: true)
class GetCurrentSchoolyearResponse extends ApiResponse {
GetCurrentSchoolyearResponseObject result;
GetCurrentSchoolyearResponse(this.result);
factory GetCurrentSchoolyearResponse.fromJson(Map<String, dynamic> json) =>
_$GetCurrentSchoolyearResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetCurrentSchoolyearResponseToJson(this);
}
@JsonSerializable()
class GetCurrentSchoolyearResponseObject {
int id;
String name;
int startDate;
int endDate;
GetCurrentSchoolyearResponseObject(
this.id,
this.name,
this.startDate,
this.endDate,
);
factory GetCurrentSchoolyearResponseObject.fromJson(
Map<String, dynamic> json,
) => _$GetCurrentSchoolyearResponseObjectFromJson(json);
Map<String, dynamic> toJson() =>
_$GetCurrentSchoolyearResponseObjectToJson(this);
}
@@ -1,44 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_current_schoolyear_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetCurrentSchoolyearResponse _$GetCurrentSchoolyearResponseFromJson(
Map<String, dynamic> json,
) =>
GetCurrentSchoolyearResponse(
GetCurrentSchoolyearResponseObject.fromJson(
json['result'] as Map<String, dynamic>,
),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetCurrentSchoolyearResponseToJson(
GetCurrentSchoolyearResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.toJson(),
};
GetCurrentSchoolyearResponseObject _$GetCurrentSchoolyearResponseObjectFromJson(
Map<String, dynamic> json,
) => GetCurrentSchoolyearResponseObject(
(json['id'] as num).toInt(),
json['name'] as String,
(json['startDate'] as num).toInt(),
(json['endDate'] as num).toInt(),
);
Map<String, dynamic> _$GetCurrentSchoolyearResponseObjectToJson(
GetCurrentSchoolyearResponseObject instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'startDate': instance.startDate,
'endDate': instance.endDate,
};
@@ -1,34 +0,0 @@
import 'dart:convert';
import '../../webuntis_api.dart';
import 'get_holidays_response.dart';
class GetHolidays extends WebuntisApi {
GetHolidays() : super('getHolidays', null);
@override
Future<GetHolidaysResponse> run() async {
final rawAnswer = await query(this);
return finalize(
GetHolidaysResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
}
static GetHolidaysResponseObject? find(
GetHolidaysResponse holidaysResponse, {
DateTime? time,
}) {
time ??= DateTime.now();
time = DateTime(time.year, time.month, time.day, 0, 0, 0, 0, 0);
for (var element in holidaysResponse.result) {
var start = DateTime.parse(element.startDate.toString());
var end = DateTime.parse(element.endDate.toString());
if (!start.isAfter(time) && !end.isBefore(time)) return element;
}
return null;
}
}
@@ -1,14 +0,0 @@
import '../../../request_cache.dart';
import 'get_holidays.dart';
import 'get_holidays_response.dart';
class GetHolidaysCache extends SimpleCache<GetHolidaysResponse> {
GetHolidaysCache({super.onUpdate, super.onError, super.renew})
: super(
cacheTime: RequestCache.cacheDay,
loader: () => GetHolidays().run(),
fromJson: GetHolidaysResponse.fromJson,
) {
start('wu-holidays');
}
}
@@ -1,37 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'get_holidays_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetHolidaysResponse extends ApiResponse {
Set<GetHolidaysResponseObject> result;
GetHolidaysResponse(this.result);
factory GetHolidaysResponse.fromJson(Map<String, dynamic> json) =>
_$GetHolidaysResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetHolidaysResponseToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetHolidaysResponseObject {
int id;
String name;
String longName;
int startDate;
int endDate;
GetHolidaysResponseObject(
this.id,
this.name,
this.longName,
this.startDate,
this.endDate,
);
factory GetHolidaysResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetHolidaysResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetHolidaysResponseObjectToJson(this);
}
@@ -1,47 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_holidays_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetHolidaysResponse _$GetHolidaysResponseFromJson(Map<String, dynamic> json) =>
GetHolidaysResponse(
(json['result'] as List<dynamic>)
.map(
(e) =>
GetHolidaysResponseObject.fromJson(e as Map<String, dynamic>),
)
.toSet(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetHolidaysResponseToJson(
GetHolidaysResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetHolidaysResponseObject _$GetHolidaysResponseObjectFromJson(
Map<String, dynamic> json,
) => GetHolidaysResponseObject(
(json['id'] as num).toInt(),
json['name'] as String,
json['longName'] as String,
(json['startDate'] as num).toInt(),
(json['endDate'] as num).toInt(),
);
Map<String, dynamic> _$GetHolidaysResponseObjectToJson(
GetHolidaysResponseObject instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'longName': instance.longName,
'startDate': instance.startDate,
'endDate': instance.endDate,
};
@@ -1,26 +0,0 @@
import 'dart:convert';
import 'dart:developer';
import '../../webuntis_api.dart';
import 'get_rooms_response.dart';
class GetRooms extends WebuntisApi {
GetRooms() : super('getRooms', null);
@override
Future<GetRoomsResponse> run() async {
final rawAnswer = await query(this);
try {
return finalize(
GetRoomsResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
} catch (e, trace) {
log(trace.toString());
log('Failed to parse getRoom data with server response: $rawAnswer');
}
throw Exception('Failed to parse getRoom server response: $rawAnswer');
}
}
@@ -1,14 +0,0 @@
import '../../../request_cache.dart';
import 'get_rooms.dart';
import 'get_rooms_response.dart';
class GetRoomsCache extends SimpleCache<GetRoomsResponse> {
GetRoomsCache({super.onUpdate, super.onError, super.renew})
: super(
cacheTime: RequestCache.cacheHour,
loader: () => GetRooms().run(),
fromJson: GetRoomsResponse.fromJson,
) {
start('wu-rooms');
}
}
@@ -1,37 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'get_rooms_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetRoomsResponse extends ApiResponse {
Set<GetRoomsResponseObject> result;
GetRoomsResponse(this.result);
factory GetRoomsResponse.fromJson(Map<String, dynamic> json) =>
_$GetRoomsResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetRoomsResponseToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetRoomsResponseObject {
int id;
String name;
String longName;
bool active;
String building;
GetRoomsResponseObject(
this.id,
this.name,
this.longName,
this.active,
this.building,
);
factory GetRoomsResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetRoomsResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetRoomsResponseObjectToJson(this);
}
@@ -1,45 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_rooms_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetRoomsResponse _$GetRoomsResponseFromJson(Map<String, dynamic> json) =>
GetRoomsResponse(
(json['result'] as List<dynamic>)
.map(
(e) => GetRoomsResponseObject.fromJson(e as Map<String, dynamic>),
)
.toSet(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetRoomsResponseToJson(GetRoomsResponse instance) =>
<String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetRoomsResponseObject _$GetRoomsResponseObjectFromJson(
Map<String, dynamic> json,
) => GetRoomsResponseObject(
(json['id'] as num).toInt(),
json['name'] as String,
json['longName'] as String,
json['active'] as bool,
json['building'] as String,
);
Map<String, dynamic> _$GetRoomsResponseObjectToJson(
GetRoomsResponseObject instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'longName': instance.longName,
'active': instance.active,
'building': instance.building,
};
@@ -1,18 +0,0 @@
import 'dart:convert';
import '../../webuntis_api.dart';
import 'get_subjects_response.dart';
class GetSubjects extends WebuntisApi {
GetSubjects() : super('getSubjects', null);
@override
Future<GetSubjectsResponse> run() async {
final rawAnswer = await query(this);
return finalize(
GetSubjectsResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
}
}
@@ -1,14 +0,0 @@
import '../../../request_cache.dart';
import 'get_subjects.dart';
import 'get_subjects_response.dart';
class GetSubjectsCache extends SimpleCache<GetSubjectsResponse> {
GetSubjectsCache({super.onUpdate, super.onError, super.renew})
: super(
cacheTime: RequestCache.cacheHour,
loader: () => GetSubjects().run(),
fromJson: GetSubjectsResponse.fromJson,
) {
start('wu-subjects');
}
}
@@ -1,37 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'get_subjects_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetSubjectsResponse extends ApiResponse {
Set<GetSubjectsResponseObject> result;
GetSubjectsResponse(this.result);
factory GetSubjectsResponse.fromJson(Map<String, dynamic> json) =>
_$GetSubjectsResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetSubjectsResponseToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetSubjectsResponseObject {
int id;
String name;
String longName;
String alternateName;
bool active;
GetSubjectsResponseObject(
this.id,
this.name,
this.longName,
this.alternateName,
this.active,
);
factory GetSubjectsResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetSubjectsResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetSubjectsResponseObjectToJson(this);
}
@@ -1,47 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_subjects_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetSubjectsResponse _$GetSubjectsResponseFromJson(Map<String, dynamic> json) =>
GetSubjectsResponse(
(json['result'] as List<dynamic>)
.map(
(e) =>
GetSubjectsResponseObject.fromJson(e as Map<String, dynamic>),
)
.toSet(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetSubjectsResponseToJson(
GetSubjectsResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetSubjectsResponseObject _$GetSubjectsResponseObjectFromJson(
Map<String, dynamic> json,
) => GetSubjectsResponseObject(
(json['id'] as num).toInt(),
json['name'] as String,
json['longName'] as String,
json['alternateName'] as String,
json['active'] as bool,
);
Map<String, dynamic> _$GetSubjectsResponseObjectToJson(
GetSubjectsResponseObject instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'longName': instance.longName,
'alternateName': instance.alternateName,
'active': instance.active,
};
@@ -1,30 +0,0 @@
import 'dart:convert';
import 'dart:developer';
import '../../webuntis_api.dart';
import 'get_timegrid_units_response.dart';
class GetTimegridUnits extends WebuntisApi {
GetTimegridUnits() : super('getTimegridUnits', null);
@override
Future<GetTimegridUnitsResponse> run() async {
final rawAnswer = await query(this);
try {
return finalize(
GetTimegridUnitsResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
} catch (e, trace) {
log(trace.toString());
log(
'Failed to parse getTimegridUnits data with server response: $rawAnswer',
);
}
throw Exception(
'Failed to parse getTimegridUnits server response: $rawAnswer',
);
}
}
@@ -1,14 +0,0 @@
import '../../../request_cache.dart';
import 'get_timegrid_units.dart';
import 'get_timegrid_units_response.dart';
class GetTimegridUnitsCache extends SimpleCache<GetTimegridUnitsResponse> {
GetTimegridUnitsCache({super.onUpdate, super.renew})
: super(
cacheTime: RequestCache.cacheDay,
loader: () => GetTimegridUnits().run(),
fromJson: GetTimegridUnitsResponse.fromJson,
) {
start('wu-timegrid');
}
}
@@ -1,41 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'get_timegrid_units_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetTimegridUnitsResponse extends ApiResponse {
List<GetTimegridUnitsResponseDay> result;
GetTimegridUnitsResponse(this.result);
factory GetTimegridUnitsResponse.fromJson(Map<String, dynamic> json) =>
_$GetTimegridUnitsResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetTimegridUnitsResponseDay {
int day;
List<GetTimegridUnitsResponseUnit> timeUnits;
GetTimegridUnitsResponseDay(this.day, this.timeUnits);
factory GetTimegridUnitsResponseDay.fromJson(Map<String, dynamic> json) =>
_$GetTimegridUnitsResponseDayFromJson(json);
Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseDayToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetTimegridUnitsResponseUnit {
String name;
int startTime;
int endTime;
GetTimegridUnitsResponseUnit(this.name, this.startTime, this.endTime);
factory GetTimegridUnitsResponseUnit.fromJson(Map<String, dynamic> json) =>
_$GetTimegridUnitsResponseUnitFromJson(json);
Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseUnitToJson(this);
}
@@ -1,64 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_timegrid_units_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetTimegridUnitsResponse _$GetTimegridUnitsResponseFromJson(
Map<String, dynamic> json,
) =>
GetTimegridUnitsResponse(
(json['result'] as List<dynamic>)
.map(
(e) => GetTimegridUnitsResponseDay.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetTimegridUnitsResponseToJson(
GetTimegridUnitsResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetTimegridUnitsResponseDay _$GetTimegridUnitsResponseDayFromJson(
Map<String, dynamic> json,
) => GetTimegridUnitsResponseDay(
(json['day'] as num).toInt(),
(json['timeUnits'] as List<dynamic>)
.map(
(e) => GetTimegridUnitsResponseUnit.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$GetTimegridUnitsResponseDayToJson(
GetTimegridUnitsResponseDay instance,
) => <String, dynamic>{
'day': instance.day,
'timeUnits': instance.timeUnits.map((e) => e.toJson()).toList(),
};
GetTimegridUnitsResponseUnit _$GetTimegridUnitsResponseUnitFromJson(
Map<String, dynamic> json,
) => GetTimegridUnitsResponseUnit(
json['name'] as String,
(json['startTime'] as num).toInt(),
(json['endTime'] as num).toInt(),
);
Map<String, dynamic> _$GetTimegridUnitsResponseUnitToJson(
GetTimegridUnitsResponseUnit instance,
) => <String, dynamic>{
'name': instance.name,
'startTime': instance.startTime,
'endTime': instance.endTime,
};
@@ -1,21 +0,0 @@
import 'dart:convert';
import '../../webuntis_api.dart';
import 'get_timetable_params.dart';
import 'get_timetable_response.dart';
class GetTimetable extends WebuntisApi {
GetTimetableParams params;
GetTimetable(this.params) : super('getTimetable', params);
@override
Future<GetTimetableResponse> run() async {
final rawAnswer = await query(this);
return finalize(
GetTimetableResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
}
}
@@ -1,43 +0,0 @@
import '../../../request_cache.dart';
import '../authenticate/authenticate.dart';
import 'get_timetable.dart';
import 'get_timetable_params.dart';
import 'get_timetable_response.dart';
class GetTimetableCache extends SimpleCache<GetTimetableResponse> {
GetTimetableCache({
required void Function(GetTimetableResponse) onUpdate,
super.onError,
required int startdate,
required int enddate,
super.renew,
}) : super(
cacheTime: RequestCache.cacheMinute,
loader: () => _load(startdate, enddate),
fromJson: GetTimetableResponse.fromJson,
onUpdate: onUpdate,
) {
start('wu-timetable-$startdate-$enddate');
}
static Future<GetTimetableResponse> _load(int startdate, int enddate) async {
final session = await Authenticate.getSession();
return GetTimetable(
GetTimetableParams(
options: GetTimetableParamsOptions(
element: GetTimetableParamsOptionsElement(
id: session.personId,
type: session.personType,
keyType: GetTimetableParamsOptionsElementKeyType.id,
),
startDate: startdate,
endDate: enddate,
teacherFields: GetTimetableParamsOptionsFields.all,
subjectFields: GetTimetableParamsOptionsFields.all,
roomFields: GetTimetableParamsOptionsFields.all,
klasseFields: GetTimetableParamsOptionsFields.all,
),
),
).run();
}
}
@@ -1,114 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_params.dart';
part 'get_timetable_params.g.dart';
@JsonSerializable(explicitToJson: true)
class GetTimetableParams extends ApiParams {
GetTimetableParamsOptions options;
GetTimetableParams({required this.options});
factory GetTimetableParams.fromJson(Map<String, dynamic> json) =>
_$GetTimetableParamsFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableParamsToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetTimetableParamsOptions {
GetTimetableParamsOptionsElement element;
@JsonKey(includeIfNull: false)
int? startDate;
@JsonKey(includeIfNull: false)
int? endDate;
@JsonKey(includeIfNull: false)
bool? onlyBaseTimetable;
@JsonKey(includeIfNull: false)
bool? showBooking;
@JsonKey(includeIfNull: false)
bool? showInfo;
@JsonKey(includeIfNull: false)
bool? showSubstText;
@JsonKey(includeIfNull: false)
bool? showLsText;
@JsonKey(includeIfNull: false)
bool? showLsNumber;
@JsonKey(includeIfNull: false)
bool? showStudentgroup;
@JsonKey(includeIfNull: false)
List<GetTimetableParamsOptionsFields>? klasseFields;
@JsonKey(includeIfNull: false)
List<GetTimetableParamsOptionsFields>? roomFields;
@JsonKey(includeIfNull: false)
List<GetTimetableParamsOptionsFields>? subjectFields;
@JsonKey(includeIfNull: false)
List<GetTimetableParamsOptionsFields>? teacherFields;
GetTimetableParamsOptions({
required this.element,
this.startDate,
this.endDate,
this.onlyBaseTimetable,
this.showBooking,
this.showInfo,
this.showSubstText,
this.showLsText,
this.showLsNumber,
this.showStudentgroup,
this.klasseFields,
this.roomFields,
this.subjectFields,
this.teacherFields,
});
factory GetTimetableParamsOptions.fromJson(Map<String, dynamic> json) =>
_$GetTimetableParamsOptionsFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableParamsOptionsToJson(this);
}
enum GetTimetableParamsOptionsFields {
@JsonValue('id')
id,
@JsonValue('name')
name,
@JsonValue('longname')
longname,
@JsonValue('externalkey')
externalkey;
static List<GetTimetableParamsOptionsFields> all = [
id,
name,
longname,
externalkey,
];
}
@JsonSerializable()
class GetTimetableParamsOptionsElement {
int id;
int type;
@JsonKey(includeIfNull: false)
GetTimetableParamsOptionsElementKeyType? keyType;
GetTimetableParamsOptionsElement({
required this.id,
required this.type,
this.keyType,
});
factory GetTimetableParamsOptionsElement.fromJson(
Map<String, dynamic> json,
) => _$GetTimetableParamsOptionsElementFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableParamsOptionsElementToJson(this);
}
enum GetTimetableParamsOptionsElementKeyType {
@JsonValue('id')
id,
@JsonValue('name')
name,
@JsonValue('externalkey')
externalkey,
}
@@ -1,106 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_timetable_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetTimetableParams _$GetTimetableParamsFromJson(Map<String, dynamic> json) =>
GetTimetableParams(
options: GetTimetableParamsOptions.fromJson(
json['options'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$GetTimetableParamsToJson(GetTimetableParams instance) =>
<String, dynamic>{'options': instance.options.toJson()};
GetTimetableParamsOptions _$GetTimetableParamsOptionsFromJson(
Map<String, dynamic> json,
) => GetTimetableParamsOptions(
element: GetTimetableParamsOptionsElement.fromJson(
json['element'] as Map<String, dynamic>,
),
startDate: (json['startDate'] as num?)?.toInt(),
endDate: (json['endDate'] as num?)?.toInt(),
onlyBaseTimetable: json['onlyBaseTimetable'] as bool?,
showBooking: json['showBooking'] as bool?,
showInfo: json['showInfo'] as bool?,
showSubstText: json['showSubstText'] as bool?,
showLsText: json['showLsText'] as bool?,
showLsNumber: json['showLsNumber'] as bool?,
showStudentgroup: json['showStudentgroup'] as bool?,
klasseFields: (json['klasseFields'] as List<dynamic>?)
?.map((e) => $enumDecode(_$GetTimetableParamsOptionsFieldsEnumMap, e))
.toList(),
roomFields: (json['roomFields'] as List<dynamic>?)
?.map((e) => $enumDecode(_$GetTimetableParamsOptionsFieldsEnumMap, e))
.toList(),
subjectFields: (json['subjectFields'] as List<dynamic>?)
?.map((e) => $enumDecode(_$GetTimetableParamsOptionsFieldsEnumMap, e))
.toList(),
teacherFields: (json['teacherFields'] as List<dynamic>?)
?.map((e) => $enumDecode(_$GetTimetableParamsOptionsFieldsEnumMap, e))
.toList(),
);
Map<String, dynamic> _$GetTimetableParamsOptionsToJson(
GetTimetableParamsOptions instance,
) => <String, dynamic>{
'element': instance.element.toJson(),
'startDate': ?instance.startDate,
'endDate': ?instance.endDate,
'onlyBaseTimetable': ?instance.onlyBaseTimetable,
'showBooking': ?instance.showBooking,
'showInfo': ?instance.showInfo,
'showSubstText': ?instance.showSubstText,
'showLsText': ?instance.showLsText,
'showLsNumber': ?instance.showLsNumber,
'showStudentgroup': ?instance.showStudentgroup,
'klasseFields': ?instance.klasseFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList(),
'roomFields': ?instance.roomFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList(),
'subjectFields': ?instance.subjectFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList(),
'teacherFields': ?instance.teacherFields
?.map((e) => _$GetTimetableParamsOptionsFieldsEnumMap[e]!)
.toList(),
};
const _$GetTimetableParamsOptionsFieldsEnumMap = {
GetTimetableParamsOptionsFields.id: 'id',
GetTimetableParamsOptionsFields.name: 'name',
GetTimetableParamsOptionsFields.longname: 'longname',
GetTimetableParamsOptionsFields.externalkey: 'externalkey',
};
GetTimetableParamsOptionsElement _$GetTimetableParamsOptionsElementFromJson(
Map<String, dynamic> json,
) => GetTimetableParamsOptionsElement(
id: (json['id'] as num).toInt(),
type: (json['type'] as num).toInt(),
keyType: $enumDecodeNullable(
_$GetTimetableParamsOptionsElementKeyTypeEnumMap,
json['keyType'],
),
);
Map<String, dynamic> _$GetTimetableParamsOptionsElementToJson(
GetTimetableParamsOptionsElement instance,
) => <String, dynamic>{
'id': instance.id,
'type': instance.type,
'keyType':
?_$GetTimetableParamsOptionsElementKeyTypeEnumMap[instance.keyType],
};
const _$GetTimetableParamsOptionsElementKeyTypeEnumMap = {
GetTimetableParamsOptionsElementKeyType.id: 'id',
GetTimetableParamsOptionsElementKeyType.name: 'name',
GetTimetableParamsOptionsElementKeyType.externalkey: 'externalkey',
};
@@ -1,171 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'get_timetable_response.g.dart';
@JsonSerializable(explicitToJson: true)
class GetTimetableResponse extends ApiResponse {
Set<GetTimetableResponseObject> result;
GetTimetableResponse(this.result);
factory GetTimetableResponse.fromJson(Map<String, dynamic> json) =>
_$GetTimetableResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableResponseToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetTimetableResponseObject {
int id;
int date;
int startTime;
int endTime;
String? lstype;
String? code;
String? info;
String? substText;
String? lstext;
int? lsnumber;
String? statflags;
String? activityType;
String? sg;
String? bkRemark;
String? bkText;
List<GetTimetableResponseObjectClass> kl;
List<GetTimetableResponseObjectTeacher> te;
List<GetTimetableResponseObjectSubject> su;
List<GetTimetableResponseObjectRoom> ro;
GetTimetableResponseObject({
required this.id,
required this.date,
required this.startTime,
required this.endTime,
this.lstype,
this.code,
this.info,
this.substText,
this.lstext,
this.lsnumber,
this.statflags,
this.activityType,
this.sg,
this.bkRemark,
required this.kl,
required this.te,
required this.su,
required this.ro,
});
factory GetTimetableResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetTimetableResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectToJson(this);
}
@JsonSerializable(explicitToJson: true)
class GetTimetableResponseObjectFields {
List<GetTimetableResponseObjectFieldsObject>? te;
GetTimetableResponseObjectFields(this.te);
factory GetTimetableResponseObjectFields.fromJson(
Map<String, dynamic> json,
) => _$GetTimetableResponseObjectFieldsFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectFieldsToJson(this);
}
@JsonSerializable()
class GetTimetableResponseObjectFieldsObject {
int? id;
String? name;
String? longname;
String? externalkey;
GetTimetableResponseObjectFieldsObject({
this.id,
this.name,
this.longname,
this.externalkey,
});
factory GetTimetableResponseObjectFieldsObject.fromJson(
Map<String, dynamic> json,
) => _$GetTimetableResponseObjectFieldsObjectFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectFieldsObjectToJson(this);
}
@JsonSerializable()
class GetTimetableResponseObjectClass {
int id;
String name;
String longname;
String? externalkey;
GetTimetableResponseObjectClass(
this.id,
this.name,
this.longname,
this.externalkey,
);
factory GetTimetableResponseObjectClass.fromJson(Map<String, dynamic> json) =>
_$GetTimetableResponseObjectClassFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectClassToJson(this);
}
@JsonSerializable()
class GetTimetableResponseObjectTeacher {
int id;
String name;
String longname;
int? orgid;
String? orgname;
String? externalkey;
GetTimetableResponseObjectTeacher(
this.id,
this.name,
this.longname,
this.orgid,
this.orgname,
this.externalkey,
);
factory GetTimetableResponseObjectTeacher.fromJson(
Map<String, dynamic> json,
) => _$GetTimetableResponseObjectTeacherFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectTeacherToJson(this);
}
@JsonSerializable()
class GetTimetableResponseObjectSubject {
int id;
String name;
String longname;
GetTimetableResponseObjectSubject(this.id, this.name, this.longname);
factory GetTimetableResponseObjectSubject.fromJson(
Map<String, dynamic> json,
) => _$GetTimetableResponseObjectSubjectFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectSubjectToJson(this);
}
@JsonSerializable()
class GetTimetableResponseObjectRoom {
int id;
String name;
String longname;
GetTimetableResponseObjectRoom(this.id, this.name, this.longname);
factory GetTimetableResponseObjectRoom.fromJson(Map<String, dynamic> json) =>
_$GetTimetableResponseObjectRoomFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectRoomToJson(this);
}
@@ -1,205 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_timetable_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetTimetableResponse _$GetTimetableResponseFromJson(
Map<String, dynamic> json,
) =>
GetTimetableResponse(
(json['result'] as List<dynamic>)
.map(
(e) => GetTimetableResponseObject.fromJson(
e as Map<String, dynamic>,
),
)
.toSet(),
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetTimetableResponseToJson(
GetTimetableResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'result': instance.result.map((e) => e.toJson()).toList(),
};
GetTimetableResponseObject _$GetTimetableResponseObjectFromJson(
Map<String, dynamic> json,
) => GetTimetableResponseObject(
id: (json['id'] as num).toInt(),
date: (json['date'] as num).toInt(),
startTime: (json['startTime'] as num).toInt(),
endTime: (json['endTime'] as num).toInt(),
lstype: json['lstype'] as String?,
code: json['code'] as String?,
info: json['info'] as String?,
substText: json['substText'] as String?,
lstext: json['lstext'] as String?,
lsnumber: (json['lsnumber'] as num?)?.toInt(),
statflags: json['statflags'] as String?,
activityType: json['activityType'] as String?,
sg: json['sg'] as String?,
bkRemark: json['bkRemark'] as String?,
kl: (json['kl'] as List<dynamic>)
.map(
(e) =>
GetTimetableResponseObjectClass.fromJson(e as Map<String, dynamic>),
)
.toList(),
te: (json['te'] as List<dynamic>)
.map(
(e) => GetTimetableResponseObjectTeacher.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
su: (json['su'] as List<dynamic>)
.map(
(e) => GetTimetableResponseObjectSubject.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
ro: (json['ro'] as List<dynamic>)
.map(
(e) =>
GetTimetableResponseObjectRoom.fromJson(e as Map<String, dynamic>),
)
.toList(),
)..bkText = json['bkText'] as String?;
Map<String, dynamic> _$GetTimetableResponseObjectToJson(
GetTimetableResponseObject instance,
) => <String, dynamic>{
'id': instance.id,
'date': instance.date,
'startTime': instance.startTime,
'endTime': instance.endTime,
'lstype': instance.lstype,
'code': instance.code,
'info': instance.info,
'substText': instance.substText,
'lstext': instance.lstext,
'lsnumber': instance.lsnumber,
'statflags': instance.statflags,
'activityType': instance.activityType,
'sg': instance.sg,
'bkRemark': instance.bkRemark,
'bkText': instance.bkText,
'kl': instance.kl.map((e) => e.toJson()).toList(),
'te': instance.te.map((e) => e.toJson()).toList(),
'su': instance.su.map((e) => e.toJson()).toList(),
'ro': instance.ro.map((e) => e.toJson()).toList(),
};
GetTimetableResponseObjectFields _$GetTimetableResponseObjectFieldsFromJson(
Map<String, dynamic> json,
) => GetTimetableResponseObjectFields(
(json['te'] as List<dynamic>?)
?.map(
(e) => GetTimetableResponseObjectFieldsObject.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
Map<String, dynamic> _$GetTimetableResponseObjectFieldsToJson(
GetTimetableResponseObjectFields instance,
) => <String, dynamic>{'te': instance.te?.map((e) => e.toJson()).toList()};
GetTimetableResponseObjectFieldsObject
_$GetTimetableResponseObjectFieldsObjectFromJson(Map<String, dynamic> json) =>
GetTimetableResponseObjectFieldsObject(
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
longname: json['longname'] as String?,
externalkey: json['externalkey'] as String?,
);
Map<String, dynamic> _$GetTimetableResponseObjectFieldsObjectToJson(
GetTimetableResponseObjectFieldsObject instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'longname': instance.longname,
'externalkey': instance.externalkey,
};
GetTimetableResponseObjectClass _$GetTimetableResponseObjectClassFromJson(
Map<String, dynamic> json,
) => GetTimetableResponseObjectClass(
(json['id'] as num).toInt(),
json['name'] as String,
json['longname'] as String,
json['externalkey'] as String?,
);
Map<String, dynamic> _$GetTimetableResponseObjectClassToJson(
GetTimetableResponseObjectClass instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'longname': instance.longname,
'externalkey': instance.externalkey,
};
GetTimetableResponseObjectTeacher _$GetTimetableResponseObjectTeacherFromJson(
Map<String, dynamic> json,
) => GetTimetableResponseObjectTeacher(
(json['id'] as num).toInt(),
json['name'] as String,
json['longname'] as String,
(json['orgid'] as num?)?.toInt(),
json['orgname'] as String?,
json['externalkey'] as String?,
);
Map<String, dynamic> _$GetTimetableResponseObjectTeacherToJson(
GetTimetableResponseObjectTeacher instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'longname': instance.longname,
'orgid': instance.orgid,
'orgname': instance.orgname,
'externalkey': instance.externalkey,
};
GetTimetableResponseObjectSubject _$GetTimetableResponseObjectSubjectFromJson(
Map<String, dynamic> json,
) => GetTimetableResponseObjectSubject(
(json['id'] as num).toInt(),
json['name'] as String,
json['longname'] as String,
);
Map<String, dynamic> _$GetTimetableResponseObjectSubjectToJson(
GetTimetableResponseObjectSubject instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'longname': instance.longname,
};
GetTimetableResponseObjectRoom _$GetTimetableResponseObjectRoomFromJson(
Map<String, dynamic> json,
) => GetTimetableResponseObjectRoom(
(json['id'] as num).toInt(),
json['name'] as String,
json['longname'] as String,
);
Map<String, dynamic> _$GetTimetableResponseObjectRoomToJson(
GetTimetableResponseObjectRoom instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'longname': instance.longname,
};
@@ -1,75 +0,0 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import '../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../queries/get_rooms/get_rooms_response.dart';
import '../queries/get_subjects/get_subjects_response.dart';
/// Resolves Webuntis IDs (subject, room) against the cached `TimetableState`.
/// When a record is missing the resolver returns a placeholder fallback
/// instead of `null` so call sites stay branch-free.
class LessonResolver {
static GetSubjectsResponseObject resolveSubject(
TimetableState state,
int? id,
) {
final fallback = GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true);
if (id == null) return fallback;
return state.subjects?.result.firstWhereOrNull((s) => s.id == id) ??
fallback;
}
static GetRoomsResponseObject resolveRoom(TimetableState state, int? id) {
final fallback = GetRoomsResponseObject(0, '?', 'Unbekannt', true, '');
if (id == null) return fallback;
return state.rooms?.result.firstWhereOrNull((r) => r.id == id) ?? fallback;
}
}
/// Pure formatting/labelling helpers for Webuntis lessons (status code →
/// icon/label, "Name (Longname) · Extra" lines, subject prefix). No widgets,
/// safe to unit-test.
class LessonFormatter {
static IconData iconForCode(String? code) {
switch (code) {
case 'cancelled':
return Icons.event_busy_outlined;
case 'irregular':
return Icons.swap_horiz;
default:
return Icons.school_outlined;
}
}
static String statusLabel(String? code) {
switch (code) {
case null:
case '':
return 'Regulär';
case 'cancelled':
return 'Entfällt';
case 'irregular':
return 'Geändert';
default:
return code;
}
}
static String codePrefix(String? code) {
if (code == 'cancelled') return 'Entfällt: ';
if (code == 'irregular') return 'Änderung: ';
return code ?? '';
}
/// Builds a single display line from the typical Webuntis triple of name,
/// optional longname (rendered in parentheses if it differs from `name`),
/// and optional extra info (joined with `·`).
static String formatLine(String name, {String? longname, String? extra}) {
final parts = <String>[if (name.isNotEmpty) name else '?'];
final ln = (longname ?? '').trim();
if (ln.isNotEmpty && ln != name) parts.add('($ln)');
final ex = (extra ?? '').trim();
if (ex.isNotEmpty) parts.add('· $ex');
return parts.join(' ');
}
}
-99
View File
@@ -1,99 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../../model/endpoint_data.dart';
import '../api_params.dart';
import '../api_request.dart';
import '../api_response.dart';
import '../errors/network_exception.dart';
import '../errors/parse_exception.dart';
import 'queries/authenticate/authenticate.dart';
import 'webuntis_error.dart';
abstract class WebuntisApi extends ApiRequest {
Uri endpoint = Uri.parse(
'https://${EndpointData().webuntis().full()}/WebUntis/jsonrpc.do?school=marianum-fulda',
);
String method;
ApiParams? genericParam;
http.Response? response;
bool authenticatedResponse;
WebuntisApi(
this.method,
this.genericParam, {
this.authenticatedResponse = true,
});
Future<String> query(WebuntisApi untis, {bool retry = false}) async {
final body =
'{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}';
var sessionId = '0';
if (authenticatedResponse) {
sessionId = (await Authenticate.getSession()).sessionId;
}
final data = await post(body, {'Cookie': 'JSESSIONID=$sessionId'});
response = data;
final Map<String, dynamic> jsonData;
try {
jsonData = jsonDecode(data.body) as Map<String, dynamic>;
} on FormatException catch (e) {
throw ParseException(
technicalDetails: 'WebUntis JSON decode: ${e.message}',
);
}
final error = jsonData['error'] as Map<String, dynamic>?;
if (error != null) {
final code = error['code'] as int;
if (code == -8520) {
if (retry) {
throw WebuntisError(
'Authentication was tried (probably session timeout), but was not successful!',
-8520,
);
}
await Authenticate.createSession();
return query(untis, retry: true);
} else {
throw WebuntisError(error['message'] as String, code);
}
}
return data.body;
}
T finalize<T extends ApiResponse>(T response) {
response.rawResponse = this.response!;
return response;
}
Future<ApiResponse> run();
String _body() => genericParam == null ? '{}' : jsonEncode(genericParam);
Future<http.Response> post(String data, Map<String, String>? headers) async {
try {
return await http
.post(endpoint, body: data, headers: headers)
.timeout(
const Duration(seconds: 10),
onTimeout: () => throw NetworkException.timeout(
technicalDetails: 'WebUntis $method timed out after 10s',
),
);
} on SocketException catch (e) {
throw NetworkException(
technicalDetails: 'WebUntis $method: ${e.message}',
);
} on http.ClientException catch (e) {
throw NetworkException(
technicalDetails: 'WebUntis $method: ${e.message}',
);
}
}
}
-9
View File
@@ -1,9 +0,0 @@
class WebuntisError implements Exception {
String message;
int code;
WebuntisError(this.message, this.code);
@override
String toString() => 'WebUntis ($code): $message';
}