implemented foreign timetable support for students, teachers, rooms, and classes, including a searchable element picker with favorites support, introduced a capabilities system for feature gating, refactored the timetable UI into a reusable TimetableCalendarView component, and redesigned the chat input field with a unified emoji picker and integrated attachment actions.
This commit is contained in:
@@ -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,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_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,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(),
|
||||
};
|
||||
Reference in New Issue
Block a user