dart format

This commit is contained in:
2026-05-08 20:12:40 +02:00
parent 9e139b5704
commit 3b8da1d3d6
295 changed files with 6404 additions and 4161 deletions
+1 -3
View File
@@ -1,3 +1 @@
class ApiParams { class ApiParams {}
}
+1 -5
View File
@@ -1,5 +1 @@
class ApiRequest {}
class ApiRequest {
}
+1
View File
@@ -1,5 +1,6 @@
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
abstract class ApiResponse { abstract class ApiResponse {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
late http.Response rawResponse; late http.Response rawResponse;
+6 -5
View File
@@ -9,15 +9,16 @@ class AuthException extends AppException {
super.technicalDetails, super.technicalDetails,
}) : super(allowRetry: false); }) : super(allowRetry: false);
factory AuthException.unauthorized({String? technicalDetails}) => AuthException( factory AuthException.unauthorized({String? technicalDetails}) =>
AuthException(
statusCode: 401, statusCode: 401,
userMessage: 'Bitte melde dich erneut an, um fortzufahren.', userMessage: 'Bitte melde dich erneut an, um fortzufahren.',
technicalDetails: technicalDetails, technicalDetails: technicalDetails,
); );
factory AuthException.forbidden({String? technicalDetails}) => AuthException( factory AuthException.forbidden({String? technicalDetails}) => AuthException(
statusCode: 403, statusCode: 403,
userMessage: 'Du hast keine Berechtigung für diese Aktion.', userMessage: 'Du hast keine Berechtigung für diese Aktion.',
technicalDetails: technicalDetails, technicalDetails: technicalDetails,
); );
} }
+11 -10
View File
@@ -14,7 +14,8 @@ import 'server_exception.dart';
import 'talk_exception.dart'; import 'talk_exception.dart';
import 'webuntis_exception.dart'; import 'webuntis_exception.dart';
const String _defaultFallback = 'Etwas ist schiefgelaufen. Bitte versuche es erneut.'; const String _defaultFallback =
'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
const String _tlsErrorMessage = const String _tlsErrorMessage =
'Die sichere Verbindung zum Server wurde abgelehnt (Zertifikat oder TLS-Fehler). ' 'Die sichere Verbindung zum Server wurde abgelehnt (Zertifikat oder TLS-Fehler). '
'Häufige Ursachen: falsche Geräte-Uhrzeit oder ein WLAN mit Anmeldeseite (z.B. Café/Hotel).'; 'Häufige Ursachen: falsche Geräte-Uhrzeit oder ein WLAN mit Anmeldeseite (z.B. Café/Hotel).';
@@ -28,9 +29,7 @@ AppException? _dioToAppException(DioException error) {
case DioExceptionType.connectionError: case DioExceptionType.connectionError:
return NetworkException(technicalDetails: error.message); return NetworkException(technicalDetails: error.message);
case DioExceptionType.badCertificate: case DioExceptionType.badCertificate:
return const NetworkException( return const NetworkException(userMessage: _tlsErrorMessage);
userMessage: _tlsErrorMessage,
);
case DioExceptionType.badResponse: case DioExceptionType.badResponse:
final status = error.response?.statusCode; final status = error.response?.statusCode;
return ServerException( return ServerException(
@@ -40,13 +39,15 @@ AppException? _dioToAppException(DioException error) {
case DioExceptionType.cancel: case DioExceptionType.cancel:
case DioExceptionType.unknown: case DioExceptionType.unknown:
final inner = error.error; final inner = error.error;
if (inner is SocketException) return NetworkException(technicalDetails: inner.message); if (inner is SocketException) {
if (inner is HandshakeException) { return NetworkException(technicalDetails: inner.message);
return const NetworkException( }
userMessage: _tlsErrorMessage, if (inner is HandshakeException) {
); return const NetworkException(userMessage: _tlsErrorMessage);
}
if (inner is FormatException) {
return ParseException(technicalDetails: inner.message);
} }
if (inner is FormatException) return ParseException(technicalDetails: inner.message);
return null; return null;
} }
} }
+6 -3
View File
@@ -2,12 +2,15 @@ import 'app_exception.dart';
class NetworkException extends AppException { class NetworkException extends AppException {
const NetworkException({ const NetworkException({
super.userMessage = 'Keine Internetverbindung. Bitte prüfe dein Netzwerk und versuche es erneut.', super.userMessage =
'Keine Internetverbindung. Bitte prüfe dein Netzwerk und versuche es erneut.',
super.technicalDetails, super.technicalDetails,
}) : super(allowRetry: true); }) : super(allowRetry: true);
factory NetworkException.timeout({String? technicalDetails}) => NetworkException( factory NetworkException.timeout({String? technicalDetails}) =>
userMessage: 'Der Server hat zu lange gebraucht. Bitte versuche es erneut.', NetworkException(
userMessage:
'Der Server hat zu lange gebraucht. Bitte versuche es erneut.',
technicalDetails: technicalDetails, technicalDetails: technicalDetails,
); );
} }
+5 -3
View File
@@ -8,7 +8,9 @@ class ServerException extends AppException {
String? userMessage, String? userMessage,
super.technicalDetails, super.technicalDetails,
}) : super( }) : super(
userMessage: userMessage ?? 'Der Server hat gerade Probleme (Status $statusCode). Bitte später erneut versuchen.', userMessage:
allowRetry: true, userMessage ??
); 'Der Server hat gerade Probleme (Status $statusCode). Bitte später erneut versuchen.',
allowRetry: true,
);
} }
+9 -6
View File
@@ -5,11 +5,12 @@ class TalkException extends AppException {
final TalkError source; final TalkError source;
TalkException(this.source) TalkException(this.source)
: super( : super(
userMessage: _mapMessage(source), userMessage: _mapMessage(source),
technicalDetails: 'Talk ${source.status} (${source.code}): ${source.message}', technicalDetails:
allowRetry: source.code >= 500, 'Talk ${source.status} (${source.code}): ${source.message}',
); allowRetry: source.code >= 500,
);
static String _mapMessage(TalkError e) { static String _mapMessage(TalkError e) {
switch (e.code) { switch (e.code) {
@@ -27,7 +28,9 @@ class TalkException extends AppException {
if (e.code >= 500) { if (e.code >= 500) {
return 'Talk-Server hat gerade Probleme (${e.code}).'; return 'Talk-Server hat gerade Probleme (${e.code}).';
} }
return e.message.isNotEmpty ? e.message : 'Talk meldet einen Fehler (${e.code}).'; return e.message.isNotEmpty
? e.message
: 'Talk meldet einen Fehler (${e.code}).';
} }
} }
} }
+5 -5
View File
@@ -5,11 +5,11 @@ class WebuntisException extends AppException {
final WebuntisError source; final WebuntisError source;
WebuntisException(this.source) WebuntisException(this.source)
: super( : super(
userMessage: _mapMessage(source), userMessage: _mapMessage(source),
technicalDetails: 'WebUntis (${source.code}): ${source.message}', technicalDetails: 'WebUntis (${source.code}): ${source.message}',
allowRetry: true, allowRetry: true,
); );
static String _mapMessage(WebuntisError e) { static String _mapMessage(WebuntisError e) {
switch (e.code) { switch (e.code) {
@@ -20,9 +20,13 @@ class AutocompleteApi {
); );
final response = await http.get(endpoint, headers: NextcloudOcs.headers()); final response = await http.get(endpoint, headers: NextcloudOcs.headers());
if (response.statusCode != HttpStatus.ok) { if (response.statusCode != HttpStatus.ok) {
throw Exception('Api call failed with ${response.statusCode}: ${response.body}'); throw Exception(
'Api call failed with ${response.statusCode}: ${response.body}',
);
} }
final decoded = jsonDecode(response.body) as Map<String, dynamic>; final decoded = jsonDecode(response.body) as Map<String, dynamic>;
return AutocompleteResponse.fromJson(decoded['ocs'] as Map<String, dynamic>); return AutocompleteResponse.fromJson(
decoded['ocs'] as Map<String, dynamic>,
);
} }
} }
@@ -8,7 +8,8 @@ class AutocompleteResponse {
AutocompleteResponse(this.data); AutocompleteResponse(this.data);
factory AutocompleteResponse.fromJson(Map<String, dynamic> json) => _$AutocompleteResponseFromJson(json); factory AutocompleteResponse.fromJson(Map<String, dynamic> json) =>
_$AutocompleteResponseFromJson(json);
Map<String, dynamic> toJson() => _$AutocompleteResponseToJson(this); Map<String, dynamic> toJson() => _$AutocompleteResponseToJson(this);
} }
@@ -22,9 +23,17 @@ class AutocompleteResponseObject {
String? subline; String? subline;
String? shareWithDisplayNameUniqe; String? shareWithDisplayNameUniqe;
AutocompleteResponseObject(this.id, this.label, this.icon, this.source, this.status, AutocompleteResponseObject(
this.subline, this.shareWithDisplayNameUniqe); this.id,
this.label,
this.icon,
this.source,
this.status,
this.subline,
this.shareWithDisplayNameUniqe,
);
factory AutocompleteResponseObject.fromJson(Map<String, dynamic> json) => _$AutocompleteResponseObjectFromJson(json); factory AutocompleteResponseObject.fromJson(Map<String, dynamic> json) =>
_$AutocompleteResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$AutocompleteResponseObjectToJson(this); Map<String, dynamic> toJson() => _$AutocompleteResponseObjectToJson(this);
} }
@@ -13,7 +13,9 @@ class FileSharingApi {
); );
final response = await http.post(endpoint, headers: NextcloudOcs.headers()); final response = await http.post(endpoint, headers: NextcloudOcs.headers());
if (response.statusCode != HttpStatus.ok) { if (response.statusCode != HttpStatus.ok) {
throw Exception('Api call failed with ${response.statusCode}: ${response.body}'); throw Exception(
'Api call failed with ${response.statusCode}: ${response.body}',
);
} }
} }
} }
@@ -15,9 +15,10 @@ class FileSharingApiParams {
required this.shareWith, required this.shareWith,
required this.path, required this.path,
this.referenceId, this.referenceId,
this.talkMetaData this.talkMetaData,
}); });
factory FileSharingApiParams.fromJson(Map<String, dynamic> json) => _$FileSharingApiParamsFromJson(json); factory FileSharingApiParams.fromJson(Map<String, dynamic> json) =>
_$FileSharingApiParamsFromJson(json);
Map<String, dynamic> toJson() => _$FileSharingApiParamsToJson(this); Map<String, dynamic> toJson() => _$FileSharingApiParamsToJson(this);
} }
+4 -4
View File
@@ -7,10 +7,10 @@ class NextcloudOcs {
NextcloudOcs._(); NextcloudOcs._();
static Map<String, String> headers() => { static Map<String, String> headers() => {
'Accept': 'application/json', 'Accept': 'application/json',
'OCS-APIRequest': 'true', 'OCS-APIRequest': 'true',
'Authorization': AccountData().getBasicAuthHeader(), 'Authorization': AccountData().getBasicAuthHeader(),
}; };
static Uri uri(String pathSuffix, {Map<String, dynamic>? queryParameters}) { static Uri uri(String pathSuffix, {Map<String, dynamic>? queryParameters}) {
final endpoint = EndpointData().nextcloud(); final endpoint = EndpointData().nextcloud();
@@ -12,39 +12,53 @@ class SetFavorite extends TalkApi {
final String chatToken; final String chatToken;
final bool favoriteState; final bool favoriteState;
SetFavorite(this.chatToken, this.favoriteState) : super('v4/room/$chatToken/favorite', null); SetFavorite(this.chatToken, this.favoriteState)
: super('v4/room/$chatToken/favorite', null);
@override @override
ApiResponse? assemble(String raw) => null; ApiResponse? assemble(String raw) => null;
@override @override
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) => Future<http.Response> request(
favoriteState ? http.post(uri, headers: headers) : http.delete(uri, headers: headers); Uri uri,
ApiParams? body,
Map<String, String>? headers,
) => favoriteState
? http.post(uri, headers: headers)
: http.delete(uri, headers: headers);
} }
class LeaveRoom extends TalkApi { class LeaveRoom extends TalkApi {
final String chatToken; final String chatToken;
LeaveRoom(this.chatToken) : super('v4/room/$chatToken/participants/self', null); LeaveRoom(this.chatToken)
: super('v4/room/$chatToken/participants/self', null);
@override @override
ApiResponse? assemble(String raw) => null; ApiResponse? assemble(String raw) => null;
@override @override
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) => Future<http.Response> request(
http.delete(uri, headers: headers); Uri uri,
ApiParams? body,
Map<String, String>? headers,
) => http.delete(uri, headers: headers);
} }
class DeleteMessage extends TalkApi { class DeleteMessage extends TalkApi {
final String chatToken; final String chatToken;
final int messageId; final int messageId;
DeleteMessage(this.chatToken, this.messageId) : super('v1/chat/$chatToken/$messageId', null); DeleteMessage(this.chatToken, this.messageId)
: super('v1/chat/$chatToken/$messageId', null);
@override @override
ApiResponse? assemble(String raw) => null; ApiResponse? assemble(String raw) => null;
@override @override
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) => Future<http.Response> request(
http.delete(uri, headers: headers); Uri uri,
ApiParams? body,
Map<String, String>? headers,
) => http.delete(uri, headers: headers);
} }
@@ -11,7 +11,8 @@ class GetChat extends TalkApi<GetChatResponse> {
String chatToken; String chatToken;
GetChatParams params; GetChatParams params;
GetChat(this.chatToken, this.params) : super('v1/chat/$chatToken', null, getParameters: params.toJson()); GetChat(this.chatToken, this.params)
: super('v1/chat/$chatToken', null, getParameters: params.toJson());
@override @override
GetChatResponse assemble(String raw) { GetChatResponse assemble(String raw) {
@@ -20,6 +21,9 @@ class GetChat extends TalkApi<GetChatResponse> {
} }
@override @override
Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers); Future<Response> request(
Uri uri,
Object? body,
Map<String, String>? headers,
) => http.get(uri, headers: headers);
} }
@@ -10,17 +10,17 @@ class GetChatCache extends SimpleCache<GetChatResponse> {
super.onError, super.onError,
required String chatToken, required String chatToken,
}) : super( }) : super(
cacheTime: RequestCache.cacheNothing, cacheTime: RequestCache.cacheNothing,
loader: () => GetChat( loader: () => GetChat(
chatToken, chatToken,
GetChatParams( GetChatParams(
lookIntoFuture: GetChatParamsSwitch.off, lookIntoFuture: GetChatParamsSwitch.off,
setReadMarker: GetChatParamsSwitch.on, setReadMarker: GetChatParamsSwitch.on,
limit: 200, limit: 200,
), ),
).run(), ).run(),
fromJson: GetChatResponse.fromJson, fromJson: GetChatResponse.fromJson,
) { ) {
start('nc-chat-$chatToken'); start('nc-chat-$chatToken');
} }
} }
@@ -15,20 +15,23 @@ class GetChatParams extends ApiParams {
GetChatParamsSwitch? includeLastKnown; GetChatParamsSwitch? includeLastKnown;
GetChatParams({ GetChatParams({
required this.lookIntoFuture, required this.lookIntoFuture,
this.limit, this.limit,
this.lastKnownMessageId, this.lastKnownMessageId,
this.lastCommonReadId, this.lastCommonReadId,
this.timeout, this.timeout,
this.setReadMarker, this.setReadMarker,
this.includeLastKnown this.includeLastKnown,
}); });
factory GetChatParams.fromJson(Map<String, dynamic> json) => _$GetChatParamsFromJson(json); factory GetChatParams.fromJson(Map<String, dynamic> json) =>
_$GetChatParamsFromJson(json);
Map<String, dynamic> toJson() => _$GetChatParamsToJson(this); Map<String, dynamic> toJson() => _$GetChatParamsToJson(this);
} }
enum GetChatParamsSwitch { enum GetChatParamsSwitch {
@JsonValue(1) on, @JsonValue(1)
@JsonValue(0) off, on,
@JsonValue(0)
off,
} }
@@ -12,7 +12,8 @@ class GetChatResponse extends ApiResponse {
GetChatResponse(this.data); GetChatResponse(this.data);
factory GetChatResponse.fromJson(Map<String, dynamic> json) => _$GetChatResponseFromJson(json); factory GetChatResponse.fromJson(Map<String, dynamic> json) =>
_$GetChatResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetChatResponseToJson(this); Map<String, dynamic> toJson() => _$GetChatResponseToJson(this);
List<GetChatResponseObject> sortByTimestamp() { List<GetChatResponseObject> sortByTimestamp() {
@@ -37,28 +38,30 @@ class GetChatResponseObject {
String message; String message;
Map<String, int>? reactions; Map<String, int>? reactions;
List<String>? reactionsSelf; List<String>? reactionsSelf;
@JsonKey(fromJson: _fromJson) Map<String, RichObjectString>? messageParameters; @JsonKey(fromJson: _fromJson)
Map<String, RichObjectString>? messageParameters;
GetChatResponseObject? parent; GetChatResponseObject? parent;
GetChatResponseObject( GetChatResponseObject(
this.id, this.id,
this.token, this.token,
this.actorType, this.actorType,
this.actorId, this.actorId,
this.actorDisplayName, this.actorDisplayName,
this.timestamp, this.timestamp,
this.systemMessage, this.systemMessage,
this.messageType, this.messageType,
this.isReplyable, this.isReplyable,
this.referenceId, this.referenceId,
this.message, this.message,
this.messageParameters, this.messageParameters,
this.reactions, this.reactions,
this.reactionsSelf, this.reactionsSelf,
this.parent, this.parent,
); );
factory GetChatResponseObject.fromJson(Map<String, dynamic> json) => _$GetChatResponseObjectFromJson(json); factory GetChatResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetChatResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetChatResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetChatResponseObjectToJson(this);
static GetChatResponseObject getDateDummy(int timestamp) { static GetChatResponseObject getDateDummy(int timestamp) {
@@ -66,7 +69,8 @@ class GetChatResponseObject {
return getTextDummy(elementDate.formatDate()); return getTextDummy(elementDate.formatDate());
} }
static GetChatResponseObject getTextDummy(String text) => GetChatResponseObject( static GetChatResponseObject getTextDummy(String text) =>
GetChatResponseObject(
0, 0,
'', '',
GetRoomResponseObjectMessageActorType.user, GetRoomResponseObjectMessageActorType.user,
@@ -82,15 +86,17 @@ class GetChatResponseObject {
null, null,
null, null,
null, null,
); );
} }
Map<String, RichObjectString>? _fromJson(dynamic json) { Map<String, RichObjectString>? _fromJson(dynamic json) {
if (json is Map<String, dynamic>) { if (json is Map<String, dynamic>) {
final data = <String, RichObjectString>{}; final data = <String, RichObjectString>{};
for (final element in json.keys) { for (final element in json.keys) {
data.putIfAbsent(element, () => RichObjectString.fromJson(json[element] as Map<String, dynamic>)); data.putIfAbsent(
element,
() => RichObjectString.fromJson(json[element] as Map<String, dynamic>),
);
} }
return data; return data;
} }
@@ -109,17 +115,26 @@ class RichObjectString {
RichObjectString(this.type, this.id, this.name, this.path, this.link); RichObjectString(this.type, this.id, this.name, this.path, this.link);
factory RichObjectString.fromJson(Map<String, dynamic> json) => _$RichObjectStringFromJson(json); factory RichObjectString.fromJson(Map<String, dynamic> json) =>
_$RichObjectStringFromJson(json);
Map<String, dynamic> toJson() => _$RichObjectStringToJson(this); Map<String, dynamic> toJson() => _$RichObjectStringToJson(this);
} }
enum RichObjectStringObjectType { enum RichObjectStringObjectType {
@JsonValue('user') user, @JsonValue('user')
@JsonValue('group') group, user,
@JsonValue('file') file, @JsonValue('group')
@JsonValue('guest') guest, group,
@JsonValue('highlight') highlight, @JsonValue('file')
@JsonValue('talk-poll') talkPoll, file,
@JsonValue('geo-location') geoLocation, @JsonValue('guest')
@JsonValue('call') call, guest,
@JsonValue('highlight')
highlight,
@JsonValue('talk-poll')
talkPoll,
@JsonValue('geo-location')
geoLocation,
@JsonValue('call')
call,
} }
@@ -1,9 +1,11 @@
import 'get_chat_response.dart'; import 'get_chat_response.dart';
class RichObjectStringProcessor { class RichObjectStringProcessor {
static String parseToString(String message, Map<String, RichObjectString>? data) { static String parseToString(
if(data == null) return message; String message,
Map<String, RichObjectString>? data,
) {
if (data == null) return message;
data.forEach((key, value) { data.forEach((key, value) {
message = message.replaceAll(RegExp('{$key}'), value.name); message = message.replaceAll(RegExp('{$key}'), value.name);
@@ -1,9 +1,9 @@
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:http/http.dart'; import 'package:http/http.dart';
import '../talk_api.dart'; import '../talk_api.dart';
import 'create_room_params.dart'; import 'create_room_params.dart';
class CreateRoom extends TalkApi { class CreateRoom extends TalkApi {
CreateRoomParams params; CreateRoomParams params;
@@ -13,9 +13,19 @@ class CreateRoom extends TalkApi {
Null assemble(String raw) => null; Null assemble(String raw) => null;
@override @override
Future<Response>? request(Uri uri, Object? body, Map<String, String>? headers) { Future<Response>? request(
if(body is CreateRoomParams) { Uri uri,
return http.post(uri, headers: headers, body: body.toJson().map((key, value) => MapEntry(key, value.toString()))); Object? body,
Map<String, String>? headers,
) {
if (body is CreateRoomParams) {
return http.post(
uri,
headers: headers,
body: body.toJson().map(
(key, value) => MapEntry(key, value.toString()),
),
);
} }
return null; return null;
@@ -19,9 +19,10 @@ class CreateRoomParams extends ApiParams {
this.source, this.source,
this.roomName, this.roomName,
this.objectType, this.objectType,
this.objectId this.objectId,
}); });
factory CreateRoomParams.fromJson(Map<String, dynamic> json) => _$CreateRoomParamsFromJson(json); factory CreateRoomParams.fromJson(Map<String, dynamic> json) =>
_$CreateRoomParamsFromJson(json);
Map<String, dynamic> toJson() => _$CreateRoomParamsToJson(this); Map<String, dynamic> toJson() => _$CreateRoomParamsToJson(this);
} }
@@ -8,17 +8,24 @@ import 'delete_react_message_params.dart';
class DeleteReactMessage extends TalkApi { class DeleteReactMessage extends TalkApi {
String chatToken; String chatToken;
int messageId; int messageId;
DeleteReactMessage({required this.chatToken, required this.messageId, required DeleteReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params); DeleteReactMessage({
required this.chatToken,
required this.messageId,
required DeleteReactMessageParams params,
}) : super('v1/reaction/$chatToken/$messageId', params);
@override @override
Null assemble(String raw) => null; Null assemble(String raw) => null;
@override @override
Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) { Future<Response>? request(
if(body is DeleteReactMessageParams) { Uri uri,
ApiParams? body,
Map<String, String>? headers,
) {
if (body is DeleteReactMessageParams) {
return http.delete(uri, headers: headers, body: body.toJson()); return http.delete(uri, headers: headers, body: body.toJson());
} }
return null; return null;
} }
} }
@@ -10,6 +10,7 @@ class DeleteReactMessageParams extends ApiParams {
DeleteReactMessageParams(this.reaction); DeleteReactMessageParams(this.reaction);
factory DeleteReactMessageParams.fromJson(Map<String, dynamic> json) => _$DeleteReactMessageParamsFromJson(json); factory DeleteReactMessageParams.fromJson(Map<String, dynamic> json) =>
_$DeleteReactMessageParamsFromJson(json);
Map<String, dynamic> toJson() => _$DeleteReactMessageParamsToJson(this); Map<String, dynamic> toJson() => _$DeleteReactMessageParamsToJson(this);
} }
@@ -12,10 +12,15 @@ class GetParticipants extends TalkApi<GetParticipantsResponse> {
@override @override
GetParticipantsResponse assemble(String raw) { GetParticipantsResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>; final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetParticipantsResponse.fromJson(decoded['ocs'] as Map<String, dynamic>); return GetParticipantsResponse.fromJson(
decoded['ocs'] as Map<String, dynamic>,
);
} }
@override @override
Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers); Future<http.Response> request(
Uri uri,
Object? body,
Map<String, String>? headers,
) => http.get(uri, headers: headers);
} }
@@ -7,11 +7,11 @@ class GetParticipantsCache extends SimpleCache<GetParticipantsResponse> {
required void Function(GetParticipantsResponse) onUpdate, required void Function(GetParticipantsResponse) onUpdate,
required String chatToken, required String chatToken,
}) : super( }) : super(
cacheTime: RequestCache.cacheNothing, cacheTime: RequestCache.cacheNothing,
loader: () => GetParticipants(chatToken).run(), loader: () => GetParticipants(chatToken).run(),
fromJson: GetParticipantsResponse.fromJson, fromJson: GetParticipantsResponse.fromJson,
onUpdate: onUpdate, onUpdate: onUpdate,
) { ) {
start('nc-chat-participants-$chatToken'); start('nc-chat-participants-$chatToken');
} }
} }
@@ -1,4 +1,3 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart'; import '../../../api_response.dart';
@@ -11,7 +10,8 @@ class GetParticipantsResponse extends ApiResponse {
GetParticipantsResponse(this.data); GetParticipantsResponse(this.data);
factory GetParticipantsResponse.fromJson(Map<String, dynamic> json) => _$GetParticipantsResponseFromJson(json); factory GetParticipantsResponse.fromJson(Map<String, dynamic> json) =>
_$GetParticipantsResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetParticipantsResponseToJson(this); Map<String, dynamic> toJson() => _$GetParticipantsResponseToJson(this);
} }
@@ -34,42 +34,55 @@ class GetParticipantsResponseObject {
String? roomToken; String? roomToken;
GetParticipantsResponseObject( GetParticipantsResponseObject(
this.attendeeId, this.attendeeId,
this.actorType, this.actorType,
this.actorId, this.actorId,
this.displayName, this.displayName,
this.participantType, this.participantType,
this.lastPing, this.lastPing,
this.inCall, this.inCall,
this.permissions, this.permissions,
this.attendeePermissions, this.attendeePermissions,
this.sessionId, this.sessionId,
this.sessionIds, this.sessionIds,
this.status, this.status,
this.statusIcon, this.statusIcon,
this.statusMessage, this.statusMessage,
this.roomToken); this.roomToken,
);
factory GetParticipantsResponseObject.fromJson(Map<String, dynamic> json) => _$GetParticipantsResponseObjectFromJson(json); factory GetParticipantsResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetParticipantsResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetParticipantsResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetParticipantsResponseObjectToJson(this);
} }
enum GetParticipantsResponseObjectParticipantType { enum GetParticipantsResponseObjectParticipantType {
@JsonValue(1) owner('Besitzer'), @JsonValue(1)
@JsonValue(2) moderator('Moderator'), owner('Besitzer'),
@JsonValue(3) user('Teilnehmer'), @JsonValue(2)
@JsonValue(4) guest('Gast'), moderator('Moderator'),
@JsonValue(5) userFollowingPublicLink('Teilnehmer über Link'), @JsonValue(3)
@JsonValue(6) guestWithModeratorPermissions('Gast Moderator'); user('Teilnehmer'),
@JsonValue(4)
guest('Gast'),
@JsonValue(5)
userFollowingPublicLink('Teilnehmer über Link'),
@JsonValue(6)
guestWithModeratorPermissions('Gast Moderator');
const GetParticipantsResponseObjectParticipantType(this.prettyName); const GetParticipantsResponseObjectParticipantType(this.prettyName);
final String prettyName; final String prettyName;
} }
enum GetParticipantsResponseObjectParticipantsInCallFlags { enum GetParticipantsResponseObjectParticipantsInCallFlags {
@JsonValue(0) disconnected, @JsonValue(0)
@JsonValue(1) inCall, disconnected,
@JsonValue(2) providesAudio, @JsonValue(1)
@JsonValue(3) providesVideo, inCall,
@JsonValue(4) usesSipDialIn @JsonValue(2)
providesAudio,
@JsonValue(3)
providesVideo,
@JsonValue(4)
usesSipDialIn,
} }
@@ -8,14 +8,21 @@ import 'get_poll_state_response.dart';
class GetPollState extends TalkApi<GetPollStateResponse> { class GetPollState extends TalkApi<GetPollStateResponse> {
String token; String token;
int pollId; int pollId;
GetPollState({required this.token, required this.pollId}) : super('v1/poll/$token/$pollId', null); GetPollState({required this.token, required this.pollId})
: super('v1/poll/$token/$pollId', null);
@override @override
GetPollStateResponse assemble(String raw) { GetPollStateResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>; final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetPollStateResponse.fromJson(decoded['ocs'] as Map<String, dynamic>); return GetPollStateResponse.fromJson(
decoded['ocs'] as Map<String, dynamic>,
);
} }
@override @override
Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers); Future<http.Response> request(
Uri uri,
Object? body,
Map<String, String>? headers,
) => http.get(uri, headers: headers);
} }
@@ -10,7 +10,8 @@ class GetPollStateResponse extends ApiResponse {
GetPollStateResponse(this.data); GetPollStateResponse(this.data);
factory GetPollStateResponse.fromJson(Map<String, dynamic> json) => _$GetPollStateResponseFromJson(json); factory GetPollStateResponse.fromJson(Map<String, dynamic> json) =>
_$GetPollStateResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetPollStateResponseToJson(this); Map<String, dynamic> toJson() => _$GetPollStateResponseToJson(this);
} }
@@ -31,20 +32,22 @@ class GetPollStateResponseObject {
List<dynamic>? details; List<dynamic>? details;
GetPollStateResponseObject( GetPollStateResponseObject(
this.id, this.id,
this.question, this.question,
this.options, this.options,
this.votes, this.votes,
this.actorType, this.actorType,
this.actorId, this.actorId,
this.actorDisplayName, this.actorDisplayName,
this.status, this.status,
this.resultMode, this.resultMode,
this.maxVotes, this.maxVotes,
this.votedSelf, this.votedSelf,
this.numVoters, this.numVoters,
this.details); this.details,
);
factory GetPollStateResponseObject.fromJson(Map<String, dynamic> json) => _$GetPollStateResponseObjectFromJson(json); factory GetPollStateResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetPollStateResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetPollStateResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetPollStateResponseObjectToJson(this);
} }
@@ -10,15 +10,21 @@ import 'get_reactions_response.dart';
class GetReactions extends TalkApi<GetReactionsResponse> { class GetReactions extends TalkApi<GetReactionsResponse> {
String chatToken; String chatToken;
int messageId; int messageId;
GetReactions({required this.chatToken, required this.messageId}) : super('v1/reaction/$chatToken/$messageId', null); GetReactions({required this.chatToken, required this.messageId})
: super('v1/reaction/$chatToken/$messageId', null);
@override @override
GetReactionsResponse assemble(String raw) { GetReactionsResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>; final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetReactionsResponse.fromJson(decoded['ocs'] as Map<String, dynamic>); return GetReactionsResponse.fromJson(
decoded['ocs'] as Map<String, dynamic>,
);
} }
@override @override
Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) => http.get(uri, headers: headers); Future<Response>? request(
Uri uri,
ApiParams? body,
Map<String, String>? headers,
) => http.get(uri, headers: headers);
} }
@@ -10,7 +10,8 @@ class GetReactionsResponse extends ApiResponse {
GetReactionsResponse(this.data); GetReactionsResponse(this.data);
factory GetReactionsResponse.fromJson(Map<String, dynamic> json) => _$GetReactionsResponseFromJson(json); factory GetReactionsResponse.fromJson(Map<String, dynamic> json) =>
_$GetReactionsResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetReactionsResponseToJson(this); Map<String, dynamic> toJson() => _$GetReactionsResponseToJson(this);
} }
@@ -21,13 +22,21 @@ class GetReactionsResponseObject {
String actorDisplayName; String actorDisplayName;
int timestamp; int timestamp;
GetReactionsResponseObject(this.actorType, this.actorId, this.actorDisplayName, this.timestamp); GetReactionsResponseObject(
this.actorType,
this.actorId,
this.actorDisplayName,
this.timestamp,
);
factory GetReactionsResponseObject.fromJson(Map<String, dynamic> json) => _$GetReactionsResponseObjectFromJson(json); factory GetReactionsResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetReactionsResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetReactionsResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetReactionsResponseObjectToJson(this);
} }
enum GetReactionsResponseObjectActorType { enum GetReactionsResponseObjectActorType {
@JsonValue('guests') guests, @JsonValue('guests')
@JsonValue('users') users, guests,
@JsonValue('users')
users,
} }
@@ -8,17 +8,24 @@ import 'react_message_params.dart';
class ReactMessage extends TalkApi { class ReactMessage extends TalkApi {
String chatToken; String chatToken;
int messageId; int messageId;
ReactMessage({required this.chatToken, required this.messageId, required ReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params); ReactMessage({
required this.chatToken,
required this.messageId,
required ReactMessageParams params,
}) : super('v1/reaction/$chatToken/$messageId', params);
@override @override
Null assemble(String raw) => null; Null assemble(String raw) => null;
@override @override
Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) { Future<Response>? request(
if(body is ReactMessageParams) { Uri uri,
ApiParams? body,
Map<String, String>? headers,
) {
if (body is ReactMessageParams) {
return http.post(uri, headers: headers, body: body.toJson()); return http.post(uri, headers: headers, body: body.toJson());
} }
return null; return null;
} }
} }
@@ -10,6 +10,7 @@ class ReactMessageParams extends ApiParams {
ReactMessageParams(this.reaction); ReactMessageParams(this.reaction);
factory ReactMessageParams.fromJson(Map<String, dynamic> json) => _$ReactMessageParamsFromJson(json); factory ReactMessageParams.fromJson(Map<String, dynamic> json) =>
_$ReactMessageParamsFromJson(json);
Map<String, dynamic> toJson() => _$ReactMessageParamsToJson(this); Map<String, dynamic> toJson() => _$ReactMessageParamsToJson(this);
} }
@@ -6,13 +6,10 @@ import '../talk_api.dart';
import 'get_room_params.dart'; import 'get_room_params.dart';
import 'get_room_response.dart'; import 'get_room_response.dart';
class GetRoom extends TalkApi<GetRoomResponse> { class GetRoom extends TalkApi<GetRoomResponse> {
GetRoomParams params; GetRoomParams params;
GetRoom(this.params) : super('v4/room', null, getParameters: params.toJson()); GetRoom(this.params) : super('v4/room', null, getParameters: params.toJson());
@override @override
GetRoomResponse assemble(String raw) { GetRoomResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>; final decoded = jsonDecode(raw) as Map<String, dynamic>;
@@ -20,6 +17,9 @@ class GetRoom extends TalkApi<GetRoomResponse> {
} }
@override @override
Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers); Future<http.Response> request(
Uri uri,
Object? body,
Map<String, String>? headers,
) => http.get(uri, headers: headers);
} }
@@ -5,11 +5,11 @@ import 'get_room_response.dart';
class GetRoomCache extends SimpleCache<GetRoomResponse> { class GetRoomCache extends SimpleCache<GetRoomResponse> {
GetRoomCache({super.onUpdate, super.onError, super.renew}) GetRoomCache({super.onUpdate, super.onError, super.renew})
: super( : super(
cacheTime: RequestCache.cacheMinute, cacheTime: RequestCache.cacheMinute,
loader: () => GetRoom(GetRoomParams(includeStatus: true)).run(), loader: () => GetRoom(GetRoomParams(includeStatus: true)).run(),
fromJson: GetRoomResponse.fromJson, fromJson: GetRoomResponse.fromJson,
) { ) {
start('nc-rooms'); start('nc-rooms');
} }
} }
@@ -1,4 +1,3 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../../../api_params.dart'; import '../../../api_params.dart';
@@ -8,18 +7,22 @@ part 'get_room_params.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class GetRoomParams extends ApiParams { class GetRoomParams extends ApiParams {
GetRoomParamsStatusUpdate? noStatusUpdate; GetRoomParamsStatusUpdate? noStatusUpdate;
@JsonKey(toJson: _format) bool? includeStatus; @JsonKey(toJson: _format)
bool? includeStatus;
int? modifiedSince; int? modifiedSince;
GetRoomParams({this.noStatusUpdate, this.includeStatus, this.modifiedSince}); GetRoomParams({this.noStatusUpdate, this.includeStatus, this.modifiedSince});
factory GetRoomParams.fromJson(Map<String, dynamic> json) => _$GetRoomParamsFromJson(json); factory GetRoomParams.fromJson(Map<String, dynamic> json) =>
_$GetRoomParamsFromJson(json);
Map<String, dynamic> toJson() => _$GetRoomParamsToJson(this); Map<String, dynamic> toJson() => _$GetRoomParamsToJson(this);
static String _format(bool? v) => v.toString(); static String _format(bool? v) => v.toString();
} }
enum GetRoomParamsStatusUpdate { enum GetRoomParamsStatusUpdate {
@JsonValue(0) defaults, @JsonValue(0)
@JsonValue(1) keepAlive, defaults,
@JsonValue(1)
keepAlive,
} }
@@ -11,17 +11,22 @@ class GetRoomResponse extends ApiResponse {
GetRoomResponse(this.data); GetRoomResponse(this.data);
factory GetRoomResponse.fromJson(Map<String, dynamic> json) => _$GetRoomResponseFromJson(json); factory GetRoomResponse.fromJson(Map<String, dynamic> json) =>
_$GetRoomResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetRoomResponseToJson(this); Map<String, dynamic> toJson() => _$GetRoomResponseToJson(this);
List<GetRoomResponseObject> sortBy({bool lastActivity = true, required bool favoritesToTop, required bool unreadToTop}) { List<GetRoomResponseObject> sortBy({
bool lastActivity = true,
required bool favoritesToTop,
required bool unreadToTop,
}) {
for (var chat in data) { for (var chat in data) {
final buffer = StringBuffer(); final buffer = StringBuffer();
if(favoritesToTop) { if (favoritesToTop) {
buffer.write(chat.isFavorite ? 'b' : 'a'); buffer.write(chat.isFavorite ? 'b' : 'a');
} }
if(unreadToTop) { if (unreadToTop) {
buffer.write(chat.unreadMessages > 0 ? 'b' : 'a'); buffer.write(chat.unreadMessages > 0 ? 'b' : 'a');
} }
@@ -69,69 +74,91 @@ class GetRoomResponseObject {
String? sort; String? sort;
GetRoomResponseObject( GetRoomResponseObject(
this.id, this.id,
this.token, this.token,
this.type, this.type,
this.name, this.name,
this.displayName, this.displayName,
this.description, this.description,
this.participantType, this.participantType,
this.participantFlags, this.participantFlags,
this.readOnly, this.readOnly,
this.listable, this.listable,
this.lastPing, this.lastPing,
this.sessionId, this.sessionId,
this.hasPassword, this.hasPassword,
this.hasCall, this.hasCall,
this.callFlag, this.callFlag,
this.canStartCall, this.canStartCall,
this.canDeleteConversation, this.canDeleteConversation,
this.canLeaveConversation, this.canLeaveConversation,
this.lastActivity, this.lastActivity,
this.isFavorite, this.isFavorite,
this.notificationLevel, this.notificationLevel,
this.unreadMessages, this.unreadMessages,
this.unreadMention, this.unreadMention,
this.unreadMentionDirect, this.unreadMentionDirect,
this.lastReadMessage, this.lastReadMessage,
this.lastCommonReadMessage, this.lastCommonReadMessage,
this.lastMessage, this.lastMessage,
this.status, this.status,
this.statusIcon, this.statusIcon,
this.statusMessage); this.statusMessage,
);
factory GetRoomResponseObject.fromJson(Map<String, dynamic> json) => _$GetRoomResponseObjectFromJson(json); factory GetRoomResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetRoomResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetRoomResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetRoomResponseObjectToJson(this);
} }
enum GetRoomResponseObjectConversationType { enum GetRoomResponseObjectConversationType {
@JsonValue(1) oneToOne, @JsonValue(1)
@JsonValue(2) group, oneToOne,
@JsonValue(3) public, @JsonValue(2)
@JsonValue(4) changelog, group,
@JsonValue(5) deleted, @JsonValue(3)
@JsonValue(6) noteToSelf, public,
@JsonValue(4)
changelog,
@JsonValue(5)
deleted,
@JsonValue(6)
noteToSelf,
} }
enum GetRoomResponseObjectParticipantNotificationLevel { enum GetRoomResponseObjectParticipantNotificationLevel {
@JsonValue(0) defaultLevel, @JsonValue(0)
@JsonValue(1) alwaysNotify, defaultLevel,
@JsonValue(2) notifyOnMention, @JsonValue(1)
@JsonValue(3) neverNotify, alwaysNotify,
@JsonValue(2)
notifyOnMention,
@JsonValue(3)
neverNotify,
} }
enum GetRoomResponseObjectMessageActorType { enum GetRoomResponseObjectMessageActorType {
@JsonValue('deleted_users') deletedUsers, @JsonValue('deleted_users')
@JsonValue('users') user, deletedUsers,
@JsonValue('guests') guest, @JsonValue('users')
@JsonValue('bots') bot, user,
@JsonValue('bridged') bridge, @JsonValue('guests')
guest,
@JsonValue('bots')
bot,
@JsonValue('bridged')
bridge,
} }
enum GetRoomResponseObjectMessageType { enum GetRoomResponseObjectMessageType {
@JsonValue('comment') comment, @JsonValue('comment')
@JsonValue('voice-message') voiceMessage, comment,
@JsonValue('comment_deleted') deletedComment, @JsonValue('voice-message')
@JsonValue('system') system, voiceMessage,
@JsonValue('command') command, @JsonValue('comment_deleted')
deletedComment,
@JsonValue('system')
system,
@JsonValue('command')
command,
} }
@@ -7,17 +7,21 @@ import 'send_message_params.dart';
class SendMessage extends TalkApi { class SendMessage extends TalkApi {
String chatToken; String chatToken;
SendMessage(this.chatToken, SendMessageParams params) : super('v1/chat/$chatToken', params); SendMessage(this.chatToken, SendMessageParams params)
: super('v1/chat/$chatToken', params);
@override @override
Null assemble(String raw) => null; Null assemble(String raw) => null;
@override @override
Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) { Future<Response>? request(
if(body is SendMessageParams) { Uri uri,
ApiParams? body,
Map<String, String>? headers,
) {
if (body is SendMessageParams) {
return http.post(uri, headers: headers, body: body.toJson()); return http.post(uri, headers: headers, body: body.toJson());
} }
return null; return null;
} }
} }
@@ -11,6 +11,7 @@ class SendMessageParams extends ApiParams {
SendMessageParams(this.message, {this.replyTo}); SendMessageParams(this.message, {this.replyTo});
factory SendMessageParams.fromJson(Map<String, dynamic> json) => _$SendMessageParamsFromJson(json); factory SendMessageParams.fromJson(Map<String, dynamic> json) =>
_$SendMessageParamsFromJson(json);
Map<String, dynamic> toJson() => _$SendMessageParamsToJson(this); Map<String, dynamic> toJson() => _$SendMessageParamsToJson(this);
} }
@@ -1,4 +1,3 @@
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:http/http.dart'; import 'package:http/http.dart';
@@ -10,21 +9,28 @@ class SetReadMarker extends TalkApi {
bool readState; bool readState;
SetReadMarkerParams? setReadMarkerParams; SetReadMarkerParams? setReadMarkerParams;
SetReadMarker(this.chatToken, this.readState, {this.setReadMarkerParams}) : super('v1/chat/$chatToken/read', null, getParameters: setReadMarkerParams?.toJson()) { SetReadMarker(this.chatToken, this.readState, {this.setReadMarkerParams})
if(readState) assert(setReadMarkerParams?.lastReadMessage != null); : super(
'v1/chat/$chatToken/read',
null,
getParameters: setReadMarkerParams?.toJson(),
) {
if (readState) assert(setReadMarkerParams?.lastReadMessage != null);
} }
@override @override
Null assemble(String raw) => null; Null assemble(String raw) => null;
@override @override
Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) { Future<Response> request(
if(readState) { Uri uri,
Object? body,
Map<String, String>? headers,
) {
if (readState) {
return http.post(uri, headers: headers); return http.post(uri, headers: headers);
} else { } else {
return http.delete(uri, headers: headers); return http.delete(uri, headers: headers);
} }
} }
} }
@@ -8,10 +8,9 @@ part 'set_read_marker_params.g.dart';
class SetReadMarkerParams extends ApiParams { class SetReadMarkerParams extends ApiParams {
int? lastReadMessage; int? lastReadMessage;
SetReadMarkerParams({ SetReadMarkerParams({this.lastReadMessage});
this.lastReadMessage
});
factory SetReadMarkerParams.fromJson(Map<String, dynamic> json) => _$SetReadMarkerParamsFromJson(json); factory SetReadMarkerParams.fromJson(Map<String, dynamic> json) =>
_$SetReadMarkerParamsFromJson(json);
Map<String, dynamic> toJson() => _$SetReadMarkerParamsToJson(this); Map<String, dynamic> toJson() => _$SetReadMarkerParamsToJson(this);
} }
+16 -10
View File
@@ -14,12 +14,7 @@ import '../../errors/parse_exception.dart';
import '../../errors/server_exception.dart'; import '../../errors/server_exception.dart';
import '../nextcloud_ocs.dart'; import '../nextcloud_ocs.dart';
enum TalkApiMethod { enum TalkApiMethod { get, post, put, delete }
get,
post,
put,
delete,
}
abstract class TalkApi<T extends ApiResponse?> extends ApiRequest { abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
String path; String path;
@@ -31,11 +26,18 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
TalkApi(this.path, this.body, {this.headers, this.getParameters}); TalkApi(this.path, this.body, {this.headers, this.getParameters});
Future<http.Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers); Future<http.Response>? request(
Uri uri,
ApiParams? body,
Map<String, String>? headers,
);
T assemble(String raw); T assemble(String raw);
Future<T> run() async { Future<T> run() async {
final endpoint = NextcloudOcs.uri('apps/spreed/api/$path', queryParameters: getParameters); final endpoint = NextcloudOcs.uri(
'apps/spreed/api/$path',
queryParameters: getParameters,
);
final mergedHeaders = {...NextcloudOcs.headers(), ...?headers}; final mergedHeaders = {...NextcloudOcs.headers(), ...?headers};
final http.Response data; final http.Response data;
@@ -60,8 +62,12 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
if (status < 200 || status >= 300) { if (status < 200 || status >= 300) {
final detail = 'Talk $endpoint -> HTTP $status'; final detail = 'Talk $endpoint -> HTTP $status';
log(detail); log(detail);
if (status == 401) throw AuthException.unauthorized(technicalDetails: detail); if (status == 401) {
if (status == 403) throw AuthException.forbidden(technicalDetails: detail); throw AuthException.unauthorized(technicalDetails: detail);
}
if (status == 403) {
throw AuthException.forbidden(technicalDetails: detail);
}
if (status == 404) throw NotFoundException(technicalDetails: detail); if (status == 404) throw NotFoundException(technicalDetails: detail);
throw ServerException(statusCode: status, technicalDetails: detail); throw ServerException(statusCode: status, technicalDetails: detail);
} }
@@ -1,5 +1,3 @@
import '../../../../api_response.dart'; import '../../../../api_response.dart';
import '../../webdav_api.dart'; import '../../webdav_api.dart';
import 'download_file_params.dart'; import 'download_file_params.dart';
@@ -10,8 +10,13 @@ class DownloadFileParams extends ApiParams {
String localTargetPath; String localTargetPath;
String filename; String filename;
DownloadFileParams(this.webdavSourcePath, this.localTargetPath, this.filename); DownloadFileParams(
this.webdavSourcePath,
this.localTargetPath,
this.filename,
);
factory DownloadFileParams.fromJson(Map<String, dynamic> json) => _$DownloadFileParamsFromJson(json); factory DownloadFileParams.fromJson(Map<String, dynamic> json) =>
_$DownloadFileParamsFromJson(json);
Map<String, dynamic> toJson() => _$DownloadFileParamsToJson(this); Map<String, dynamic> toJson() => _$DownloadFileParamsToJson(this);
} }
@@ -1,4 +1,3 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'download_file_response.g.dart'; part 'download_file_response.g.dart';
@@ -9,6 +8,7 @@ class DownloadFileResponse {
DownloadFileResponse(this.path); DownloadFileResponse(this.path);
factory DownloadFileResponse.fromJson(Map<String, dynamic> json) => _$DownloadFileResponseFromJson(json); factory DownloadFileResponse.fromJson(Map<String, dynamic> json) =>
_$DownloadFileResponseFromJson(json);
Map<String, dynamic> toJson() => _$DownloadFileResponseToJson(this); Map<String, dynamic> toJson() => _$DownloadFileResponseToJson(this);
} }
@@ -15,7 +15,16 @@ class CacheableFile {
DateTime? modifiedAt; DateTime? modifiedAt;
String? sort; String? sort;
CacheableFile({required this.path, required this.isDirectory, required this.name, this.mimeType, this.size, this.eTag, this.createdAt, this.modifiedAt}); CacheableFile({
required this.path,
required this.isDirectory,
required this.name,
this.mimeType,
this.size,
this.eTag,
this.createdAt,
this.modifiedAt,
});
CacheableFile.fromDavFile(WebDavFile file) { CacheableFile.fromDavFile(WebDavFile file) {
path = file.path.path; path = file.path.path;
@@ -28,6 +37,7 @@ class CacheableFile {
modifiedAt = file.lastModified; modifiedAt = file.lastModified;
} }
factory CacheableFile.fromJson(Map<String, dynamic> json) => _$CacheableFileFromJson(json); factory CacheableFile.fromJson(Map<String, dynamic> json) =>
_$CacheableFileFromJson(json);
Map<String, dynamic> toJson() => _$CacheableFileToJson(this); Map<String, dynamic> toJson() => _$CacheableFileToJson(this);
} }
@@ -1,4 +1,3 @@
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
import '../../webdav_api.dart'; import '../../webdav_api.dart';
@@ -26,12 +25,15 @@ class ListFiles extends WebdavApi<ListFilesParams> {
Future<ListFilesResponse> run() async { Future<ListFilesResponse> run() async {
final webdav = await WebdavApi.webdav; final webdav = await WebdavApi.webdav;
final timeout = _isRoot ? _rootTimeout : _subfolderTimeout; final timeout = _isRoot ? _rootTimeout : _subfolderTimeout;
final davFiles = (await webdav.propfind(PathUri.parse(params.path)).timeout(timeout)).toWebDavFiles(); final davFiles =
(await webdav.propfind(PathUri.parse(params.path)).timeout(timeout))
.toWebDavFiles();
final files = davFiles.map(CacheableFile.fromDavFile).toSet(); final files = davFiles.map(CacheableFile.fromDavFile).toSet();
// somehow the current working folder is also listed, it is filtered here. // somehow the current working folder is also listed, it is filtered here.
files.removeWhere((element) => element.path == '/${params.path}/' || element.path == '/'); files.removeWhere(
(element) => element.path == '/${params.path}/' || element.path == '/',
);
return ListFilesResponse(files); return ListFilesResponse(files);
} }
@@ -17,16 +17,18 @@ class ListFilesCache extends SimpleCache<ListFilesResponse> {
super.onError, super.onError,
required String path, required String path,
}) : super( }) : super(
cacheTime: RequestCache.cacheNothing, cacheTime: RequestCache.cacheNothing,
loader: () => ListFiles(ListFilesParams(path)).run(), loader: () => ListFiles(ListFilesParams(path)).run(),
fromJson: ListFilesResponse.fromJson, fromJson: ListFilesResponse.fromJson,
onUpdate: onUpdate, onUpdate: onUpdate,
) { ) {
start(_documentId(path)); start(_documentId(path));
} }
static String _documentId(String path) { static String _documentId(String path) {
final cacheName = md5.convert(utf8.encode('MarianumMobile-$path')).toString(); final cacheName = md5
.convert(utf8.encode('MarianumMobile-$path'))
.toString();
return 'wd-folder-$cacheName'; return 'wd-folder-$cacheName';
} }
@@ -35,7 +37,10 @@ class ListFilesCache extends SimpleCache<ListFilesResponse> {
/// `_FilesView` for that path via [CacheInvalidationBus] so it refetches /// `_FilesView` for that path via [CacheInvalidationBus] so it refetches
/// even while it is sitting in the background of the navigation stack. /// even while it is sitting in the background of the navigation stack.
static Future<void> invalidate(String path) async { static Future<void> invalidate(String path) async {
await Localstore.instance.collection(RequestCache.collection).doc(_documentId(path)).delete(); await Localstore.instance
.collection(RequestCache.collection)
.doc(_documentId(path))
.delete();
CacheInvalidationBus.notifyListFiles(path); CacheInvalidationBus.notifyListFiles(path);
} }
} }
@@ -10,6 +10,7 @@ class ListFilesParams extends ApiParams {
ListFilesParams(this.path); ListFilesParams(this.path);
factory ListFilesParams.fromJson(Map<String, dynamic> json) => _$ListFilesParamsFromJson(json); factory ListFilesParams.fromJson(Map<String, dynamic> json) =>
_$ListFilesParamsFromJson(json);
Map<String, dynamic> toJson() => _$ListFilesParamsToJson(this); Map<String, dynamic> toJson() => _$ListFilesParamsToJson(this);
} }
@@ -9,49 +9,73 @@ part 'list_files_response.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class ListFilesResponse extends ApiResponse { class ListFilesResponse extends ApiResponse {
Set<CacheableFile> files; Set<CacheableFile> files;
ListFilesResponse(this.files); ListFilesResponse(this.files);
factory ListFilesResponse.fromJson(Map<String, dynamic> json) => _$ListFilesResponseFromJson(json); factory ListFilesResponse.fromJson(Map<String, dynamic> json) =>
Map<String, dynamic> toJson() => _$ListFilesResponseToJson(this); _$ListFilesResponseFromJson(json);
Map<String, dynamic> toJson() => _$ListFilesResponseToJson(this);
List<CacheableFile> sortBy({bool foldersToTop = true, SortOption sortOption = SortOption.name, bool reversed = false}) { List<CacheableFile> sortBy({
var list = List<CacheableFile>.empty(growable: true); bool foldersToTop = true,
SortOption sortOption = SortOption.name,
bool reversed = false,
}) {
var list = List<CacheableFile>.empty(growable: true);
if(foldersToTop) { if (foldersToTop) {
list.addAll(_sort(files.where((element) => element.isDirectory).toSet(), reversed: reversed, sortOption: sortOption)); list.addAll(
list.addAll(_sort(files.where((element) => !element.isDirectory).toSet(), reversed: reversed, sortOption: sortOption)); _sort(
} else { files.where((element) => element.isDirectory).toSet(),
list.addAll(_sort(files, reversed: reversed, sortOption: sortOption)); reversed: reversed,
} sortOption: sortOption,
),
return list; );
list.addAll(
_sort(
files.where((element) => !element.isDirectory).toSet(),
reversed: reversed,
sortOption: sortOption,
),
);
} else {
list.addAll(_sort(files, reversed: reversed, sortOption: sortOption));
} }
List<CacheableFile> _sort(Set<CacheableFile> files, {SortOption sortOption = SortOption.name, bool reversed = false}) { return list;
for (var file in files) { }
final buffer = StringBuffer();
switch(sortOption) { List<CacheableFile> _sort(
case SortOption.date: Set<CacheableFile> files, {
buffer.write(Jiffy.parseFromMillisecondsSinceEpoch(file.modifiedAt?.millisecondsSinceEpoch ?? 0).format(pattern: 'yyyyMMddhhmmss')); SortOption sortOption = SortOption.name,
break; bool reversed = false,
}) {
for (var file in files) {
final buffer = StringBuffer();
case SortOption.name: switch (sortOption) {
buffer.write(file.name.toLowerCase()); case SortOption.date:
break; buffer.write(
Jiffy.parseFromMillisecondsSinceEpoch(
file.modifiedAt?.millisecondsSinceEpoch ?? 0,
).format(pattern: 'yyyyMMddhhmmss'),
);
break;
case SortOption.size: case SortOption.name:
buffer.write(file.size); buffer.write(file.name.toLowerCase());
break; break;
}
file.sort = buffer.toString(); case SortOption.size:
} buffer.write(file.size);
break;
}
file.sort = buffer.toString();
var list = files.toList()..sort((a, b) => b.sort!.compareTo(a.sort!));
return reversed ? list.reversed.toList() : list;
} }
var list = files.toList()..sort((a, b) => b.sort!.compareTo(a.sort!));
return reversed ? list.reversed.toList() : list;
}
} }
+6 -1
View File
@@ -16,7 +16,12 @@ abstract class WebdavApi<T> extends ApiRequest {
static Future<WebDavClient> webdav = establishWebdavConnection(); static Future<WebDavClient> webdav = establishWebdavConnection();
static Future<WebDavClient> establishWebdavConnection() async => NextcloudClient(Uri.parse('https://${EndpointData().nextcloud().full()}'), password: AccountData().getPassword(), loginName: AccountData().getUsername()).webdav; static Future<WebDavClient> establishWebdavConnection() async =>
NextcloudClient(
Uri.parse('https://${EndpointData().nextcloud().full()}'),
password: AccountData().getPassword(),
loginName: AccountData().getUsername(),
).webdav;
/// Builds the WebDAV download URL without embedded credentials. Callers must /// Builds the WebDAV download URL without embedded credentials. Callers must
/// authenticate via the [AccountData.authHeaders] header instead. /// authenticate via the [AccountData.authHeaders] header instead.
@@ -9,9 +9,9 @@ class GetBreakers extends MhslApi<GetBreakersResponse> {
GetBreakers() : super('breaker/'); GetBreakers() : super('breaker/');
@override @override
GetBreakersResponse assemble(String raw) => GetBreakersResponse.fromJson(jsonDecode(raw) as Map<String, dynamic>); GetBreakersResponse assemble(String raw) =>
GetBreakersResponse.fromJson(jsonDecode(raw) as Map<String, dynamic>);
@override @override
Future<Response>? request(Uri uri) => http.get(uri); Future<Response>? request(Uri uri) => http.get(uri);
} }
@@ -4,11 +4,11 @@ import 'get_breakers_response.dart';
class GetBreakersCache extends SimpleCache<GetBreakersResponse> { class GetBreakersCache extends SimpleCache<GetBreakersResponse> {
GetBreakersCache({super.onUpdate, super.renew}) GetBreakersCache({super.onUpdate, super.renew})
: super( : super(
cacheTime: RequestCache.cacheMinute, cacheTime: RequestCache.cacheMinute,
loader: () => GetBreakers().run(), loader: () => GetBreakers().run(),
fromJson: GetBreakersResponse.fromJson, fromJson: GetBreakersResponse.fromJson,
) { ) {
start('breakers'); start('breakers');
} }
} }
@@ -1,4 +1,3 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart'; import '../../../api_response.dart';
@@ -12,7 +11,8 @@ class GetBreakersResponse extends ApiResponse {
GetBreakersResponse(this.global, this.regional); GetBreakersResponse(this.global, this.regional);
factory GetBreakersResponse.fromJson(Map<String, dynamic> json) => _$GetBreakersResponseFromJson(json); factory GetBreakersResponse.fromJson(Map<String, dynamic> json) =>
_$GetBreakersResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetBreakersResponseToJson(this); Map<String, dynamic> toJson() => _$GetBreakersResponseToJson(this);
} }
@@ -23,14 +23,20 @@ class GetBreakersReponseObject {
GetBreakersReponseObject(this.areas, this.message); GetBreakersReponseObject(this.areas, this.message);
factory GetBreakersReponseObject.fromJson(Map<String, dynamic> json) => _$GetBreakersReponseObjectFromJson(json); factory GetBreakersReponseObject.fromJson(Map<String, dynamic> json) =>
_$GetBreakersReponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetBreakersReponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetBreakersReponseObjectToJson(this);
} }
enum BreakerArea { enum BreakerArea {
@JsonValue('GLOBAL') global, @JsonValue('GLOBAL')
@JsonValue('TIMETABLE') timetable, global,
@JsonValue('TALK') talk, @JsonValue('TIMETABLE')
@JsonValue('FILES') files, timetable,
@JsonValue('MORE') more, @JsonValue('TALK')
talk,
@JsonValue('FILES')
files,
@JsonValue('MORE')
more,
} }
@@ -11,6 +11,7 @@ class AddCustomTimetableEventParams {
AddCustomTimetableEventParams(this.user, this.event); AddCustomTimetableEventParams(this.user, this.event);
factory AddCustomTimetableEventParams.fromJson(Map<String, dynamic> json) => _$AddCustomTimetableEventParamsFromJson(json); factory AddCustomTimetableEventParams.fromJson(Map<String, dynamic> json) =>
_$AddCustomTimetableEventParamsFromJson(json);
Map<String, dynamic> toJson() => _$AddCustomTimetableEventParamsToJson(this); Map<String, dynamic> toJson() => _$AddCustomTimetableEventParamsToJson(this);
} }
@@ -20,9 +20,19 @@ class CustomTimetableEvent {
@JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson) @JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
DateTime updatedAt; DateTime updatedAt;
CustomTimetableEvent({required this.id, required this.title, required this.description, required this.startDate, CustomTimetableEvent({
required this.endDate, required this.color, required this.rrule, required this.createdAt, required this.updatedAt}); required this.id,
required this.title,
required this.description,
required this.startDate,
required this.endDate,
required this.color,
required this.rrule,
required this.createdAt,
required this.updatedAt,
});
factory CustomTimetableEvent.fromJson(Map<String, dynamic> json) => _$CustomTimetableEventFromJson(json); factory CustomTimetableEvent.fromJson(Map<String, dynamic> json) =>
_$CustomTimetableEventFromJson(json);
Map<String, dynamic> toJson() => _$CustomTimetableEventToJson(this); Map<String, dynamic> toJson() => _$CustomTimetableEventToJson(this);
} }
@@ -9,10 +9,12 @@ import 'get_custom_timetable_event_response.dart';
class GetCustomTimetableEvent extends MhslApi<GetCustomTimetableEventResponse> { class GetCustomTimetableEvent extends MhslApi<GetCustomTimetableEventResponse> {
GetCustomTimetableEventParams params; GetCustomTimetableEventParams params;
GetCustomTimetableEvent(this.params) : super('server/timetable/customEvents?user=${params.user}'); GetCustomTimetableEvent(this.params)
: super('server/timetable/customEvents?user=${params.user}');
@override @override
GetCustomTimetableEventResponse assemble(String raw) => GetCustomTimetableEventResponse.fromJson({'events': jsonDecode(raw)}); GetCustomTimetableEventResponse assemble(String raw) =>
GetCustomTimetableEventResponse.fromJson({'events': jsonDecode(raw)});
@override @override
Future<Response>? request(Uri uri) => http.get(uri); Future<Response>? request(Uri uri) => http.get(uri);
@@ -3,17 +3,18 @@ import 'get_custom_timetable_event.dart';
import 'get_custom_timetable_event_params.dart'; import 'get_custom_timetable_event_params.dart';
import 'get_custom_timetable_event_response.dart'; import 'get_custom_timetable_event_response.dart';
class GetCustomTimetableEventCache extends SimpleCache<GetCustomTimetableEventResponse> { class GetCustomTimetableEventCache
extends SimpleCache<GetCustomTimetableEventResponse> {
GetCustomTimetableEventCache( GetCustomTimetableEventCache(
GetCustomTimetableEventParams params, { GetCustomTimetableEventParams params, {
super.onUpdate, super.onUpdate,
super.onError, super.onError,
super.renew, super.renew,
}) : super( }) : super(
cacheTime: RequestCache.cacheMinute, cacheTime: RequestCache.cacheMinute,
loader: () => GetCustomTimetableEvent(params).run(), loader: () => GetCustomTimetableEvent(params).run(),
fromJson: GetCustomTimetableEventResponse.fromJson, fromJson: GetCustomTimetableEventResponse.fromJson,
) { ) {
start('customTimetableEvents'); start('customTimetableEvents');
} }
} }
@@ -8,6 +8,7 @@ class GetCustomTimetableEventParams {
GetCustomTimetableEventParams(this.user); GetCustomTimetableEventParams(this.user);
factory GetCustomTimetableEventParams.fromJson(Map<String, dynamic> json) => _$GetCustomTimetableEventParamsFromJson(json); factory GetCustomTimetableEventParams.fromJson(Map<String, dynamic> json) =>
_$GetCustomTimetableEventParamsFromJson(json);
Map<String, dynamic> toJson() => _$GetCustomTimetableEventParamsToJson(this); Map<String, dynamic> toJson() => _$GetCustomTimetableEventParamsToJson(this);
} }
@@ -11,6 +11,8 @@ class GetCustomTimetableEventResponse extends ApiResponse {
GetCustomTimetableEventResponse(this.events); GetCustomTimetableEventResponse(this.events);
factory GetCustomTimetableEventResponse.fromJson(Map<String, dynamic> json) => _$GetCustomTimetableEventResponseFromJson(json); factory GetCustomTimetableEventResponse.fromJson(Map<String, dynamic> json) =>
Map<String, dynamic> toJson() => _$GetCustomTimetableEventResponseToJson(this); _$GetCustomTimetableEventResponseFromJson(json);
Map<String, dynamic> toJson() =>
_$GetCustomTimetableEventResponseToJson(this);
} }
@@ -9,11 +9,13 @@ import 'remove_custom_timetable_event_params.dart';
class RemoveCustomTimetableEvent extends MhslApi<void> { class RemoveCustomTimetableEvent extends MhslApi<void> {
RemoveCustomTimetableEventParams params; RemoveCustomTimetableEventParams params;
RemoveCustomTimetableEvent(this.params) : super('server/timetable/customEvents'); RemoveCustomTimetableEvent(this.params)
: super('server/timetable/customEvents');
@override @override
void assemble(String raw) {} void assemble(String raw) {}
@override @override
Future<Response>? request(Uri uri) => http.delete(uri, body: jsonEncode(params.toJson())); Future<Response>? request(Uri uri) =>
http.delete(uri, body: jsonEncode(params.toJson()));
} }
@@ -8,6 +8,9 @@ class RemoveCustomTimetableEventParams {
RemoveCustomTimetableEventParams(this.id); RemoveCustomTimetableEventParams(this.id);
factory RemoveCustomTimetableEventParams.fromJson(Map<String, dynamic> json) => _$RemoveCustomTimetableEventParamsFromJson(json); factory RemoveCustomTimetableEventParams.fromJson(
Map<String, dynamic> toJson() => _$RemoveCustomTimetableEventParamsToJson(this); Map<String, dynamic> json,
) => _$RemoveCustomTimetableEventParamsFromJson(json);
Map<String, dynamic> toJson() =>
_$RemoveCustomTimetableEventParamsToJson(this);
} }
@@ -9,11 +9,13 @@ import 'update_custom_timetable_event_params.dart';
class UpdateCustomTimetableEvent extends MhslApi<void> { class UpdateCustomTimetableEvent extends MhslApi<void> {
UpdateCustomTimetableEventParams params; UpdateCustomTimetableEventParams params;
UpdateCustomTimetableEvent(this.params) : super('server/timetable/customEvents'); UpdateCustomTimetableEvent(this.params)
: super('server/timetable/customEvents');
@override @override
void assemble(String raw) {} void assemble(String raw) {}
@override @override
Future<Response>? request(Uri uri) => http.patch(uri, body: jsonEncode(params.toJson())); Future<Response>? request(Uri uri) =>
http.patch(uri, body: jsonEncode(params.toJson()));
} }
@@ -1,4 +1,3 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../custom_timetable_event.dart'; import '../custom_timetable_event.dart';
@@ -12,6 +11,9 @@ class UpdateCustomTimetableEventParams {
UpdateCustomTimetableEventParams(this.id, this.event); UpdateCustomTimetableEventParams(this.id, this.event);
factory UpdateCustomTimetableEventParams.fromJson(Map<String, dynamic> json) => _$UpdateCustomTimetableEventParamsFromJson(json); factory UpdateCustomTimetableEventParams.fromJson(
Map<String, dynamic> toJson() => _$UpdateCustomTimetableEventParamsToJson(this); Map<String, dynamic> json,
) => _$UpdateCustomTimetableEventParamsFromJson(json);
Map<String, dynamic> toJson() =>
_$UpdateCustomTimetableEventParamsToJson(this);
} }
+5 -2
View File
@@ -20,7 +20,9 @@ abstract class MhslApi<T> extends ApiRequest {
T assemble(String raw); T assemble(String raw);
Future<T> run() async { Future<T> run() async {
final endpoint = Uri.parse('https://mhsl.eu/marianum/marianummobile/$subpath'); final endpoint = Uri.parse(
'https://mhsl.eu/marianum/marianummobile/$subpath',
);
final http.Response data; final http.Response data;
try { try {
@@ -54,6 +56,7 @@ abstract class MhslApi<T> extends ApiRequest {
} }
} }
static String dateTimeToJson(DateTime time) => Jiffy.parseFromDateTime(time).format(pattern: 'yyyy-MM-dd HH:mm:ss'); static String dateTimeToJson(DateTime time) =>
Jiffy.parseFromDateTime(time).format(pattern: 'yyyy-MM-dd HH:mm:ss');
static DateTime dateTimeFromJson(String time) => DateTime.parse(time); static DateTime dateTimeFromJson(String time) => DateTime.parse(time);
} }
@@ -1,4 +1,3 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
@@ -11,11 +10,8 @@ class NotifyRegister extends MhslApi<void> {
NotifyRegisterParams params; NotifyRegisterParams params;
NotifyRegister(this.params) : super('notify/register/'); NotifyRegister(this.params) : super('notify/register/');
@override @override
void assemble(String raw) { void assemble(String raw) {}
}
@override @override
Future<http.Response> request(Uri uri) { Future<http.Response> request(Uri uri) {
@@ -11,9 +11,10 @@ class NotifyRegisterParams {
NotifyRegisterParams({ NotifyRegisterParams({
required this.username, required this.username,
required this.password, required this.password,
required this.fcmToken required this.fcmToken,
}); });
factory NotifyRegisterParams.fromJson(Map<String, dynamic> json) => _$NotifyRegisterParamsFromJson(json); factory NotifyRegisterParams.fromJson(Map<String, dynamic> json) =>
_$NotifyRegisterParamsFromJson(json);
Map<String, dynamic> toJson() => _$NotifyRegisterParamsToJson(this); Map<String, dynamic> toJson() => _$NotifyRegisterParamsToJson(this);
} }
@@ -6,7 +6,6 @@ import 'package:http/http.dart' as http;
import '../../mhsl_api.dart'; import '../../mhsl_api.dart';
import 'add_feedback_params.dart'; import 'add_feedback_params.dart';
class AddFeedback extends MhslApi<void> { class AddFeedback extends MhslApi<void> {
AddFeedbackParams params; AddFeedbackParams params;
AddFeedback(this.params) : super('server/feedback'); AddFeedback(this.params) : super('server/feedback');
@@ -15,5 +14,6 @@ class AddFeedback extends MhslApi<void> {
void assemble(String raw) {} void assemble(String raw) {}
@override @override
Future<Response>? request(Uri uri) => http.post(uri, body: jsonEncode(params.toJson())); Future<Response>? request(Uri uri) =>
http.post(uri, body: jsonEncode(params.toJson()));
} }
@@ -9,7 +9,6 @@ class AddFeedbackParams {
String? screenshot; String? screenshot;
int appVersion; int appVersion;
AddFeedbackParams({ AddFeedbackParams({
required this.user, required this.user,
required this.feedback, required this.feedback,
@@ -17,6 +16,7 @@ class AddFeedbackParams {
required this.appVersion, required this.appVersion,
}); });
factory AddFeedbackParams.fromJson(Map<String, dynamic> json) => _$AddFeedbackParamsFromJson(json); factory AddFeedbackParams.fromJson(Map<String, dynamic> json) =>
_$AddFeedbackParamsFromJson(json);
Map<String, dynamic> toJson() => _$AddFeedbackParamsToJson(this); Map<String, dynamic> toJson() => _$AddFeedbackParamsToJson(this);
} }
@@ -10,15 +10,15 @@ class UpdateUserIndexParams {
int appVersion; int appVersion;
String deviceInfo; String deviceInfo;
UpdateUserIndexParams({ UpdateUserIndexParams({
required this.user, required this.user,
required this.username, required this.username,
required this.device, required this.device,
required this.appVersion, required this.appVersion,
required this.deviceInfo required this.deviceInfo,
}); });
factory UpdateUserIndexParams.fromJson(Map<String, dynamic> json) => _$UpdateUserIndexParamsFromJson(json); factory UpdateUserIndexParams.fromJson(Map<String, dynamic> json) =>
_$UpdateUserIndexParamsFromJson(json);
Map<String, dynamic> toJson() => _$UpdateUserIndexParamsToJson(this); Map<String, dynamic> toJson() => _$UpdateUserIndexParamsToJson(this);
} }
@@ -1,4 +1,3 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
@@ -26,14 +25,18 @@ class UpdateUserIndex extends MhslApi<void> {
} }
static Future<void> index() async { static Future<void> index() async {
unawaited(UpdateUserIndex( unawaited(
UpdateUserIndexParams( UpdateUserIndex(
username: AccountData().getUsername(), UpdateUserIndexParams(
user: AccountData().getUserSecret(), username: AccountData().getUsername(),
device: await AccountData().getDeviceId(), user: AccountData().getUserSecret(),
appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber), device: await AccountData().getDeviceId(),
deviceInfo: jsonEncode((await DeviceInfoPlugin().deviceInfo).data).toString(), appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber),
), deviceInfo: jsonEncode(
).run()); (await DeviceInfoPlugin().deviceInfo).data,
).toString(),
),
).run(),
);
} }
} }
+36 -27
View File
@@ -49,7 +49,10 @@ abstract class RequestCache<T extends ApiResponse?> {
Future<void> start(String document) async { Future<void> start(String document) async {
try { try {
final tableData = await Localstore.instance.collection(collection).doc(document).get(); final tableData = await Localstore.instance
.collection(collection)
.doc(document)
.get();
if (tableData != null) { if (tableData != null) {
final cached = onLocalData(tableData['json'] as String); final cached = onLocalData(tableData['json'] as String);
onUpdate?.call(cached); onUpdate?.call(cached);
@@ -57,7 +60,8 @@ abstract class RequestCache<T extends ApiResponse?> {
} }
final lastUpdate = (tableData?['lastupdate'] as num?) ?? 0; final lastUpdate = (tableData?['lastupdate'] as num?) ?? 0;
if (DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) < lastUpdate) { if (DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) <
lastUpdate) {
if (renew == null || !renew!) return; if (renew == null || !renew!) return;
} }
@@ -65,10 +69,12 @@ abstract class RequestCache<T extends ApiResponse?> {
final newValue = await onLoad(); final newValue = await onLoad();
onUpdate?.call(newValue); onUpdate?.call(newValue);
onNetworkData?.call(newValue); onNetworkData?.call(newValue);
unawaited(Localstore.instance.collection(collection).doc(document).set({ unawaited(
'json': jsonEncode(newValue), Localstore.instance.collection(collection).doc(document).set({
'lastupdate': DateTime.now().millisecondsSinceEpoch, 'json': jsonEncode(newValue),
})); 'lastupdate': DateTime.now().millisecondsSinceEpoch,
}),
);
} on Exception catch (e) { } on Exception catch (e) {
onError(e); onError(e);
} }
@@ -79,7 +85,6 @@ abstract class RequestCache<T extends ApiResponse?> {
T onLocalData(String json); T onLocalData(String json);
Future<T> onLoad(); Future<T> onLoad();
} }
/// Concrete [RequestCache] that takes the two overrides as constructor /// Concrete [RequestCache] that takes the two overrides as constructor
@@ -97,22 +102,23 @@ class SimpleCache<T extends ApiResponse?> extends RequestCache<T> {
void Function(T)? onNetworkData, void Function(T)? onNetworkData,
void Function(Exception)? onError, void Function(Exception)? onError,
bool? renew, bool? renew,
}) : _loader = loader, }) : _loader = loader,
_fromJson = fromJson, _fromJson = fromJson,
super( super(
cacheTime, cacheTime,
onUpdate, onUpdate,
onError: onError ?? RequestCache.ignore, onError: onError ?? RequestCache.ignore,
renew: renew, renew: renew,
onCacheData: onCacheData, onCacheData: onCacheData,
onNetworkData: onNetworkData, onNetworkData: onNetworkData,
); );
@override @override
Future<T> onLoad() => _loader(); Future<T> onLoad() => _loader();
@override @override
T onLocalData(String json) => _fromJson(jsonDecode(json) as Map<String, dynamic>); T onLocalData(String json) =>
_fromJson(jsonDecode(json) as Map<String, dynamic>);
} }
/// Captures the latest cache payload (cached or network) and rethrows the /// Captures the latest cache payload (cached or network) and rethrows the
@@ -120,24 +126,27 @@ class SimpleCache<T extends ApiResponse?> extends RequestCache<T> {
/// `latest`/`capturedError`/`await ready` boilerplate that DataProviders /// `latest`/`capturedError`/`await ready` boilerplate that DataProviders
/// otherwise repeat per endpoint. /// otherwise repeat per endpoint.
Future<T> resolveFromCache<T extends ApiResponse?>( Future<T> resolveFromCache<T extends ApiResponse?>(
RequestCache<T> Function(void Function(T) onUpdate, void Function(Exception) onError) build, { RequestCache<T> Function(
void Function(T) onUpdate,
void Function(Exception) onError,
)
build, {
void Function(Object)? onError, void Function(Object)? onError,
String? operationName, String? operationName,
}) async { }) async {
T? latest; T? latest;
Object? capturedError; Object? capturedError;
final cache = build( final cache = build((data) => latest = data, (e) {
(data) => latest = data, capturedError = e;
(e) { onError?.call(e);
capturedError = e; });
onError?.call(e);
},
);
await cache.ready; await cache.ready;
if (latest != null) return latest as T; if (latest != null) return latest as T;
final err = capturedError; final err = capturedError;
if (err != null) throw err; if (err != null) throw err;
throw ParseException( throw ParseException(
technicalDetails: operationName != null ? 'No data and no error from $operationName' : null, technicalDetails: operationName != null
? 'No data and no error from $operationName'
: null,
); );
} }
@@ -9,7 +9,8 @@ import 'authenticate_response.dart';
class Authenticate extends WebuntisApi { class Authenticate extends WebuntisApi {
AuthenticateParams param; AuthenticateParams param;
Authenticate(this.param) : super('authenticate', param, authenticatedResponse: false); Authenticate(this.param)
: super('authenticate', param, authenticatedResponse: false);
@override @override
Future<AuthenticateResponse> run() async { Future<AuthenticateResponse> run() async {
@@ -17,7 +18,11 @@ class Authenticate extends WebuntisApi {
try { try {
final rawAnswer = await query(this); final rawAnswer = await query(this);
final decoded = jsonDecode(rawAnswer) as Map<String, dynamic>; final decoded = jsonDecode(rawAnswer) as Map<String, dynamic>;
final response = finalize(AuthenticateResponse.fromJson(decoded['result'] as Map<String, dynamic>)); final response = finalize(
AuthenticateResponse.fromJson(
decoded['result'] as Map<String, dynamic>,
),
);
_lastResponse = response; _lastResponse = response;
if (!awaitedResponse.isCompleted) awaitedResponse.complete(); if (!awaitedResponse.isCompleted) awaitedResponse.complete();
return response; return response;
@@ -40,23 +45,22 @@ class Authenticate extends WebuntisApi {
static Future<void> createSession() async { static Future<void> createSession() async {
_lastResponse = await Authenticate( _lastResponse = await Authenticate(
AuthenticateParams( AuthenticateParams(
user: AccountData().getUsername(), user: AccountData().getUsername(),
password: AccountData().getPassword(), password: AccountData().getPassword(),
) ),
).run(); ).run();
} }
static Future<AuthenticateResponse> getSession() async { static Future<AuthenticateResponse> getSession() async {
if(awaitingResponse) { if (awaitingResponse) {
await awaitedResponse.future; await awaitedResponse.future;
} }
if(_lastResponse == null) { if (_lastResponse == null) {
awaitingResponse = true; awaitingResponse = true;
await createSession(); await createSession();
} }
return _lastResponse!; return _lastResponse!;
} }
} }
@@ -6,12 +6,12 @@ part 'authenticate_params.g.dart';
@JsonSerializable() @JsonSerializable()
class AuthenticateParams extends ApiParams { class AuthenticateParams extends ApiParams {
String user; String user;
String password; String password;
AuthenticateParams({required this.user, required this.password}); AuthenticateParams({required this.user, required this.password});
factory AuthenticateParams.fromJson(Map<String, dynamic> json) => _$AuthenticateParamsFromJson(json); factory AuthenticateParams.fromJson(Map<String, dynamic> json) =>
_$AuthenticateParamsFromJson(json);
Map<String, dynamic> toJson() => _$AuthenticateParamsToJson(this); Map<String, dynamic> toJson() => _$AuthenticateParamsToJson(this);
} }
@@ -6,14 +6,19 @@ part 'authenticate_response.g.dart';
@JsonSerializable() @JsonSerializable()
class AuthenticateResponse extends ApiResponse { class AuthenticateResponse extends ApiResponse {
String sessionId; String sessionId;
int personType; int personType;
int personId; int personId;
int klasseId; int klasseId;
AuthenticateResponse(this.sessionId, this.personType, this.personId, this.klasseId); AuthenticateResponse(
this.sessionId,
this.personType,
this.personId,
this.klasseId,
);
factory AuthenticateResponse.fromJson(Map<String, dynamic> json) => _$AuthenticateResponseFromJson(json); factory AuthenticateResponse.fromJson(Map<String, dynamic> json) =>
_$AuthenticateResponseFromJson(json);
Map<String, dynamic> toJson() => _$AuthenticateResponseToJson(this); Map<String, dynamic> toJson() => _$AuthenticateResponseToJson(this);
} }
@@ -9,10 +9,17 @@ class GetHolidays extends WebuntisApi {
@override @override
Future<GetHolidaysResponse> run() async { Future<GetHolidaysResponse> run() async {
final rawAnswer = await query(this); final rawAnswer = await query(this);
return finalize(GetHolidaysResponse.fromJson(jsonDecode(rawAnswer) as Map<String, dynamic>)); return finalize(
GetHolidaysResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
} }
static GetHolidaysResponseObject? find(GetHolidaysResponse holidaysResponse, {DateTime? time}) { static GetHolidaysResponseObject? find(
GetHolidaysResponse holidaysResponse, {
DateTime? time,
}) {
time ??= DateTime.now(); time ??= DateTime.now();
time = DateTime(time.year, time.month, time.day, 0, 0, 0, 0, 0); time = DateTime(time.year, time.month, time.day, 0, 0, 0, 0, 0);
@@ -20,9 +27,8 @@ class GetHolidays extends WebuntisApi {
var start = DateTime.parse(element.startDate.toString()); var start = DateTime.parse(element.startDate.toString());
var end = DateTime.parse(element.endDate.toString()); var end = DateTime.parse(element.endDate.toString());
if(!start.isAfter(time) && !end.isBefore(time)) return element; if (!start.isAfter(time) && !end.isBefore(time)) return element;
} }
return null; return null;
} }
} }
@@ -4,11 +4,11 @@ import 'get_holidays_response.dart';
class GetHolidaysCache extends SimpleCache<GetHolidaysResponse> { class GetHolidaysCache extends SimpleCache<GetHolidaysResponse> {
GetHolidaysCache({super.onUpdate, super.onError, super.renew}) GetHolidaysCache({super.onUpdate, super.onError, super.renew})
: super( : super(
cacheTime: RequestCache.cacheDay, cacheTime: RequestCache.cacheDay,
loader: () => GetHolidays().run(), loader: () => GetHolidays().run(),
fromJson: GetHolidaysResponse.fromJson, fromJson: GetHolidaysResponse.fromJson,
) { ) {
start('wu-holidays'); start('wu-holidays');
} }
} }
@@ -10,7 +10,8 @@ class GetHolidaysResponse extends ApiResponse {
GetHolidaysResponse(this.result); GetHolidaysResponse(this.result);
factory GetHolidaysResponse.fromJson(Map<String, dynamic> json) => _$GetHolidaysResponseFromJson(json); factory GetHolidaysResponse.fromJson(Map<String, dynamic> json) =>
_$GetHolidaysResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetHolidaysResponseToJson(this); Map<String, dynamic> toJson() => _$GetHolidaysResponseToJson(this);
} }
@@ -22,8 +23,15 @@ class GetHolidaysResponseObject {
int startDate; int startDate;
int endDate; int endDate;
GetHolidaysResponseObject(this.id, this.name, this.longName, this.startDate, this.endDate); GetHolidaysResponseObject(
this.id,
this.name,
this.longName,
this.startDate,
this.endDate,
);
factory GetHolidaysResponseObject.fromJson(Map<String, dynamic> json) => _$GetHolidaysResponseObjectFromJson(json); factory GetHolidaysResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetHolidaysResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetHolidaysResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetHolidaysResponseObjectToJson(this);
} }
@@ -11,7 +11,11 @@ class GetRooms extends WebuntisApi {
Future<GetRoomsResponse> run() async { Future<GetRoomsResponse> run() async {
final rawAnswer = await query(this); final rawAnswer = await query(this);
try { try {
return finalize(GetRoomsResponse.fromJson(jsonDecode(rawAnswer) as Map<String, dynamic>)); return finalize(
GetRoomsResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
} catch (e, trace) { } catch (e, trace) {
log(trace.toString()); log(trace.toString());
log('Failed to parse getRoom data with server response: $rawAnswer'); log('Failed to parse getRoom data with server response: $rawAnswer');
@@ -19,5 +23,4 @@ class GetRooms extends WebuntisApi {
throw Exception('Failed to parse getRoom server response: $rawAnswer'); throw Exception('Failed to parse getRoom server response: $rawAnswer');
} }
} }
@@ -4,11 +4,11 @@ import 'get_rooms_response.dart';
class GetRoomsCache extends SimpleCache<GetRoomsResponse> { class GetRoomsCache extends SimpleCache<GetRoomsResponse> {
GetRoomsCache({super.onUpdate, super.onError, super.renew}) GetRoomsCache({super.onUpdate, super.onError, super.renew})
: super( : super(
cacheTime: RequestCache.cacheHour, cacheTime: RequestCache.cacheHour,
loader: () => GetRooms().run(), loader: () => GetRooms().run(),
fromJson: GetRoomsResponse.fromJson, fromJson: GetRoomsResponse.fromJson,
) { ) {
start('wu-rooms'); start('wu-rooms');
} }
} }
@@ -10,7 +10,8 @@ class GetRoomsResponse extends ApiResponse {
GetRoomsResponse(this.result); GetRoomsResponse(this.result);
factory GetRoomsResponse.fromJson(Map<String, dynamic> json) => _$GetRoomsResponseFromJson(json); factory GetRoomsResponse.fromJson(Map<String, dynamic> json) =>
_$GetRoomsResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetRoomsResponseToJson(this); Map<String, dynamic> toJson() => _$GetRoomsResponseToJson(this);
} }
@@ -22,8 +23,15 @@ class GetRoomsResponseObject {
bool active; bool active;
String building; String building;
GetRoomsResponseObject(this.id, this.name, this.longName, this.active, this.building); GetRoomsResponseObject(
this.id,
this.name,
this.longName,
this.active,
this.building,
);
factory GetRoomsResponseObject.fromJson(Map<String, dynamic> json) => _$GetRoomsResponseObjectFromJson(json); factory GetRoomsResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetRoomsResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetRoomsResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetRoomsResponseObjectToJson(this);
} }
@@ -9,6 +9,10 @@ class GetSubjects extends WebuntisApi {
@override @override
Future<GetSubjectsResponse> run() async { Future<GetSubjectsResponse> run() async {
final rawAnswer = await query(this); final rawAnswer = await query(this);
return finalize(GetSubjectsResponse.fromJson(jsonDecode(rawAnswer) as Map<String, dynamic>)); return finalize(
GetSubjectsResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
} }
} }
@@ -4,11 +4,11 @@ import 'get_subjects_response.dart';
class GetSubjectsCache extends SimpleCache<GetSubjectsResponse> { class GetSubjectsCache extends SimpleCache<GetSubjectsResponse> {
GetSubjectsCache({super.onUpdate, super.onError, super.renew}) GetSubjectsCache({super.onUpdate, super.onError, super.renew})
: super( : super(
cacheTime: RequestCache.cacheHour, cacheTime: RequestCache.cacheHour,
loader: () => GetSubjects().run(), loader: () => GetSubjects().run(),
fromJson: GetSubjectsResponse.fromJson, fromJson: GetSubjectsResponse.fromJson,
) { ) {
start('wu-subjects'); start('wu-subjects');
} }
} }
@@ -10,7 +10,8 @@ class GetSubjectsResponse extends ApiResponse {
GetSubjectsResponse(this.result); GetSubjectsResponse(this.result);
factory GetSubjectsResponse.fromJson(Map<String, dynamic> json) => _$GetSubjectsResponseFromJson(json); factory GetSubjectsResponse.fromJson(Map<String, dynamic> json) =>
_$GetSubjectsResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetSubjectsResponseToJson(this); Map<String, dynamic> toJson() => _$GetSubjectsResponseToJson(this);
} }
@@ -22,8 +23,15 @@ class GetSubjectsResponseObject {
String alternateName; String alternateName;
bool active; bool active;
GetSubjectsResponseObject(this.id, this.name, this.longName, this.alternateName, this.active); GetSubjectsResponseObject(
this.id,
this.name,
this.longName,
this.alternateName,
this.active,
);
factory GetSubjectsResponseObject.fromJson(Map<String, dynamic> json) => _$GetSubjectsResponseObjectFromJson(json); factory GetSubjectsResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetSubjectsResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetSubjectsResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetSubjectsResponseObjectToJson(this);
} }
@@ -11,12 +11,20 @@ class GetTimegridUnits extends WebuntisApi {
Future<GetTimegridUnitsResponse> run() async { Future<GetTimegridUnitsResponse> run() async {
final rawAnswer = await query(this); final rawAnswer = await query(this);
try { try {
return finalize(GetTimegridUnitsResponse.fromJson(jsonDecode(rawAnswer) as Map<String, dynamic>)); return finalize(
GetTimegridUnitsResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
} catch (e, trace) { } catch (e, trace) {
log(trace.toString()); log(trace.toString());
log('Failed to parse getTimegridUnits data with server response: $rawAnswer'); log(
'Failed to parse getTimegridUnits data with server response: $rawAnswer',
);
} }
throw Exception('Failed to parse getTimegridUnits server response: $rawAnswer'); throw Exception(
'Failed to parse getTimegridUnits server response: $rawAnswer',
);
} }
} }
@@ -4,11 +4,11 @@ import 'get_timegrid_units_response.dart';
class GetTimegridUnitsCache extends SimpleCache<GetTimegridUnitsResponse> { class GetTimegridUnitsCache extends SimpleCache<GetTimegridUnitsResponse> {
GetTimegridUnitsCache({super.onUpdate, super.renew}) GetTimegridUnitsCache({super.onUpdate, super.renew})
: super( : super(
cacheTime: RequestCache.cacheDay, cacheTime: RequestCache.cacheDay,
loader: () => GetTimegridUnits().run(), loader: () => GetTimegridUnits().run(),
fromJson: GetTimegridUnitsResponse.fromJson, fromJson: GetTimegridUnitsResponse.fromJson,
) { ) {
start('wu-timegrid'); start('wu-timegrid');
} }
} }
@@ -10,7 +10,8 @@ class GetTimegridUnitsResponse extends ApiResponse {
GetTimegridUnitsResponse(this.result); GetTimegridUnitsResponse(this.result);
factory GetTimegridUnitsResponse.fromJson(Map<String, dynamic> json) => _$GetTimegridUnitsResponseFromJson(json); factory GetTimegridUnitsResponse.fromJson(Map<String, dynamic> json) =>
_$GetTimegridUnitsResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseToJson(this); Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseToJson(this);
} }
@@ -21,7 +22,8 @@ class GetTimegridUnitsResponseDay {
GetTimegridUnitsResponseDay(this.day, this.timeUnits); GetTimegridUnitsResponseDay(this.day, this.timeUnits);
factory GetTimegridUnitsResponseDay.fromJson(Map<String, dynamic> json) => _$GetTimegridUnitsResponseDayFromJson(json); factory GetTimegridUnitsResponseDay.fromJson(Map<String, dynamic> json) =>
_$GetTimegridUnitsResponseDayFromJson(json);
Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseDayToJson(this); Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseDayToJson(this);
} }
@@ -33,6 +35,7 @@ class GetTimegridUnitsResponseUnit {
GetTimegridUnitsResponseUnit(this.name, this.startTime, this.endTime); GetTimegridUnitsResponseUnit(this.name, this.startTime, this.endTime);
factory GetTimegridUnitsResponseUnit.fromJson(Map<String, dynamic> json) => _$GetTimegridUnitsResponseUnitFromJson(json); factory GetTimegridUnitsResponseUnit.fromJson(Map<String, dynamic> json) =>
_$GetTimegridUnitsResponseUnitFromJson(json);
Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseUnitToJson(this); Map<String, dynamic> toJson() => _$GetTimegridUnitsResponseUnitToJson(this);
} }
@@ -12,7 +12,10 @@ class GetTimetable extends WebuntisApi {
@override @override
Future<GetTimetableResponse> run() async { Future<GetTimetableResponse> run() async {
final rawAnswer = await query(this); final rawAnswer = await query(this);
return finalize(GetTimetableResponse.fromJson(jsonDecode(rawAnswer) as Map<String, dynamic>)); return finalize(
GetTimetableResponse.fromJson(
jsonDecode(rawAnswer) as Map<String, dynamic>,
),
);
} }
} }
@@ -12,11 +12,11 @@ class GetTimetableCache extends SimpleCache<GetTimetableResponse> {
required int enddate, required int enddate,
super.renew, super.renew,
}) : super( }) : super(
cacheTime: RequestCache.cacheMinute, cacheTime: RequestCache.cacheMinute,
loader: () => _load(startdate, enddate), loader: () => _load(startdate, enddate),
fromJson: GetTimetableResponse.fromJson, fromJson: GetTimetableResponse.fromJson,
onUpdate: onUpdate, onUpdate: onUpdate,
) { ) {
start('wu-timetable-$startdate-$enddate'); start('wu-timetable-$startdate-$enddate');
} }
@@ -10,11 +10,11 @@ class GetTimetableParams extends ApiParams {
GetTimetableParams({required this.options}); GetTimetableParams({required this.options});
factory GetTimetableParams.fromJson(Map<String, dynamic> json) => _$GetTimetableParamsFromJson(json); factory GetTimetableParams.fromJson(Map<String, dynamic> json) =>
_$GetTimetableParamsFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableParamsToJson(this); Map<String, dynamic> toJson() => _$GetTimetableParamsToJson(this);
} }
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class GetTimetableParamsOptions { class GetTimetableParamsOptions {
GetTimetableParamsOptionsElement element; GetTimetableParamsOptionsElement element;
@@ -59,20 +59,30 @@ class GetTimetableParamsOptions {
this.klasseFields, this.klasseFields,
this.roomFields, this.roomFields,
this.subjectFields, this.subjectFields,
this.teacherFields this.teacherFields,
}); });
factory GetTimetableParamsOptions.fromJson(Map<String, dynamic> json) => _$GetTimetableParamsOptionsFromJson(json); factory GetTimetableParamsOptions.fromJson(Map<String, dynamic> json) =>
_$GetTimetableParamsOptionsFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableParamsOptionsToJson(this); Map<String, dynamic> toJson() => _$GetTimetableParamsOptionsToJson(this);
} }
enum GetTimetableParamsOptionsFields { enum GetTimetableParamsOptionsFields {
@JsonValue('id') id, @JsonValue('id')
@JsonValue('name') name, id,
@JsonValue('longname') longname, @JsonValue('name')
@JsonValue('externalkey') externalkey; name,
@JsonValue('longname')
longname,
@JsonValue('externalkey')
externalkey;
static List<GetTimetableParamsOptionsFields> all = [id, name, longname, externalkey]; static List<GetTimetableParamsOptionsFields> all = [
id,
name,
longname,
externalkey,
];
} }
@JsonSerializable() @JsonSerializable()
@@ -82,13 +92,23 @@ class GetTimetableParamsOptionsElement {
@JsonKey(includeIfNull: false) @JsonKey(includeIfNull: false)
GetTimetableParamsOptionsElementKeyType? keyType; GetTimetableParamsOptionsElementKeyType? keyType;
GetTimetableParamsOptionsElement({required this.id, required this.type, this.keyType}); GetTimetableParamsOptionsElement({
factory GetTimetableParamsOptionsElement.fromJson(Map<String, dynamic> json) => _$GetTimetableParamsOptionsElementFromJson(json); required this.id,
Map<String, dynamic> toJson() => _$GetTimetableParamsOptionsElementToJson(this); required this.type,
this.keyType,
});
factory GetTimetableParamsOptionsElement.fromJson(
Map<String, dynamic> json,
) => _$GetTimetableParamsOptionsElementFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableParamsOptionsElementToJson(this);
} }
enum GetTimetableParamsOptionsElementKeyType { enum GetTimetableParamsOptionsElementKeyType {
@JsonValue('id') id, @JsonValue('id')
@JsonValue('name') name, id,
@JsonValue('externalkey') externalkey @JsonValue('name')
name,
@JsonValue('externalkey')
externalkey,
} }
@@ -10,9 +10,9 @@ class GetTimetableResponse extends ApiResponse {
GetTimetableResponse(this.result); GetTimetableResponse(this.result);
factory GetTimetableResponse.fromJson(Map<String, dynamic> json) => _$GetTimetableResponseFromJson(json); factory GetTimetableResponse.fromJson(Map<String, dynamic> json) =>
_$GetTimetableResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableResponseToJson(this); Map<String, dynamic> toJson() => _$GetTimetableResponseToJson(this);
} }
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
@@ -55,10 +55,11 @@ class GetTimetableResponseObject {
required this.kl, required this.kl,
required this.te, required this.te,
required this.su, required this.su,
required this.ro required this.ro,
}); });
factory GetTimetableResponseObject.fromJson(Map<String, dynamic> json) => _$GetTimetableResponseObjectFromJson(json); factory GetTimetableResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetTimetableResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectToJson(this); Map<String, dynamic> toJson() => _$GetTimetableResponseObjectToJson(this);
} }
@@ -68,8 +69,11 @@ class GetTimetableResponseObjectFields {
GetTimetableResponseObjectFields(this.te); GetTimetableResponseObjectFields(this.te);
factory GetTimetableResponseObjectFields.fromJson(Map<String, dynamic> json) => _$GetTimetableResponseObjectFieldsFromJson(json); factory GetTimetableResponseObjectFields.fromJson(
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectFieldsToJson(this); Map<String, dynamic> json,
) => _$GetTimetableResponseObjectFieldsFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectFieldsToJson(this);
} }
@JsonSerializable() @JsonSerializable()
@@ -79,10 +83,18 @@ class GetTimetableResponseObjectFieldsObject {
String? longname; String? longname;
String? externalkey; String? externalkey;
GetTimetableResponseObjectFieldsObject({this.id, this.name, this.longname, this.externalkey}); GetTimetableResponseObjectFieldsObject({
this.id,
this.name,
this.longname,
this.externalkey,
});
factory GetTimetableResponseObjectFieldsObject.fromJson(Map<String, dynamic> json) => _$GetTimetableResponseObjectFieldsObjectFromJson(json); factory GetTimetableResponseObjectFieldsObject.fromJson(
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectFieldsObjectToJson(this); Map<String, dynamic> json,
) => _$GetTimetableResponseObjectFieldsObjectFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectFieldsObjectToJson(this);
} }
@JsonSerializable() @JsonSerializable()
@@ -92,10 +104,17 @@ class GetTimetableResponseObjectClass {
String longname; String longname;
String? externalkey; String? externalkey;
GetTimetableResponseObjectClass(this.id, this.name, this.longname, this.externalkey); GetTimetableResponseObjectClass(
this.id,
this.name,
this.longname,
this.externalkey,
);
factory GetTimetableResponseObjectClass.fromJson(Map<String, dynamic> json) => _$GetTimetableResponseObjectClassFromJson(json); factory GetTimetableResponseObjectClass.fromJson(Map<String, dynamic> json) =>
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectClassToJson(this); _$GetTimetableResponseObjectClassFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectClassToJson(this);
} }
@JsonSerializable() @JsonSerializable()
@@ -107,11 +126,20 @@ class GetTimetableResponseObjectTeacher {
String? orgname; String? orgname;
String? externalkey; String? externalkey;
GetTimetableResponseObjectTeacher(
this.id,
this.name,
this.longname,
this.orgid,
this.orgname,
this.externalkey,
);
GetTimetableResponseObjectTeacher(this.id, this.name, this.longname, this.orgid, this.orgname, this.externalkey); factory GetTimetableResponseObjectTeacher.fromJson(
Map<String, dynamic> json,
factory GetTimetableResponseObjectTeacher.fromJson(Map<String, dynamic> json) => _$GetTimetableResponseObjectTeacherFromJson(json); ) => _$GetTimetableResponseObjectTeacherFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectTeacherToJson(this); Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectTeacherToJson(this);
} }
@JsonSerializable() @JsonSerializable()
@@ -122,8 +150,11 @@ class GetTimetableResponseObjectSubject {
GetTimetableResponseObjectSubject(this.id, this.name, this.longname); GetTimetableResponseObjectSubject(this.id, this.name, this.longname);
factory GetTimetableResponseObjectSubject.fromJson(Map<String, dynamic> json) => _$GetTimetableResponseObjectSubjectFromJson(json); factory GetTimetableResponseObjectSubject.fromJson(
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectSubjectToJson(this); Map<String, dynamic> json,
) => _$GetTimetableResponseObjectSubjectFromJson(json);
Map<String, dynamic> toJson() =>
_$GetTimetableResponseObjectSubjectToJson(this);
} }
@JsonSerializable() @JsonSerializable()
@@ -134,6 +165,7 @@ class GetTimetableResponseObjectRoom {
GetTimetableResponseObjectRoom(this.id, this.name, this.longname); GetTimetableResponseObjectRoom(this.id, this.name, this.longname);
factory GetTimetableResponseObjectRoom.fromJson(Map<String, dynamic> json) => _$GetTimetableResponseObjectRoomFromJson(json); factory GetTimetableResponseObjectRoom.fromJson(Map<String, dynamic> json) =>
_$GetTimetableResponseObjectRoomFromJson(json);
Map<String, dynamic> toJson() => _$GetTimetableResponseObjectRoomToJson(this); Map<String, dynamic> toJson() => _$GetTimetableResponseObjectRoomToJson(this);
} }
@@ -9,10 +9,14 @@ import '../queries/get_subjects/get_subjects_response.dart';
/// When a record is missing the resolver returns a placeholder fallback /// When a record is missing the resolver returns a placeholder fallback
/// instead of `null` so call sites stay branch-free. /// instead of `null` so call sites stay branch-free.
class LessonResolver { class LessonResolver {
static GetSubjectsResponseObject resolveSubject(TimetableState state, int? id) { static GetSubjectsResponseObject resolveSubject(
TimetableState state,
int? id,
) {
final fallback = GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true); final fallback = GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true);
if (id == null) return fallback; if (id == null) return fallback;
return state.subjects?.result.firstWhereOrNull((s) => s.id == id) ?? fallback; return state.subjects?.result.firstWhereOrNull((s) => s.id == id) ??
fallback;
} }
static GetRoomsResponseObject resolveRoom(TimetableState state, int? id) { static GetRoomsResponseObject resolveRoom(TimetableState state, int? id) {
@@ -61,9 +65,7 @@ class LessonFormatter {
/// optional longname (rendered in parentheses if it differs from `name`), /// optional longname (rendered in parentheses if it differs from `name`),
/// and optional extra info (joined with `·`). /// and optional extra info (joined with `·`).
static String formatLine(String name, {String? longname, String? extra}) { static String formatLine(String name, {String? longname, String? extra}) {
final parts = <String>[ final parts = <String>[if (name.isNotEmpty) name else '?'];
if (name.isNotEmpty) name else '?',
];
final ln = (longname ?? '').trim(); final ln = (longname ?? '').trim();
if (ln.isNotEmpty && ln != name) parts.add('($ln)'); if (ln.isNotEmpty && ln != name) parts.add('($ln)');
final ex = (extra ?? '').trim(); final ex = (extra ?? '').trim();
+31 -10
View File
@@ -14,18 +14,24 @@ import 'queries/authenticate/authenticate.dart';
import 'webuntis_error.dart'; import 'webuntis_error.dart';
abstract class WebuntisApi extends ApiRequest { abstract class WebuntisApi extends ApiRequest {
Uri endpoint = Uri.parse('https://${EndpointData().webuntis().full()}/WebUntis/jsonrpc.do?school=marianum-fulda'); Uri endpoint = Uri.parse(
'https://${EndpointData().webuntis().full()}/WebUntis/jsonrpc.do?school=marianum-fulda',
);
String method; String method;
ApiParams? genericParam; ApiParams? genericParam;
http.Response? response; http.Response? response;
bool authenticatedResponse; bool authenticatedResponse;
WebuntisApi(this.method, this.genericParam, {this.authenticatedResponse = true}); WebuntisApi(
this.method,
this.genericParam, {
this.authenticatedResponse = true,
});
Future<String> query(WebuntisApi untis, {bool retry = false}) async { Future<String> query(WebuntisApi untis, {bool retry = false}) async {
final body = '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}'; final body =
'{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}';
var sessionId = '0'; var sessionId = '0';
if (authenticatedResponse) { if (authenticatedResponse) {
@@ -38,13 +44,20 @@ abstract class WebuntisApi extends ApiRequest {
try { try {
jsonData = jsonDecode(data.body) as Map<String, dynamic>; jsonData = jsonDecode(data.body) as Map<String, dynamic>;
} on FormatException catch (e) { } on FormatException catch (e) {
throw ParseException(technicalDetails: 'WebUntis JSON decode: ${e.message}'); throw ParseException(
technicalDetails: 'WebUntis JSON decode: ${e.message}',
);
} }
final error = jsonData['error'] as Map<String, dynamic>?; final error = jsonData['error'] as Map<String, dynamic>?;
if (error != null) { if (error != null) {
final code = error['code'] as int; final code = error['code'] as int;
if (code == -8520) { if (code == -8520) {
if (retry) throw WebuntisError('Authentication was tried (probably session timeout), but was not successful!', -8520); if (retry) {
throw WebuntisError(
'Authentication was tried (probably session timeout), but was not successful!',
-8520,
);
}
await Authenticate.createSession(); await Authenticate.createSession();
return query(untis, retry: true); return query(untis, retry: true);
} else { } else {
@@ -65,14 +78,22 @@ abstract class WebuntisApi extends ApiRequest {
Future<http.Response> post(String data, Map<String, String>? headers) async { Future<http.Response> post(String data, Map<String, String>? headers) async {
try { try {
return await http.post(endpoint, body: data, headers: headers).timeout( return await http
.post(endpoint, body: data, headers: headers)
.timeout(
const Duration(seconds: 10), const Duration(seconds: 10),
onTimeout: () => throw NetworkException.timeout(technicalDetails: 'WebUntis $method timed out after 10s'), onTimeout: () => throw NetworkException.timeout(
technicalDetails: 'WebUntis $method timed out after 10s',
),
); );
} on SocketException catch (e) { } on SocketException catch (e) {
throw NetworkException(technicalDetails: 'WebUntis $method: ${e.message}'); throw NetworkException(
technicalDetails: 'WebUntis $method: ${e.message}',
);
} on http.ClientException catch (e) { } on http.ClientException catch (e) {
throw NetworkException(technicalDetails: 'WebUntis $method: ${e.message}'); throw NetworkException(
technicalDetails: 'WebUntis $method: ${e.message}',
);
} }
} }
} }
+86 -78
View File
@@ -95,7 +95,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (!mounted) return; if (!mounted) return;
NotificationController.onForegroundMessageHandler(message, context); NotificationController.onForegroundMessageHandler(message, context);
}); });
FirebaseMessaging.onBackgroundMessage(NotificationController.onBackgroundMessageHandler); FirebaseMessaging.onBackgroundMessage(
NotificationController.onBackgroundMessageHandler,
);
FirebaseMessaging.onMessageOpenedApp.listen((message) { FirebaseMessaging.onMessageOpenedApp.listen((message) {
if (!mounted) return; if (!mounted) return;
@@ -119,83 +121,89 @@ class _AppState extends State<App> with WidgetsBindingObserver {
} }
@override @override
Widget build(BuildContext context) => BlocBuilder<SettingsCubit, model.Settings>( Widget build(
builder: (context, _) { BuildContext context,
final bottomBarModules = AppModule.getBottomBarModules(context); ) => BlocBuilder<SettingsCubit, model.Settings>(
final totalTabs = bottomBarModules.length + 1; builder: (context, _) {
final currentIndex = Main.bottomNavigator.index; final bottomBarModules = AppModule.getBottomBarModules(context);
final totalTabs = bottomBarModules.length + 1;
final currentIndex = Main.bottomNavigator.index;
// The bottom-bar layout is identified by the ordered list of module // The bottom-bar layout is identified by the ordered list of module
// names plus the trailing 'more' slot. Whenever this layout changes // names plus the trailing 'more' slot. Whenever this layout changes
// — slot count, reordering, or hiding a module — we recreate the // — slot count, reordering, or hiding a module — we recreate the
// entire PersistentTabView via the [layoutKey] below. The package // entire PersistentTabView via the [layoutKey] below. The package
// caches per-tab navigator state by index in `_navigatorKeys`, and // caches per-tab navigator state by index in `_navigatorKeys`, and
// its internal `alignLength` only ever appends or trims at the end. // its internal `alignLength` only ever appends or trims at the end.
// So when the module sitting at e.g. index 3 changes, the navigator // So when the module sitting at e.g. index 3 changes, the navigator
// at that index still serves the old screen's route stack and the // at that index still serves the old screen's route stack and the
// user sees stale content. Re-mounting clears those stacks; the // user sees stale content. Re-mounting clears those stacks; the
// trade-off (losing in-tab pushed routes on a settings change) is // trade-off (losing in-tab pushed routes on a settings change) is
// acceptable since the user explicitly re-shaped the bar. // acceptable since the user explicitly re-shaped the bar.
final layoutKey = ValueKey('${bottomBarModules.map((m) => m.module.name).join('|')}|more'); final layoutKey = ValueKey(
'${bottomBarModules.map((m) => m.module.name).join('|')}|more',
if (totalTabs != _knownTotalTabs) {
var targetIndex = currentIndex;
if (_userOnLastTab) {
targetIndex = totalTabs - 1;
} else if (currentIndex >= totalTabs) {
targetIndex = totalTabs - 1;
}
// Re-mounting PTV with a new key constructs fresh internals from
// its controller's current index. If the controller still points
// past the new tab list, Style6BottomNavBar (and others) crash on
// out-of-range access during initState. Replace the controller
// atomically with one initialised at the safe target index so the
// new PTV mounts cleanly.
if (targetIndex != currentIndex) {
Main.bottomNavigator.removeListener(_onTabControllerChanged);
Main.bottomNavigator = PersistentTabController(initialIndex: targetIndex);
Main.bottomNavigator.addListener(_onTabControllerChanged);
_userOnLastTab = targetIndex == totalTabs - 1;
}
}
_knownTotalTabs = totalTabs;
return PersistentTabView(
key: layoutKey,
controller: Main.bottomNavigator,
navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary,
handleAndroidBackButtonPress: true,
screenTransitionAnimation: const ScreenTransitionAnimation(
curve: Curves.easeOutQuad,
duration: Duration(milliseconds: 200),
),
tabs: [
...bottomBarModules.map((e) => e.toBottomTab(context)),
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.apps),
title: 'Mehr',
),
),
],
navBarBuilder: (config) => Style6BottomNavBar(
// Style6BottomNavBar builds its internal animation controller list
// in initState and never grows it on didUpdateWidget. Keying by the
// item count forces a fresh State whenever the slot count changes,
// which avoids a RangeError when more tabs slide in.
key: ValueKey(config.items.length),
navBarConfig: config,
navBarDecoration: NavBarDecoration(
border: const Border(top: BorderSide(width: 1, color: Colors.grey)),
color: Theme.of(context).colorScheme.surface,
),
),
);
},
); );
if (totalTabs != _knownTotalTabs) {
var targetIndex = currentIndex;
if (_userOnLastTab) {
targetIndex = totalTabs - 1;
} else if (currentIndex >= totalTabs) {
targetIndex = totalTabs - 1;
}
// Re-mounting PTV with a new key constructs fresh internals from
// its controller's current index. If the controller still points
// past the new tab list, Style6BottomNavBar (and others) crash on
// out-of-range access during initState. Replace the controller
// atomically with one initialised at the safe target index so the
// new PTV mounts cleanly.
if (targetIndex != currentIndex) {
Main.bottomNavigator.removeListener(_onTabControllerChanged);
Main.bottomNavigator = PersistentTabController(
initialIndex: targetIndex,
);
Main.bottomNavigator.addListener(_onTabControllerChanged);
_userOnLastTab = targetIndex == totalTabs - 1;
}
}
_knownTotalTabs = totalTabs;
return PersistentTabView(
key: layoutKey,
controller: Main.bottomNavigator,
navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary,
handleAndroidBackButtonPress: true,
screenTransitionAnimation: const ScreenTransitionAnimation(
curve: Curves.easeOutQuad,
duration: Duration(milliseconds: 200),
),
tabs: [
...bottomBarModules.map((e) => e.toBottomTab(context)),
PersistentTabConfig(
screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
item: ItemConfig(
activeForegroundColor: Theme.of(context).primaryColor,
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.apps),
title: 'Mehr',
),
),
],
navBarBuilder: (config) => Style6BottomNavBar(
// Style6BottomNavBar builds its internal animation controller list
// in initState and never grows it on didUpdateWidget. Keying by the
// item count forces a fresh State whenever the slot count changes,
// which avoids a RangeError when more tabs slide in.
key: ValueKey(config.items.length),
navBarConfig: config,
navBarDecoration: NavBarDecoration(
border: const Border(top: BorderSide(width: 1, color: Colors.grey)),
color: Theme.of(context).colorScheme.surface,
),
),
);
},
);
} }
+16 -8
View File
@@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
extension IsSameDay on DateTime { extension IsSameDay on DateTime {
bool isSameDay(DateTime other) => year == other.year && month == other.month && day == other.day; bool isSameDay(DateTime other) =>
year == other.year && month == other.month && day == other.day;
DateTime nextWeekday(int day) => add(Duration(days: (day - weekday) % DateTime.daysPerWeek)); DateTime nextWeekday(int day) =>
add(Duration(days: (day - weekday) % DateTime.daysPerWeek));
DateTime withTime(TimeOfDay time) => copyWith(hour: time.hour, minute: time.minute); DateTime withTime(TimeOfDay time) =>
copyWith(hour: time.hour, minute: time.minute);
TimeOfDay toTimeOfDay() => TimeOfDay(hour: hour, minute: minute); TimeOfDay toTimeOfDay() => TimeOfDay(hour: hour, minute: minute);
@@ -25,15 +28,20 @@ extension IsSameDay on DateTime {
extension DateTimeFormatting on DateTime { extension DateTimeFormatting on DateTime {
String formatHm() => Jiffy.parseFromDateTime(this).format(pattern: 'HH:mm'); String formatHm() => Jiffy.parseFromDateTime(this).format(pattern: 'HH:mm');
String formatDate() => Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.yyyy'); String formatDate() =>
Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.yyyy');
String formatDateTime() => Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.yyyy HH:mm'); String formatDateTime() =>
Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.yyyy HH:mm');
String formatDateShort() => Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.'); String formatDateShort() =>
Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.');
String formatDateShortHm() => Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM. HH:mm'); String formatDateShortHm() =>
Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM. HH:mm');
String formatMonthYear() => Jiffy.parseFromDateTime(this).format(pattern: 'MMMM yyyy'); String formatMonthYear() =>
Jiffy.parseFromDateTime(this).format(pattern: 'MMMM yyyy');
String formatRelative() => Jiffy.parseFromDateTime(this).fromNow(); String formatRelative() => Jiffy.parseFromDateTime(this).fromNow();
+2 -1
View File
@@ -1,3 +1,4 @@
extension RenderNotNullExt<T> on T? { extension RenderNotNullExt<T> on T? {
R? wrapNullable<R>(R Function(T data) defaultValueCallback) => this != null ? defaultValueCallback(this as T) : null; R? wrapNullable<R>(R Function(T data) defaultValueCallback) =>
this != null ? defaultValueCallback(this as T) : null;
} }
+3 -3
View File
@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
extension TextExt on Text { extension TextExt on Text {
Size get size { Size get size {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan(text: data, style: style), text: TextSpan(text: data, style: style),
maxLines: 1, maxLines: 1,
textDirection: TextDirection.ltr textDirection: TextDirection.ltr,
)..layout(minWidth: 0, maxWidth: double.infinity); )..layout(minWidth: 0, maxWidth: double.infinity);
return textPainter.size; return textPainter.size;
} }
+2 -1
View File
@@ -5,5 +5,6 @@ extension TimeOfDayExt on TimeOfDay {
bool isAfter(TimeOfDay other) => hour > other.hour && minute > other.minute; bool isAfter(TimeOfDay other) => hour > other.hour && minute > other.minute;
TimeOfDay add({int hours = 0, int minutes = 0}) => replacing(hour: hour + hours, minute: minute + minutes); TimeOfDay add({int hours = 0, int minutes = 0}) =>
replacing(hour: hour + hours, minute: minute + minutes);
} }
+2 -1
View File
@@ -63,7 +63,8 @@ class DefaultFirebaseOptions {
messagingSenderId: '522850592536', messagingSenderId: '522850592536',
projectId: 'marmobile-33b10', projectId: 'marmobile-33b10',
storageBucket: 'marmobile-33b10.appspot.com', storageBucket: 'marmobile-33b10.appspot.com',
iosClientId: '522850592536-edj90sbbnkjqe3aqui37j8enu93v4fk8.apps.googleusercontent.com', iosClientId:
'522850592536-edj90sbbnkjqe3aqui37j8enu93v4fk8.apps.googleusercontent.com',
iosBundleId: 'eu.mhsl.marianum.mobile.client', iosBundleId: 'eu.mhsl.marianum.mobile.client',
); );
} }
+146 -119
View File
@@ -40,19 +40,28 @@ Future<void> main() async {
log('MarianumMobile started'); log('MarianumMobile started');
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
void addCertificateAsTrusted(ByteData certificate) => void addCertificateAsTrusted(ByteData certificate) => SecurityContext
SecurityContext.defaultContext.setTrustedCertificatesBytes(certificate.buffer.asUint8List()); .defaultContext
.setTrustedCertificatesBytes(certificate.buffer.asUint8List());
final initialisationTasks = [ final initialisationTasks = [
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)
.then<void>((_) {}) .then<void>((_) {})
.onError((error, _) => log('Error initializing Firebase: $error')), .onError((error, _) => log('Error initializing Firebase: $error')),
PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem').then(addCertificateAsTrusted), PlatformAssetBundle()
PlatformAssetBundle().load('assets/ca/lets-encrypt-r10.pem').then(addCertificateAsTrusted), .load('assets/ca/lets-encrypt-r3.pem')
PlatformAssetBundle().load('assets/ca/lets-encrypt-r13.pem').then(addCertificateAsTrusted), .then(addCertificateAsTrusted),
PlatformAssetBundle()
.load('assets/ca/lets-encrypt-r10.pem')
.then(addCertificateAsTrusted),
PlatformAssetBundle()
.load('assets/ca/lets-encrypt-r13.pem')
.then(addCertificateAsTrusted),
Future(() async { Future(() async {
final storage = await HydratedStorage.build( final storage = await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory((await getTemporaryDirectory()).path), storageDirectory: HydratedStorageDirectory(
(await getTemporaryDirectory()).path,
),
); );
HydratedBloc.storage = storage; HydratedBloc.storage = storage;
}), }),
@@ -71,27 +80,30 @@ Future<void> main() async {
if (kReleaseMode) { if (kReleaseMode) {
ErrorWidget.builder = (error) => Material( ErrorWidget.builder = (error) => Material(
color: Colors.white, color: Colors.white,
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.phonelink_erase_rounded, size: 40), const Icon(Icons.phonelink_erase_rounded, size: 40),
const SizedBox(height: 12), const SizedBox(height: 12),
Text(error.toStringShort(), textAlign: TextAlign.center), Text(error.toStringShort(), textAlign: TextAlign.center),
], ],
),
),
), ),
); ),
),
);
} }
// Capture uncaught Flutter and platform errors so they show up in logs // Capture uncaught Flutter and platform errors so they show up in logs
// instead of being silently swallowed. // instead of being silently swallowed.
FlutterError.onError = (details) { FlutterError.onError = (details) {
log('Uncaught Flutter error: ${details.exception}', stackTrace: details.stack); log(
'Uncaught Flutter error: ${details.exception}',
stackTrace: details.stack,
);
FlutterError.presentError(details); FlutterError.presentError(details);
}; };
PlatformDispatcher.instance.onError = (error, stack) { PlatformDispatcher.instance.onError = (error, stack) {
@@ -104,9 +116,13 @@ Future<void> main() async {
MultiBlocProvider( MultiBlocProvider(
providers: [ providers: [
BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()), BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
BlocProvider<AccountBloc>(create: (_) => AccountBloc( BlocProvider<AccountBloc>(
initialStatus: AccountData().isPopulated() ? AccountStatus.loggedIn : AccountStatus.loggedOut, create: (_) => AccountBloc(
)), initialStatus: AccountData().isPopulated()
? AccountStatus.loggedIn
: AccountStatus.loggedOut,
),
),
BlocProvider<BreakerBloc>(create: (_) => BreakerBloc()), BlocProvider<BreakerBloc>(create: (_) => BreakerBloc()),
BlocProvider<ChatListBloc>(create: (_) => ChatListBloc()), BlocProvider<ChatListBloc>(create: (_) => ChatListBloc()),
BlocProvider<ChatBloc>(create: (_) => ChatBloc()), BlocProvider<ChatBloc>(create: (_) => ChatBloc()),
@@ -120,7 +136,9 @@ Future<void> main() async {
class Main extends StatefulWidget { class Main extends StatefulWidget {
const Main({super.key}); const Main({super.key});
static PersistentTabController bottomNavigator = PersistentTabController(initialIndex: 0); static PersistentTabController bottomNavigator = PersistentTabController(
initialIndex: 0,
);
@override @override
State<Main> createState() => _MainState(); State<Main> createState() => _MainState();
@@ -134,107 +152,116 @@ class _MainState extends State<Main> {
AccountData().waitForPopulation().then((value) { AccountData().waitForPopulation().then((value) {
if (!mounted) return; if (!mounted) return;
context.read<AccountBloc>().setStatus(value ? AccountStatus.loggedIn : AccountStatus.loggedOut); context.read<AccountBloc>().setStatus(
value ? AccountStatus.loggedIn : AccountStatus.loggedOut,
);
}); });
} }
@override @override
Widget build(BuildContext context) => Directionality( Widget build(BuildContext context) => Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: BlocBuilder<SettingsCubit, Settings>( child: BlocBuilder<SettingsCubit, Settings>(
builder: (context, settings) { builder: (context, settings) {
final devToolsSettings = settings.devToolsSettings; final devToolsSettings = settings.devToolsSettings;
return MaterialApp( return MaterialApp(
showPerformanceOverlay: devToolsSettings.showPerformanceOverlay, showPerformanceOverlay: devToolsSettings.showPerformanceOverlay,
checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers, checkerboardOffscreenLayers:
checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages, devToolsSettings.checkerboardOffscreenLayers,
debugShowCheckedModeBanner: false, checkerboardRasterCacheImages:
localizationsDelegates: const [ devToolsSettings.checkerboardRasterCacheImages,
...GlobalMaterialLocalizations.delegates, debugShowCheckedModeBanner: false,
GlobalWidgetsLocalizations.delegate, localizationsDelegates: const [
], ...GlobalMaterialLocalizations.delegates,
supportedLocales: const [Locale('de'), Locale('en')], GlobalWidgetsLocalizations.delegate,
locale: const Locale('de'), ],
title: 'Marianum Fulda', supportedLocales: const [Locale('de'), Locale('en')],
themeMode: settings.appTheme, locale: const Locale('de'),
theme: LightAppTheme.theme, title: 'Marianum Fulda',
darkTheme: DarkAppTheme.theme, themeMode: settings.appTheme,
// Brand-colored backdrop behind every route. During the logout theme: LightAppTheme.theme,
// home-swap and route pop animations the framework can briefly darkTheme: DarkAppTheme.theme,
// expose the layer below the topmost Scaffold; without this // Brand-colored backdrop behind every route. During the logout
// the dark Material default shows through and the user sees a // home-swap and route pop animations the framework can briefly
// black flash. // expose the layer below the topmost Scaffold; without this
builder: (context, child) => ColoredBox( // the dark Material default shows through and the user sees a
color: LightAppTheme.marianumRed, // black flash.
child: child ?? const SizedBox.shrink(), builder: (context, child) => ColoredBox(
), color: LightAppTheme.marianumRed,
home: LoaderOverlay( child: child ?? const SizedBox.shrink(),
child: Breaker( ),
breaker: BreakerArea.global, home: LoaderOverlay(
child: BlocConsumer<AccountBloc, AccountState>( child: Breaker(
listenWhen: (previous, current) => previous.status != current.status, breaker: BreakerArea.global,
listener: (context, accountState) { child: BlocConsumer<AccountBloc, AccountState>(
if (accountState.status != AccountStatus.loggedOut) return; listenWhen: (previous, current) =>
// Routes pushed via AppRoutes (e.g. Settings) live on the previous.status != current.status,
// root navigator and survive the home swap below, so they listener: (context, accountState) {
// would still cover the Login screen after logout. Pop if (accountState.status != AccountStatus.loggedOut) return;
// them here so the user immediately sees Login. // Routes pushed via AppRoutes (e.g. Settings) live on the
final navigator = Navigator.of(context); // root navigator and survive the home swap below, so they
if (navigator.canPop()) { // would still cover the Login screen after logout. Pop
navigator.popUntil((route) => route.isFirst); // them here so the user immediately sees Login.
} final navigator = Navigator.of(context);
// Capture bloc references before the post-frame callback if (navigator.canPop()) {
// — by the time it runs the dialog/Settings context is navigator.popUntil((route) => route.isFirst);
// gone but this listener context is still valid. }
final settingsCubit = context.read<SettingsCubit>(); // Capture bloc references before the post-frame callback
final timetableBloc = context.read<TimetableBloc>(); // — by the time it runs the dialog/Settings context is
final chatListBloc = context.read<ChatListBloc>(); // gone but this listener context is still valid.
final chatBloc = context.read<ChatBloc>(); final settingsCubit = context.read<SettingsCubit>();
final breakerBloc = context.read<BreakerBloc>(); final timetableBloc = context.read<TimetableBloc>();
// Defer the actual wipe until after this frame so the final chatListBloc = context.read<ChatListBloc>();
// App tree (TimetableBloc/ChatListBloc watchers etc.) final chatBloc = context.read<ChatBloc>();
// is already torn down. Resetting blocs while App is final breakerBloc = context.read<BreakerBloc>();
// still in front caused a black-frame race. // Defer the actual wipe until after this frame so the
WidgetsBinding.instance.addPostFrameCallback((_) { // App tree (TimetableBloc/ChatListBloc watchers etc.)
unawaited(_wipeUserState( // is already torn down. Resetting blocs while App is
settingsCubit: settingsCubit, // still in front caused a black-frame race.
timetableBloc: timetableBloc, WidgetsBinding.instance.addPostFrameCallback((_) {
chatListBloc: chatListBloc, unawaited(
chatBloc: chatBloc, _wipeUserState(
breakerBloc: breakerBloc, settingsCubit: settingsCubit,
)); timetableBloc: timetableBloc,
}); chatListBloc: chatListBloc,
}, chatBloc: chatBloc,
builder: (context, accountState) { breakerBloc: breakerBloc,
switch (accountState.status) { ),
case AccountStatus.loggedIn: );
return const App(); });
case AccountStatus.loggedOut: },
return const Login(); builder: (context, accountState) {
case AccountStatus.undefined: switch (accountState.status) {
return Scaffold( case AccountStatus.loggedIn:
backgroundColor: LightAppTheme.marianumRed, return const App();
body: const Center( case AccountStatus.loggedOut:
child: Column( return const Login();
mainAxisAlignment: MainAxisAlignment.center, case AccountStatus.undefined:
children: [ return Scaffold(
AppProgressIndicator.large(color: Colors.white), backgroundColor: LightAppTheme.marianumRed,
SizedBox(height: 16), body: const Center(
Text('Konto wird geladen…', child: Column(
style: TextStyle(color: Colors.white)), mainAxisAlignment: MainAxisAlignment.center,
], children: [
AppProgressIndicator.large(color: Colors.white),
SizedBox(height: 16),
Text(
'Konto wird geladen…',
style: TextStyle(color: Colors.white),
), ),
), ],
); ),
} ),
}, );
), }
), },
), ),
); ),
}, ),
), );
); },
),
);
} }
Future<void> _wipeUserState({ Future<void> _wipeUserState({

Some files were not shown because too many files have changed in this diff Show More