Merge branch 'develop' into develop-connectedDoubleLessons
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -349,3 +349,4 @@ hs_err_pid*
 | 
			
		||||
# End of https://www.toptal.com/developers/gitignore/api/flutter,intellij,androidstudio,xcode,dart
 | 
			
		||||
 | 
			
		||||
*.idea*
 | 
			
		||||
**/.DS_store
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,10 @@ linter:
 | 
			
		||||
  rules:
 | 
			
		||||
    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
 | 
			
		||||
    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
 | 
			
		||||
    file_names: false
 | 
			
		||||
    prefer_relative_imports: true
 | 
			
		||||
    unnecessary_lambdas: true
 | 
			
		||||
    prefer_single_quotes: true
 | 
			
		||||
 | 
			
		||||
# Additional information about this file can be found at
 | 
			
		||||
# https://dart.dev/guides/language/analysis-options
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,6 @@ class ApiError {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return "ApiError: $message";
 | 
			
		||||
    return 'ApiError: $message';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,11 +6,11 @@ import 'getHolidaysResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetHolidays {
 | 
			
		||||
  Future<GetHolidaysResponse> query() async {
 | 
			
		||||
    String response = (await http.get(Uri.parse("https://ferien-api.de/api/v1/holidays/HE"))).body;
 | 
			
		||||
    String response = (await http.get(Uri.parse('https://ferien-api.de/api/v1/holidays/HE'))).body;
 | 
			
		||||
    return GetHolidaysResponse(
 | 
			
		||||
        List<GetHolidaysResponseObject>.from(
 | 
			
		||||
            jsonDecode(response).map<GetHolidaysResponseObject>(
 | 
			
		||||
                    (dynamic i) => GetHolidaysResponseObject.fromJson(i)
 | 
			
		||||
                    GetHolidaysResponseObject.fromJson
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import 'getHolidaysResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
 | 
			
		||||
  GetHolidaysCache({onUpdate, renew}) : super(RequestCache.cacheDay, onUpdate, renew: renew) {
 | 
			
		||||
    start("MarianumMobile", "state-holidays");
 | 
			
		||||
    start('MarianumMobile', 'state-holidays');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -15,6 +15,7 @@ class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
 | 
			
		||||
    return GetHolidaysResponse(
 | 
			
		||||
        List<GetHolidaysResponseObject>.from(
 | 
			
		||||
            parsedListJson.map<GetHolidaysResponseObject>(
 | 
			
		||||
                    // ignore: unnecessary_lambdas
 | 
			
		||||
                    (dynamic i) => GetHolidaysResponseObject.fromJson(i)
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -11,21 +11,21 @@ import 'autocompleteResponse.dart';
 | 
			
		||||
class AutocompleteApi {
 | 
			
		||||
  Future<AutocompleteResponse> find(String query) async {
 | 
			
		||||
    Map<String, dynamic> getParameters = {
 | 
			
		||||
      "search": query,
 | 
			
		||||
      "itemType": " ",
 | 
			
		||||
      "itemId": " ",
 | 
			
		||||
      "shareTypes[]": ["0"],
 | 
			
		||||
      "limit": "10",
 | 
			
		||||
      'search': query,
 | 
			
		||||
      'itemType': ' ',
 | 
			
		||||
      'itemId': ' ',
 | 
			
		||||
      'shareTypes[]': ['0'],
 | 
			
		||||
      'limit': '10',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Map<String, String> headers = {};
 | 
			
		||||
    headers.putIfAbsent("Accept", () => "application/json");
 | 
			
		||||
    headers.putIfAbsent("OCS-APIRequest", () => "true");
 | 
			
		||||
    headers.putIfAbsent('Accept', () => 'application/json');
 | 
			
		||||
    headers.putIfAbsent('OCS-APIRequest', () => 'true');
 | 
			
		||||
 | 
			
		||||
    Uri endpoint = Uri.https("${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}", "${EndpointData().nextcloud().path}/ocs/v2.php/core/autocomplete/get", getParameters);
 | 
			
		||||
    Uri endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/core/autocomplete/get', getParameters);
 | 
			
		||||
 | 
			
		||||
    Response response = await http.get(endpoint, headers: headers);
 | 
			
		||||
    if(response.statusCode != HttpStatus.ok) throw Exception("Api call failed with ${response.statusCode}: ${response.body}");
 | 
			
		||||
    if(response.statusCode != HttpStatus.ok) throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
 | 
			
		||||
    String result = response.body;
 | 
			
		||||
    return AutocompleteResponse.fromJson(jsonDecode(result)['ocs']);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,14 +10,14 @@ import 'fileSharingApiParams.dart';
 | 
			
		||||
class FileSharingApi {
 | 
			
		||||
  Future<void> share(FileSharingApiParams query) async {
 | 
			
		||||
    Map<String, String> headers = {};
 | 
			
		||||
    headers.putIfAbsent("Accept", () => "application/json");
 | 
			
		||||
    headers.putIfAbsent("OCS-APIRequest", () => "true");
 | 
			
		||||
    headers.putIfAbsent('Accept', () => 'application/json');
 | 
			
		||||
    headers.putIfAbsent('OCS-APIRequest', () => 'true');
 | 
			
		||||
 | 
			
		||||
    Uri endpoint = Uri.https("${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}", "${EndpointData().nextcloud().path}/ocs/v2.php/apps/files_sharing/api/v1/shares", query.toJson().map((key, value) => MapEntry(key, value.toString())));
 | 
			
		||||
    Uri endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/apps/files_sharing/api/v1/shares', query.toJson().map((key, value) => MapEntry(key, value.toString())));
 | 
			
		||||
    Response response = await http.post(endpoint, headers: headers);
 | 
			
		||||
 | 
			
		||||
    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}');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,7 +11,7 @@ class GetChat extends TalkApi<GetChatResponse> {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ class GetChatCache extends RequestCache<GetChatResponse> {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
 | 
			
		||||
  GetChatCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
 | 
			
		||||
    start("MarianumMobile", "nc-chat-$chatToken");
 | 
			
		||||
    start('MarianumMobile', 'nc-chat-$chatToken');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -61,21 +61,21 @@ class GetChatResponseObject {
 | 
			
		||||
 | 
			
		||||
  static GetChatResponseObject getDateDummy(int timestamp) {
 | 
			
		||||
    DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
 | 
			
		||||
    return getTextDummy(Jiffy.parseFromDateTime(elementDate).format(pattern: "dd.MM.yyyy"));
 | 
			
		||||
    return getTextDummy(Jiffy.parseFromDateTime(elementDate).format(pattern: 'dd.MM.yyyy'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static GetChatResponseObject getTextDummy(String text) {
 | 
			
		||||
    return GetChatResponseObject(
 | 
			
		||||
        0,
 | 
			
		||||
        "",
 | 
			
		||||
        '',
 | 
			
		||||
        GetRoomResponseObjectMessageActorType.user,
 | 
			
		||||
        "",
 | 
			
		||||
        "",
 | 
			
		||||
        '',
 | 
			
		||||
        '',
 | 
			
		||||
        0,
 | 
			
		||||
        "",
 | 
			
		||||
        '',
 | 
			
		||||
        GetRoomResponseObjectMessageType.system,
 | 
			
		||||
        false,
 | 
			
		||||
        "",
 | 
			
		||||
        '',
 | 
			
		||||
        text,
 | 
			
		||||
        null,
 | 
			
		||||
        null,
 | 
			
		||||
@@ -113,10 +113,10 @@ class RichObjectString {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum RichObjectStringObjectType {
 | 
			
		||||
  @JsonValue("user") user,
 | 
			
		||||
  @JsonValue("group") group,
 | 
			
		||||
  @JsonValue("file") file,
 | 
			
		||||
  @JsonValue("guest") guest,
 | 
			
		||||
  @JsonValue("highlight") highlight,
 | 
			
		||||
  @JsonValue("talk-poll") talkPoll,
 | 
			
		||||
  @JsonValue('user') user,
 | 
			
		||||
  @JsonValue('group') group,
 | 
			
		||||
  @JsonValue('file') file,
 | 
			
		||||
  @JsonValue('guest') guest,
 | 
			
		||||
  @JsonValue('highlight') highlight,
 | 
			
		||||
  @JsonValue('talk-poll') talkPoll,
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ class RichObjectStringProcessor {
 | 
			
		||||
    if(data == null) return message;
 | 
			
		||||
 | 
			
		||||
    data.forEach((key, value) {
 | 
			
		||||
      message = message.replaceAll(RegExp("{$key}"), value.name);
 | 
			
		||||
      message = message.replaceAll(RegExp('{$key}'), value.name);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return message;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import 'createRoomParams.dart';
 | 
			
		||||
class CreateRoom extends TalkApi {
 | 
			
		||||
  CreateRoomParams params;
 | 
			
		||||
 | 
			
		||||
  CreateRoom(this.params) : super("v4/room", params);
 | 
			
		||||
  CreateRoom(this.params) : super('v4/room', params);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import '../talkApi.dart';
 | 
			
		||||
class DeleteMessage extends TalkApi {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
  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
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import 'deleteReactMessageParams.dart';
 | 
			
		||||
class DeleteReactMessage extends TalkApi {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
  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
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import 'getParticipantsResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetParticipants extends TalkApi<GetParticipantsResponse> {
 | 
			
		||||
  String token;
 | 
			
		||||
  GetParticipants(this.token) : super("v4/room/$token/participants", null);
 | 
			
		||||
  GetParticipants(this.token) : super('v4/room/$token/participants', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetParticipantsResponse assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ class GetParticipantsCache extends RequestCache<GetParticipantsResponse> {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
 | 
			
		||||
  GetParticipantsCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
 | 
			
		||||
    start("MarianumMobile", "nc-chat-participants-$chatToken");
 | 
			
		||||
    start('MarianumMobile', 'nc-chat-participants-$chatToken');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import 'getReactionsResponse.dart';
 | 
			
		||||
class GetReactions extends TalkApi<GetReactionsResponse> {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
  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
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,6 @@ class GetReactionsResponseObject {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum GetReactionsResponseObjectActorType {
 | 
			
		||||
  @JsonValue("guests") guests,
 | 
			
		||||
  @JsonValue("users") users,
 | 
			
		||||
  @JsonValue('guests') guests,
 | 
			
		||||
  @JsonValue('users') users,
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ import '../talkApi.dart';
 | 
			
		||||
class LeaveRoom extends TalkApi {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
 | 
			
		||||
  LeaveRoom(this.chatToken) : super("v4/room/$chatToken/participants/self", null);
 | 
			
		||||
  LeaveRoom(this.chatToken) : super('v4/room/$chatToken/participants/self', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import 'reactMessageParams.dart';
 | 
			
		||||
class ReactMessage extends TalkApi {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
  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
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import 'getRoomResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetRoom extends TalkApi<GetRoomResponse> {
 | 
			
		||||
  GetRoomParams params;
 | 
			
		||||
  GetRoom(this.params) : super("v4/room", null, getParameters: params.toJson());
 | 
			
		||||
  GetRoom(this.params) : super('v4/room', null, getParameters: params.toJson());
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import 'getRoomResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetRoomCache extends RequestCache<GetRoomResponse> {
 | 
			
		||||
  GetRoomCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
 | 
			
		||||
    start("MarianumMobile", "nc-rooms");
 | 
			
		||||
    start('MarianumMobile', 'nc-rooms');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,10 @@ class GetRoomResponse extends ApiResponse {
 | 
			
		||||
      final buffer = StringBuffer();
 | 
			
		||||
 | 
			
		||||
      if(favoritesToTop) {
 | 
			
		||||
        buffer.write(chat.isFavorite ? "b" : "a");
 | 
			
		||||
        buffer.write(chat.isFavorite ? 'b' : 'a');
 | 
			
		||||
      }
 | 
			
		||||
      if(unreadToTop) {
 | 
			
		||||
        buffer.write(chat.unreadMessages > 0 ? "b" : "a");
 | 
			
		||||
        buffer.write(chat.unreadMessages > 0 ? 'b' : 'a');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      buffer.write(chat.lastActivity);
 | 
			
		||||
@@ -152,16 +152,16 @@ enum GetRoomResponseObjectParticipantNotificationLevel {
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
enum GetRoomResponseObjectMessageActorType {
 | 
			
		||||
  @JsonValue("deleted_users") deletedUsers,
 | 
			
		||||
  @JsonValue("users") user,
 | 
			
		||||
  @JsonValue("guests") guest,
 | 
			
		||||
  @JsonValue("bots") bot,
 | 
			
		||||
  @JsonValue("bridged") bridge,
 | 
			
		||||
  @JsonValue('deleted_users') deletedUsers,
 | 
			
		||||
  @JsonValue('users') user,
 | 
			
		||||
  @JsonValue('guests') guest,
 | 
			
		||||
  @JsonValue('bots') bot,
 | 
			
		||||
  @JsonValue('bridged') bridge,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum GetRoomResponseObjectMessageType {
 | 
			
		||||
  @JsonValue("comment") comment,
 | 
			
		||||
  @JsonValue("comment_deleted") deletedComment,
 | 
			
		||||
  @JsonValue("system") system,
 | 
			
		||||
  @JsonValue("command") command,
 | 
			
		||||
  @JsonValue('comment') comment,
 | 
			
		||||
  @JsonValue('comment_deleted') deletedComment,
 | 
			
		||||
  @JsonValue('system') system,
 | 
			
		||||
  @JsonValue('command') command,
 | 
			
		||||
}
 | 
			
		||||
@@ -7,7 +7,7 @@ import 'sendMessageParams.dart';
 | 
			
		||||
 | 
			
		||||
class SendMessage extends TalkApi {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
  SendMessage(this.chatToken, SendMessageParams params) : super("v1/chat/$chatToken", params);
 | 
			
		||||
  SendMessage(this.chatToken, SendMessageParams params) : super('v1/chat/$chatToken', params);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ class SetFavorite extends TalkApi {
 | 
			
		||||
  String chatToken;
 | 
			
		||||
  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
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ class SetReadMarker extends TalkApi {
 | 
			
		||||
  bool readState;
 | 
			
		||||
  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}) : super('v1/chat/$chatToken/read', null, getParameters: setReadMarkerParams?.toJson()) {
 | 
			
		||||
    if(readState) assert(setReadMarkerParams?.lastReadMessage != null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -34,21 +34,21 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
 | 
			
		||||
      getParameters?.update(key, (value) => value.toString());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    Uri endpoint = Uri.https("${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}", "${EndpointData().nextcloud().path}/ocs/v2.php/apps/spreed/api/$path", getParameters);
 | 
			
		||||
    Uri endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/apps/spreed/api/$path', getParameters);
 | 
			
		||||
 | 
			
		||||
    headers ??= {};
 | 
			
		||||
    headers?.putIfAbsent("Accept", () => "application/json");
 | 
			
		||||
    headers?.putIfAbsent("OCS-APIRequest", () => "true");
 | 
			
		||||
    headers?.putIfAbsent('Accept', () => 'application/json');
 | 
			
		||||
    headers?.putIfAbsent('OCS-APIRequest', () => 'true');
 | 
			
		||||
 | 
			
		||||
    http.Response? data;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      data = await request(endpoint, body, headers);
 | 
			
		||||
      if(data == null) throw Exception("No response Data");
 | 
			
		||||
      if(data == null) throw Exception('No response Data');
 | 
			
		||||
      if(data.statusCode >= 400 || data.statusCode < 200) throw Exception("Response status code '${data.statusCode}' might indicate an error");
 | 
			
		||||
    } catch(e) {
 | 
			
		||||
      log(e.toString());
 | 
			
		||||
      throw ApiError("Request $endpoint could not be dispatched: ${e.toString()}");
 | 
			
		||||
      throw ApiError('Request $endpoint could not be dispatched: ${e.toString()}');
 | 
			
		||||
    }
 | 
			
		||||
    //dynamic jsonData = jsonDecode(data.body);
 | 
			
		||||
 | 
			
		||||
@@ -59,10 +59,10 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
 | 
			
		||||
      return assembled;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // TODO report error
 | 
			
		||||
      log("Error assembling Talk API ${T.toString()} message: ${e.toString()} response on ${endpoint.path} with request body: $body and request headers: ${headers.toString()}");
 | 
			
		||||
      log('Error assembling Talk API ${T.toString()} message: ${e.toString()} response on ${endpoint.path} with request body: $body and request headers: ${headers.toString()}');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw Exception("Error assembling Talk API response");
 | 
			
		||||
    throw Exception('Error assembling Talk API response');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -7,6 +7,6 @@ class TalkError {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return "Talk - $status - ($code): $message";
 | 
			
		||||
    return 'Talk - $status - ($code): $message';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -24,7 +24,7 @@ class CacheableFile {
 | 
			
		||||
  CacheableFile.fromDavFile(WebDavFile file) {
 | 
			
		||||
    path = file.path.path;
 | 
			
		||||
    isDirectory = file.isDirectory;
 | 
			
		||||
    name = file.isDirectory ? file.name : file.path.path.split("/").last;
 | 
			
		||||
    name = file.isDirectory ? file.name : file.path.path.split('/').last;
 | 
			
		||||
    mimeType = file.mimeType;
 | 
			
		||||
    size = file.size;
 | 
			
		||||
    eTag = file.etag;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
 | 
			
		||||
import 'package:nextcloud/nextcloud.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../../model/endpointData.dart';
 | 
			
		||||
import '../../webdavApi.dart';
 | 
			
		||||
import 'cacheableFile.dart';
 | 
			
		||||
import 'listFilesParams.dart';
 | 
			
		||||
@@ -15,18 +14,19 @@ class ListFiles extends WebdavApi<ListFilesParams> {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<ListFilesResponse> run() async {
 | 
			
		||||
    List<WebDavFile> davFiles = (await (await WebdavApi.webdav).propfind(PathUri.parse(params.path))).toWebDavFiles();
 | 
			
		||||
    Set<CacheableFile> files = davFiles.map((e) => CacheableFile.fromDavFile(e)).toSet();
 | 
			
		||||
    Set<CacheableFile> files = davFiles.map(CacheableFile.fromDavFile).toSet();
 | 
			
		||||
 | 
			
		||||
    // webdav handles subdirectories wrong, this is a fix
 | 
			
		||||
    if(EndpointData().getEndpointMode() == EndpointMode.stage) {
 | 
			
		||||
      files = files.map((e) { // somehow
 | 
			
		||||
        e.path = e.path.split("mobile/cloud/remote.php/webdav")[1];
 | 
			
		||||
        return e;
 | 
			
		||||
      }).toSet();
 | 
			
		||||
    }
 | 
			
		||||
    // currently this fix is not needed anymore
 | 
			
		||||
    // if(EndpointData().getEndpointMode() == EndpointMode.stage) {
 | 
			
		||||
    //   files = files.map((e) { // somehow
 | 
			
		||||
    //     e.path = e.path.split("mobile/cloud/remote.php/webdav")[1];
 | 
			
		||||
    //     return e;
 | 
			
		||||
    //   }).toSet();
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,9 @@ class ListFilesCache extends RequestCache<ListFilesResponse> {
 | 
			
		||||
  String path;
 | 
			
		||||
 | 
			
		||||
  ListFilesCache({required onUpdate, required this.path}) : super(RequestCache.cacheNothing, onUpdate) {
 | 
			
		||||
    var bytes = utf8.encode("MarianumMobile-$path");
 | 
			
		||||
    var bytes = utf8.encode('MarianumMobile-$path');
 | 
			
		||||
    String cacheName = md5.convert(bytes).toString();
 | 
			
		||||
    start("MarianumMobile", "wd-folder-$cacheName");
 | 
			
		||||
    start('MarianumMobile', 'wd-folder-$cacheName');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ class ListFilesResponse extends ApiResponse {
 | 
			
		||||
 | 
			
		||||
            switch(sortOption) {
 | 
			
		||||
                case SortOption.date:
 | 
			
		||||
                    buffer.write(Jiffy.parseFromMillisecondsSinceEpoch(file.modifiedAt?.millisecondsSinceEpoch ?? 0).format(pattern: "yyyyMMddhhmmss"));
 | 
			
		||||
                    buffer.write(Jiffy.parseFromMillisecondsSinceEpoch(file.modifiedAt?.millisecondsSinceEpoch ?? 0).format(pattern: 'yyyyMMddhhmmss'));
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case SortOption.name:
 | 
			
		||||
 
 | 
			
		||||
@@ -18,10 +18,10 @@ abstract class WebdavApi<T> extends ApiRequest {
 | 
			
		||||
  static Future<String> webdavConnectString = buildWebdavConnectString();
 | 
			
		||||
 | 
			
		||||
  static Future<WebDavClient> establishWebdavConnection() async {
 | 
			
		||||
    return NextcloudClient(Uri.parse("https://${EndpointData().nextcloud().full()}"), password: AccountData().getPassword(), loginName: AccountData().getUsername()).webdav;
 | 
			
		||||
    return NextcloudClient(Uri.parse('https://${EndpointData().nextcloud().full()}'), password: AccountData().getPassword(), loginName: AccountData().getUsername()).webdav;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Future<String> buildWebdavConnectString() async {
 | 
			
		||||
    return "https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/remote.php/dav/files/${AccountData().getUsername()}/";
 | 
			
		||||
    return 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/remote.php/dav/files/${AccountData().getUsername()}/';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ import '../../mhslApi.dart';
 | 
			
		||||
import 'getBreakersResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetBreakers extends MhslApi<GetBreakersResponse> {
 | 
			
		||||
  GetBreakers() : super("breaker/");
 | 
			
		||||
  GetBreakers() : super('breaker/');
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetBreakersResponse assemble(String raw) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import 'getBreakersResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetBreakersCache extends RequestCache<GetBreakersResponse> {
 | 
			
		||||
  GetBreakersCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
 | 
			
		||||
    start("MarianumMobile", "breakers");
 | 
			
		||||
    start('MarianumMobile', 'breakers');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -28,9 +28,9 @@ class GetBreakersReponseObject {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum BreakerArea {
 | 
			
		||||
  @JsonValue("GLOBAL") global,
 | 
			
		||||
  @JsonValue("TIMETABLE") timetable,
 | 
			
		||||
  @JsonValue("TALK") talk,
 | 
			
		||||
  @JsonValue("FILES") files,
 | 
			
		||||
  @JsonValue("MORE") more,
 | 
			
		||||
  @JsonValue('GLOBAL') global,
 | 
			
		||||
  @JsonValue('TIMETABLE') timetable,
 | 
			
		||||
  @JsonValue('TALK') talk,
 | 
			
		||||
  @JsonValue('FILES') files,
 | 
			
		||||
  @JsonValue('MORE') more,
 | 
			
		||||
}
 | 
			
		||||
@@ -13,6 +13,7 @@ class CustomTimetableEvent {
 | 
			
		||||
  DateTime startDate;
 | 
			
		||||
  @JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
 | 
			
		||||
  DateTime endDate;
 | 
			
		||||
  String? color;
 | 
			
		||||
  String rrule;
 | 
			
		||||
  @JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson)
 | 
			
		||||
  DateTime createdAt;
 | 
			
		||||
@@ -20,7 +21,7 @@ class CustomTimetableEvent {
 | 
			
		||||
  DateTime updatedAt;
 | 
			
		||||
 | 
			
		||||
  CustomTimetableEvent({required this.id, required this.title, required this.description, required this.startDate,
 | 
			
		||||
    required this.endDate, required this.rrule, required this.createdAt, required this.updatedAt});
 | 
			
		||||
    required this.endDate, required this.color, required this.rrule, required this.createdAt, required this.updatedAt});
 | 
			
		||||
 | 
			
		||||
  factory CustomTimetableEvent.fromJson(Map<String, dynamic> json) => _$CustomTimetableEventFromJson(json);
 | 
			
		||||
  Map<String, dynamic> toJson() => _$CustomTimetableEventToJson(this);
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ CustomTimetableEvent _$CustomTimetableEventFromJson(
 | 
			
		||||
      description: json['description'] as String,
 | 
			
		||||
      startDate: MhslApi.dateTimeFromJson(json['startDate'] as String),
 | 
			
		||||
      endDate: MhslApi.dateTimeFromJson(json['endDate'] as String),
 | 
			
		||||
      color: json['color'] as String?,
 | 
			
		||||
      rrule: json['rrule'] as String,
 | 
			
		||||
      createdAt: MhslApi.dateTimeFromJson(json['createdAt'] as String),
 | 
			
		||||
      updatedAt: MhslApi.dateTimeFromJson(json['updatedAt'] as String),
 | 
			
		||||
@@ -27,6 +28,7 @@ Map<String, dynamic> _$CustomTimetableEventToJson(
 | 
			
		||||
      'description': instance.description,
 | 
			
		||||
      'startDate': MhslApi.dateTimeToJson(instance.startDate),
 | 
			
		||||
      'endDate': MhslApi.dateTimeToJson(instance.endDate),
 | 
			
		||||
      'color': instance.color,
 | 
			
		||||
      'rrule': instance.rrule,
 | 
			
		||||
      'createdAt': MhslApi.dateTimeToJson(instance.createdAt),
 | 
			
		||||
      'updatedAt': MhslApi.dateTimeToJson(instance.updatedAt),
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,11 @@ import 'getCustomTimetableEventResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetCustomTimetableEvent extends MhslApi<GetCustomTimetableEventResponse> {
 | 
			
		||||
  GetCustomTimetableEventParams params;
 | 
			
		||||
  GetCustomTimetableEvent(this.params) : super("server/timetable/customEvents?user=${params.user}");
 | 
			
		||||
  GetCustomTimetableEvent(this.params) : super('server/timetable/customEvents?user=${params.user}');
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetCustomTimetableEventResponse assemble(String raw) {
 | 
			
		||||
    return GetCustomTimetableEventResponse.fromJson({"events": jsonDecode(raw)});
 | 
			
		||||
    return GetCustomTimetableEventResponse.fromJson({'events': jsonDecode(raw)});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ class GetCustomTimetableEventCache extends RequestCache<GetCustomTimetableEventR
 | 
			
		||||
  GetCustomTimetableEventParams params;
 | 
			
		||||
 | 
			
		||||
  GetCustomTimetableEventCache(this.params, {onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
 | 
			
		||||
    start("MarianumMobile", "customTimetableEvents");
 | 
			
		||||
    start('MarianumMobile', 'customTimetableEvents');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import '../../mhslApi.dart';
 | 
			
		||||
import 'getMessagesResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetMessages extends MhslApi<GetMessagesResponse> {
 | 
			
		||||
  GetMessages() : super("message/messages.json");
 | 
			
		||||
  GetMessages() : super('message/messages.json');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import 'getMessagesResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetMessagesCache extends RequestCache<GetMessagesResponse> {
 | 
			
		||||
  GetMessagesCache({onUpdate, renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
 | 
			
		||||
    start("MarianumMobile", "message");
 | 
			
		||||
    start('MarianumMobile', 'message');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -15,21 +15,21 @@ abstract class MhslApi<T> extends ApiRequest {
 | 
			
		||||
  T assemble(String raw);
 | 
			
		||||
 | 
			
		||||
  Future<T> run() async {
 | 
			
		||||
    Uri endpoint = Uri.parse("https://mhsl.eu/marianum/marianummobile/$subpath");
 | 
			
		||||
    Uri endpoint = Uri.parse('https://mhsl.eu/marianum/marianummobile/$subpath');
 | 
			
		||||
 | 
			
		||||
    http.Response? data = await request(endpoint);
 | 
			
		||||
    if(data == null) {
 | 
			
		||||
      throw ApiError("Request could not be dispatched!");
 | 
			
		||||
      throw ApiError('Request could not be dispatched!');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(data.statusCode > 299) {
 | 
			
		||||
      throw ApiError("Non 200 Status code from mhsl services: $subpath: ${data.statusCode}");
 | 
			
		||||
      throw ApiError('Non 200 Status code from mhsl services: $subpath: ${data.statusCode}');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return assemble(utf8.decode(data.bodyBytes));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -9,7 +9,7 @@ import 'notifyRegisterParams.dart';
 | 
			
		||||
 | 
			
		||||
class NotifyRegister extends MhslApi<void> {
 | 
			
		||||
  NotifyRegisterParams params;
 | 
			
		||||
  NotifyRegister(this.params) : super("notify/register/");
 | 
			
		||||
  NotifyRegister(this.params) : super('notify/register/');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,12 @@ part 'addFeedbackParams.g.dart';
 | 
			
		||||
class AddFeedbackParams {
 | 
			
		||||
  String user;
 | 
			
		||||
  String feedback;
 | 
			
		||||
  String? screenshot;
 | 
			
		||||
  int appVersion;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  AddFeedbackParams({
 | 
			
		||||
    required this.user,
 | 
			
		||||
    required this.feedback,
 | 
			
		||||
    this.screenshot,
 | 
			
		||||
    required this.appVersion,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ AddFeedbackParams _$AddFeedbackParamsFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    AddFeedbackParams(
 | 
			
		||||
      user: json['user'] as String,
 | 
			
		||||
      feedback: json['feedback'] as String,
 | 
			
		||||
      screenshot: json['screenshot'] as String?,
 | 
			
		||||
      appVersion: json['appVersion'] as int,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@@ -18,6 +17,5 @@ Map<String, dynamic> _$AddFeedbackParamsToJson(AddFeedbackParams instance) =>
 | 
			
		||||
    <String, dynamic>{
 | 
			
		||||
      'user': instance.user,
 | 
			
		||||
      'feedback': instance.feedback,
 | 
			
		||||
      'screenshot': instance.screenshot,
 | 
			
		||||
      'appVersion': instance.appVersion,
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import 'updateUserIndexParams.dart';
 | 
			
		||||
 | 
			
		||||
class UpdateUserIndex extends MhslApi<void> {
 | 
			
		||||
  UpdateUserIndexParams params;
 | 
			
		||||
  UpdateUserIndex(this.params) : super("server/userIndex/update");
 | 
			
		||||
  UpdateUserIndex(this.params) : super('server/userIndex/update');
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void assemble(String raw) {}
 | 
			
		||||
@@ -20,7 +20,7 @@ class UpdateUserIndex extends MhslApi<void> {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<http.Response> request(Uri uri) {
 | 
			
		||||
    String data = jsonEncode(params.toJson());
 | 
			
		||||
    log("Updating userindex:\n $data");
 | 
			
		||||
    log('Updating userindex:\n $data');
 | 
			
		||||
    return http.post(uri, body: data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,8 +35,8 @@ abstract class RequestCache<T extends ApiResponse?> {
 | 
			
		||||
      onUpdate(newValue);
 | 
			
		||||
 | 
			
		||||
      Localstore.instance.collection(file).doc(document).set({
 | 
			
		||||
        "json": jsonEncode(newValue),
 | 
			
		||||
        "lastupdate": DateTime.now().millisecondsSinceEpoch
 | 
			
		||||
        'json': jsonEncode(newValue),
 | 
			
		||||
        'lastupdate': DateTime.now().millisecondsSinceEpoch
 | 
			
		||||
      });
 | 
			
		||||
    } on WebuntisError catch(e) {
 | 
			
		||||
      onError(e);
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import 'authenticateResponse.dart';
 | 
			
		||||
class Authenticate extends WebuntisApi {
 | 
			
		||||
  AuthenticateParams param;
 | 
			
		||||
 | 
			
		||||
  Authenticate(this.param) : super("authenticate", param, authenticatedResponse: false);
 | 
			
		||||
  Authenticate(this.param) : super('authenticate', param, authenticatedResponse: false);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<AuthenticateResponse> run() async {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import '../../webuntisApi.dart';
 | 
			
		||||
import 'getHolidaysResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetHolidays extends WebuntisApi {
 | 
			
		||||
  GetHolidays() : super("getHolidays", null);
 | 
			
		||||
  GetHolidays() : super('getHolidays', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetHolidaysResponse> run() async {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import 'getHolidaysResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
 | 
			
		||||
  GetHolidaysCache({onUpdate}) : super(RequestCache.cacheDay, onUpdate) {
 | 
			
		||||
    start("MarianumMobile", "wu-holidays");
 | 
			
		||||
    start('MarianumMobile', 'wu-holidays');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import '../../webuntisApi.dart';
 | 
			
		||||
import 'getRoomsResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetRooms extends WebuntisApi {
 | 
			
		||||
  GetRooms() : super("getRooms", null);
 | 
			
		||||
  GetRooms() : super('getRooms', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetRoomsResponse> run() async {
 | 
			
		||||
@@ -14,10 +14,10 @@ class GetRooms extends WebuntisApi {
 | 
			
		||||
      return finalize(GetRoomsResponse.fromJson(jsonDecode(rawAnswer)));
 | 
			
		||||
    } catch(e, trace) {
 | 
			
		||||
      log(trace.toString());
 | 
			
		||||
      log("Failed to parse getRoom data with server response: $rawAnswer");
 | 
			
		||||
      log('Failed to parse getRoom data with server response: $rawAnswer');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw Exception("Failed to parse getRoom server response: $rawAnswer");
 | 
			
		||||
    throw Exception('Failed to parse getRoom server response: $rawAnswer');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ import 'getRoomsResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetRoomsCache extends RequestCache<GetRoomsResponse> {
 | 
			
		||||
  GetRoomsCache({onUpdate}) : super(RequestCache.cacheHour, onUpdate) {
 | 
			
		||||
    start("MarianumMobile", "wu-rooms");
 | 
			
		||||
    start('MarianumMobile', 'wu-rooms');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import '../../webuntisApi.dart';
 | 
			
		||||
import 'getSubjectsResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetSubjects extends WebuntisApi {
 | 
			
		||||
  GetSubjects() : super("getSubjects", null);
 | 
			
		||||
  GetSubjects() : super('getSubjects', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetSubjectsResponse> run() async {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import 'getSubjectsResponse.dart';
 | 
			
		||||
 | 
			
		||||
class GetSubjectsCache extends RequestCache<GetSubjectsResponse> {
 | 
			
		||||
  GetSubjectsCache({onUpdate}) : super(RequestCache.cacheHour, onUpdate) {
 | 
			
		||||
    start("MarianumMobile", "wu-subjects");
 | 
			
		||||
    start('MarianumMobile', 'wu-subjects');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import 'getTimetableResponse.dart';
 | 
			
		||||
class GetTimetable extends WebuntisApi {
 | 
			
		||||
  GetTimetableParams params;
 | 
			
		||||
 | 
			
		||||
  GetTimetable(this.params) : super("getTimetable", params);
 | 
			
		||||
  GetTimetable(this.params) : super('getTimetable', params);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetTimetableResponse> run() async {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class GetTimetableCache extends RequestCache<GetTimetableResponse> {
 | 
			
		||||
  int enddate;
 | 
			
		||||
 | 
			
		||||
  GetTimetableCache({required onUpdate, onError, required this.startdate, required this.enddate}) : super(RequestCache.cacheMinute, onUpdate, onError: onError) {
 | 
			
		||||
    start("MarianumMobile", "wu-timetable-$startdate-$enddate");
 | 
			
		||||
    start('MarianumMobile', 'wu-timetable-$startdate-$enddate');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -67,10 +67,10 @@ class GetTimetableParamsOptions {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum GetTimetableParamsOptionsFields {
 | 
			
		||||
  @JsonValue("id") id,
 | 
			
		||||
  @JsonValue("name") name,
 | 
			
		||||
  @JsonValue("longname") longname,
 | 
			
		||||
  @JsonValue("externalkey") externalkey;
 | 
			
		||||
  @JsonValue('id') id,
 | 
			
		||||
  @JsonValue('name') name,
 | 
			
		||||
  @JsonValue('longname') longname,
 | 
			
		||||
  @JsonValue('externalkey') externalkey;
 | 
			
		||||
 | 
			
		||||
  static List<GetTimetableParamsOptionsFields> all = [id, name, longname, externalkey];
 | 
			
		||||
}
 | 
			
		||||
@@ -88,7 +88,7 @@ class GetTimetableParamsOptionsElement {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum GetTimetableParamsOptionsElementKeyType {
 | 
			
		||||
  @JsonValue("id") id,
 | 
			
		||||
  @JsonValue("name") name,
 | 
			
		||||
  @JsonValue("externalkey") externalkey
 | 
			
		||||
  @JsonValue('id') id,
 | 
			
		||||
  @JsonValue('name') name,
 | 
			
		||||
  @JsonValue('externalkey') externalkey
 | 
			
		||||
}
 | 
			
		||||
@@ -9,7 +9,7 @@ import 'queries/authenticate/authenticate.dart';
 | 
			
		||||
import 'webuntisError.dart';
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
  ApiParams? genericParam;
 | 
			
		||||
  http.Response? response;
 | 
			
		||||
@@ -22,11 +22,11 @@ abstract class WebuntisApi extends ApiRequest {
 | 
			
		||||
  Future<String> query(WebuntisApi untis) async {
 | 
			
		||||
    String query = '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}';
 | 
			
		||||
 | 
			
		||||
    String sessionId = "0";
 | 
			
		||||
    String sessionId = '0';
 | 
			
		||||
    if(authenticatedResponse) {
 | 
			
		||||
      sessionId = (await Authenticate.getSession()).sessionId;
 | 
			
		||||
    }
 | 
			
		||||
    http.Response data = await post(query, {"Cookie": "JSESSIONID=$sessionId"});
 | 
			
		||||
    http.Response data = await post(query, {'Cookie': 'JSESSIONID=$sessionId'});
 | 
			
		||||
    response = data;
 | 
			
		||||
 | 
			
		||||
    dynamic jsonData = jsonDecode(data.body);
 | 
			
		||||
@@ -49,7 +49,7 @@ abstract class WebuntisApi extends ApiRequest {
 | 
			
		||||
  Future<ApiResponse> run();
 | 
			
		||||
 | 
			
		||||
  String _body() {
 | 
			
		||||
    return genericParam == null ? "{}" : jsonEncode(genericParam);
 | 
			
		||||
    return genericParam == null ? '{}' : jsonEncode(genericParam);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<http.Response> post(String data, Map<String, String>? headers) async {
 | 
			
		||||
@@ -57,7 +57,7 @@ abstract class WebuntisApi extends ApiRequest {
 | 
			
		||||
        .post(endpoint, body: data, headers: headers)
 | 
			
		||||
        .timeout(
 | 
			
		||||
        const Duration(seconds: 10),
 | 
			
		||||
        onTimeout: () => throw WebuntisError("Timeout", 1)
 | 
			
		||||
        onTimeout: () => throw WebuntisError('Timeout', 1)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,6 +6,6 @@ class WebuntisError implements Exception {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return "WebUntis ($code): $message";
 | 
			
		||||
    return 'WebUntis ($code): $message';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										134
									
								
								lib/app.dart
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								lib/app.dart
									
									
									
									
									
								
							@@ -5,7 +5,7 @@ import 'dart:developer';
 | 
			
		||||
import 'package:easy_debounce/easy_throttle.dart';
 | 
			
		||||
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:persistent_bottom_nav_bar_v2/persistent-tab-view.dart';
 | 
			
		||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:badges/badges.dart' as badges;
 | 
			
		||||
 | 
			
		||||
@@ -39,14 +39,14 @@ class _AppState extends State<App> with WidgetsBindingObserver {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void didChangeAppLifecycleState(AppLifecycleState state) {
 | 
			
		||||
    log("AppLifecycle: ${state.toString()}");
 | 
			
		||||
    log('AppLifecycle: ${state.toString()}');
 | 
			
		||||
 | 
			
		||||
    if(state == AppLifecycleState.resumed) {
 | 
			
		||||
      EasyThrottle.throttle(
 | 
			
		||||
        "appLifecycleState",
 | 
			
		||||
        'appLifecycleState',
 | 
			
		||||
        const Duration(seconds: 10),
 | 
			
		||||
        () {
 | 
			
		||||
          log("Refreshing due to LifecycleChange");
 | 
			
		||||
          log('Refreshing due to LifecycleChange');
 | 
			
		||||
          NotificationTasks.updateProviders(context);
 | 
			
		||||
          Provider.of<TimetableProps>(context, listen: false).run();
 | 
			
		||||
        }
 | 
			
		||||
@@ -93,67 +93,75 @@ class _AppState extends State<App> with WidgetsBindingObserver {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return PersistentTabView(
 | 
			
		||||
      context,
 | 
			
		||||
      controller: App.bottomNavigator,
 | 
			
		||||
      navBarStyle: NavBarStyle.style6,
 | 
			
		||||
      hideNavigationBarWhenKeyboardShows: true,
 | 
			
		||||
      navBarHeight: MediaQuery.of(context).viewInsets.bottom > 0 ? 0.0 : kBottomNavigationBarHeight,
 | 
			
		||||
      backgroundColor: Theme.of(context).colorScheme.surface,
 | 
			
		||||
      decoration: const NavBarDecoration(
 | 
			
		||||
        border: Border(top: BorderSide(width: 1, color: Colors.grey)),
 | 
			
		||||
      ),
 | 
			
		||||
      screenTransitionAnimation: const ScreenTransitionAnimation(animateTabTransition: true, curve: Curves.ease, duration: Duration(milliseconds: 200)),
 | 
			
		||||
      screens: const [
 | 
			
		||||
        Breaker(breaker: BreakerArea.timetable, child: Timetable()),
 | 
			
		||||
        Breaker(breaker: BreakerArea.talk, child: ChatList()),
 | 
			
		||||
        Breaker(breaker: BreakerArea.files, child: Files([])),
 | 
			
		||||
        Breaker(breaker: BreakerArea.more, child: Overhang()),
 | 
			
		||||
      ],
 | 
			
		||||
      items: [
 | 
			
		||||
        PersistentBottomNavBarItem(
 | 
			
		||||
          activeColorPrimary: Theme.of(context).primaryColor,
 | 
			
		||||
          inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
          icon: const Icon(Icons.calendar_month),
 | 
			
		||||
          title: "Vertretung"
 | 
			
		||||
        ),
 | 
			
		||||
        PersistentBottomNavBarItem(
 | 
			
		||||
          activeColorPrimary: Theme.of(context).primaryColor,
 | 
			
		||||
          inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
          icon: Consumer<ChatListProps>(
 | 
			
		||||
            builder: (context, value, child) {
 | 
			
		||||
              if(value.primaryLoading()) return const Icon(Icons.chat);
 | 
			
		||||
              int messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b);
 | 
			
		||||
              return badges.Badge(
 | 
			
		||||
                showBadge: messages > 0,
 | 
			
		||||
                position: badges.BadgePosition.topEnd(top: -3, end: -3),
 | 
			
		||||
                stackFit: StackFit.loose,
 | 
			
		||||
                badgeStyle: badges.BadgeStyle(
 | 
			
		||||
                  padding: const EdgeInsets.all(3),
 | 
			
		||||
                  badgeColor: Theme.of(context).primaryColor,
 | 
			
		||||
                  elevation: 1,
 | 
			
		||||
                ),
 | 
			
		||||
                badgeContent: Text("$messages", style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
 | 
			
		||||
                child: const Icon(Icons.chat),
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          title: "Talk",
 | 
			
		||||
        ),
 | 
			
		||||
        PersistentBottomNavBarItem(
 | 
			
		||||
          activeColorPrimary: Theme.of(context).primaryColor,
 | 
			
		||||
          inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
          icon: const Icon(Icons.folder),
 | 
			
		||||
          title: "Dateien"
 | 
			
		||||
        ),
 | 
			
		||||
        PersistentBottomNavBarItem(
 | 
			
		||||
          activeColorPrimary: Theme.of(context).primaryColor,
 | 
			
		||||
          inactiveColorPrimary: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
          icon: const Icon(Icons.apps),
 | 
			
		||||
          title: "Mehr"
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
      gestureNavigationEnabled: true,
 | 
			
		||||
      navBarOverlap: const NavBarOverlap.none(),
 | 
			
		||||
      backgroundColor: Theme.of(context).colorScheme.primary,
 | 
			
		||||
 | 
			
		||||
      screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)),
 | 
			
		||||
      tabs: [
 | 
			
		||||
        PersistentTabConfig(
 | 
			
		||||
          screen: const Breaker(breaker: BreakerArea.timetable, child: Timetable()),
 | 
			
		||||
          item: ItemConfig(
 | 
			
		||||
            activeForegroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
            inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
            icon: const Icon(Icons.calendar_month),
 | 
			
		||||
            title: 'Vertretung'
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        PersistentTabConfig(
 | 
			
		||||
          screen: const Breaker(breaker: BreakerArea.talk, child: ChatList()),
 | 
			
		||||
          item: ItemConfig(
 | 
			
		||||
            activeForegroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
            inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
            icon: Consumer<ChatListProps>(
 | 
			
		||||
              builder: (context, value, child) {
 | 
			
		||||
                if(value.primaryLoading()) return const Icon(Icons.chat);
 | 
			
		||||
                int messages = value.getRoomsResponse.data.map((e) => e.unreadMessages).reduce((a, b) => a+b);
 | 
			
		||||
                return badges.Badge(
 | 
			
		||||
                  showBadge: messages > 0,
 | 
			
		||||
                  position: badges.BadgePosition.topEnd(top: -3, end: -3),
 | 
			
		||||
                  stackFit: StackFit.loose,
 | 
			
		||||
                  badgeStyle: badges.BadgeStyle(
 | 
			
		||||
                    padding: const EdgeInsets.all(3),
 | 
			
		||||
                    badgeColor: Theme.of(context).primaryColor,
 | 
			
		||||
                    elevation: 1,
 | 
			
		||||
                  ),
 | 
			
		||||
                  badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
 | 
			
		||||
                  child: const Icon(Icons.chat),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            title: 'Talk',
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        PersistentTabConfig(
 | 
			
		||||
          screen: const Breaker(breaker: BreakerArea.files, child: Files([])),
 | 
			
		||||
          item: ItemConfig(
 | 
			
		||||
              activeForegroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
              inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
              icon: const Icon(Icons.folder),
 | 
			
		||||
              title: 'Dateien'
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        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(
 | 
			
		||||
        navBarConfig: config,
 | 
			
		||||
        navBarDecoration: NavBarDecoration(
 | 
			
		||||
          border: const Border(top: BorderSide(width: 1, color: Colors.grey)),
 | 
			
		||||
          color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								lib/extensions/renderNotNull.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/extensions/renderNotNull.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
extension RenderNotNullExt<T> on T? {
 | 
			
		||||
  R? wrapNullable<R>(R Function(T data) defaultValueCallback) {
 | 
			
		||||
    return this != null ? defaultValueCallback(this as T) : null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,7 +2,6 @@ import 'dart:async';
 | 
			
		||||
import 'dart:developer';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:feedback/feedback.dart';
 | 
			
		||||
import 'package:firebase_core/firebase_core.dart';
 | 
			
		||||
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
@@ -30,7 +29,6 @@ import 'storage/base/settingsProvider.dart';
 | 
			
		||||
import 'theming/darkAppTheme.dart';
 | 
			
		||||
import 'theming/lightAppTheme.dart';
 | 
			
		||||
import 'view/login/login.dart';
 | 
			
		||||
import 'view/pages/more/feedback/feedbackForm.dart';
 | 
			
		||||
import 'widget/placeholderView.dart';
 | 
			
		||||
 | 
			
		||||
Future<void> main() async {
 | 
			
		||||
@@ -40,7 +38,7 @@ Future<void> main() async {
 | 
			
		||||
    await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
 | 
			
		||||
    log("Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}");
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    log("Error initializing Firebase app!");
 | 
			
		||||
    log('Error initializing Firebase app!');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ByteData data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
 | 
			
		||||
@@ -71,12 +69,7 @@ Future<void> main() async {
 | 
			
		||||
          ChangeNotifierProvider(create: (context) => MessageProps()),
 | 
			
		||||
          ChangeNotifierProvider(create: (context) => HolidaysProps()),
 | 
			
		||||
        ],
 | 
			
		||||
        child: BetterFeedback(
 | 
			
		||||
          themeMode: ThemeMode.dark,
 | 
			
		||||
          feedbackBuilder: (context, callback, scrollController) => FeedbackForm(callback: callback, scrollController: scrollController),
 | 
			
		||||
          localeOverride: const Locale('de'),
 | 
			
		||||
          child: const Main(),
 | 
			
		||||
        ),
 | 
			
		||||
        child: const Main(),
 | 
			
		||||
      )
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -93,7 +86,7 @@ class _MainState extends State<Main> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    Jiffy.setLocale("de");
 | 
			
		||||
    Jiffy.setLocale('de');
 | 
			
		||||
 | 
			
		||||
    AccountData().waitForPopulation().then((value) {
 | 
			
		||||
      Provider.of<AccountModel>(context, listen: false)
 | 
			
		||||
@@ -113,7 +106,12 @@ class _MainState extends State<Main> {
 | 
			
		||||
      textDirection: TextDirection.ltr,
 | 
			
		||||
      child: Consumer<SettingsProvider>(
 | 
			
		||||
        builder: (context, settings, child) {
 | 
			
		||||
          var devToolsSettings = settings.val().devToolsSettings;
 | 
			
		||||
          return MaterialApp(
 | 
			
		||||
            showPerformanceOverlay: devToolsSettings.showPerformanceOverlay,
 | 
			
		||||
            checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers,
 | 
			
		||||
            checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages,
 | 
			
		||||
 | 
			
		||||
            debugShowCheckedModeBanner: false,
 | 
			
		||||
            localizationsDelegates: const [
 | 
			
		||||
              ...GlobalMaterialLocalizations.delegates,
 | 
			
		||||
@@ -132,16 +130,16 @@ class _MainState extends State<Main> {
 | 
			
		||||
            darkTheme: DarkAppTheme.theme,
 | 
			
		||||
            home: LoaderOverlay(
 | 
			
		||||
              child: Breaker(
 | 
			
		||||
                breaker: BreakerArea.global,
 | 
			
		||||
                child: Consumer<AccountModel>(
 | 
			
		||||
                  builder: (context, accountModel, child) {
 | 
			
		||||
                    switch(accountModel.state) {
 | 
			
		||||
                      case AccountModelState.loggedIn: return const App();
 | 
			
		||||
                      case AccountModelState.loggedOut: return const Login();
 | 
			
		||||
                      case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: "Daten werden geladen");
 | 
			
		||||
                    }
 | 
			
		||||
                  },
 | 
			
		||||
                )
 | 
			
		||||
                  breaker: BreakerArea.global,
 | 
			
		||||
                  child: Consumer<AccountModel>(
 | 
			
		||||
                    builder: (context, accountModel, child) {
 | 
			
		||||
                      switch(accountModel.state) {
 | 
			
		||||
                        case AccountModelState.loggedIn: return const App();
 | 
			
		||||
                        case AccountModelState.loggedOut: return const Login();
 | 
			
		||||
                        case AccountModelState.undefined: return const PlaceholderView(icon: Icons.timer, text: 'Daten werden geladen');
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  )
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,8 @@ import 'package:shared_preferences/shared_preferences.dart';
 | 
			
		||||
import 'accountModel.dart';
 | 
			
		||||
 | 
			
		||||
class AccountData {
 | 
			
		||||
  static const _usernameField = "username";
 | 
			
		||||
  static const _passwordField = "password";
 | 
			
		||||
  static const _usernameField = 'username';
 | 
			
		||||
  static const _passwordField = 'password';
 | 
			
		||||
  
 | 
			
		||||
  static final AccountData _instance = AccountData._construct();
 | 
			
		||||
  final Future<SharedPreferences> _storage = SharedPreferences.getInstance();
 | 
			
		||||
@@ -29,21 +29,21 @@ class AccountData {
 | 
			
		||||
  String? _password;
 | 
			
		||||
 | 
			
		||||
  String getUsername() {
 | 
			
		||||
    if(_username == null) throw Exception("Username not initialized");
 | 
			
		||||
    if(_username == null) throw Exception('Username not initialized');
 | 
			
		||||
    return _username!;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String getPassword() {
 | 
			
		||||
    if(_password == null) throw Exception("Password not initialized");
 | 
			
		||||
    if(_password == null) throw Exception('Password not initialized');
 | 
			
		||||
    return _password!;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String getUserSecret() {
 | 
			
		||||
    return sha512.convert(utf8.encode("${AccountData().getUsername()}:${AccountData().getPassword()}")).toString();
 | 
			
		||||
    return sha512.convert(utf8.encode('${AccountData().getUsername()}:${AccountData().getPassword()}')).toString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String> getDeviceId() async {
 | 
			
		||||
    return sha512.convert(utf8.encode("${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}")).toString();
 | 
			
		||||
    return sha512.convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}')).toString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setData(String username, String password) async {
 | 
			
		||||
@@ -84,7 +84,7 @@ class AccountData {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String buildHttpAuthString() {
 | 
			
		||||
    if(!isPopulated()) throw Exception("AccountData (e.g. username or password) is not initialized!");
 | 
			
		||||
    return "$_username:$_password";
 | 
			
		||||
    if(!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!');
 | 
			
		||||
    return '$_username:$_password';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -26,7 +26,7 @@ class BreakerProps extends DataHolder {
 | 
			
		||||
    for(var key in breakers.regional.keys) {
 | 
			
		||||
      GetBreakersReponseObject value = breakers.regional[key]!;
 | 
			
		||||
 | 
			
		||||
      if(int.parse(key.split("b")[1]) >= selfVersion) {
 | 
			
		||||
      if(int.parse(key.split('b')[1]) >= selfVersion) {
 | 
			
		||||
        if(value.areas.contains(type)) return value.message;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import '../../api/marianumcloud/talk/chat/getChatResponse.dart';
 | 
			
		||||
import '../dataHolder.dart';
 | 
			
		||||
 | 
			
		||||
class ChatProps extends DataHolder {
 | 
			
		||||
  String _queryToken = "";
 | 
			
		||||
  String _queryToken = '';
 | 
			
		||||
  DateTime _lastTokenSet = DateTime.now();
 | 
			
		||||
 | 
			
		||||
  GetChatResponse? _getChatResponse;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ class Endpoint {
 | 
			
		||||
  String domain;
 | 
			
		||||
  String path;
 | 
			
		||||
 | 
			
		||||
  Endpoint({required this.domain, this.path = ""});
 | 
			
		||||
  Endpoint({required this.domain, this.path = ''});
 | 
			
		||||
 | 
			
		||||
  String full() {
 | 
			
		||||
    return domain + path;
 | 
			
		||||
@@ -40,17 +40,17 @@ class EndpointData {
 | 
			
		||||
  EndpointMode getEndpointMode() {
 | 
			
		||||
    late String existingName;
 | 
			
		||||
    existingName = AccountData().getUsername();
 | 
			
		||||
    return existingName.startsWith("google") ? EndpointMode.stage : EndpointMode.live;
 | 
			
		||||
    return existingName.startsWith('google') ? EndpointMode.stage : EndpointMode.live;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Endpoint webuntis() {
 | 
			
		||||
    return EndpointOptions(
 | 
			
		||||
      live: Endpoint(
 | 
			
		||||
        domain: "peleus.webuntis.com",
 | 
			
		||||
        domain: 'peleus.webuntis.com',
 | 
			
		||||
      ),
 | 
			
		||||
      staged: Endpoint(
 | 
			
		||||
        domain: "mhsl.eu",
 | 
			
		||||
        path: "/marianum/marianummobile/webuntis/public/index.php/api"
 | 
			
		||||
        domain: 'mhsl.eu',
 | 
			
		||||
        path: '/marianum/marianummobile/webuntis/public/index.php/api'
 | 
			
		||||
      ),
 | 
			
		||||
    ).get(getEndpointMode());
 | 
			
		||||
  }
 | 
			
		||||
@@ -58,11 +58,11 @@ class EndpointData {
 | 
			
		||||
  Endpoint nextcloud() {
 | 
			
		||||
    return EndpointOptions(
 | 
			
		||||
      live: Endpoint(
 | 
			
		||||
        domain: "cloud.marianum-fulda.de",
 | 
			
		||||
        domain: 'cloud.marianum-fulda.de',
 | 
			
		||||
      ),
 | 
			
		||||
      staged: Endpoint(
 | 
			
		||||
        domain: "mhsl.eu",
 | 
			
		||||
        path: "/marianum/marianummobile/cloud",
 | 
			
		||||
        domain: 'mhsl.eu',
 | 
			
		||||
        path: '/marianum/marianummobile/cloud',
 | 
			
		||||
      )
 | 
			
		||||
    ).get(getEndpointMode());
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ extension ExtendedList on List {
 | 
			
		||||
 | 
			
		||||
class FilesProps extends DataHolder {
 | 
			
		||||
  List<String> folderPath = List<String>.empty(growable: true);
 | 
			
		||||
  String currentFolderName = "Home";
 | 
			
		||||
  String currentFolderName = 'Home';
 | 
			
		||||
 | 
			
		||||
  ListFilesResponse? _listFilesResponse;
 | 
			
		||||
  ListFilesResponse get listFilesResponse => _listFilesResponse!;
 | 
			
		||||
@@ -32,7 +32,7 @@ class FilesProps extends DataHolder {
 | 
			
		||||
    _listFilesResponse = null;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
    ListFilesCache(
 | 
			
		||||
      path: folderPath.isEmpty ? "/" : folderPath.join("/"),
 | 
			
		||||
      path: folderPath.isEmpty ? '/' : folderPath.join('/'),
 | 
			
		||||
      onUpdate: (ListFilesResponse data) => {
 | 
			
		||||
        _listFilesResponse = data,
 | 
			
		||||
        notifyListeners(),
 | 
			
		||||
@@ -48,7 +48,7 @@ class FilesProps extends DataHolder {
 | 
			
		||||
 | 
			
		||||
  void popFolder() {
 | 
			
		||||
    folderPath.removeLast();
 | 
			
		||||
    if(folderPath.isEmpty) currentFolderName = "Home";
 | 
			
		||||
    if(folderPath.isEmpty) currentFolderName = 'Home';
 | 
			
		||||
    run();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -49,8 +49,8 @@ class TimetableProps extends DataHolder {
 | 
			
		||||
  @override
 | 
			
		||||
  void run({renew}) {
 | 
			
		||||
    GetTimetableCache(
 | 
			
		||||
      startdate: int.parse(DateFormat("yyyyMMdd").format(startDate)),
 | 
			
		||||
      enddate: int.parse(DateFormat("yyyyMMdd").format(endDate)),
 | 
			
		||||
      startdate: int.parse(DateFormat('yyyyMMdd').format(startDate)),
 | 
			
		||||
      enddate: int.parse(DateFormat('yyyyMMdd').format(endDate)),
 | 
			
		||||
      onUpdate: (GetTimetableResponse data) => {
 | 
			
		||||
        _getTimetableResponse = data,
 | 
			
		||||
        notifyListeners(),
 | 
			
		||||
 
 | 
			
		||||
@@ -49,11 +49,11 @@ class NotificationController {
 | 
			
		||||
 | 
			
		||||
    DebugTile(context).run(() {
 | 
			
		||||
      showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
        title: const Text("Notification report"),
 | 
			
		||||
        title: const Text('Notification report'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            const Text("Dieser Bericht wird angezeigt, da du den Entwicklermodus aktiviert hast und die App über eine Benachrichtigung geöffnet wurde."),
 | 
			
		||||
            const Text('Dieser Bericht wird angezeigt, da du den Entwicklermodus aktiviert hast und die App über eine Benachrichtigung geöffnet wurde.'),
 | 
			
		||||
            Text(JsonViewer.format(message.data)),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
 
 | 
			
		||||
@@ -10,13 +10,13 @@ import '../widget/confirmDialog.dart';
 | 
			
		||||
class NotifyUpdater {
 | 
			
		||||
  static ConfirmDialog enableAfterDisclaimer(SettingsProvider settings) {
 | 
			
		||||
    return ConfirmDialog(
 | 
			
		||||
      title: "Warnung",
 | 
			
		||||
      title: 'Warnung',
 | 
			
		||||
      icon: Icons.warning_amber,
 | 
			
		||||
      content: ""
 | 
			
		||||
          "Die Push-Benachrichtigungen werden durch mhsl.eu versendet.\n\n"
 | 
			
		||||
          "Durch das aktivieren dieser Funktion wird dein Nutzername, dein Password und eine Geräte-ID von mhsl dauerhaft gespeichert und verarbeitet.\n\n"
 | 
			
		||||
          "Für mehr Informationen drücke lange auf die Einstellungsoption!",
 | 
			
		||||
      confirmButton: "Aktivieren",
 | 
			
		||||
      content: ''
 | 
			
		||||
          'Die Push-Benachrichtigungen werden durch mhsl.eu versendet.\n\n'
 | 
			
		||||
          'Durch das aktivieren dieser Funktion wird dein Nutzername, dein Password und eine Geräte-ID von mhsl dauerhaft gespeichert und verarbeitet.\n\n'
 | 
			
		||||
          'Für mehr Informationen drücke lange auf die Einstellungsoption!',
 | 
			
		||||
      confirmButton: 'Aktivieren',
 | 
			
		||||
      onConfirm: () {
 | 
			
		||||
        FirebaseMessaging.instance.requestPermission(
 | 
			
		||||
            provisional: false
 | 
			
		||||
@@ -29,7 +29,7 @@ class NotifyUpdater {
 | 
			
		||||
  static void registerToServer() async {
 | 
			
		||||
    String? fcmToken = await FirebaseMessaging.instance.getToken();
 | 
			
		||||
 | 
			
		||||
    if(fcmToken == null) throw Exception("Failed to register push notification because there is no FBC token!");
 | 
			
		||||
    if(fcmToken == null) throw Exception('Failed to register push notification because there is no FBC token!');
 | 
			
		||||
 | 
			
		||||
    NotifyRegister(
 | 
			
		||||
      NotifyRegisterParams(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:json_annotation/json_annotation.dart';
 | 
			
		||||
 | 
			
		||||
import '../devTools/devToolsSettings.dart';
 | 
			
		||||
import '../file/fileSettings.dart';
 | 
			
		||||
import '../fileView/fileViewSettings.dart';
 | 
			
		||||
import '../gradeAverages/gradeAveragesSettings.dart';
 | 
			
		||||
@@ -27,6 +28,7 @@ class Settings {
 | 
			
		||||
  HolidaysSettings holidaysSettings;
 | 
			
		||||
  FileViewSettings fileViewSettings;
 | 
			
		||||
  NotificationSettings notificationSettings;
 | 
			
		||||
  DevToolsSettings devToolsSettings;
 | 
			
		||||
 | 
			
		||||
  Settings({
 | 
			
		||||
    required this.appTheme,
 | 
			
		||||
@@ -38,6 +40,7 @@ class Settings {
 | 
			
		||||
    required this.holidaysSettings,
 | 
			
		||||
    required this.fileViewSettings,
 | 
			
		||||
    required this.notificationSettings,
 | 
			
		||||
    required this.devToolsSettings,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  static String _themeToJson(ThemeMode m) => m.name;
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,8 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
 | 
			
		||||
          json['fileViewSettings'] as Map<String, dynamic>),
 | 
			
		||||
      notificationSettings: NotificationSettings.fromJson(
 | 
			
		||||
          json['notificationSettings'] as Map<String, dynamic>),
 | 
			
		||||
      devToolsSettings: DevToolsSettings.fromJson(
 | 
			
		||||
          json['devToolsSettings'] as Map<String, dynamic>),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
 | 
			
		||||
@@ -35,4 +37,5 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
 | 
			
		||||
      'holidaysSettings': instance.holidaysSettings.toJson(),
 | 
			
		||||
      'fileViewSettings': instance.fileViewSettings.toJson(),
 | 
			
		||||
      'notificationSettings': instance.notificationSettings.toJson(),
 | 
			
		||||
      'devToolsSettings': instance.devToolsSettings.toJson(),
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import '../../view/settings/defaultSettings.dart';
 | 
			
		||||
import 'settings.dart';
 | 
			
		||||
 | 
			
		||||
class SettingsProvider extends ChangeNotifier {
 | 
			
		||||
  static const String _fieldName = "settings";
 | 
			
		||||
  static const String _fieldName = 'settings';
 | 
			
		||||
 | 
			
		||||
  late SharedPreferences _storage;
 | 
			
		||||
  late Settings _settings = DefaultSettings.get();
 | 
			
		||||
@@ -45,13 +45,13 @@ class SettingsProvider extends ChangeNotifier {
 | 
			
		||||
      _settings = Settings.fromJson(jsonDecode(_storage.getString(_fieldName)!));
 | 
			
		||||
    } catch(exception) {
 | 
			
		||||
      try {
 | 
			
		||||
        log("Settings were changed, trying to recover from old Settings: ${exception.toString()}");
 | 
			
		||||
        log('Settings were changed, trying to recover from old Settings: ${exception.toString()}');
 | 
			
		||||
        _settings = Settings.fromJson(_mergeSettings(jsonDecode(_storage.getString(_fieldName)!), DefaultSettings.get().toJson()));
 | 
			
		||||
        log("Settings recovered successfully: ${_settings.toJson().toString()}");
 | 
			
		||||
        log('Settings recovered successfully: ${_settings.toJson().toString()}');
 | 
			
		||||
      } catch(exception) {
 | 
			
		||||
        log("Settings are defective and not recoverable, using defaults: ${exception.toString()}");
 | 
			
		||||
        log('Settings are defective and not recoverable, using defaults: ${exception.toString()}');
 | 
			
		||||
        _settings = DefaultSettings.get();
 | 
			
		||||
        log("Settings were reset to defaults!");
 | 
			
		||||
        log('Settings were reset to defaults!');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								lib/storage/devTools/devToolsSettings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/storage/devTools/devToolsSettings.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import 'package:json_annotation/json_annotation.dart';
 | 
			
		||||
 | 
			
		||||
part 'devToolsSettings.g.dart';
 | 
			
		||||
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
class DevToolsSettings {
 | 
			
		||||
  bool showPerformanceOverlay;
 | 
			
		||||
  bool checkerboardOffscreenLayers;
 | 
			
		||||
  bool checkerboardRasterCacheImages;
 | 
			
		||||
 | 
			
		||||
  DevToolsSettings({required this.showPerformanceOverlay, required this.checkerboardOffscreenLayers, required this.checkerboardRasterCacheImages});
 | 
			
		||||
 | 
			
		||||
  factory DevToolsSettings.fromJson(Map<String, dynamic> json) => _$DevToolsSettingsFromJson(json);
 | 
			
		||||
  Map<String, dynamic> toJson() => _$DevToolsSettingsToJson(this);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								lib/storage/devTools/devToolsSettings.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								lib/storage/devTools/devToolsSettings.g.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
			
		||||
 | 
			
		||||
part of 'devToolsSettings.dart';
 | 
			
		||||
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
// JsonSerializableGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
DevToolsSettings _$DevToolsSettingsFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    DevToolsSettings(
 | 
			
		||||
      showPerformanceOverlay: json['showPerformanceOverlay'] as bool,
 | 
			
		||||
      checkerboardOffscreenLayers: json['checkerboardOffscreenLayers'] as bool,
 | 
			
		||||
      checkerboardRasterCacheImages:
 | 
			
		||||
          json['checkerboardRasterCacheImages'] as bool,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$DevToolsSettingsToJson(DevToolsSettings instance) =>
 | 
			
		||||
    <String, dynamic>{
 | 
			
		||||
      'showPerformanceOverlay': instance.showPerformanceOverlay,
 | 
			
		||||
      'checkerboardOffscreenLayers': instance.checkerboardOffscreenLayers,
 | 
			
		||||
      'checkerboardRasterCacheImages': instance.checkerboardRasterCacheImages,
 | 
			
		||||
    };
 | 
			
		||||
@@ -4,13 +4,13 @@ class AppTheme {
 | 
			
		||||
  static ThemeModeDisplay getDisplayOptions(ThemeMode theme) {
 | 
			
		||||
    switch(theme) {
 | 
			
		||||
      case ThemeMode.system:
 | 
			
		||||
        return ThemeModeDisplay(icon: Icons.auto_fix_high_outlined, displayName: "Systemvorgabe");
 | 
			
		||||
        return ThemeModeDisplay(icon: Icons.auto_fix_high_outlined, displayName: 'Systemvorgabe');
 | 
			
		||||
 | 
			
		||||
      case ThemeMode.light:
 | 
			
		||||
        return ThemeModeDisplay(icon: Icons.wb_sunny_outlined, displayName: "Hell");
 | 
			
		||||
        return ThemeModeDisplay(icon: Icons.wb_sunny_outlined, displayName: 'Hell');
 | 
			
		||||
 | 
			
		||||
      case ThemeMode.dark:
 | 
			
		||||
        return ThemeModeDisplay(icon: Icons.dark_mode_outlined, displayName: "Dunkel");
 | 
			
		||||
        return ThemeModeDisplay(icon: Icons.dark_mode_outlined, displayName: 'Dunkel');
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ class _LoginState extends State<Login> {
 | 
			
		||||
  bool displayDisclaimerText = true;
 | 
			
		||||
 | 
			
		||||
  String? _checkInput(value){
 | 
			
		||||
    return (value ?? "").length == 0 ? "Eingabe erforderlich" : null;
 | 
			
		||||
    return (value ?? '').length == 0 ? 'Eingabe erforderlich' : null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String?> _login(LoginData data) async {
 | 
			
		||||
@@ -42,7 +42,7 @@ class _LoginState extends State<Login> {
 | 
			
		||||
    } catch(e) {
 | 
			
		||||
      await AccountData().removeData();
 | 
			
		||||
      log(e.toString());
 | 
			
		||||
      return "Benutzername oder Password falsch! (${e.toString()})";
 | 
			
		||||
      return 'Benutzername oder Password falsch! (${e.toString()})';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await Future.delayed(const Duration(seconds: 1));
 | 
			
		||||
@@ -51,14 +51,14 @@ class _LoginState extends State<Login> {
 | 
			
		||||
 | 
			
		||||
  Future<String> _resetPassword(String name) {
 | 
			
		||||
    return Future.delayed(Duration.zero).then((_) {
 | 
			
		||||
      return "Diese Funktion steht nicht zur Verfügung!";
 | 
			
		||||
      return 'Diese Funktion steht nicht zur Verfügung!';
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return FlutterLogin(
 | 
			
		||||
      logo: Image.asset("assets/logo/icon.png").image,
 | 
			
		||||
      logo: Image.asset('assets/logo/icon.png').image,
 | 
			
		||||
 | 
			
		||||
      userValidator: _checkInput,
 | 
			
		||||
      passwordValidator: _checkInput,
 | 
			
		||||
@@ -84,9 +84,9 @@ class _LoginState extends State<Login> {
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
      messages: LoginMessages(
 | 
			
		||||
        loginButton: "Anmelden",
 | 
			
		||||
        userHint: "Nutzername",
 | 
			
		||||
        passwordHint: "Passwort",
 | 
			
		||||
        loginButton: 'Anmelden',
 | 
			
		||||
        userHint: 'Nutzername',
 | 
			
		||||
        passwordHint: 'Passwort',
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
      disableCustomPageTransformer: true,
 | 
			
		||||
@@ -97,15 +97,15 @@ class _LoginState extends State<Login> {
 | 
			
		||||
          child: Visibility(
 | 
			
		||||
            visible: displayDisclaimerText,
 | 
			
		||||
            child: const Text(
 | 
			
		||||
            "Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\nKeinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!",
 | 
			
		||||
            'Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\nKeinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!',
 | 
			
		||||
            textAlign: TextAlign.center,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
      footer: "Marianum Fulda - Die persönliche Schule",
 | 
			
		||||
      title: "Marianum Fulda",
 | 
			
		||||
      footer: 'Marianum Fulda - Die persönliche Schule',
 | 
			
		||||
      title: 'Marianum Fulda',
 | 
			
		||||
 | 
			
		||||
      userType: LoginUserType.name,
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -6,11 +6,13 @@ import 'package:flowder/flowder.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:jiffy/jiffy.dart';
 | 
			
		||||
import '../../../widget/infoDialog.dart';
 | 
			
		||||
import 'package:nextcloud/nextcloud.dart';
 | 
			
		||||
import 'package:path_provider/path_provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
 | 
			
		||||
import '../../../api/marianumcloud/webdav/webdavApi.dart';
 | 
			
		||||
import '../../../model/endpointData.dart';
 | 
			
		||||
import '../../../widget/centeredLeading.dart';
 | 
			
		||||
import '../../../widget/confirmDialog.dart';
 | 
			
		||||
import '../../../widget/fileViewer.dart';
 | 
			
		||||
@@ -27,7 +29,7 @@ class FileElement extends StatefulWidget {
 | 
			
		||||
    Directory paths = await getTemporaryDirectory();
 | 
			
		||||
    
 | 
			
		||||
    var encodedPath = Uri.encodeComponent(remotePath);
 | 
			
		||||
    encodedPath = encodedPath.replaceAll("%2F", "/");
 | 
			
		||||
    encodedPath = encodedPath.replaceAll('%2F', '/');
 | 
			
		||||
 | 
			
		||||
    String local = paths.path + Platform.pathSeparator + name;
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +44,7 @@ class FileElement extends StatefulWidget {
 | 
			
		||||
      onDone: () {
 | 
			
		||||
        //Future<OpenResult> result = OpenFile.open(local); // TODO legacy - refactor: remove onDone parameter
 | 
			
		||||
        Navigator.of(context).push(MaterialPageRoute(builder: (context) => FileViewer(path: local)));
 | 
			
		||||
        onDone(OpenResult(message: "File viewer opened", type: ResultType.done));
 | 
			
		||||
        onDone(OpenResult(message: 'File viewer opened', type: ResultType.done));
 | 
			
		||||
        // result.then((value) => {
 | 
			
		||||
        //   onDone(value)
 | 
			
		||||
        // });
 | 
			
		||||
@@ -69,21 +71,21 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
        children: [
 | 
			
		||||
          Container(
 | 
			
		||||
            margin: const EdgeInsets.only(right: 10),
 | 
			
		||||
            child: const Text("Download:"),
 | 
			
		||||
            child: const Text('Download:'),
 | 
			
		||||
          ),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: LinearProgressIndicator(value: percent/100),
 | 
			
		||||
          ),
 | 
			
		||||
          Container(
 | 
			
		||||
            margin: const EdgeInsets.only(left: 10),
 | 
			
		||||
            child: Text("${percent.round()}%"),
 | 
			
		||||
            child: Text('${percent.round()}%'),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return widget.file.isDirectory
 | 
			
		||||
        ? Text("geändert ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}")
 | 
			
		||||
        : Text("${filesize(widget.file.size)}, ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}");
 | 
			
		||||
        ? Text('geändert ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}')
 | 
			
		||||
        : Text('${filesize(widget.file.size)}, ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -103,14 +105,18 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
            },
 | 
			
		||||
          ));
 | 
			
		||||
        } else {
 | 
			
		||||
          if(EndpointData().getEndpointMode() == EndpointMode.stage) {
 | 
			
		||||
            InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!');
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          if(widget.file.currentlyDownloading) {
 | 
			
		||||
            showDialog(
 | 
			
		||||
              context: context,
 | 
			
		||||
              builder: (context) => ConfirmDialog(
 | 
			
		||||
                title: "Download abbrechen?",
 | 
			
		||||
                content: "Möchtest du den Download abbrechen?",
 | 
			
		||||
                cancelButton: "Nein",
 | 
			
		||||
                confirmButton: "Ja, Abbrechen",
 | 
			
		||||
                title: 'Download abbrechen?',
 | 
			
		||||
                content: 'Möchtest du den Download abbrechen?',
 | 
			
		||||
                cancelButton: 'Nein',
 | 
			
		||||
                confirmButton: 'Ja, Abbrechen',
 | 
			
		||||
                onConfirm: () {
 | 
			
		||||
                  downloadCore?.then((value) {
 | 
			
		||||
                    if(!value.isCancelled) value.cancel();
 | 
			
		||||
@@ -137,7 +143,7 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
            if(result.type != ResultType.done) {
 | 
			
		||||
              showDialog(context: context, builder: (context) {
 | 
			
		||||
                return AlertDialog(
 | 
			
		||||
                  title: const Text("Download"),
 | 
			
		||||
                  title: const Text('Download'),
 | 
			
		||||
                  content: Text(result.message),
 | 
			
		||||
                );
 | 
			
		||||
              });
 | 
			
		||||
@@ -157,12 +163,12 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
            children: [
 | 
			
		||||
              ListTile(
 | 
			
		||||
                leading: const Icon(Icons.delete_outline),
 | 
			
		||||
                title: const Text("Löschen"),
 | 
			
		||||
                title: const Text('Löschen'),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  Navigator.of(context).pop();
 | 
			
		||||
                  showDialog(context: context, builder: (context) => ConfirmDialog(
 | 
			
		||||
                    title: "Element löschen?",
 | 
			
		||||
                    content: "Das Element wird unwiederruflich gelöscht.",
 | 
			
		||||
                    title: 'Element löschen?',
 | 
			
		||||
                    content: 'Das Element wird unwiederruflich gelöscht.',
 | 
			
		||||
                    onConfirm: () {
 | 
			
		||||
                      WebdavApi.webdav
 | 
			
		||||
                          .then((value) => value.delete(PathUri.parse(widget.file.path)))
 | 
			
		||||
@@ -175,7 +181,7 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
                visible: !kReleaseMode,
 | 
			
		||||
                child: ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.share_outlined),
 | 
			
		||||
                  title: const Text("Teilen"),
 | 
			
		||||
                  title: const Text('Teilen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                    UnimplementedDialog.show(context);
 | 
			
		||||
 
 | 
			
		||||
@@ -43,8 +43,8 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        state = FileUploadState.checkConflict;
 | 
			
		||||
      });
 | 
			
		||||
      List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath.join("/")))).responses;
 | 
			
		||||
      if(result.any((element) => element.href!.endsWith("/$targetFileName"))) {
 | 
			
		||||
      List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath.join('/')))).responses;
 | 
			
		||||
      if(result.any((element) => element.href!.endsWith('/$targetFileName'))) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          state = FileUploadState.conflict;
 | 
			
		||||
        });
 | 
			
		||||
@@ -57,7 +57,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Future<HttpClientResponse> uploadTask = webdavClient.putFile(File(widget.localPath), FileStat.statSync(widget.localPath), PathUri.parse(fullRemotePath)); // TODO use onProgress from putFile
 | 
			
		||||
    uploadTask.then((value) => Future<HttpClientResponse?>.value(value)).catchError((e) {
 | 
			
		||||
    uploadTask.then(Future<HttpClientResponse?>.value).catchError((e) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        state = FileUploadState.error;
 | 
			
		||||
      });
 | 
			
		||||
@@ -67,7 +67,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
 | 
			
		||||
    cancelableOperation = CancelableOperation<HttpClientResponse>.fromFuture(
 | 
			
		||||
      uploadTask,
 | 
			
		||||
      onCancel: () => log("Upload cancelled"),
 | 
			
		||||
      onCancel: () => log('Upload cancelled'),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    cancelableOperation!.then((value) {
 | 
			
		||||
@@ -88,7 +88,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    targetFileName = widget.fileName;
 | 
			
		||||
    remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : "/";
 | 
			
		||||
    remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : '/';
 | 
			
		||||
    fileNameController.text = widget.fileName;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
@@ -96,7 +96,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if(state == FileUploadState.naming) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        title: const Text("Datei hochladen"),
 | 
			
		||||
        title: const Text('Datei hochladen'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -107,7 +107,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
              },
 | 
			
		||||
              autocorrect: false,
 | 
			
		||||
              decoration: const InputDecoration(
 | 
			
		||||
                labelText: "Dateiname",
 | 
			
		||||
                labelText: 'Dateiname',
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
@@ -115,10 +115,10 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text("Abbrechen")),
 | 
			
		||||
          }, child: const Text('Abbrechen')),
 | 
			
		||||
          TextButton(onPressed: () async {
 | 
			
		||||
            upload();
 | 
			
		||||
          }, child: const Text("Hochladen")),
 | 
			
		||||
          }, child: const Text('Hochladen')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
@@ -127,7 +127,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
    if(state == FileUploadState.conflict) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.error_outline),
 | 
			
		||||
        title: const Text("Datei konflikt"),
 | 
			
		||||
        title: const Text('Datei konflikt'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -139,10 +139,10 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
            setState(() {
 | 
			
		||||
              state = FileUploadState.naming;
 | 
			
		||||
            });
 | 
			
		||||
          }, child: const Text("Datei umbenennen")),
 | 
			
		||||
          }, child: const Text('Datei umbenennen')),
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            upload(override: true);
 | 
			
		||||
          }, child: const Text("Datei überschreiben")),
 | 
			
		||||
          }, child: const Text('Datei überschreiben')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
@@ -151,15 +151,15 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
    if(state == FileUploadState.upload || state == FileUploadState.checkConflict) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.upload),
 | 
			
		||||
        title: const Text("Hochladen"),
 | 
			
		||||
        title: const Text('Hochladen'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            Visibility(
 | 
			
		||||
              visible: state == FileUploadState.upload,
 | 
			
		||||
              replacement: const Text("Prüfe auf dateikonflikte..."),
 | 
			
		||||
              child: const Text("Upload läuft!\nDies kann je nach Dateigröße einige Zeit dauern...", textAlign: TextAlign.center),
 | 
			
		||||
              replacement: const Text('Prüfe auf dateikonflikte...'),
 | 
			
		||||
              child: const Text('Upload läuft!\nDies kann je nach Dateigröße einige Zeit dauern...', textAlign: TextAlign.center),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(height: 30),
 | 
			
		||||
            const CircularProgressIndicator()
 | 
			
		||||
@@ -183,7 +183,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
      }
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.done),
 | 
			
		||||
        title: const Text("Upload fertig"),
 | 
			
		||||
        title: const Text('Upload fertig'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -193,7 +193,7 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text("Fertig")),
 | 
			
		||||
          }, child: const Text('Fertig')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
@@ -202,23 +202,23 @@ class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
    if(state == FileUploadState.error) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.error_outline),
 | 
			
		||||
        title: const Text("Fehler"),
 | 
			
		||||
        title: const Text('Fehler'),
 | 
			
		||||
        content: const Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text("Es ist ein Fehler aufgetreten!", textAlign: TextAlign.center),
 | 
			
		||||
            Text('Es ist ein Fehler aufgetreten!', textAlign: TextAlign.center),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text("Schlißen")),
 | 
			
		||||
          }, child: const Text('Schlißen')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw UnimplementedError("Invalid state");
 | 
			
		||||
    throw UnimplementedError('Invalid state');
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -43,17 +43,17 @@ enum SortOption {
 | 
			
		||||
class SortOptions {
 | 
			
		||||
  static Map<SortOption, BetterSortOption> options = {
 | 
			
		||||
    SortOption.name: BetterSortOption(
 | 
			
		||||
      displayName: "Name",
 | 
			
		||||
      displayName: 'Name',
 | 
			
		||||
      icon: Icons.sort_by_alpha_outlined,
 | 
			
		||||
      compare: (CacheableFile a, CacheableFile b) => a.name.compareTo(b.name)
 | 
			
		||||
    ),
 | 
			
		||||
    SortOption.date: BetterSortOption(
 | 
			
		||||
      displayName: "Datum",
 | 
			
		||||
      displayName: 'Datum',
 | 
			
		||||
      icon: Icons.history_outlined,
 | 
			
		||||
      compare: (CacheableFile a, CacheableFile b) => a.modifiedAt!.compareTo(b.modifiedAt!)
 | 
			
		||||
    ),
 | 
			
		||||
    SortOption.size: BetterSortOption(
 | 
			
		||||
      displayName: "Größe",
 | 
			
		||||
      displayName: 'Größe',
 | 
			
		||||
      icon: Icons.sd_card_outlined,
 | 
			
		||||
      compare: (CacheableFile a, CacheableFile b) {
 | 
			
		||||
        if(a.isDirectory || b.isDirectory) return a.isDirectory ? 1 : 0;
 | 
			
		||||
@@ -88,7 +88,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
 | 
			
		||||
  void _query() {
 | 
			
		||||
    ListFilesCache(
 | 
			
		||||
        path: widget.path.isEmpty ? "/" : widget.path.join("/"),
 | 
			
		||||
        path: widget.path.isEmpty ? '/' : widget.path.join('/'),
 | 
			
		||||
        onUpdate: (ListFilesResponse d) {
 | 
			
		||||
          if(!context.mounted) return; // prevent setState when widget is possibly already disposed
 | 
			
		||||
          d.files.removeWhere((element) => element.name.isEmpty || element.name == widget.path.lastOrNull());
 | 
			
		||||
@@ -109,7 +109,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(widget.path.isNotEmpty ? widget.path.last : "Dateien"),
 | 
			
		||||
        title: Text(widget.path.isNotEmpty ? widget.path.last : 'Dateien'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          // IconButton(
 | 
			
		||||
          //   icon: const Icon(Icons.search),
 | 
			
		||||
@@ -127,7 +127,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Icon(e ? Icons.text_rotate_up : Icons.text_rotation_down, color: Theme.of(context).colorScheme.onSurface),
 | 
			
		||||
                      const SizedBox(width: 15),
 | 
			
		||||
                      Text(e ? "Aufsteigend" : "Absteigend")
 | 
			
		||||
                      Text(e ? 'Aufsteigend' : 'Absteigend')
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
              )).toList();
 | 
			
		||||
@@ -164,7 +164,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      floatingActionButton: FloatingActionButton(
 | 
			
		||||
        heroTag: "uploadFile",
 | 
			
		||||
        heroTag: 'uploadFile',
 | 
			
		||||
        backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          showDialog(context: context, builder: (context) {
 | 
			
		||||
@@ -172,29 +172,29 @@ class _FilesState extends State<Files> {
 | 
			
		||||
              children: [
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.create_new_folder_outlined),
 | 
			
		||||
                  title: const Text("Ordner erstellen"),
 | 
			
		||||
                  title: const Text('Ordner erstellen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                    showDialog(context: context, builder: (context) {
 | 
			
		||||
                      var inputController = TextEditingController();
 | 
			
		||||
                      return AlertDialog(
 | 
			
		||||
                        title: const Text("Neuer Ordner"),
 | 
			
		||||
                        title: const Text('Neuer Ordner'),
 | 
			
		||||
                        content: TextField(
 | 
			
		||||
                          controller: inputController,
 | 
			
		||||
                          decoration: const InputDecoration(
 | 
			
		||||
                            labelText: "Name",
 | 
			
		||||
                            labelText: 'Name',
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        actions: [
 | 
			
		||||
                          TextButton(onPressed: () {
 | 
			
		||||
                            Navigator.of(context).pop();
 | 
			
		||||
                          }, child: const Text("Abbrechen")),
 | 
			
		||||
                          }, child: const Text('Abbrechen')),
 | 
			
		||||
                          TextButton(onPressed: () {
 | 
			
		||||
                            WebdavApi.webdav.then((webdav) {
 | 
			
		||||
                              webdav.mkcol(PathUri.parse("${widget.path.join("/")}/${inputController.text}")).then((value) => _query());
 | 
			
		||||
                            });
 | 
			
		||||
                            Navigator.of(context).pop();
 | 
			
		||||
                          }, child: const Text("Ordner erstellen")),
 | 
			
		||||
                          }, child: const Text('Ordner erstellen')),
 | 
			
		||||
                        ],
 | 
			
		||||
                      );
 | 
			
		||||
                    });
 | 
			
		||||
@@ -202,12 +202,10 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                ),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.upload_file),
 | 
			
		||||
                  title: const Text("Aus Dateien hochladen"),
 | 
			
		||||
                  title: const Text('Aus Dateien hochladen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    context.loaderOverlay.show();
 | 
			
		||||
                    FilePick.documentPick().then((value) {
 | 
			
		||||
                      mediaUpload(value);
 | 
			
		||||
                    });
 | 
			
		||||
                    FilePick.documentPick().then(mediaUpload);
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
@@ -215,7 +213,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                  visible: !Platform.isIOS,
 | 
			
		||||
                  child: ListTile(
 | 
			
		||||
                    leading: const Icon(Icons.add_a_photo_outlined),
 | 
			
		||||
                    title: const Text("Aus Gallerie hochladen"),
 | 
			
		||||
                    title: const Text('Aus Gallerie hochladen'),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      context.loaderOverlay.show();
 | 
			
		||||
                      FilePick.galleryPick().then((value) {
 | 
			
		||||
@@ -231,13 +229,14 @@ class _FilesState extends State<Files> {
 | 
			
		||||
        },
 | 
			
		||||
        child: const Icon(Icons.add),
 | 
			
		||||
      ),
 | 
			
		||||
      body: data == null ? const LoadingSpinner() : data!.files.isEmpty ? const PlaceholderView(icon: Icons.folder_off_rounded, text: "Der Ordner ist leer") : LoaderOverlay(
 | 
			
		||||
      body: data == null ? const LoadingSpinner() : data!.files.isEmpty ? const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer') : LoaderOverlay(
 | 
			
		||||
        child: RefreshIndicator(
 | 
			
		||||
          onRefresh: () {
 | 
			
		||||
            _query();
 | 
			
		||||
            return Future.delayed(const Duration(seconds: 3));
 | 
			
		||||
          },
 | 
			
		||||
          child: ListView.builder(
 | 
			
		||||
            padding: EdgeInsets.zero,
 | 
			
		||||
            itemCount: files.length,
 | 
			
		||||
            itemBuilder: (context, index) {
 | 
			
		||||
              CacheableFile file = files.toList()[index];
 | 
			
		||||
@@ -257,6 +256,6 @@ class _FilesState extends State<Files> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var fileName = path.split(Platform.pathSeparator).last;
 | 
			
		||||
    showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, onUploadFinished: () => _query()), barrierDismissible: false);
 | 
			
		||||
    showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, onUploadFinished: _query), barrierDismissible: false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								lib/view/pages/more/feedback/feedbackDialog.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								lib/view/pages/more/feedback/feedbackDialog.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:package_info/package_info.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../api/mhsl/server/feedback/addFeedback.dart';
 | 
			
		||||
import '../../../../api/mhsl/server/feedback/addFeedbackParams.dart';
 | 
			
		||||
import '../../../../model/accountData.dart';
 | 
			
		||||
import '../../../../widget/infoDialog.dart';
 | 
			
		||||
 | 
			
		||||
class FeedbackDialog extends StatefulWidget {
 | 
			
		||||
  const FeedbackDialog({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<FeedbackDialog> createState() => _FeedbackDialogState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _FeedbackDialogState extends State<FeedbackDialog> {
 | 
			
		||||
  final TextEditingController _feedbackInput = TextEditingController();
 | 
			
		||||
  String? _error;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AlertDialog(
 | 
			
		||||
 | 
			
		||||
      title: const Text('Feedback'),
 | 
			
		||||
      content: Column(
 | 
			
		||||
        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
        children: [
 | 
			
		||||
          const Text('Feedback, Anregungen, Ideen, Fehler und Verbesserungen'),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          const Text('Bitte gib keine geheimen Daten wie z.B. Passwörter weiter.', style: TextStyle(fontSize: 10)),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          TextField(
 | 
			
		||||
            controller: _feedbackInput,
 | 
			
		||||
            autofocus: true,
 | 
			
		||||
            decoration: const InputDecoration(
 | 
			
		||||
              border: OutlineInputBorder(),
 | 
			
		||||
              label: Text('Feedback und Verbesserungen')
 | 
			
		||||
            ),
 | 
			
		||||
            // style: TextStyle(),
 | 
			
		||||
            // expands: true,
 | 
			
		||||
            minLines: 3,
 | 
			
		||||
            maxLines: 5,
 | 
			
		||||
          ),
 | 
			
		||||
          Visibility(
 | 
			
		||||
            visible: _error != null,
 | 
			
		||||
            child: Text('Senden fehlgeschlagen: $_error', style: const TextStyle(color: Colors.red))
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      actions: [
 | 
			
		||||
        TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen')),
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: () async {
 | 
			
		||||
            AddFeedback(
 | 
			
		||||
              AddFeedbackParams(
 | 
			
		||||
                user: AccountData().getUserSecret(),
 | 
			
		||||
                feedback: _feedbackInput.text,
 | 
			
		||||
                appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber)
 | 
			
		||||
              )
 | 
			
		||||
            )
 | 
			
		||||
            .run()
 | 
			
		||||
            .then((value) {
 | 
			
		||||
              Navigator.of(context).pop();
 | 
			
		||||
              InfoDialog.show(context, 'Danke für dein Feedback!');
 | 
			
		||||
            })
 | 
			
		||||
            .catchError((error, trace) {
 | 
			
		||||
                setState(() {
 | 
			
		||||
                  _error = error.toString();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
          child: const Text('Senden'),
 | 
			
		||||
        )
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,99 +0,0 @@
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../theming/darkAppTheme.dart';
 | 
			
		||||
import '../../../../widget/loadingSpinner.dart';
 | 
			
		||||
 | 
			
		||||
class FeedbackForm extends StatefulWidget {
 | 
			
		||||
  final Future<void> Function(String, {Map<String, dynamic>? extras}) callback;
 | 
			
		||||
  final ScrollController? scrollController;
 | 
			
		||||
  const FeedbackForm({required this.scrollController, required this.callback, super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<FeedbackForm> createState() => _FeedbackFormState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _FeedbackFormState extends State<FeedbackForm> {
 | 
			
		||||
  final TextEditingController _feedbackInput = TextEditingController();
 | 
			
		||||
  bool _textFieldEmpty = false;
 | 
			
		||||
  bool _isSending = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _feedbackInput.addListener(() {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _textFieldEmpty = _feedbackInput.text.isEmpty;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Theme(
 | 
			
		||||
      data: DarkAppTheme.theme,
 | 
			
		||||
      child: Visibility(
 | 
			
		||||
        visible: !_isSending,
 | 
			
		||||
        replacement: const LoadingSpinner(infoText: "Daten werden ermittelt"),
 | 
			
		||||
        child: SingleChildScrollView(
 | 
			
		||||
          controller: widget.scrollController,
 | 
			
		||||
          padding: const EdgeInsets.all(20),
 | 
			
		||||
          child: Column(
 | 
			
		||||
            mainAxisSize: MainAxisSize.min,
 | 
			
		||||
            children: [
 | 
			
		||||
              const Text("Bitte gib keine geheimen Daten wie z.B. Passwörter weiter!", style: TextStyle(fontSize: 10)),
 | 
			
		||||
              const SizedBox(height: 10),
 | 
			
		||||
              TextField(
 | 
			
		||||
                controller: _feedbackInput,
 | 
			
		||||
                autofocus: true,
 | 
			
		||||
                decoration: InputDecoration(
 | 
			
		||||
                    border: const OutlineInputBorder(),
 | 
			
		||||
                    label: const Text("Dein Feedback"),
 | 
			
		||||
                    errorText: _textFieldEmpty ? "Bitte gib eine Beschreibung an" : null
 | 
			
		||||
                ),
 | 
			
		||||
                minLines: 1,
 | 
			
		||||
                maxLines: 2,
 | 
			
		||||
              ),
 | 
			
		||||
              Row(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
                children: [
 | 
			
		||||
                  TextButton(
 | 
			
		||||
                    onPressed: () async {
 | 
			
		||||
                      if(_isSending) return;
 | 
			
		||||
                      if(_feedbackInput.text.isEmpty) {
 | 
			
		||||
                        setState(() {
 | 
			
		||||
                          _textFieldEmpty = true;
 | 
			
		||||
                        });
 | 
			
		||||
                        return;
 | 
			
		||||
                      }
 | 
			
		||||
 | 
			
		||||
                      setState(() {
 | 
			
		||||
                        _isSending = true;
 | 
			
		||||
                      });
 | 
			
		||||
 | 
			
		||||
                      widget.callback(_feedbackInput.text);
 | 
			
		||||
                    },
 | 
			
		||||
                    child: const Text("Senden"),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 40),
 | 
			
		||||
              const Center(
 | 
			
		||||
                  child: Column(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Text(
 | 
			
		||||
                        "Feedback, mal süß wie Kuchen, mal sauer wie Gurken, doch immer ein Schlüssel fürs Wachsen und Lernen.",
 | 
			
		||||
                        textAlign: TextAlign.center,
 | 
			
		||||
                      ),
 | 
			
		||||
                      SizedBox(height: 10),
 | 
			
		||||
                      Icon(Icons.emoji_objects_outlined)
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:feedback/feedback.dart';
 | 
			
		||||
import 'package:flutter/cupertino.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:loader_overlay/loader_overlay.dart';
 | 
			
		||||
import 'package:package_info/package_info.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../api/mhsl/server/feedback/addFeedback.dart';
 | 
			
		||||
import '../../../../api/mhsl/server/feedback/addFeedbackParams.dart';
 | 
			
		||||
import '../../../../model/accountData.dart';
 | 
			
		||||
import '../../../../widget/infoDialog.dart';
 | 
			
		||||
 | 
			
		||||
class FeedbackSender {
 | 
			
		||||
  static send(BuildContext context, UserFeedback feedback) async {
 | 
			
		||||
    BetterFeedback.of(context).hide();
 | 
			
		||||
    context.loaderOverlay.show();
 | 
			
		||||
    AddFeedbackParams params = AddFeedbackParams(
 | 
			
		||||
        user: AccountData().getUserSecret(),
 | 
			
		||||
        feedback: feedback.text,
 | 
			
		||||
        screenshot: await compute((message) => base64Encode(message), feedback.screenshot),
 | 
			
		||||
        appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber)
 | 
			
		||||
    );
 | 
			
		||||
    AddFeedback(params).run().then((value) {
 | 
			
		||||
      InfoDialog.show(context, "Danke für dein Feedback!");
 | 
			
		||||
      context.loaderOverlay.hide();
 | 
			
		||||
    }).catchError((error, trace) {
 | 
			
		||||
      InfoDialog.show(context, error.toString());
 | 
			
		||||
      context.loaderOverlay.hide();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -24,7 +24,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
 | 
			
		||||
  String getGradeDisplay(int grade) {
 | 
			
		||||
    if(gradeSystem) {
 | 
			
		||||
      return "Note $grade";
 | 
			
		||||
      return 'Note $grade';
 | 
			
		||||
    } else {
 | 
			
		||||
      return "$grade Punkt${grade > 1 ? "e" : ""}";
 | 
			
		||||
    }
 | 
			
		||||
@@ -48,22 +48,22 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
      if(!settings.val().gradeAveragesSettings.askedForPreferredGradeSystem) {
 | 
			
		||||
        settings.val(write: true).gradeAveragesSettings.askedForPreferredGradeSystem = true;
 | 
			
		||||
        showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
          title: const Text("Notensystem"),
 | 
			
		||||
          content: const Text("Wähle dein bevorzugtes Schulnotensystem"),
 | 
			
		||||
          title: const Text('Notensystem'),
 | 
			
		||||
          content: const Text('Wähle dein bevorzugtes Schulnotensystem'),
 | 
			
		||||
          actions: [
 | 
			
		||||
            TextButton(
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                switchSystem(true);
 | 
			
		||||
                Navigator.of(context).pop();
 | 
			
		||||
              },
 | 
			
		||||
              child: const Text("Realschule"),
 | 
			
		||||
              child: const Text('Realschule'),
 | 
			
		||||
            ),
 | 
			
		||||
            TextButton(
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                switchSystem(false);
 | 
			
		||||
                Navigator.of(context).pop();
 | 
			
		||||
              },
 | 
			
		||||
              child: const Text("Oberstufe"),
 | 
			
		||||
              child: const Text('Oberstufe'),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ));
 | 
			
		||||
@@ -81,7 +81,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Notendurschnittsrechner"),
 | 
			
		||||
        title: const Text('Notendurschnittsrechner'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          Visibility(
 | 
			
		||||
            visible: grades.isNotEmpty,
 | 
			
		||||
@@ -89,9 +89,9 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
              showDialog(
 | 
			
		||||
                context: context,
 | 
			
		||||
                builder: (context) => ConfirmDialog(
 | 
			
		||||
                  title: "Zurücksetzen?",
 | 
			
		||||
                  content: "Alle Einträge werden entfernt.",
 | 
			
		||||
                  confirmButton: "Zurücksetzen",
 | 
			
		||||
                  title: 'Zurücksetzen?',
 | 
			
		||||
                  content: 'Alle Einträge werden entfernt.',
 | 
			
		||||
                  confirmButton: 'Zurücksetzen',
 | 
			
		||||
                  onConfirm: () {
 | 
			
		||||
                    grades.clear();
 | 
			
		||||
                    setState(() {});
 | 
			
		||||
@@ -109,7 +109,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
                children: [
 | 
			
		||||
                  Icon(e ? Icons.calculate_outlined : Icons.school_outlined, color: Theme.of(context).colorScheme.onSurface),
 | 
			
		||||
                  const SizedBox(width: 15),
 | 
			
		||||
                  Text(e ? "Notensystem" : "Punktesystem"),
 | 
			
		||||
                  Text(e ? 'Notensystem' : 'Punktesystem'),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            )).toList(),
 | 
			
		||||
@@ -120,9 +120,9 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
                showDialog(
 | 
			
		||||
                  context: context,
 | 
			
		||||
                  builder: (context) => ConfirmDialog(
 | 
			
		||||
                    title: "Notensystem wechseln",
 | 
			
		||||
                    content: "Beim wechsel des Notensystems werden alle Einträge zurückgesetzt.",
 | 
			
		||||
                    confirmButton: "Fortfahren",
 | 
			
		||||
                    title: 'Notensystem wechseln',
 | 
			
		||||
                    content: 'Beim wechsel des Notensystems werden alle Einträge zurückgesetzt.',
 | 
			
		||||
                    confirmButton: 'Fortfahren',
 | 
			
		||||
                    onConfirm: () => switchSystem(e),
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
@@ -142,7 +142,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          const Divider(),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          Text(gradeSystem ? "Wähle unten die Anzahl deiner jewiligen Noten aus" : "Wähle unten die Anzahl deiner jeweiligen Punkte aus"),
 | 
			
		||||
          Text(gradeSystem ? 'Wähle unten die Anzahl deiner jewiligen Noten aus' : 'Wähle unten die Anzahl deiner jeweiligen Punkte aus'),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: ListView.builder(
 | 
			
		||||
@@ -169,7 +169,7 @@ class _GradeAverageState extends State<GradeAverage> {
 | 
			
		||||
                            icon: const Icon(Icons.remove),
 | 
			
		||||
                            color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          ),
 | 
			
		||||
                          Text("${grades.where(isThis).length}", style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
 | 
			
		||||
                          Text('${grades.where(isThis).length}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
 | 
			
		||||
                          IconButton(
 | 
			
		||||
                            onPressed: () {
 | 
			
		||||
                              setState(() {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,23 +42,23 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String parseString(String enDate) {
 | 
			
		||||
    return Jiffy.parse(enDate).format(pattern: "dd.MM.yyyy");
 | 
			
		||||
    return Jiffy.parse(enDate).format(pattern: 'dd.MM.yyyy');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void showDisclaimer() {
 | 
			
		||||
    showDialog(context: context, builder: (context) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        title: const Text("Richtigkeit und Bereitstellung der Daten"),
 | 
			
		||||
        title: const Text('Richtigkeit und Bereitstellung der Daten'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            const Text(""
 | 
			
		||||
                "Sämtliche Datumsangaben sind ohne Gewähr.\n"
 | 
			
		||||
                "Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n"
 | 
			
		||||
                "Die Daten stammen von https://ferien-api.de/"),
 | 
			
		||||
            const Text(''
 | 
			
		||||
                'Sämtliche Datumsangaben sind ohne Gewähr.\n'
 | 
			
		||||
                'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n'
 | 
			
		||||
                'Die Daten stammen von https://ferien-api.de/'),
 | 
			
		||||
            const SizedBox(height: 30),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: const Text("Diese Meldung nicht mehr anzeigen"),
 | 
			
		||||
              title: const Text('Diese Meldung nicht mehr anzeigen'),
 | 
			
		||||
              trailing: Checkbox(
 | 
			
		||||
                value: settings.val().holidaysSettings.dismissedDisclaimer,
 | 
			
		||||
                onChanged: (value) => settings.val(write: true).holidaysSettings.dismissedDisclaimer = value!,
 | 
			
		||||
@@ -67,8 +67,8 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(child: const Text("ferien-api.de besuchen"), onPressed: () => ConfirmDialog.openBrowser(context, "https://ferien-api.de/")),
 | 
			
		||||
          TextButton(child: const Text("Okay"), onPressed: () => Navigator.of(context).pop()),
 | 
			
		||||
          TextButton(child: const Text('ferien-api.de besuchen'), onPressed: () => ConfirmDialog.openBrowser(context, 'https://ferien-api.de/')),
 | 
			
		||||
          TextButton(child: const Text('Okay'), onPressed: () => Navigator.of(context).pop()),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
@@ -78,11 +78,11 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Schulferien in Hessen"),
 | 
			
		||||
        title: const Text('Schulferien in Hessen'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: const Icon(Icons.warning_amber_outlined),
 | 
			
		||||
            onPressed: () => showDisclaimer(),
 | 
			
		||||
            onPressed: showDisclaimer,
 | 
			
		||||
          ),
 | 
			
		||||
          PopupMenuButton<bool>(
 | 
			
		||||
            initialValue: settings.val().holidaysSettings.showPastEvents,
 | 
			
		||||
@@ -95,7 +95,7 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
 | 
			
		||||
                      const SizedBox(width: 15),
 | 
			
		||||
                      Text(e ? "Alle anzeigen" : "Nur zukünftige anzeigen")
 | 
			
		||||
                      Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen')
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
              )).toList();
 | 
			
		||||
@@ -115,19 +115,19 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
        List<GetHolidaysResponseObject> holidays = value.getHolidaysResponse.data;
 | 
			
		||||
        if(!showPastEvents) holidays = holidays.where((element) => DateTime.parse(element.end).isAfter(DateTime.now())).toList();
 | 
			
		||||
 | 
			
		||||
        if(holidays.isEmpty) return const PlaceholderView(icon: Icons.search_off, text: "Es wurden keine Ferieneinträge gefunden!");
 | 
			
		||||
        if(holidays.isEmpty) return const PlaceholderView(icon: Icons.search_off, text: 'Es wurden keine Ferieneinträge gefunden!');
 | 
			
		||||
 | 
			
		||||
        return ListView.builder(
 | 
			
		||||
            itemCount: holidays.length,
 | 
			
		||||
            itemBuilder: (context, index) {
 | 
			
		||||
              GetHolidaysResponseObject holiday = holidays[index];
 | 
			
		||||
              String holidayType = holiday.name.split(" ").first.capitalize();
 | 
			
		||||
              String holidayType = holiday.name.split(' ').first.capitalize();
 | 
			
		||||
              return ListTile(
 | 
			
		||||
                leading: const CenteredLeading(Icon(Icons.calendar_month)),
 | 
			
		||||
                title: Text("$holidayType ab ${parseString(holiday.start)}"),
 | 
			
		||||
                subtitle: Text("bis ${parseString(holiday.end)}"),
 | 
			
		||||
                title: Text('$holidayType ab ${parseString(holiday.start)}'),
 | 
			
		||||
                subtitle: Text('bis ${parseString(holiday.end)}'),
 | 
			
		||||
                onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
 | 
			
		||||
                  title: Text("$holidayType ${holiday.year} in Hessen"),
 | 
			
		||||
                  title: Text('$holidayType ${holiday.year} in Hessen'),
 | 
			
		||||
                  children: [
 | 
			
		||||
                    ListTile(
 | 
			
		||||
                      leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
 | 
			
		||||
@@ -136,11 +136,11 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
                    ),
 | 
			
		||||
                    ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.arrow_forward),
 | 
			
		||||
                      title: Text("vom ${parseString(holiday.start)}"),
 | 
			
		||||
                      title: Text('vom ${parseString(holiday.start)}'),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.arrow_back),
 | 
			
		||||
                      title: Text("bis zum ${parseString(holiday.end)}"),
 | 
			
		||||
                      title: Text('bis zum ${parseString(holiday.end)}'),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Visibility(
 | 
			
		||||
                      visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ class _MessageState extends State<Message> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Marianum Message"),
 | 
			
		||||
        title: const Text('Marianum Message'),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Consumer<MessageProps>(builder: (context, value, child) {
 | 
			
		||||
        if(value.primaryLoading()) return const LoadingSpinner();
 | 
			
		||||
@@ -43,7 +43,7 @@ class _MessageState extends State<Message> {
 | 
			
		||||
                    children: [Icon(Icons.newspaper)],
 | 
			
		||||
                  ),
 | 
			
		||||
                  title: Text(message.name, overflow: TextOverflow.ellipsis),
 | 
			
		||||
                  subtitle: Text("vom ${message.date}"),
 | 
			
		||||
                  subtitle: Text('vom ${message.date}'),
 | 
			
		||||
                  trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: value.getMessagesResponse.base, message: message)));
 | 
			
		||||
 
 | 
			
		||||
@@ -29,12 +29,12 @@ class _MessageViewState extends State<MessageView> {
 | 
			
		||||
          Navigator.of(context).pop();
 | 
			
		||||
          showDialog(context: context, builder: (context) {
 | 
			
		||||
            return AlertDialog(
 | 
			
		||||
              title: const Text("Fehler beim öffnen"),
 | 
			
		||||
              title: const Text('Fehler beim öffnen'),
 | 
			
		||||
              content: Text("Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}"),
 | 
			
		||||
              actions: [
 | 
			
		||||
                TextButton(onPressed: () {
 | 
			
		||||
                  Navigator.of(context).pop();
 | 
			
		||||
                }, child: const Text("Ok"))
 | 
			
		||||
                }, child: const Text('Ok'))
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
@@ -43,9 +43,9 @@ class _MessageViewState extends State<MessageView> {
 | 
			
		||||
          showDialog(
 | 
			
		||||
            context: context,
 | 
			
		||||
            builder: (context) => ConfirmDialog(
 | 
			
		||||
              title: "Link öffnen",
 | 
			
		||||
              content: "Möchtest du den folgenden Link öffnen?\n${e.uri}",
 | 
			
		||||
              confirmButton: "Öffnen",
 | 
			
		||||
              title: 'Link öffnen',
 | 
			
		||||
              content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}',
 | 
			
		||||
              confirmButton: 'Öffnen',
 | 
			
		||||
              onConfirm: () => launchUrl(Uri.parse(e.uri), mode: LaunchMode.externalApplication),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,10 @@ class Roomplan extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Raumplan"),
 | 
			
		||||
        title: const Text('Raumplan'),
 | 
			
		||||
      ),
 | 
			
		||||
      body: PhotoView(
 | 
			
		||||
        imageProvider: Image.asset("assets/img/raumplan.jpg").image,
 | 
			
		||||
        imageProvider: Image.asset('assets/img/raumplan.jpg').image,
 | 
			
		||||
        minScale: 0.5,
 | 
			
		||||
        maxScale: 2.0,
 | 
			
		||||
        backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.background),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,18 +16,18 @@ class _QrShareViewState extends State<QrShareView> {
 | 
			
		||||
      length: 2,
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          title: const Text("Teile die App"),
 | 
			
		||||
          title: const Text('Teile die App'),
 | 
			
		||||
          bottom: const TabBar(
 | 
			
		||||
            tabs: [
 | 
			
		||||
              Tab(icon: Icon(Icons.android_outlined), text: "Android"),
 | 
			
		||||
              Tab(icon: Icon(Icons.apple_outlined), text: "iOS & iPadOS"),
 | 
			
		||||
              Tab(icon: Icon(Icons.android_outlined), text: 'Android'),
 | 
			
		||||
              Tab(icon: Icon(Icons.apple_outlined), text: 'iOS & iPadOS'),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        body: const TabBarView(
 | 
			
		||||
          children: [
 | 
			
		||||
            AppSharePlatformView("Für Android", "https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client"),
 | 
			
		||||
            AppSharePlatformView("Für iOS & iPad", "https://apps.apple.com/us/app/marianum-fulda/id6458789560"),
 | 
			
		||||
            AppSharePlatformView('Für Android', 'https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client'),
 | 
			
		||||
            AppSharePlatformView('Für iOS & iPad', 'https://apps.apple.com/us/app/marianum-fulda/id6458789560'),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:loader_overlay/loader_overlay.dart';
 | 
			
		||||
import 'package:share_plus/share_plus.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../widget/sharePositionOrigin.dart';
 | 
			
		||||
@@ -10,34 +9,32 @@ class SelectShareTypeDialog extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return LoaderOverlay(
 | 
			
		||||
      child: SimpleDialog(
 | 
			
		||||
        children: [
 | 
			
		||||
          ListTile(
 | 
			
		||||
            leading: const Icon(Icons.qr_code_2_outlined),
 | 
			
		||||
            title: const Text("Per QR-Code"),
 | 
			
		||||
            trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrShareView()));
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            leading: const Icon(Icons.link_outlined),
 | 
			
		||||
            title: const Text("Per Link teilen"),
 | 
			
		||||
            trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              Share.share(
 | 
			
		||||
                  sharePositionOrigin: SharePositionOrigin.get(context),
 | 
			
		||||
                  subject: "App Teilen",
 | 
			
		||||
                  "Hol dir die für das Marianum maßgeschneiderte App:"
 | 
			
		||||
                      "\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client "
 | 
			
		||||
                      "\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 "
 | 
			
		||||
                      "\n\nViel Spaß!"
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    return SimpleDialog(
 | 
			
		||||
      children: [
 | 
			
		||||
        ListTile(
 | 
			
		||||
          leading: const Icon(Icons.qr_code_2_outlined),
 | 
			
		||||
          title: const Text('Per QR-Code'),
 | 
			
		||||
          trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrShareView()));
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          leading: const Icon(Icons.link_outlined),
 | 
			
		||||
          title: const Text('Per Link teilen'),
 | 
			
		||||
          trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            Share.share(
 | 
			
		||||
                sharePositionOrigin: SharePositionOrigin.get(context),
 | 
			
		||||
                subject: 'App Teilen',
 | 
			
		||||
                'Hol dir die für das Marianum maßgeschneiderte App:'
 | 
			
		||||
                    '\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client '
 | 
			
		||||
                    '\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 '
 | 
			
		||||
                    '\n\nViel Spaß!'
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        )
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,16 @@
 | 
			
		||||
 | 
			
		||||
import 'package:feedback/feedback.dart';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:in_app_review/in_app_review.dart';
 | 
			
		||||
import '../../extensions/renderNotNull.dart';
 | 
			
		||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
 | 
			
		||||
 | 
			
		||||
import '../../widget/ListItem.dart';
 | 
			
		||||
import '../../widget/centeredLeading.dart';
 | 
			
		||||
import '../../widget/infoDialog.dart';
 | 
			
		||||
import '../settings/settings.dart';
 | 
			
		||||
import 'more/feedback/feedbackSender.dart';
 | 
			
		||||
import 'more/feedback/feedbackDialog.dart';
 | 
			
		||||
import 'more/gradeAverages/gradeAverage.dart';
 | 
			
		||||
import 'more/holidays/holidays.dart';
 | 
			
		||||
import 'more/message/message.dart';
 | 
			
		||||
@@ -21,21 +25,22 @@ class Overhang extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Mehr"),
 | 
			
		||||
        title: const Text('Mehr'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const Settings())), icon: const Icon(Icons.settings))
 | 
			
		||||
          IconButton(onPressed: () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings))
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      body: ListView(
 | 
			
		||||
        children: [
 | 
			
		||||
          const ListItemNavigator(icon: Icons.newspaper, text: "Marianum Message", target: Message()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.room, text: "Raumplan", target: Roomplan()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.calculate, text: "Notendurschnittsrechner", target: GradeAverage()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.calendar_month, text: "Schulferien", target: Holidays()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.newspaper, text: 'Marianum Message', target: Message()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.room, text: 'Raumplan', target: Roomplan()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.calculate, text: 'Notendurschnittsrechner', target: GradeAverage()),
 | 
			
		||||
          const ListItemNavigator(icon: Icons.calendar_month, text: 'Schulferien', target: Holidays()),
 | 
			
		||||
          const Divider(),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            leading: const Icon(Icons.share_outlined),
 | 
			
		||||
            title: const Text("Teile die App"),
 | 
			
		||||
            title: const Text('Teile die App'),
 | 
			
		||||
            subtitle: const Text('Mit Freunden und deiner Klasse teilen'),
 | 
			
		||||
            trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
            onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog())
 | 
			
		||||
          ),
 | 
			
		||||
@@ -44,30 +49,32 @@ class Overhang extends StatelessWidget {
 | 
			
		||||
            builder: (context, snapshot) {
 | 
			
		||||
              if(!snapshot.hasData) return const SizedBox.shrink();
 | 
			
		||||
 | 
			
		||||
              return Visibility(
 | 
			
		||||
                visible: snapshot.requireData,
 | 
			
		||||
                child: ListTile(
 | 
			
		||||
                  leading: const CenteredLeading(Icon(Icons.star_rate_outlined)),
 | 
			
		||||
                  title: const Text("App Bewerten"),
 | 
			
		||||
                  trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    InAppReview.instance.openStoreListing(appStoreId: "6458789560").then(
 | 
			
		||||
                      (value) => InfoDialog.show(context, "Vielen Dank!"),
 | 
			
		||||
              String? getPlatformStoreName() {
 | 
			
		||||
                if(Platform.isAndroid) return 'Play store';
 | 
			
		||||
                if(Platform.isIOS) return 'App store';
 | 
			
		||||
                return null;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              return ListTile(
 | 
			
		||||
                leading: const CenteredLeading(Icon(Icons.star_rate_outlined)),
 | 
			
		||||
                title: const Text('App bewerten'),
 | 
			
		||||
                subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')),
 | 
			
		||||
                trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
 | 
			
		||||
                          (value) => InfoDialog.show(context, 'Vielen Dank!'),
 | 
			
		||||
                      onError: (error) => InfoDialog.show(context, error.toString())
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            leading: const CenteredLeading(Icon(Icons.feedback_outlined)),
 | 
			
		||||
            title: const Text("Du hast eine Idee?"),
 | 
			
		||||
            subtitle: const Text("Fehler und Verbessungsvorschläge"),
 | 
			
		||||
            title: const Text('Du hast eine Idee?'),
 | 
			
		||||
            subtitle: const Text('Fehler und Verbessungsvorschläge'),
 | 
			
		||||
            trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              BetterFeedback.of(context).show((UserFeedback feedback) => FeedbackSender.send(context, feedback));
 | 
			
		||||
            },
 | 
			
		||||
            onTap: () => showDialog(context: context, barrierDismissible: false, builder: (context) => const FeedbackDialog()),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ class _ChatInfoState extends State<ChatInfo> {
 | 
			
		||||
            if(participants != null) ...[
 | 
			
		||||
              ListTile(
 | 
			
		||||
                leading: const Icon(Icons.supervised_user_circle),
 | 
			
		||||
                title: Text("${participants!.data.length} Teilnehmer"),
 | 
			
		||||
                title: Text('${participants!.data.length} Teilnehmer'),
 | 
			
		||||
                trailing: const Icon(Icons.arrow_right),
 | 
			
		||||
                onTap: () => TalkNavigator.pushSplitView(context, ParticipantsListView(participants!)),
 | 
			
		||||
              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ class _ParticipantsListViewState extends State<ParticipantsListView> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text("Teilnehmende"),
 | 
			
		||||
        title: const Text('Teilnehmende'),
 | 
			
		||||
      ),
 | 
			
		||||
      body: ListView(
 | 
			
		||||
        children: widget.participantsResponse.data.map((participant) {
 | 
			
		||||
 
 | 
			
		||||
@@ -40,9 +40,9 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
 | 
			
		||||
        ConfirmDialog(
 | 
			
		||||
          icon: Icons.notifications_active_outlined,
 | 
			
		||||
          title: "Benachrichtigungen aktivieren",
 | 
			
		||||
          content: "Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.",
 | 
			
		||||
          confirmButton: "Weiter",
 | 
			
		||||
          title: 'Benachrichtigungen aktivieren',
 | 
			
		||||
          content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
 | 
			
		||||
          confirmButton: 'Weiter',
 | 
			
		||||
          onConfirm: () {
 | 
			
		||||
            FirebaseMessaging.instance.requestPermission(
 | 
			
		||||
                provisional: false
 | 
			
		||||
@@ -53,7 +53,7 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
                  break;
 | 
			
		||||
                case AuthorizationStatus.denied:
 | 
			
		||||
                  showDialog(context: context, builder: (context) => const AlertDialog(
 | 
			
		||||
                    content: Text("Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren."),
 | 
			
		||||
                    content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
 | 
			
		||||
                  ));
 | 
			
		||||
                  break;
 | 
			
		||||
                default:
 | 
			
		||||
@@ -79,7 +79,7 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
      breakpoint: 1000,
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          title: const Text("Talk"),
 | 
			
		||||
          title: const Text('Talk'),
 | 
			
		||||
          actions: [
 | 
			
		||||
            IconButton(
 | 
			
		||||
              icon: const Icon(Icons.search),
 | 
			
		||||
@@ -91,16 +91,16 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        floatingActionButton: FloatingActionButton(
 | 
			
		||||
          heroTag: "createChat",
 | 
			
		||||
          heroTag: 'createChat',
 | 
			
		||||
          backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
          onPressed: () async {
 | 
			
		||||
            showSearch(context: context, delegate: JoinChat()).then((username) {
 | 
			
		||||
              if(username == null) return;
 | 
			
		||||
 | 
			
		||||
              ConfirmDialog(
 | 
			
		||||
                title: "Chat starten",
 | 
			
		||||
                title: 'Chat starten',
 | 
			
		||||
                content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
 | 
			
		||||
                confirmButton: "Chat starten",
 | 
			
		||||
                confirmButton: 'Chat starten',
 | 
			
		||||
                onConfirm: () {
 | 
			
		||||
                  CreateRoom(CreateRoomParams(
 | 
			
		||||
                    roomType: 1,
 | 
			
		||||
@@ -136,7 +136,10 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
                _query(renew: true);
 | 
			
		||||
                return Future.delayed(const Duration(seconds: 3));
 | 
			
		||||
              },
 | 
			
		||||
              child: ListView(children: chats),
 | 
			
		||||
              child: ListView(
 | 
			
		||||
                padding: EdgeInsets.zero,
 | 
			
		||||
                children: chats
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:loader_overlay/loader_overlay.dart';
 | 
			
		||||
import 'package:marianum_mobile/extensions/dateTime.dart';
 | 
			
		||||
import '../../../extensions/dateTime.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
 | 
			
		||||
@@ -52,8 +51,8 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
          data.getChatResponse.sortByTimestamp().forEach((element) {
 | 
			
		||||
            DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
 | 
			
		||||
 | 
			
		||||
            if(element.systemMessage.contains("reaction")) return;
 | 
			
		||||
            int commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? "0");
 | 
			
		||||
            if(element.systemMessage.contains('reaction')) return;
 | 
			
		||||
            int commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? '0');
 | 
			
		||||
 | 
			
		||||
            if(!elementDate.isSameDay(lastDate)) {
 | 
			
		||||
              lastDate = elementDate;
 | 
			
		||||
@@ -82,8 +81,8 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
              context: context,
 | 
			
		||||
              isSender: false,
 | 
			
		||||
              bubbleData: GetChatResponseObject.getTextDummy(
 | 
			
		||||
                  "Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. "
 | 
			
		||||
                  "Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de"
 | 
			
		||||
                  'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
 | 
			
		||||
                  'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de'
 | 
			
		||||
              ),
 | 
			
		||||
              chatData: widget.room,
 | 
			
		||||
              refetch: _query,
 | 
			
		||||
@@ -92,6 +91,7 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Scaffold(
 | 
			
		||||
          backgroundColor: const Color(0xffefeae2),
 | 
			
		||||
          appBar: ClickableAppBar(
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              TalkNavigator.pushSplitView(context, ChatInfo(widget.room));
 | 
			
		||||
@@ -111,30 +111,31 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
          body: Container(
 | 
			
		||||
            decoration: BoxDecoration(
 | 
			
		||||
              image: DecorationImage(
 | 
			
		||||
                image: const AssetImage("assets/background/chat.png"),
 | 
			
		||||
                image: const AssetImage('assets/background/chat.png'),
 | 
			
		||||
                scale: 1.5,
 | 
			
		||||
                opacity: 1,
 | 
			
		||||
                repeat: ImageRepeat.repeat,
 | 
			
		||||
                invertColors: AppTheme.isDarkMode(context)
 | 
			
		||||
              )
 | 
			
		||||
            ),
 | 
			
		||||
            child: LoaderOverlay(
 | 
			
		||||
              child: data.primaryLoading() ? const LoadingSpinner() : Column(
 | 
			
		||||
                children: [
 | 
			
		||||
                  Expanded(
 | 
			
		||||
                    child: ListView(
 | 
			
		||||
                      reverse: true,
 | 
			
		||||
                      controller: _listController,
 | 
			
		||||
                      children: messages.reversed.toList(),
 | 
			
		||||
                    ),
 | 
			
		||||
            child: data.primaryLoading() ? const LoadingSpinner() : Column(
 | 
			
		||||
              children: [
 | 
			
		||||
                Expanded(
 | 
			
		||||
                  child: ListView(
 | 
			
		||||
                    reverse: true,
 | 
			
		||||
                    controller: _listController,
 | 
			
		||||
                    children: messages.reversed.toList(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Container(
 | 
			
		||||
                    color: Theme.of(context).colorScheme.background,
 | 
			
		||||
                    child: SafeArea(child: ChatTextfield(widget.room.token)),
 | 
			
		||||
                  )
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            )
 | 
			
		||||
                ),
 | 
			
		||||
                Container(
 | 
			
		||||
                  color: Theme.of(context).colorScheme.background,
 | 
			
		||||
                  child: TalkNavigator.isSecondaryVisible(context)
 | 
			
		||||
                      ? ChatTextfield(widget.room.token)
 | 
			
		||||
                      : SafeArea(child: ChatTextfield(widget.room.token)
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:jiffy/jiffy.dart';
 | 
			
		||||
import 'package:marianum_mobile/extensions/text.dart';
 | 
			
		||||
import '../../../../extensions/text.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
 | 
			
		||||
@@ -114,7 +114,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Text timeText = Text(
 | 
			
		||||
      Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: "HH:mm"),
 | 
			
		||||
      Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: 'HH:mm'),
 | 
			
		||||
      textAlign: TextAlign.end,
 | 
			
		||||
      style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize),
 | 
			
		||||
    );
 | 
			
		||||
@@ -184,7 +184,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
          ),
 | 
			
		||||
          onLongPress: () {
 | 
			
		||||
            showDialog(context: context, builder: (context) {
 | 
			
		||||
              List<String> commonReactions = ["👍", "👎", "😆", "❤️", "👀", "🤔"];
 | 
			
		||||
              List<String> commonReactions = ['👍', '👎', '😆', '❤️', '👀', '🤔'];
 | 
			
		||||
              bool canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
 | 
			
		||||
              return SimpleDialog(
 | 
			
		||||
                children: [
 | 
			
		||||
@@ -222,7 +222,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                    visible: canReact,
 | 
			
		||||
                    child: ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.add_reaction_outlined),
 | 
			
		||||
                      title: const Text("Reaktionen"),
 | 
			
		||||
                      title: const Text('Reaktionen'),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions(
 | 
			
		||||
                          token: widget.chatData.token,
 | 
			
		||||
@@ -235,7 +235,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                    visible: !message.containsFile,
 | 
			
		||||
                    child: ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.copy),
 | 
			
		||||
                      title: const Text("Nachricht kopieren"),
 | 
			
		||||
                      title: const Text('Nachricht kopieren'),
 | 
			
		||||
                      onTap: () => {
 | 
			
		||||
                        Clipboard.setData(ClipboardData(text: widget.bubbleData.message)),
 | 
			
		||||
                        Navigator.of(context).pop(),
 | 
			
		||||
@@ -256,7 +256,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                    visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()),
 | 
			
		||||
                    child: ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.delete_outline),
 | 
			
		||||
                      title: const Text("Nachricht löschen"),
 | 
			
		||||
                      title: const Text('Nachricht löschen'),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) {
 | 
			
		||||
                          Provider.of<ChatProps>(context, listen: false).run();
 | 
			
		||||
@@ -276,12 +276,12 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
            if(downloadProgress > 0) {
 | 
			
		||||
              showDialog(context: context, builder: (context) {
 | 
			
		||||
                return AlertDialog(
 | 
			
		||||
                  title: const Text("Download abbrechen?"),
 | 
			
		||||
                  content: const Text("Möchtest du den Download abbrechen?"),
 | 
			
		||||
                  title: const Text('Download abbrechen?'),
 | 
			
		||||
                  content: const Text('Möchtest du den Download abbrechen?'),
 | 
			
		||||
                  actions: [
 | 
			
		||||
                    TextButton(onPressed: () {
 | 
			
		||||
                      Navigator.of(context).pop();
 | 
			
		||||
                    }, child: const Text("Nein")),
 | 
			
		||||
                    }, child: const Text('Nein')),
 | 
			
		||||
                    TextButton(onPressed: () {
 | 
			
		||||
                      downloadCore?.then((value) {
 | 
			
		||||
                        if(!value.isCancelled) value.cancel();
 | 
			
		||||
@@ -291,7 +291,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                        downloadProgress = 0;
 | 
			
		||||
                        downloadCore = null;
 | 
			
		||||
                      });
 | 
			
		||||
                    }, child: const Text("Ja, Abbrechen"))
 | 
			
		||||
                    }, child: const Text('Ja, Abbrechen'))
 | 
			
		||||
                  ],
 | 
			
		||||
                );
 | 
			
		||||
              });
 | 
			
		||||
@@ -336,7 +336,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                  return Container(
 | 
			
		||||
                    margin: const EdgeInsets.only(right: 2.5, left: 2.5),
 | 
			
		||||
                    child: ActionChip(
 | 
			
		||||
                      label: Text("${e.key} ${e.value}"),
 | 
			
		||||
                      label: Text('${e.key} ${e.value}'),
 | 
			
		||||
                      visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity),
 | 
			
		||||
                      padding: EdgeInsets.zero,
 | 
			
		||||
                      backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null,
 | 
			
		||||
 
 | 
			
		||||
@@ -14,14 +14,14 @@ class ChatMessage {
 | 
			
		||||
  Map<String, RichObjectString>? originalData;
 | 
			
		||||
 | 
			
		||||
  RichObjectString? file;
 | 
			
		||||
  String content = "";
 | 
			
		||||
  String content = '';
 | 
			
		||||
 | 
			
		||||
  bool get containsFile => file != null;
 | 
			
		||||
 | 
			
		||||
  ChatMessage({required this.originalMessage, this.originalData}) {
 | 
			
		||||
    if(originalData?.containsKey("file") ?? false) {
 | 
			
		||||
    if(originalData?.containsKey('file') ?? false) {
 | 
			
		||||
      file = originalData?['file'];
 | 
			
		||||
      content = file?.name ?? "Datei";
 | 
			
		||||
      content = file?.name ?? 'Datei';
 | 
			
		||||
    } else {
 | 
			
		||||
      content = RichObjectStringProcessor.parseToString(originalMessage, originalData);
 | 
			
		||||
    }
 | 
			
		||||
@@ -56,7 +56,7 @@ class ChatMessage {
 | 
			
		||||
      fadeInDuration: Duration.zero,
 | 
			
		||||
      fadeOutDuration: Duration.zero,
 | 
			
		||||
      errorListener: (value) {},
 | 
			
		||||
      imageUrl: "https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1",
 | 
			
		||||
      imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1',
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -41,9 +41,9 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    String filename = "${path.split("/").last.split(".").first}-${const Uuid().v4()}.${path.split(".").last}";
 | 
			
		||||
    String shareFolder = "MarianumMobile";
 | 
			
		||||
    String shareFolder = 'MarianumMobile';
 | 
			
		||||
    WebdavApi.webdav.then((webdav) {
 | 
			
		||||
      webdav.mkcol(PathUri.parse("/$shareFolder"));
 | 
			
		||||
      webdav.mkcol(PathUri.parse('/$shareFolder'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    showDialog(context: context, builder: (context) => FileUploadDialog(
 | 
			
		||||
@@ -55,7 +55,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
        FileSharingApi().share(FileSharingApiParams(
 | 
			
		||||
          shareType: 10,
 | 
			
		||||
          shareWith: widget.sendToToken,
 | 
			
		||||
          path: "$shareFolder/$filename",
 | 
			
		||||
          path: '$shareFolder/$filename',
 | 
			
		||||
        )).then((value) => _query());
 | 
			
		||||
      },
 | 
			
		||||
    ), barrierDismissible: false);
 | 
			
		||||
@@ -77,7 +77,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    _textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? "";
 | 
			
		||||
    _textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
 | 
			
		||||
 | 
			
		||||
    return Stack(
 | 
			
		||||
      children: <Widget>[
 | 
			
		||||
@@ -95,12 +95,10 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                        children: [
 | 
			
		||||
                          ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.file_open),
 | 
			
		||||
                            title: const Text("Aus Dateien auswählen"),
 | 
			
		||||
                            title: const Text('Aus Dateien auswählen'),
 | 
			
		||||
                            onTap: () {
 | 
			
		||||
                              context.loaderOverlay.show();
 | 
			
		||||
                              FilePick.documentPick().then((value) {
 | 
			
		||||
                                mediaUpload(value);
 | 
			
		||||
                              });
 | 
			
		||||
                              FilePick.documentPick().then(mediaUpload);
 | 
			
		||||
                              Navigator.of(context).pop();
 | 
			
		||||
                            },
 | 
			
		||||
                          ),
 | 
			
		||||
@@ -108,7 +106,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                            visible: !Platform.isIOS,
 | 
			
		||||
                            child: ListTile(
 | 
			
		||||
                              leading: const Icon(Icons.image),
 | 
			
		||||
                              title: const Text("Aus Gallerie auswählen"),
 | 
			
		||||
                              title: const Text('Aus Gallerie auswählen'),
 | 
			
		||||
                              onTap: () {
 | 
			
		||||
                                context.loaderOverlay.show();
 | 
			
		||||
                                FilePick.galleryPick().then((value) {
 | 
			
		||||
@@ -147,12 +145,12 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                    maxLines: 7,
 | 
			
		||||
                    minLines: 1,
 | 
			
		||||
                    decoration: const InputDecoration(
 | 
			
		||||
                      hintText: "Nachricht schreiben...",
 | 
			
		||||
                      hintText: 'Nachricht schreiben...',
 | 
			
		||||
                      border: InputBorder.none,
 | 
			
		||||
                    ),
 | 
			
		||||
                    onChanged: (String text) {
 | 
			
		||||
                      if(text.trim().toLowerCase() == "marbot marbot marbot") {
 | 
			
		||||
                        var newText = "Roboter sind cool und so, aber Marbots sind besser!";
 | 
			
		||||
                      if(text.trim().toLowerCase() == 'marbot marbot marbot') {
 | 
			
		||||
                        var newText = 'Roboter sind cool und so, aber Marbots sind besser!';
 | 
			
		||||
                        _textBoxController.text = newText;
 | 
			
		||||
                        text = newText;
 | 
			
		||||
                      }
 | 
			
		||||
@@ -175,8 +173,8 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                      setState(() {
 | 
			
		||||
                        isLoading = false;
 | 
			
		||||
                      });
 | 
			
		||||
                      _textBoxController.text = "";
 | 
			
		||||
                      setDraft("");
 | 
			
		||||
                      _textBoxController.text = '';
 | 
			
		||||
                      setDraft('');
 | 
			
		||||
                    });
 | 
			
		||||
                  },
 | 
			
		||||
                  backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user