Compare commits
10 Commits
develop-rmv
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| baa26a6e79 | |||
| b6d06dd3b4 | |||
| 6e12da08c0 | |||
| 5ebf5bccdb | |||
| f966cf302b | |||
| 582432dbb9 | |||
| ece0669f7d | |||
| 01b4b44010 | |||
| 93b9929f8f | |||
| 2858f910c9 |
@@ -4,4 +4,10 @@
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- Allow cleartext HTTP in debug builds so developers can point the
|
||||
Marianum-Connect custom endpoint at a local backend (e.g.
|
||||
http://10.0.2.2:8080 from the Android emulator). Release builds
|
||||
keep the default cleartext block. -->
|
||||
<application android:usesCleartextTraffic="true" />
|
||||
</manifest>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}).';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,39 @@ import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'autocomplete_response.dart';
|
||||
|
||||
class AutocompleteApi {
|
||||
Future<AutocompleteResponse> find(String query) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'core/autocomplete/get',
|
||||
queryParameters: {
|
||||
/// Searches sharees (users by default). Pass [shareTypes] to widen the search
|
||||
/// — e.g. `[0, 1]` for both users and groups (0 = user, 1 = group).
|
||||
Future<AutocompleteResponse> find(
|
||||
String query, {
|
||||
List<int> shareTypes = const [0],
|
||||
}) async {
|
||||
// NextcloudOcs.uri serialises every query value via `toString()`, which
|
||||
// would turn the `shareTypes[]` list into `"[0, 1]"`. Build the Uri here so
|
||||
// Dart encodes the list as repeated `shareTypes[]=0&shareTypes[]=1` params.
|
||||
final endpoint = EndpointData().nextcloud();
|
||||
final uri = Uri.https(
|
||||
endpoint.domain,
|
||||
'${endpoint.path}/ocs/v2.php/core/autocomplete/get',
|
||||
{
|
||||
'format': 'json',
|
||||
'search': query,
|
||||
'itemType': ' ',
|
||||
'itemId': ' ',
|
||||
'shareTypes[]': ['0'],
|
||||
'shareTypes[]': shareTypes.map((t) => t.toString()).toList(),
|
||||
'limit': '10',
|
||||
},
|
||||
);
|
||||
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
||||
final response = await http.get(uri, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw Exception(
|
||||
'Api call failed with ${response.statusCode}: ${response.body}',
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
technicalDetails: 'core/autocomplete/get: ${response.body}',
|
||||
);
|
||||
}
|
||||
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../files_sharing/queries/share/share.dart';
|
||||
|
||||
part 'autocomplete_response.g.dart';
|
||||
|
||||
/// Maps an autocomplete result's `source` to the matching Nextcloud share type.
|
||||
/// Groups become [kShareTypeGroup]; everything else (users, and any unknown
|
||||
/// source) defaults to [kShareTypeUser].
|
||||
int shareTypeFromSource(String? source) {
|
||||
if (source != null && source.startsWith('groups')) return kShareTypeGroup;
|
||||
return kShareTypeUser;
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class AutocompleteResponse {
|
||||
List<AutocompleteResponseObject> data;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'nextcloud_sharing_capabilities.dart';
|
||||
|
||||
/// Fetches the current user's Nextcloud capabilities via OCS
|
||||
/// `GET cloud/capabilities` and extracts the `files_sharing` block. This is the
|
||||
/// per-user, group-aware source of truth the sharing UI gates on — no custom
|
||||
/// backend involved.
|
||||
class GetNextcloudCapabilities {
|
||||
Future<NextcloudSharingCapabilities> run() async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'cloud/capabilities',
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
technicalDetails: 'cloud/capabilities: ${response.body}',
|
||||
);
|
||||
}
|
||||
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final data =
|
||||
(decoded['ocs'] as Map<String, dynamic>?)?['data']
|
||||
as Map<String, dynamic>?;
|
||||
final capabilities = data?['capabilities'] as Map<String, dynamic>?;
|
||||
final filesSharing = capabilities?['files_sharing'];
|
||||
if (filesSharing is! Map<String, dynamic>) {
|
||||
// Server doesn't advertise files_sharing (app disabled) — treat as no
|
||||
// sharing capability rather than failing the whole load.
|
||||
return const NextcloudSharingCapabilities();
|
||||
}
|
||||
final passwordPolicy = capabilities?['password_policy'];
|
||||
return NextcloudSharingCapabilities.fromFilesSharing(
|
||||
filesSharing,
|
||||
passwordPolicy: passwordPolicy is Map<String, dynamic>
|
||||
? passwordPolicy
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/// Subset of Nextcloud's `files_sharing` capabilities block that the mobile
|
||||
/// sharing UI gates on. Nextcloud reports these per authenticated user, so a
|
||||
/// group that an admin excluded from creating public links sees
|
||||
/// `public.enabled == false` here — exactly how the web UI hides those buttons.
|
||||
///
|
||||
/// The block is deeply nested and varies between server versions, so this is
|
||||
/// parsed by hand from the raw OCS map with safe fallbacks rather than via
|
||||
/// code generation. Missing fields default to the most restrictive value so a
|
||||
/// newer/older server never accidentally unlocks a capability.
|
||||
class NextcloudSharingCapabilities {
|
||||
/// `files_sharing.api_enabled` — master switch. When false the user may not
|
||||
/// create any share (user, group or link).
|
||||
final bool apiEnabled;
|
||||
|
||||
/// `files_sharing.public.enabled` — public link shares allowed.
|
||||
final bool publicEnabled;
|
||||
|
||||
/// `files_sharing.public.multiple_links` — more than one link per file.
|
||||
final bool publicMultipleLinks;
|
||||
|
||||
/// `files_sharing.public.upload` — public upload / file-drop folders.
|
||||
final bool publicUploadEnabled;
|
||||
|
||||
/// `files_sharing.public.password.enforced` — a password is mandatory on
|
||||
/// public links, so the create flow must collect one upfront.
|
||||
final bool publicPasswordEnforced;
|
||||
|
||||
/// `files_sharing.public.expire_date.enabled`.
|
||||
final bool publicExpireEnabled;
|
||||
|
||||
/// `files_sharing.public.expire_date.days` — default/maximum lifetime.
|
||||
final int? publicExpireDays;
|
||||
|
||||
/// `files_sharing.public.expire_date.enforced` — expiry cannot be removed.
|
||||
final bool publicExpireEnforced;
|
||||
|
||||
/// `files_sharing.group.enabled` (falls back to the older `group_sharing`).
|
||||
final bool groupEnabled;
|
||||
|
||||
/// `files_sharing.resharing` — recipients may reshare.
|
||||
final bool resharing;
|
||||
|
||||
// --- password_policy (a sibling capability of files_sharing) ---
|
||||
// These let the link-password UI state the rules up front instead of only
|
||||
// surfacing them after the server rejects a weak password. The
|
||||
// "non-common password" (breach) check can only be enforced server-side.
|
||||
|
||||
/// `password_policy.minLength`.
|
||||
final int? passwordMinLength;
|
||||
|
||||
/// `password_policy.enforceUpperLowerCase`.
|
||||
final bool passwordEnforceUpperLower;
|
||||
|
||||
/// `password_policy.enforceNumericCharacters`.
|
||||
final bool passwordEnforceNumeric;
|
||||
|
||||
/// `password_policy.enforceSpecialCharacters`.
|
||||
final bool passwordEnforceSpecial;
|
||||
|
||||
const NextcloudSharingCapabilities({
|
||||
this.apiEnabled = false,
|
||||
this.publicEnabled = false,
|
||||
this.publicMultipleLinks = false,
|
||||
this.publicUploadEnabled = false,
|
||||
this.publicPasswordEnforced = false,
|
||||
this.publicExpireEnabled = false,
|
||||
this.publicExpireDays,
|
||||
this.publicExpireEnforced = false,
|
||||
this.groupEnabled = false,
|
||||
this.resharing = false,
|
||||
this.passwordMinLength,
|
||||
this.passwordEnforceUpperLower = false,
|
||||
this.passwordEnforceNumeric = false,
|
||||
this.passwordEnforceSpecial = false,
|
||||
});
|
||||
|
||||
/// Parses the `files_sharing` sub-map of an OCS `cloud/capabilities`
|
||||
/// response, plus the optional sibling `password_policy` map. Tolerates
|
||||
/// missing intermediate maps and type drift.
|
||||
factory NextcloudSharingCapabilities.fromFilesSharing(
|
||||
Map<String, dynamic> filesSharing, {
|
||||
Map<String, dynamic>? passwordPolicy,
|
||||
}) {
|
||||
Map<String, dynamic>? sub(Map<String, dynamic>? m, String key) {
|
||||
final value = m?[key];
|
||||
return value is Map<String, dynamic> ? value : null;
|
||||
}
|
||||
|
||||
bool boolAt(Map<String, dynamic>? m, String key) => m?[key] == true;
|
||||
int? intAt(Map<String, dynamic>? m, String key) {
|
||||
final v = m?[key];
|
||||
if (v is int) return v;
|
||||
if (v is String) return int.tryParse(v);
|
||||
return null;
|
||||
}
|
||||
|
||||
final public = sub(filesSharing, 'public');
|
||||
final password = sub(public, 'password');
|
||||
final expire = sub(public, 'expire_date');
|
||||
final group = sub(filesSharing, 'group');
|
||||
|
||||
return NextcloudSharingCapabilities(
|
||||
apiEnabled: boolAt(filesSharing, 'api_enabled'),
|
||||
publicEnabled: boolAt(public, 'enabled'),
|
||||
publicMultipleLinks: boolAt(public, 'multiple_links'),
|
||||
publicUploadEnabled: boolAt(public, 'upload'),
|
||||
publicPasswordEnforced: boolAt(password, 'enforced'),
|
||||
publicExpireEnabled: boolAt(expire, 'enabled'),
|
||||
publicExpireDays: intAt(expire, 'days'),
|
||||
publicExpireEnforced: boolAt(expire, 'enforced'),
|
||||
// Newer servers nest it under `group.enabled`; older ones expose a flat
|
||||
// `group_sharing` boolean.
|
||||
groupEnabled:
|
||||
boolAt(group, 'enabled') || boolAt(filesSharing, 'group_sharing'),
|
||||
resharing: boolAt(filesSharing, 'resharing'),
|
||||
passwordMinLength: intAt(passwordPolicy, 'minLength'),
|
||||
passwordEnforceUpperLower: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceUpperLowerCase',
|
||||
),
|
||||
passwordEnforceNumeric: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceNumericCharacters',
|
||||
),
|
||||
passwordEnforceSpecial: boolAt(
|
||||
passwordPolicy,
|
||||
'enforceSpecialCharacters',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../errors/auth_exception.dart';
|
||||
import '../../errors/network_exception.dart';
|
||||
import '../../errors/not_found_exception.dart';
|
||||
import '../../errors/parse_exception.dart';
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
|
||||
/// Mix of two Nextcloud surfaces:
|
||||
/// - User info comes from the OCS provisioning API
|
||||
/// (`/ocs/v2.php/cloud/users/{userId}`).
|
||||
/// - The own-avatar upload/delete uses the *core* AvatarController at
|
||||
/// `/avatar/` — the OCS provisioning route has no POST (it answers 405).
|
||||
/// This is the same controller the read path (`/avatar/{id}/{size}` in
|
||||
/// [UserAvatar]) already talks to. CSRF is bypassed because we use Basic
|
||||
/// Auth without a session cookie.
|
||||
|
||||
/// Core AvatarController endpoint for the logged-in user (POST sets, DELETE
|
||||
/// removes). Built against the bare Nextcloud base (domain + optional path),
|
||||
/// not the OCS wrapper.
|
||||
Uri _coreAvatarUri() {
|
||||
final endpoint = EndpointData().nextcloud();
|
||||
return Uri.https(endpoint.domain, '${endpoint.path}/avatar/');
|
||||
}
|
||||
|
||||
Uri _userInfoUri() =>
|
||||
NextcloudOcs.uri('cloud/users/${AccountData().getUsername()}');
|
||||
|
||||
Future<http.Response> _send(
|
||||
Future<http.Response> Function(Uri uri, Map<String, String> headers)
|
||||
perform,
|
||||
Uri uri,
|
||||
) async {
|
||||
final headers = NextcloudOcs.headers();
|
||||
|
||||
final http.Response response;
|
||||
try {
|
||||
response = await perform(uri, headers);
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
||||
} on TimeoutException catch (e) {
|
||||
throw NetworkException.timeout(technicalDetails: 'Cloud $uri: $e');
|
||||
} on http.ClientException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
||||
}
|
||||
|
||||
final status = response.statusCode;
|
||||
if (status >= 200 && status < 300) return response;
|
||||
|
||||
final body = response.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
||||
final detail = body.isEmpty
|
||||
? 'Cloud $uri -> HTTP $status'
|
||||
: 'Cloud $uri -> HTTP $status body=$preview';
|
||||
log(detail);
|
||||
if (status == 401) throw AuthException.unauthorized(technicalDetails: detail);
|
||||
if (status == 403) throw AuthException.forbidden(technicalDetails: detail);
|
||||
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
||||
throw ServerException(statusCode: status, technicalDetails: detail);
|
||||
}
|
||||
|
||||
class SetUserAvatar {
|
||||
final Uint8List bytes;
|
||||
final String filename;
|
||||
SetUserAvatar(this.bytes, {this.filename = 'avatar.jpg'});
|
||||
|
||||
Future<void> run() async {
|
||||
await _send((uri, headers) async {
|
||||
// Core AvatarController reads $_FILES['files']['error'][0] — the field
|
||||
// must be `files[]` so PHP exposes it as an array, matching the web UI.
|
||||
final req = http.MultipartRequest('POST', uri)
|
||||
..headers.addAll(headers)
|
||||
..files.add(
|
||||
http.MultipartFile.fromBytes('files[]', bytes, filename: filename),
|
||||
);
|
||||
final streamed = await req.send();
|
||||
return http.Response.fromStream(streamed);
|
||||
}, _coreAvatarUri());
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteUserAvatar {
|
||||
Future<void> run() async {
|
||||
await _send(
|
||||
(uri, headers) => http.delete(uri, headers: headers),
|
||||
_coreAvatarUri(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CloudUserInfo {
|
||||
final String userId;
|
||||
final String displayName;
|
||||
const CloudUserInfo({required this.userId, required this.displayName});
|
||||
}
|
||||
|
||||
/// Reads the current user's provisioning record. The OCS wrapper looks like:
|
||||
/// `{ "ocs": { "meta": {...}, "data": { "id": "...", "displayname": "...", ... } } }`.
|
||||
/// We only need displayname; everything else is discarded.
|
||||
class GetUserInfo {
|
||||
Future<CloudUserInfo> run() async {
|
||||
final uri = _userInfoUri();
|
||||
final response = await _send(
|
||||
(u, headers) => http.get(u, headers: headers),
|
||||
uri,
|
||||
);
|
||||
try {
|
||||
final root = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final data =
|
||||
(root['ocs'] as Map<String, dynamic>)['data']
|
||||
as Map<String, dynamic>;
|
||||
return CloudUserInfo(
|
||||
userId: data['id'] as String,
|
||||
displayName: (data['displayname'] as String?) ?? '',
|
||||
);
|
||||
} catch (e) {
|
||||
throw ParseException(
|
||||
technicalDetails: 'Cloud $uri user info parse: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,144 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'file_sharing_api_params.dart';
|
||||
import 'queries/share/share.dart';
|
||||
import 'queries/share/share_update_params.dart';
|
||||
|
||||
/// OCS `files_sharing` API (Nextcloud Sharing API v1). Per the official docs:
|
||||
/// list/delete pass parameters via the URL (query / path), create and update
|
||||
/// pass them in the request **body** (form-urlencoded), and every call adds
|
||||
/// `format=json` so the server replies with JSON instead of XML.
|
||||
///
|
||||
/// All calls surface failures as [ServerException] so they map to friendly
|
||||
/// messages via `errorToUserMessage`.
|
||||
class FileSharingApi {
|
||||
Future<void> share(FileSharingApiParams query) async {
|
||||
static const String _base = 'apps/files_sharing/api/v1/shares';
|
||||
|
||||
/// Creates a share. Returns the created [Share] (callers that don't need it —
|
||||
/// e.g. Talk file sharing — can ignore the result).
|
||||
Future<Share> share(FileSharingApiParams query) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'apps/files_sharing/api/v1/shares',
|
||||
queryParameters: query.toJson(),
|
||||
_base,
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.post(endpoint, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw Exception(
|
||||
'Api call failed with ${response.statusCode}: ${response.body}',
|
||||
final response = await http.post(
|
||||
endpoint,
|
||||
headers: NextcloudOcs.headers(),
|
||||
body: _stringForm(query.toJson()),
|
||||
);
|
||||
return _decodeShare(response, action: 'Freigabe erstellen');
|
||||
}
|
||||
|
||||
/// Lists shares for the given OCS [path] (see `ocs_path.dart`). [reshares]
|
||||
/// includes shares the current user received and re-shared.
|
||||
Future<List<Share>> listForPath(String path, {bool reshares = false}) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
_base,
|
||||
queryParameters: {
|
||||
'format': 'json',
|
||||
'path': path,
|
||||
'reshares': reshares.toString(),
|
||||
'subfiles': 'false',
|
||||
},
|
||||
);
|
||||
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
||||
final data = _decodeData(response, action: 'Freigaben laden');
|
||||
if (data is! List) return const [];
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(Share.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
/// Updates an existing share. Returns the updated [Share].
|
||||
Future<Share> update(int shareId, ShareUpdateParams params) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'$_base/$shareId',
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.put(
|
||||
endpoint,
|
||||
headers: NextcloudOcs.headers(),
|
||||
body: params.toQuery(),
|
||||
);
|
||||
return _decodeShare(response, action: 'Freigabe ändern');
|
||||
}
|
||||
|
||||
/// Deletes (revokes) a share.
|
||||
Future<void> remove(int shareId) async {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'$_base/$shareId',
|
||||
queryParameters: {'format': 'json'},
|
||||
);
|
||||
final response = await http.delete(
|
||||
endpoint,
|
||||
headers: NextcloudOcs.headers(),
|
||||
);
|
||||
_decodeData(response, action: 'Freigabe löschen');
|
||||
}
|
||||
|
||||
/// Stringifies a json map into form fields (the OCS body is
|
||||
/// `application/x-www-form-urlencoded`). Null values are already dropped by
|
||||
/// the params' `includeIfNull: false`.
|
||||
Map<String, String> _stringForm(Map<String, dynamic> json) =>
|
||||
json.map((key, value) => MapEntry(key, value.toString()));
|
||||
|
||||
/// Decodes a single-share response (create/update). Throws if the payload is
|
||||
/// not a share object.
|
||||
Share _decodeShare(http.Response response, {required String action}) {
|
||||
final data = _decodeData(response, action: action);
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
technicalDetails: 'Unerwartete Antwort für "$action": ${response.body}',
|
||||
);
|
||||
}
|
||||
return Share.fromJson(data);
|
||||
}
|
||||
|
||||
/// Validates the HTTP/OCS envelope and returns the `ocs.data` payload, or
|
||||
/// throws a [ServerException] carrying the server's OCS message when present.
|
||||
///
|
||||
/// Every access is type-checked rather than cast: PHP's `json_encode` turns
|
||||
/// an empty associative array into a JSON array (`[]`), and `statuscode` can
|
||||
/// arrive as a string — a hard `as` cast on either would throw a raw
|
||||
/// TypeError that surfaces as the generic "something went wrong" message.
|
||||
Object? _decodeData(http.Response response, {required String action}) {
|
||||
dynamic decoded;
|
||||
try {
|
||||
decoded = jsonDecode(response.body);
|
||||
} catch (_) {
|
||||
decoded = null;
|
||||
}
|
||||
final ocs = decoded is Map<String, dynamic> ? decoded['ocs'] : null;
|
||||
final ocsMap = ocs is Map<String, dynamic> ? ocs : null;
|
||||
final meta = ocsMap?['meta'];
|
||||
final metaMap = meta is Map<String, dynamic> ? meta : null;
|
||||
|
||||
final rawStatus = metaMap?['statuscode'];
|
||||
final ocsStatus = rawStatus is int
|
||||
? rawStatus
|
||||
: (rawStatus is String ? int.tryParse(rawStatus) : null);
|
||||
final rawMessage = metaMap?['message'];
|
||||
final ocsMessage = rawMessage is String ? rawMessage : null;
|
||||
|
||||
// OCS v2 mirrors the HTTP status; accept any 2xx. Success OCS statuscodes
|
||||
// are 100 (v1 carry-over) or 200.
|
||||
final httpOk = response.statusCode >= 200 && response.statusCode < 300;
|
||||
final ocsOk = ocsStatus == null || ocsStatus == 100 || ocsStatus == 200;
|
||||
if (!httpOk || !ocsOk) {
|
||||
throw ServerException(
|
||||
statusCode: response.statusCode,
|
||||
userMessage: ocsMessage != null && ocsMessage.isNotEmpty
|
||||
? '$action fehlgeschlagen: $ocsMessage'
|
||||
: null,
|
||||
technicalDetails: '$action: ${response.statusCode} ${response.body}',
|
||||
);
|
||||
}
|
||||
return ocsMap?['data'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'file_sharing_api_params.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
// includeIfNull:false so the optional sharing fields below are only sent when
|
||||
// set — otherwise the OCS query would carry `permissions=null` etc. and be
|
||||
// rejected.
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class FileSharingApiParams {
|
||||
int shareType;
|
||||
String shareWith;
|
||||
@@ -10,12 +13,24 @@ class FileSharingApiParams {
|
||||
String? referenceId;
|
||||
String? talkMetaData;
|
||||
|
||||
/// Permission bitmask (see `share_permissions.dart`).
|
||||
int? permissions;
|
||||
|
||||
/// Public link password.
|
||||
String? password;
|
||||
|
||||
/// Expiry as `YYYY-MM-DD`.
|
||||
String? expireDate;
|
||||
|
||||
FileSharingApiParams({
|
||||
required this.shareType,
|
||||
required this.shareWith,
|
||||
required this.path,
|
||||
this.referenceId,
|
||||
this.talkMetaData,
|
||||
this.permissions,
|
||||
this.password,
|
||||
this.expireDate,
|
||||
});
|
||||
|
||||
factory FileSharingApiParams.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -14,6 +14,9 @@ FileSharingApiParams _$FileSharingApiParamsFromJson(
|
||||
path: json['path'] as String,
|
||||
referenceId: json['referenceId'] as String?,
|
||||
talkMetaData: json['talkMetaData'] as String?,
|
||||
permissions: (json['permissions'] as num?)?.toInt(),
|
||||
password: json['password'] as String?,
|
||||
expireDate: json['expireDate'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FileSharingApiParamsToJson(
|
||||
@@ -22,6 +25,9 @@ Map<String, dynamic> _$FileSharingApiParamsToJson(
|
||||
'shareType': instance.shareType,
|
||||
'shareWith': instance.shareWith,
|
||||
'path': instance.path,
|
||||
'referenceId': instance.referenceId,
|
||||
'talkMetaData': instance.talkMetaData,
|
||||
'referenceId': ?instance.referenceId,
|
||||
'talkMetaData': ?instance.talkMetaData,
|
||||
'permissions': ?instance.permissions,
|
||||
'password': ?instance.password,
|
||||
'expireDate': ?instance.expireDate,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import '../webdav/queries/list_files/cacheable_file.dart';
|
||||
|
||||
/// Converts a [CacheableFile.path] (relative to the WebDAV files root, folders
|
||||
/// ending in `/`) into the path the OCS `files_sharing` API expects: rooted
|
||||
/// with a single leading slash and without a trailing slash on folders.
|
||||
///
|
||||
/// Examples:
|
||||
/// '' -> '/' (files root)
|
||||
/// 'Documents/x.pdf' -> '/Documents/x.pdf'
|
||||
/// 'Documents/' -> '/Documents'
|
||||
/// '/Shared/a/' -> '/Shared/a'
|
||||
String ocsPathFor(String webdavPath) {
|
||||
final trimmed = webdavPath.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||
return trimmed.isEmpty ? '/' : '/$trimmed';
|
||||
}
|
||||
|
||||
/// Convenience wrapper for a [CacheableFile].
|
||||
String ocsPathOf(CacheableFile file) => ocsPathFor(file.path);
|
||||
@@ -0,0 +1,108 @@
|
||||
/// Nextcloud share types (subset the app uses).
|
||||
const int kShareTypeUser = 0;
|
||||
const int kShareTypeGroup = 1;
|
||||
const int kShareTypePublicLink = 3;
|
||||
const int kShareTypeEmail = 4;
|
||||
|
||||
/// A Talk conversation ("room") the file is linked into.
|
||||
const int kShareTypeRoom = 10;
|
||||
|
||||
/// A single share as returned by the OCS `files_sharing` API.
|
||||
///
|
||||
/// Parsed by hand rather than via code generation: OCS is inconsistent about
|
||||
/// types across versions (e.g. `id`/`share_type` may arrive as either strings
|
||||
/// or numbers) and omits optional fields entirely, so defensive parsing is
|
||||
/// safer than generated `as int` casts.
|
||||
class Share {
|
||||
final int id;
|
||||
final int shareType;
|
||||
final int permissions;
|
||||
|
||||
/// Server path of the shared item (e.g. `/Documents/x.pdf`).
|
||||
final String? path;
|
||||
|
||||
/// `'file'` or `'folder'`.
|
||||
final String? itemType;
|
||||
|
||||
/// Recipient id (user/group id); empty for public links.
|
||||
final String? shareWith;
|
||||
final String? shareWithDisplayname;
|
||||
|
||||
/// Public link URL (only set for [kShareTypePublicLink]).
|
||||
final String? url;
|
||||
|
||||
/// Raw expiration as `"YYYY-MM-DD HH:MM:SS"` (or null when none).
|
||||
final String? expiration;
|
||||
final String? label;
|
||||
|
||||
/// Redacted password marker: the server returns `null` when no password is
|
||||
/// set and a placeholder (`"redacted"`) when one is — never the real value.
|
||||
final String? password;
|
||||
|
||||
const Share({
|
||||
required this.id,
|
||||
required this.shareType,
|
||||
required this.permissions,
|
||||
this.path,
|
||||
this.itemType,
|
||||
this.shareWith,
|
||||
this.shareWithDisplayname,
|
||||
this.url,
|
||||
this.expiration,
|
||||
this.label,
|
||||
this.password,
|
||||
});
|
||||
|
||||
bool get isPublicLink => shareType == kShareTypePublicLink;
|
||||
bool get isGroup => shareType == kShareTypeGroup;
|
||||
bool get isEmail => shareType == kShareTypeEmail;
|
||||
bool get isRoom => shareType == kShareTypeRoom;
|
||||
bool get isFolder => itemType == 'folder';
|
||||
|
||||
/// Whether a (link) password is currently set. See [password].
|
||||
bool get hasPassword => password != null && password!.isNotEmpty;
|
||||
|
||||
/// Best display title for the share row.
|
||||
String get displayTitle {
|
||||
if (isPublicLink) return label?.isNotEmpty == true ? label! : 'Link';
|
||||
final name = shareWithDisplayname;
|
||||
if (name != null && name.isNotEmpty) return name;
|
||||
return shareWith ?? 'Unbekannt';
|
||||
}
|
||||
|
||||
/// Human label for the kind of share (for subtitles/headers).
|
||||
String get kindLabel {
|
||||
if (isPublicLink) return 'Öffentlicher Link';
|
||||
if (isGroup) return 'Gruppe';
|
||||
if (isRoom) return 'Talk-Chat';
|
||||
if (isEmail) return 'E-Mail';
|
||||
if (shareType == kShareTypeUser) return 'Person';
|
||||
return 'Freigabe';
|
||||
}
|
||||
|
||||
static int _asInt(Object? value, {int fallback = 0}) {
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static String? _asString(Object? value) {
|
||||
if (value == null) return null;
|
||||
final s = value.toString();
|
||||
return s.isEmpty ? null : s;
|
||||
}
|
||||
|
||||
factory Share.fromJson(Map<String, dynamic> json) => Share(
|
||||
id: _asInt(json['id']),
|
||||
shareType: _asInt(json['share_type']),
|
||||
permissions: _asInt(json['permissions']),
|
||||
path: _asString(json['path']),
|
||||
itemType: _asString(json['item_type']),
|
||||
shareWith: _asString(json['share_with']),
|
||||
shareWithDisplayname: _asString(json['share_with_displayname']),
|
||||
url: _asString(json['url']),
|
||||
expiration: _asString(json['expiration']),
|
||||
label: _asString(json['label']),
|
||||
password: _asString(json['password']),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/// Parameters for updating an existing share via OCS `PUT shares/{id}`.
|
||||
/// Only the fields that are explicitly set are sent — every field is optional
|
||||
/// and a null is omitted from the request (sending `permissions=null` would be
|
||||
/// rejected by the server).
|
||||
///
|
||||
/// Use an empty string for [expireDate]/[password] to explicitly clear the
|
||||
/// value server-side (Nextcloud treats `expireDate=` as "remove expiry").
|
||||
class ShareUpdateParams {
|
||||
final int? permissions;
|
||||
final String? password;
|
||||
final String? expireDate;
|
||||
|
||||
const ShareUpdateParams({this.permissions, this.password, this.expireDate});
|
||||
|
||||
Map<String, String> toQuery() => {
|
||||
if (permissions != null) 'permissions': permissions.toString(),
|
||||
'password': ?password,
|
||||
'expireDate': ?expireDate,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/// Nextcloud share permission bitmask helpers. These mirror the constants the
|
||||
/// OCS `files_sharing` API expects in the `permissions` field. Kept as pure
|
||||
/// functions (no Flutter/IO) so they are unit-testable.
|
||||
library;
|
||||
|
||||
/// Individual permission bits (Nextcloud `OCS\Constants`).
|
||||
const int kPermissionRead = 1;
|
||||
const int kPermissionUpdate = 2;
|
||||
const int kPermissionCreate = 4;
|
||||
const int kPermissionDelete = 8;
|
||||
const int kPermissionShare = 16;
|
||||
|
||||
/// User-facing presets that map onto a bitmask.
|
||||
enum SharePreset {
|
||||
/// Recipient can only view/download.
|
||||
readOnly,
|
||||
|
||||
/// Recipient can view, edit, add and remove (full editing).
|
||||
edit,
|
||||
|
||||
/// Upload-only "file request" — recipient can add files to a folder but not
|
||||
/// see existing contents. Only meaningful for folders.
|
||||
fileDrop,
|
||||
}
|
||||
|
||||
extension SharePresetLabel on SharePreset {
|
||||
String get label {
|
||||
switch (this) {
|
||||
case SharePreset.readOnly:
|
||||
return 'Nur Lesen';
|
||||
case SharePreset.edit:
|
||||
return 'Bearbeiten';
|
||||
case SharePreset.fileDrop:
|
||||
return 'Datei-Anfrage';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if [mask] contains the given [flag].
|
||||
bool hasPermission(int mask, int flag) => mask & flag == flag;
|
||||
|
||||
/// Builds the permission bitmask for a [preset].
|
||||
///
|
||||
/// [isFolder] matters for the `edit` preset: a file can only carry
|
||||
/// read+update, while a folder additionally supports create+delete. Nextcloud
|
||||
/// rejects create/delete on a file ("Failed to update share"), so they must be
|
||||
/// omitted there. When [allowReshare] is true the reshare bit is added to the
|
||||
/// editing presets — mirroring how the Nextcloud clients respect the
|
||||
/// `resharing` capability.
|
||||
int permissionsFor(
|
||||
SharePreset preset, {
|
||||
bool allowReshare = false,
|
||||
bool isFolder = false,
|
||||
}) {
|
||||
switch (preset) {
|
||||
case SharePreset.readOnly:
|
||||
return kPermissionRead;
|
||||
case SharePreset.edit:
|
||||
var base = kPermissionRead | kPermissionUpdate;
|
||||
if (isFolder) base |= kPermissionCreate | kPermissionDelete;
|
||||
return allowReshare ? base | kPermissionShare : base;
|
||||
case SharePreset.fileDrop:
|
||||
return kPermissionCreate;
|
||||
}
|
||||
}
|
||||
|
||||
/// Classifies an arbitrary permission bitmask into the closest preset, or null
|
||||
/// if it doesn't match any (e.g. a custom combination). The reshare bit is
|
||||
/// ignored for matching so an "edit" share stays "edit" regardless of reshare.
|
||||
SharePreset? presetFromBitmask(int mask) {
|
||||
final normalized = mask & ~kPermissionShare;
|
||||
if (normalized == kPermissionCreate) return SharePreset.fileDrop;
|
||||
if (normalized == kPermissionRead) return SharePreset.readOnly;
|
||||
// Any read share that also carries a write bit (update/create/delete) is
|
||||
// surfaced as "edit".
|
||||
const writeBits = kPermissionUpdate | kPermissionCreate | kPermissionDelete;
|
||||
if (hasPermission(normalized, kPermissionRead) &&
|
||||
normalized & writeBits != 0) {
|
||||
return SharePreset.edit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../api_params.dart';
|
||||
@@ -62,3 +64,44 @@ class DeleteMessage extends TalkApi {
|
||||
Map<String, String>? headers,
|
||||
) => http.delete(uri, headers: headers);
|
||||
}
|
||||
|
||||
class SetRoomAvatar extends TalkApi {
|
||||
final String chatToken;
|
||||
final Uint8List bytes;
|
||||
final String filename;
|
||||
|
||||
SetRoomAvatar(this.chatToken, this.bytes, {this.filename = 'avatar.jpg'})
|
||||
: super('v1/room/$chatToken/avatar', null);
|
||||
|
||||
@override
|
||||
ApiResponse? assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
ApiParams? body,
|
||||
Map<String, String>? headers,
|
||||
) async {
|
||||
final req = http.MultipartRequest('POST', uri)
|
||||
..headers.addAll(headers ?? const {})
|
||||
..files.add(http.MultipartFile.fromBytes('file', bytes, filename: filename));
|
||||
final streamed = await req.send();
|
||||
return http.Response.fromStream(streamed);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteRoomAvatar extends TalkApi {
|
||||
final String chatToken;
|
||||
|
||||
DeleteRoomAvatar(this.chatToken) : super('v1/room/$chatToken/avatar', null);
|
||||
|
||||
@override
|
||||
ApiResponse? assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
ApiParams? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.delete(uri, headers: headers);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ class CacheableFile {
|
||||
/// when a preview is going to load anyway.
|
||||
bool? hasPreview;
|
||||
|
||||
/// True when this entry is an incoming share — i.e. shared with the current
|
||||
/// user by someone else (`nc:mount-type == 'shared'`). Used to badge the
|
||||
/// file/folder in the list. Nullable so older cached entries decode fine.
|
||||
bool? isSharedWithMe;
|
||||
|
||||
CacheableFile({
|
||||
required this.path,
|
||||
required this.isDirectory,
|
||||
@@ -35,6 +40,7 @@ class CacheableFile {
|
||||
this.modifiedAt,
|
||||
this.fileId,
|
||||
this.hasPreview,
|
||||
this.isSharedWithMe,
|
||||
});
|
||||
|
||||
CacheableFile.fromDavFile(WebDavFile file) {
|
||||
@@ -48,6 +54,11 @@ class CacheableFile {
|
||||
modifiedAt = file.lastModified;
|
||||
fileId = int.tryParse(file.fileId ?? '');
|
||||
hasPreview = file.hasPreview;
|
||||
// Incoming share: the item is mounted into the user's files by someone
|
||||
// else. Outgoing shares ([isSharedByMe]) can't be derived from WebDAV with
|
||||
// the pinned package, so they are filled in by ListFiles via one OCS call
|
||||
// per folder.
|
||||
isSharedWithMe = file.props.ncmounttype == 'shared';
|
||||
}
|
||||
|
||||
factory CacheableFile.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -22,6 +22,7 @@ CacheableFile _$CacheableFileFromJson(Map<String, dynamic> json) =>
|
||||
: DateTime.parse(json['modifiedAt'] as String),
|
||||
fileId: (json['fileId'] as num?)?.toInt(),
|
||||
hasPreview: json['hasPreview'] as bool?,
|
||||
isSharedWithMe: json['isSharedWithMe'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CacheableFileToJson(CacheableFile instance) =>
|
||||
@@ -36,4 +37,5 @@ Map<String, dynamic> _$CacheableFileToJson(CacheableFile instance) =>
|
||||
'modifiedAt': instance.modifiedAt?.toIso8601String(),
|
||||
'fileId': instance.fileId,
|
||||
'hasPreview': instance.hasPreview,
|
||||
'isSharedWithMe': instance.isSharedWithMe,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
|
||||
import '../../webdav_api.dart';
|
||||
@@ -37,19 +39,46 @@ class ListFiles extends WebdavApi<ListFilesParams> {
|
||||
ocsize: true,
|
||||
nccreationtime: true,
|
||||
nchaspreview: true,
|
||||
// 'shared' here means an incoming share (mounted into the user's files
|
||||
// by someone else); used to badge those entries in the list.
|
||||
ncmounttype: true,
|
||||
);
|
||||
|
||||
var files = await _fetch(webdav, prop, timeout);
|
||||
// A freshly-entered incoming share sometimes answers its first PROPFIND
|
||||
// without the OC/NC props (no fileid / has-preview / mount-type) while the
|
||||
// share mount warms up server-side — which drops thumbnails AND share
|
||||
// badges together. Retry a couple of times so the folder self-heals
|
||||
// instead of needing manual re-entry.
|
||||
for (var attempt = 0; attempt < 2 && _looksIncomplete(files); attempt++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 700));
|
||||
files = await _fetch(webdav, prop, timeout);
|
||||
}
|
||||
|
||||
return ListFilesResponse(files);
|
||||
}
|
||||
|
||||
Future<Set<CacheableFile>> _fetch(
|
||||
WebDavClient webdav,
|
||||
WebDavPropWithoutValues prop,
|
||||
Duration timeout,
|
||||
) async {
|
||||
final davFiles =
|
||||
(await webdav
|
||||
.propfind(PathUri.parse(params.path), prop: prop)
|
||||
.timeout(timeout))
|
||||
.toWebDavFiles();
|
||||
final files = davFiles.map(CacheableFile.fromDavFile).toSet();
|
||||
|
||||
// somehow the current working folder is also listed, it is filtered here.
|
||||
files.removeWhere(
|
||||
(element) => element.path == '/${params.path}/' || element.path == '/',
|
||||
);
|
||||
|
||||
return ListFilesResponse(files);
|
||||
return files;
|
||||
}
|
||||
|
||||
/// True when the server returned entries but none carry a `fileId` — a sign
|
||||
/// the OC/NC properties were omitted (cold share mount), so thumbnails and
|
||||
/// share badges would be missing for the whole folder.
|
||||
bool _looksIncomplete(Set<CacheableFile> files) =>
|
||||
files.isNotEmpty && files.every((file) => file.fileId == null);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ class ListFilesCache extends SimpleCache<ListFilesResponse> {
|
||||
/// [invalidate].
|
||||
static int _cacheTimeFor(String path) {
|
||||
final stripped = path.replaceAll('/', '').trim();
|
||||
return stripped.isEmpty
|
||||
? RequestCache.cacheDay
|
||||
: RequestCache.cacheNothing;
|
||||
return stripped.isEmpty ? RequestCache.cacheDay : RequestCache.cacheNothing;
|
||||
}
|
||||
|
||||
/// Triggers a root-listing fetch in the background if no cached payload
|
||||
|
||||
@@ -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,30 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'mc_holiday.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')}';
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'mc_holiday.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),
|
||||
};
|
||||
@@ -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 'get_capabilities_response.dart';
|
||||
|
||||
/// Fetches the current user's mobile capability flags from
|
||||
/// `GET /api/mobile/v1/me/capabilities`. Goes through the shared dio singleton
|
||||
/// so the bearer token is attached automatically.
|
||||
class GetCapabilities {
|
||||
final Dio _dio;
|
||||
|
||||
GetCapabilities({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<CapabilitiesResponse> run() async {
|
||||
try {
|
||||
final response = await _dio.get<Map<String, dynamic>>(
|
||||
MarianumConnectEndpoint.resolve('me/capabilities'),
|
||||
);
|
||||
return CapabilitiesResponse.fromJson(response.data!);
|
||||
} on DioException catch (e) {
|
||||
throw mapMarianumConnectError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'get_capabilities_response.g.dart';
|
||||
|
||||
/// Slimmed-down capability flags the mobile UI gates features on. The backend
|
||||
/// only returns the handful of permissions the app actually consumes — not a
|
||||
/// full permission dump. Unknown/missing fields default to `false` so a stale
|
||||
/// client never accidentally enables a feature it shouldn't.
|
||||
@JsonSerializable()
|
||||
class CapabilitiesResponse {
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool viewForeignTimetables;
|
||||
|
||||
CapabilitiesResponse({required this.viewForeignTimetables});
|
||||
|
||||
factory CapabilitiesResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$CapabilitiesResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CapabilitiesResponseToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'get_capabilities_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CapabilitiesResponse _$CapabilitiesResponseFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => CapabilitiesResponse(
|
||||
viewForeignTimetables: json['viewForeignTimetables'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CapabilitiesResponseToJson(
|
||||
CapabilitiesResponse instance,
|
||||
) => <String, dynamic>{'viewForeignTimetables': instance.viewForeignTimetables};
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../errors/marianumconnect_error.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
import '../../models/mc_holiday.dart';
|
||||
|
||||
class GetHolidays {
|
||||
final Dio _dio;
|
||||
|
||||
GetHolidays({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<List<McHoliday>> run() async {
|
||||
try {
|
||||
final response = await _dio.get<List<dynamic>>(
|
||||
MarianumConnectEndpoint.resolve('holidays'),
|
||||
);
|
||||
return response.data!
|
||||
.map((e) => McHoliday.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
} 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_classes_response.dart';
|
||||
|
||||
class TimetableGetClasses {
|
||||
final Dio _dio;
|
||||
|
||||
TimetableGetClasses({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<TimetableGetClassesResponse> run() async {
|
||||
try {
|
||||
final response = await _dio.get<List<dynamic>>(
|
||||
MarianumConnectEndpoint.resolve('timetable/elements/classes'),
|
||||
);
|
||||
final list = response.data!
|
||||
.map((e) => McTimetableClass.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return TimetableGetClassesResponse(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_classes_response.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class McTimetableClass {
|
||||
final int id;
|
||||
final String shortName;
|
||||
final String longName;
|
||||
|
||||
McTimetableClass({
|
||||
required this.id,
|
||||
required this.shortName,
|
||||
required this.longName,
|
||||
});
|
||||
|
||||
factory McTimetableClass.fromJson(Map<String, dynamic> json) =>
|
||||
_$McTimetableClassFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$McTimetableClassToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class TimetableGetClassesResponse extends ApiResponse {
|
||||
final List<McTimetableClass> result;
|
||||
|
||||
TimetableGetClassesResponse({required this.result});
|
||||
|
||||
factory TimetableGetClassesResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$TimetableGetClassesResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TimetableGetClassesResponseToJson(this);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'timetable_get_classes_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
McTimetableClass _$McTimetableClassFromJson(Map<String, dynamic> json) =>
|
||||
McTimetableClass(
|
||||
id: (json['id'] as num).toInt(),
|
||||
shortName: json['shortName'] as String,
|
||||
longName: json['longName'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$McTimetableClassToJson(McTimetableClass instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'shortName': instance.shortName,
|
||||
'longName': instance.longName,
|
||||
};
|
||||
|
||||
TimetableGetClassesResponse _$TimetableGetClassesResponseFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
TimetableGetClassesResponse(
|
||||
result: (json['result'] as List<dynamic>)
|
||||
.map((e) => McTimetableClass.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> _$TimetableGetClassesResponseToJson(
|
||||
TimetableGetClassesResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'headers': ?instance.headers,
|
||||
'result': instance.result.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/// A concrete, selectable timetable element: its type, WebUntis element id and
|
||||
/// a human-readable label (room short name, abbreviated student name, …). Used
|
||||
/// to hand a picker selection back to the timetable view and to drive the
|
||||
/// inline foreign-plan rendering.
|
||||
typedef TimetableElementRef = ({TimetableElementType type, int id, String label});
|
||||
|
||||
/// The four kinds of timetable elements whose schedule can be requested via
|
||||
/// `timetable/{type}/{id}`. `schoolClass` is named to avoid the reserved Dart
|
||||
/// keyword `class`; its [pathSegment] maps back to the backend's `class`.
|
||||
enum TimetableElementType {
|
||||
student,
|
||||
teacher,
|
||||
room,
|
||||
schoolClass;
|
||||
|
||||
/// Path segment used in the backend timetable endpoint URL.
|
||||
String get pathSegment {
|
||||
switch (this) {
|
||||
case TimetableElementType.student:
|
||||
return 'student';
|
||||
case TimetableElementType.teacher:
|
||||
return 'teacher';
|
||||
case TimetableElementType.room:
|
||||
return 'room';
|
||||
case TimetableElementType.schoolClass:
|
||||
return 'class';
|
||||
}
|
||||
}
|
||||
|
||||
/// Singular German label for the UI (picker segments, hints).
|
||||
String get label {
|
||||
switch (this) {
|
||||
case TimetableElementType.student:
|
||||
return 'Schüler';
|
||||
case TimetableElementType.teacher:
|
||||
return 'Lehrer';
|
||||
case TimetableElementType.room:
|
||||
return 'Raum';
|
||||
case TimetableElementType.schoolClass:
|
||||
return 'Klasse';
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../errors/marianumconnect_error.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
import '../timetable_get_week/timetable_get_week_response.dart';
|
||||
import 'timetable_element_type.dart';
|
||||
|
||||
/// Fetches a foreign element's weekly timetable from
|
||||
/// `timetable/{student|teacher|room|class}/{id}`. The response shape is
|
||||
/// identical to `timetable/me`, so [TimetableGetWeekResponse] is reused.
|
||||
class TimetableGetElementWeek {
|
||||
final Dio _dio;
|
||||
|
||||
TimetableGetElementWeek({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<TimetableGetWeekResponse> run({
|
||||
required TimetableElementType type,
|
||||
required int id,
|
||||
required DateTime from,
|
||||
required DateTime until,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get<Map<String, dynamic>>(
|
||||
MarianumConnectEndpoint.resolve('timetable/${type.pathSegment}/$id'),
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../api_response.dart';
|
||||
import '../../models/mc_holiday.dart';
|
||||
|
||||
export '../../models/mc_holiday.dart';
|
||||
|
||||
part 'timetable_get_holidays_response.g.dart';
|
||||
|
||||
@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);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'timetable_get_holidays_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
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_students_response.dart';
|
||||
|
||||
class TimetableGetStudents {
|
||||
final Dio _dio;
|
||||
|
||||
TimetableGetStudents({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<TimetableGetStudentsResponse> run() async {
|
||||
try {
|
||||
final response = await _dio.get<List<dynamic>>(
|
||||
MarianumConnectEndpoint.resolve('timetable/elements/students'),
|
||||
);
|
||||
final list = response.data!
|
||||
.map((e) => McTimetableStudent.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return TimetableGetStudentsResponse(result: list);
|
||||
} on DioException catch (e) {
|
||||
throw mapMarianumConnectError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../api_response.dart';
|
||||
|
||||
part 'timetable_get_students_response.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class McTimetableStudent {
|
||||
final int id;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String displayName;
|
||||
|
||||
McTimetableStudent({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.displayName,
|
||||
});
|
||||
|
||||
factory McTimetableStudent.fromJson(Map<String, dynamic> json) =>
|
||||
_$McTimetableStudentFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$McTimetableStudentToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class TimetableGetStudentsResponse extends ApiResponse {
|
||||
final List<McTimetableStudent> result;
|
||||
|
||||
TimetableGetStudentsResponse({required this.result});
|
||||
|
||||
factory TimetableGetStudentsResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$TimetableGetStudentsResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TimetableGetStudentsResponseToJson(this);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'timetable_get_students_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
McTimetableStudent _$McTimetableStudentFromJson(Map<String, dynamic> json) =>
|
||||
McTimetableStudent(
|
||||
id: (json['id'] as num).toInt(),
|
||||
firstName: json['firstName'] as String,
|
||||
lastName: json['lastName'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$McTimetableStudentToJson(McTimetableStudent instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'firstName': instance.firstName,
|
||||
'lastName': instance.lastName,
|
||||
'displayName': instance.displayName,
|
||||
};
|
||||
|
||||
TimetableGetStudentsResponse _$TimetableGetStudentsResponseFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
TimetableGetStudentsResponse(
|
||||
result: (json['result'] as List<dynamic>)
|
||||
.map((e) => McTimetableStudent.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> _$TimetableGetStudentsResponseToJson(
|
||||
TimetableGetStudentsResponse 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_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,29 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../errors/marianumconnect_error.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
import 'timetable_get_teachers_response.dart';
|
||||
|
||||
class TimetableGetTeachers {
|
||||
final Dio _dio;
|
||||
|
||||
TimetableGetTeachers({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<TimetableGetTeachersResponse> run() async {
|
||||
try {
|
||||
final response = await _dio.get<List<dynamic>>(
|
||||
MarianumConnectEndpoint.resolve('timetable/elements/teachers'),
|
||||
);
|
||||
final list = response.data!
|
||||
.map(
|
||||
(e) =>
|
||||
McTimetableTeacherElement.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
return TimetableGetTeachersResponse(result: list);
|
||||
} on DioException catch (e) {
|
||||
throw mapMarianumConnectError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../api_response.dart';
|
||||
|
||||
part 'timetable_get_teachers_response.g.dart';
|
||||
|
||||
/// Picker list entry for a teacher. Named `...Element` to avoid colliding with
|
||||
/// `McTimetableteacher` from the week response, which models the teacher *of a
|
||||
/// lesson* (with substitution fields) rather than a selectable element.
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class McTimetableTeacherElement {
|
||||
final int id;
|
||||
final String shortName;
|
||||
final String displayName;
|
||||
|
||||
McTimetableTeacherElement({
|
||||
required this.id,
|
||||
required this.shortName,
|
||||
required this.displayName,
|
||||
});
|
||||
|
||||
factory McTimetableTeacherElement.fromJson(Map<String, dynamic> json) =>
|
||||
_$McTimetableTeacherElementFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$McTimetableTeacherElementToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class TimetableGetTeachersResponse extends ApiResponse {
|
||||
final List<McTimetableTeacherElement> result;
|
||||
|
||||
TimetableGetTeachersResponse({required this.result});
|
||||
|
||||
factory TimetableGetTeachersResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$TimetableGetTeachersResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TimetableGetTeachersResponseToJson(this);
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'timetable_get_teachers_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
McTimetableTeacherElement _$McTimetableTeacherElementFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => McTimetableTeacherElement(
|
||||
id: (json['id'] as num).toInt(),
|
||||
shortName: json['shortName'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$McTimetableTeacherElementToJson(
|
||||
McTimetableTeacherElement instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'shortName': instance.shortName,
|
||||
'displayName': instance.displayName,
|
||||
};
|
||||
|
||||
TimetableGetTeachersResponse _$TimetableGetTeachersResponseFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
TimetableGetTeachersResponse(
|
||||
result: (json['result'] as List<dynamic>)
|
||||
.map(
|
||||
(e) =>
|
||||
McTimetableTeacherElement.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> _$TimetableGetTeachersResponseToJson(
|
||||
TimetableGetTeachersResponse 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(),
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
-44
@@ -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(' ');
|
||||
}
|
||||
}
|
||||
@@ -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}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user