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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -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);
|
||||
}
|
||||
+40
@@ -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);
|
||||
}
|
||||
+38
@@ -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(),
|
||||
};
|
||||
+23
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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')}';
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
+38
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -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);
|
||||
}
|
||||
+42
@@ -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(),
|
||||
};
|
||||
Reference in New Issue
Block a user