double tap on messages to show options #63
@@ -28,6 +28,13 @@ linter:
 | 
			
		||||
    prefer_relative_imports: true
 | 
			
		||||
    unnecessary_lambdas: true
 | 
			
		||||
    prefer_single_quotes: true
 | 
			
		||||
    prefer_if_elements_to_conditional_expressions: true
 | 
			
		||||
    prefer_expression_function_bodies: true
 | 
			
		||||
    omit_local_variable_types: true
 | 
			
		||||
    eol_at_end_of_file: true
 | 
			
		||||
    cast_nullable_to_non_nullable: true
 | 
			
		||||
    avoid_void_async: true
 | 
			
		||||
    avoid_multiple_declarations_per_line: true
 | 
			
		||||
 | 
			
		||||
# Additional information about this file can be found at
 | 
			
		||||
# https://dart.dev/guides/language/analysis-options
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,5 @@ class ApiError {
 | 
			
		||||
  ApiError(this.message);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'ApiError: $message';
 | 
			
		||||
  }
 | 
			
		||||
  String toString() => 'ApiError: $message';
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ 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;
 | 
			
		||||
    var response = (await http.get(Uri.parse('https://ferien-api.de/api/v1/holidays/HE'))).body;
 | 
			
		||||
    return GetHolidaysResponse(
 | 
			
		||||
        List<GetHolidaysResponseObject>.from(
 | 
			
		||||
            jsonDecode(response).map<GetHolidaysResponseObject>(
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,5 @@ class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetHolidaysResponse> onLoad() {
 | 
			
		||||
    return GetHolidays().query();
 | 
			
		||||
  }
 | 
			
		||||
  Future<GetHolidaysResponse> onLoad() => GetHolidays().query();
 | 
			
		||||
}
 | 
			
		||||
@@ -2,7 +2,6 @@ import 'dart:convert';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../model/accountData.dart';
 | 
			
		||||
import '../../../model/endpointData.dart';
 | 
			
		||||
@@ -10,7 +9,7 @@ import 'autocompleteResponse.dart';
 | 
			
		||||
 | 
			
		||||
class AutocompleteApi {
 | 
			
		||||
  Future<AutocompleteResponse> find(String query) async {
 | 
			
		||||
    Map<String, dynamic> getParameters = {
 | 
			
		||||
    var getParameters = <String, dynamic>{
 | 
			
		||||
      'search': query,
 | 
			
		||||
      'itemType': ' ',
 | 
			
		||||
      'itemId': ' ',
 | 
			
		||||
@@ -18,15 +17,15 @@ class AutocompleteApi {
 | 
			
		||||
      'limit': '10',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Map<String, String> headers = {};
 | 
			
		||||
    var headers = <String, String>{};
 | 
			
		||||
    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);
 | 
			
		||||
    var 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);
 | 
			
		||||
    var response = await http.get(endpoint, headers: headers);
 | 
			
		||||
    if(response.statusCode != HttpStatus.ok) throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
 | 
			
		||||
    String result = response.body;
 | 
			
		||||
    var result = response.body;
 | 
			
		||||
    return AutocompleteResponse.fromJson(jsonDecode(result)['ocs']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../model/accountData.dart';
 | 
			
		||||
import '../../../model/endpointData.dart';
 | 
			
		||||
@@ -9,12 +8,12 @@ import 'fileSharingApiParams.dart';
 | 
			
		||||
 | 
			
		||||
class FileSharingApi {
 | 
			
		||||
  Future<void> share(FileSharingApiParams query) async {
 | 
			
		||||
    Map<String, String> headers = {};
 | 
			
		||||
    var headers = <String, String>{};
 | 
			
		||||
    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())));
 | 
			
		||||
    Response response = await http.post(endpoint, headers: headers);
 | 
			
		||||
    var 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())));
 | 
			
		||||
    var response = await http.post(endpoint, headers: headers);
 | 
			
		||||
 | 
			
		||||
    if(response.statusCode != HttpStatus.ok) {
 | 
			
		||||
      throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
 | 
			
		||||
 
 | 
			
		||||
@@ -14,13 +14,9 @@ class GetChat extends TalkApi<GetChatResponse> {
 | 
			
		||||
  GetChat(this.chatToken, this.params) : super('v1/chat/$chatToken', null, getParameters: params.toJson());
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return GetChatResponse.fromJson(jsonDecode(raw)['ocs']);
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => GetChatResponse.fromJson(jsonDecode(raw)['ocs']);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) {
 | 
			
		||||
    return http.get(uri, headers: headers);
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers);
 | 
			
		||||
  
 | 
			
		||||
}
 | 
			
		||||
@@ -13,8 +13,7 @@ class GetChatCache extends RequestCache<GetChatResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetChatResponse> onLoad() {
 | 
			
		||||
    return GetChat(
 | 
			
		||||
  Future<GetChatResponse> onLoad() => GetChat(
 | 
			
		||||
      chatToken,
 | 
			
		||||
      GetChatParams(
 | 
			
		||||
        lookIntoFuture: GetChatParamsSwitch.off,
 | 
			
		||||
@@ -22,11 +21,8 @@ class GetChatCache extends RequestCache<GetChatResponse> {
 | 
			
		||||
        limit: 200,
 | 
			
		||||
      )
 | 
			
		||||
    ).run();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetChatResponse onLocalData(String json) {
 | 
			
		||||
    return GetChatResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetChatResponse onLocalData(String json) => GetChatResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -16,7 +16,7 @@ class GetChatResponse extends ApiResponse {
 | 
			
		||||
  Map<String, dynamic> toJson() => _$GetChatResponseToJson(this);
 | 
			
		||||
 | 
			
		||||
  List<GetChatResponseObject> sortByTimestamp() {
 | 
			
		||||
    List<GetChatResponseObject> sorted = data.toList();
 | 
			
		||||
    var sorted = data.toList();
 | 
			
		||||
    sorted.sort((a, b) => a.timestamp.compareTo(b.timestamp));
 | 
			
		||||
    return sorted;
 | 
			
		||||
  }
 | 
			
		||||
@@ -60,12 +60,11 @@ class GetChatResponseObject {
 | 
			
		||||
  Map<String, dynamic> toJson() => _$GetChatResponseObjectToJson(this);
 | 
			
		||||
 | 
			
		||||
  static GetChatResponseObject getDateDummy(int timestamp) {
 | 
			
		||||
    DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
 | 
			
		||||
    var elementDate = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
 | 
			
		||||
    return getTextDummy(Jiffy.parseFromDateTime(elementDate).format(pattern: 'dd.MM.yyyy'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static GetChatResponseObject getTextDummy(String text) {
 | 
			
		||||
    return GetChatResponseObject(
 | 
			
		||||
  static GetChatResponseObject getTextDummy(String text) => GetChatResponseObject(
 | 
			
		||||
        0,
 | 
			
		||||
        '',
 | 
			
		||||
        GetRoomResponseObjectMessageActorType.user,
 | 
			
		||||
@@ -81,13 +80,12 @@ class GetChatResponseObject {
 | 
			
		||||
        null,
 | 
			
		||||
        null
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Map<String, RichObjectString>? _fromJson(json) {
 | 
			
		||||
  if(json is Map<String, dynamic>) {
 | 
			
		||||
    Map<String, RichObjectString> data = {};
 | 
			
		||||
    var data = <String, RichObjectString>{};
 | 
			
		||||
    for (var element in json.keys) {
 | 
			
		||||
      data.putIfAbsent(element, () => RichObjectString.fromJson(json[element]));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,7 @@ class CreateRoom extends TalkApi {
 | 
			
		||||
  CreateRoom(this.params) : super('v4/room', params);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri, Object? body, Map<String, String>? headers) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,13 +10,9 @@ class DeleteMessage extends TalkApi {
 | 
			
		||||
  DeleteMessage(this.chatToken, this.messageId) : super('v1/chat/$chatToken/$messageId', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) {
 | 
			
		||||
    return http.delete(uri, headers: headers);
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) => http.delete(uri, headers: headers);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -11,9 +11,7 @@ class DeleteReactMessage extends TalkApi {
 | 
			
		||||
  DeleteReactMessage({required this.chatToken, required this.messageId, required DeleteReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,13 +10,9 @@ class GetParticipants extends TalkApi<GetParticipantsResponse> {
 | 
			
		||||
  GetParticipants(this.token) : super('v4/room/$token/participants', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetParticipantsResponse assemble(String raw) {
 | 
			
		||||
    return GetParticipantsResponse.fromJson(jsonDecode(raw)['ocs']);
 | 
			
		||||
  }
 | 
			
		||||
  GetParticipantsResponse assemble(String raw) => GetParticipantsResponse.fromJson(jsonDecode(raw)['ocs']);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) {
 | 
			
		||||
    return http.get(uri, headers: headers);
 | 
			
		||||
  }
 | 
			
		||||
  Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -12,15 +12,11 @@ class GetParticipantsCache extends RequestCache<GetParticipantsResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetParticipantsResponse> onLoad() {
 | 
			
		||||
    return GetParticipants(
 | 
			
		||||
  Future<GetParticipantsResponse> onLoad() => GetParticipants(
 | 
			
		||||
        chatToken,
 | 
			
		||||
    ).run();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetParticipantsResponse onLocalData(String json) {
 | 
			
		||||
    return GetParticipantsResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetParticipantsResponse onLocalData(String json) => GetParticipantsResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -13,13 +13,9 @@ class GetReactions extends TalkApi<GetReactionsResponse> {
 | 
			
		||||
  GetReactions({required this.chatToken, required this.messageId}) : super('v1/reaction/$chatToken/$messageId', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return GetReactionsResponse.fromJson(jsonDecode(raw)['ocs']);
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => GetReactionsResponse.fromJson(jsonDecode(raw)['ocs']);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) {
 | 
			
		||||
    return http.get(uri, headers: headers);
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) => http.get(uri, headers: headers);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -9,12 +9,8 @@ class LeaveRoom extends TalkApi {
 | 
			
		||||
  LeaveRoom(this.chatToken) : super('v4/room/$chatToken/participants/self', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) {
 | 
			
		||||
    return http.delete(uri, headers: headers);
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.delete(uri, headers: headers);
 | 
			
		||||
}
 | 
			
		||||
@@ -11,9 +11,7 @@ class ReactMessage extends TalkApi {
 | 
			
		||||
  ReactMessage({required this.chatToken, required this.messageId, required ReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,13 +14,9 @@ class GetRoom extends TalkApi<GetRoomResponse> {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetRoomResponse assemble(String raw) {
 | 
			
		||||
    return GetRoomResponse.fromJson(jsonDecode(raw)['ocs']);
 | 
			
		||||
  }
 | 
			
		||||
  GetRoomResponse assemble(String raw) => GetRoomResponse.fromJson(jsonDecode(raw)['ocs']);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) {
 | 
			
		||||
    return http.get(uri, headers: headers);
 | 
			
		||||
  }
 | 
			
		||||
  Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.get(uri, headers: headers);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -12,16 +12,12 @@ class GetRoomCache extends RequestCache<GetRoomResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetRoomResponse onLocalData(String json) {
 | 
			
		||||
    return GetRoomResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetRoomResponse onLocalData(String json) => GetRoomResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetRoomResponse> onLoad() {
 | 
			
		||||
    return GetRoom(
 | 
			
		||||
  Future<GetRoomResponse> onLoad() => GetRoom(
 | 
			
		||||
      GetRoomParams(
 | 
			
		||||
        includeStatus: true,
 | 
			
		||||
      )
 | 
			
		||||
    ).run();
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -10,9 +10,7 @@ class SendMessage extends TalkApi {
 | 
			
		||||
  SendMessage(this.chatToken, SendMessageParams params) : super('v1/chat/$chatToken', params);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,7 @@ class SetFavorite extends TalkApi {
 | 
			
		||||
  SetFavorite(this.chatToken, this.favoriteState) : super('v4/room/$chatToken/favorite', null);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,7 @@ class SetReadMarker extends TalkApi {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  assemble(String raw) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  assemble(String raw) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) {
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ 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);
 | 
			
		||||
    var 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');
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,5 @@ class TalkError {
 | 
			
		||||
  TalkError(this.status, this.code, this.message);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'Talk - $status - ($code): $message';
 | 
			
		||||
  }
 | 
			
		||||
  String toString() => 'Talk - $status - ($code): $message';
 | 
			
		||||
}
 | 
			
		||||
@@ -13,8 +13,8 @@ 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(CacheableFile.fromDavFile).toSet();
 | 
			
		||||
    var davFiles = (await (await WebdavApi.webdav).propfind(PathUri.parse(params.path))).toWebDavFiles();
 | 
			
		||||
    var files = davFiles.map(CacheableFile.fromDavFile).toSet();
 | 
			
		||||
 | 
			
		||||
    // webdav handles subdirectories wrong, this is a fix
 | 
			
		||||
    // currently this fix is not needed anymore
 | 
			
		||||
 
 | 
			
		||||
@@ -11,19 +11,17 @@ class ListFilesCache extends RequestCache<ListFilesResponse> {
 | 
			
		||||
 | 
			
		||||
  ListFilesCache({required onUpdate, required this.path}) : super(RequestCache.cacheNothing, onUpdate) {
 | 
			
		||||
    var bytes = utf8.encode('MarianumMobile-$path');
 | 
			
		||||
    String cacheName = md5.convert(bytes).toString();
 | 
			
		||||
    var cacheName = md5.convert(bytes).toString();
 | 
			
		||||
    start('MarianumMobile', 'wd-folder-$cacheName');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<ListFilesResponse> onLoad() async {
 | 
			
		||||
    ListFilesResponse data = await ListFiles(ListFilesParams(path)).run();
 | 
			
		||||
    var data = await ListFiles(ListFilesParams(path)).run();
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  ListFilesResponse onLocalData(String json) {
 | 
			
		||||
    return ListFilesResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  ListFilesResponse onLocalData(String json) => ListFilesResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -17,11 +17,7 @@ abstract class WebdavApi<T> extends ApiRequest {
 | 
			
		||||
  static Future<WebDavClient> webdav = establishWebdavConnection();
 | 
			
		||||
  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;
 | 
			
		||||
  }
 | 
			
		||||
  static Future<WebDavClient> establishWebdavConnection() async => 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()}/';
 | 
			
		||||
  }
 | 
			
		||||
  static Future<String> buildWebdavConnectString() async => 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/remote.php/dav/files/${AccountData().getUsername()}/';
 | 
			
		||||
}
 | 
			
		||||
@@ -9,13 +9,9 @@ class GetBreakers extends MhslApi<GetBreakersResponse> {
 | 
			
		||||
  GetBreakers() : super('breaker/');
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetBreakersResponse assemble(String raw) {
 | 
			
		||||
    return GetBreakersResponse.fromJson(jsonDecode(raw));
 | 
			
		||||
  }
 | 
			
		||||
  GetBreakersResponse assemble(String raw) => GetBreakersResponse.fromJson(jsonDecode(raw));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri) {
 | 
			
		||||
    return http.get(uri);
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response>? request(Uri uri) => http.get(uri);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -10,12 +10,8 @@ class GetBreakersCache extends RequestCache<GetBreakersResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetBreakersResponse onLocalData(String json) {
 | 
			
		||||
    return GetBreakersResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetBreakersResponse onLocalData(String json) => GetBreakersResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetBreakersResponse> onLoad() {
 | 
			
		||||
    return GetBreakers().run();
 | 
			
		||||
  }
 | 
			
		||||
  Future<GetBreakersResponse> onLoad() => GetBreakers().run();
 | 
			
		||||
}
 | 
			
		||||
@@ -16,7 +16,7 @@ class AddCustomTimetableEvent extends MhslApi<void> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri) {
 | 
			
		||||
    String body = jsonEncode(params.toJson());
 | 
			
		||||
    var body = jsonEncode(params.toJson());
 | 
			
		||||
    return http.post(uri, body: body);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,12 +12,8 @@ class GetCustomTimetableEvent extends MhslApi<GetCustomTimetableEventResponse> {
 | 
			
		||||
  GetCustomTimetableEvent(this.params) : super('server/timetable/customEvents?user=${params.user}');
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetCustomTimetableEventResponse assemble(String raw) {
 | 
			
		||||
    return GetCustomTimetableEventResponse.fromJson({'events': jsonDecode(raw)});
 | 
			
		||||
  }
 | 
			
		||||
  GetCustomTimetableEventResponse assemble(String raw) => GetCustomTimetableEventResponse.fromJson({'events': jsonDecode(raw)});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri) {
 | 
			
		||||
    return http.get(uri);
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response>? request(Uri uri) => http.get(uri);
 | 
			
		||||
}
 | 
			
		||||
@@ -13,12 +13,8 @@ class GetCustomTimetableEventCache extends RequestCache<GetCustomTimetableEventR
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetCustomTimetableEventResponse> onLoad() {
 | 
			
		||||
    return GetCustomTimetableEvent(params).run();
 | 
			
		||||
  }
 | 
			
		||||
  Future<GetCustomTimetableEventResponse> onLoad() => GetCustomTimetableEvent(params).run();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetCustomTimetableEventResponse onLocalData(String json) {
 | 
			
		||||
    return GetCustomTimetableEventResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetCustomTimetableEventResponse onLocalData(String json) => GetCustomTimetableEventResponse.fromJson(jsonDecode(json));
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,5 @@ class RemoveCustomTimetableEvent extends MhslApi<void> {
 | 
			
		||||
  void assemble(String raw) {}
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri) {
 | 
			
		||||
    return http.delete(uri, body: jsonEncode(params.toJson()));
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response>? request(Uri uri) => http.delete(uri, body: jsonEncode(params.toJson()));
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,5 @@ class UpdateCustomTimetableEvent extends MhslApi<void> {
 | 
			
		||||
  void assemble(String raw) {}
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri) {
 | 
			
		||||
    return http.patch(uri, body: jsonEncode(params.toJson()));
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response>? request(Uri uri) => http.patch(uri, body: jsonEncode(params.toJson()));
 | 
			
		||||
}
 | 
			
		||||
@@ -10,12 +10,8 @@ class GetMessages extends MhslApi<GetMessagesResponse> {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetMessagesResponse assemble(String raw) {
 | 
			
		||||
    return GetMessagesResponse.fromJson(jsonDecode(raw));
 | 
			
		||||
  }
 | 
			
		||||
  GetMessagesResponse assemble(String raw) => GetMessagesResponse.fromJson(jsonDecode(raw));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<http.Response> request(Uri uri) {
 | 
			
		||||
    return http.get(uri);
 | 
			
		||||
  }
 | 
			
		||||
  Future<http.Response> request(Uri uri) => http.get(uri);
 | 
			
		||||
}
 | 
			
		||||
@@ -10,12 +10,8 @@ class GetMessagesCache extends RequestCache<GetMessagesResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetMessagesResponse onLocalData(String json) {
 | 
			
		||||
    return GetMessagesResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetMessagesResponse onLocalData(String json) => GetMessagesResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetMessagesResponse> onLoad() {
 | 
			
		||||
    return GetMessages().run();
 | 
			
		||||
  }
 | 
			
		||||
  Future<GetMessagesResponse> onLoad() => GetMessages().run();
 | 
			
		||||
}
 | 
			
		||||
@@ -15,9 +15,9 @@ abstract class MhslApi<T> extends ApiRequest {
 | 
			
		||||
  T assemble(String raw);
 | 
			
		||||
 | 
			
		||||
  Future<T> run() async {
 | 
			
		||||
    Uri endpoint = Uri.parse('https://mhsl.eu/marianum/marianummobile/$subpath');
 | 
			
		||||
    var endpoint = Uri.parse('https://mhsl.eu/marianum/marianummobile/$subpath');
 | 
			
		||||
 | 
			
		||||
    http.Response? data = await request(endpoint);
 | 
			
		||||
    var data = await request(endpoint);
 | 
			
		||||
    if(data == null) {
 | 
			
		||||
      throw ApiError('Request could not be dispatched!');
 | 
			
		||||
    }
 | 
			
		||||
@@ -31,5 +31,4 @@ abstract class MhslApi<T> extends ApiRequest {
 | 
			
		||||
 | 
			
		||||
  static String dateTimeToJson(DateTime time) => Jiffy.parseFromDateTime(time).format(pattern: 'yyyy-MM-dd HH:mm:ss');
 | 
			
		||||
  static DateTime dateTimeFromJson(String time) => DateTime.parse(time);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -19,7 +19,7 @@ class NotifyRegister extends MhslApi<void> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<http.Response> request(Uri uri) {
 | 
			
		||||
    String requestString = jsonEncode(params.toJson());
 | 
			
		||||
    var requestString = jsonEncode(params.toJson());
 | 
			
		||||
    log(requestString);
 | 
			
		||||
    return http.post(uri, body: requestString);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,5 @@ class AddFeedback extends MhslApi<void> {
 | 
			
		||||
  void assemble(String raw) {}
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Response>? request(Uri uri) {
 | 
			
		||||
    return http.post(uri, body: jsonEncode(params.toJson()));
 | 
			
		||||
  }
 | 
			
		||||
  Future<Response>? request(Uri uri) => http.post(uri, body: jsonEncode(params.toJson()));
 | 
			
		||||
}
 | 
			
		||||
@@ -19,12 +19,12 @@ class UpdateUserIndex extends MhslApi<void> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<http.Response> request(Uri uri) {
 | 
			
		||||
    String data = jsonEncode(params.toJson());
 | 
			
		||||
    var data = jsonEncode(params.toJson());
 | 
			
		||||
    log('Updating userindex:\n $data');
 | 
			
		||||
    return http.post(uri, body: data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static void index() async {
 | 
			
		||||
  static Future<void> index() async {
 | 
			
		||||
    UpdateUserIndex(
 | 
			
		||||
      UpdateUserIndexParams(
 | 
			
		||||
        username: AccountData().getUsername(),
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,8 @@ abstract class RequestCache<T extends ApiResponse?> {
 | 
			
		||||
 | 
			
		||||
  static void ignore(Exception e) {}
 | 
			
		||||
 | 
			
		||||
  void start(String file, String document) async {
 | 
			
		||||
    Map<String, dynamic>? tableData = await Localstore.instance.collection(file).doc(document).get();
 | 
			
		||||
  Future<void> start(String file, String document) async {
 | 
			
		||||
    var tableData = await Localstore.instance.collection(file).doc(document).get();
 | 
			
		||||
    if(tableData != null) {
 | 
			
		||||
      onUpdate(onLocalData(tableData['json']));
 | 
			
		||||
    }
 | 
			
		||||
@@ -31,7 +31,7 @@ abstract class RequestCache<T extends ApiResponse?> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      T newValue = await onLoad();
 | 
			
		||||
      var newValue = await onLoad();
 | 
			
		||||
      onUpdate(newValue);
 | 
			
		||||
 | 
			
		||||
      Localstore.instance.collection(file).doc(document).set({
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ class Authenticate extends WebuntisApi {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<AuthenticateResponse> run() async {
 | 
			
		||||
    awaitingResponse = true;
 | 
			
		||||
    String rawAnswer = await query(this);
 | 
			
		||||
    var rawAnswer = await query(this);
 | 
			
		||||
    AuthenticateResponse response = finalize(AuthenticateResponse.fromJson(jsonDecode(rawAnswer)['result']));
 | 
			
		||||
    _lastResponse = response;
 | 
			
		||||
    if(!awaitedResponse.isCompleted) awaitedResponse.complete();
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ class GetHolidays extends WebuntisApi {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetHolidaysResponse> run() async {
 | 
			
		||||
    String rawAnswer = await query(this);
 | 
			
		||||
    var rawAnswer = await query(this);
 | 
			
		||||
    return finalize(GetHolidaysResponse.fromJson(jsonDecode(rawAnswer)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -17,8 +17,8 @@ class GetHolidays extends WebuntisApi {
 | 
			
		||||
    time = DateTime(time.year, time.month, time.day, 0, 0, 0, 0, 0);
 | 
			
		||||
 | 
			
		||||
    for (var element in holidaysResponse.result) {
 | 
			
		||||
      DateTime start = DateTime.parse(element.startDate.toString());
 | 
			
		||||
      DateTime end = DateTime.parse(element.endDate.toString());
 | 
			
		||||
      var start = DateTime.parse(element.startDate.toString());
 | 
			
		||||
      var end = DateTime.parse(element.endDate.toString());
 | 
			
		||||
 | 
			
		||||
      if(!start.isAfter(time) && !end.isBefore(time)) return element;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,8 @@ class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetHolidaysResponse> onLoad() {
 | 
			
		||||
    return GetHolidays().run();
 | 
			
		||||
  }
 | 
			
		||||
  Future<GetHolidaysResponse> onLoad() => GetHolidays().run();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetHolidaysResponse onLocalData(String json) {
 | 
			
		||||
    return GetHolidaysResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetHolidaysResponse onLocalData(String json) => GetHolidaysResponse.fromJson(jsonDecode(json));
 | 
			
		||||
}
 | 
			
		||||
@@ -9,7 +9,7 @@ class GetRooms extends WebuntisApi {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetRoomsResponse> run() async {
 | 
			
		||||
    String rawAnswer = await query(this);
 | 
			
		||||
    var rawAnswer = await query(this);
 | 
			
		||||
    try {
 | 
			
		||||
      return finalize(GetRoomsResponse.fromJson(jsonDecode(rawAnswer)));
 | 
			
		||||
    } catch(e, trace) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,13 +10,9 @@ class GetRoomsCache extends RequestCache<GetRoomsResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetRoomsResponse> onLoad() {
 | 
			
		||||
    return GetRooms().run();
 | 
			
		||||
  }
 | 
			
		||||
  Future<GetRoomsResponse> onLoad() => GetRooms().run();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetRoomsResponse onLocalData(String json) {
 | 
			
		||||
    return GetRoomsResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetRoomsResponse onLocalData(String json) => GetRoomsResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -8,7 +8,7 @@ class GetSubjects extends WebuntisApi {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetSubjectsResponse> run() async {
 | 
			
		||||
    String rawAnswer = await query(this);
 | 
			
		||||
    var rawAnswer = await query(this);
 | 
			
		||||
    return finalize(GetSubjectsResponse.fromJson(jsonDecode(rawAnswer)));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -10,13 +10,9 @@ class GetSubjectsCache extends RequestCache<GetSubjectsResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetSubjectsResponse> onLoad() {
 | 
			
		||||
    return GetSubjects().run();
 | 
			
		||||
  }
 | 
			
		||||
  Future<GetSubjectsResponse> onLoad() => GetSubjects().run();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  onLocalData(String json) {
 | 
			
		||||
    return GetSubjectsResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  onLocalData(String json) => GetSubjectsResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -11,7 +11,7 @@ class GetTimetable extends WebuntisApi {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetTimetableResponse> run() async {
 | 
			
		||||
    String rawAnswer = await query(this);
 | 
			
		||||
    var rawAnswer = await query(this);
 | 
			
		||||
    return finalize(GetTimetableResponse.fromJson(jsonDecode(rawAnswer)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,13 +15,10 @@ class GetTimetableCache extends RequestCache<GetTimetableResponse> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  GetTimetableResponse onLocalData(String json) {
 | 
			
		||||
    return GetTimetableResponse.fromJson(jsonDecode(json));
 | 
			
		||||
  }
 | 
			
		||||
  GetTimetableResponse onLocalData(String json) => GetTimetableResponse.fromJson(jsonDecode(json));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<GetTimetableResponse> onLoad() async {
 | 
			
		||||
    return GetTimetable(
 | 
			
		||||
  Future<GetTimetableResponse> onLoad() async => GetTimetable(
 | 
			
		||||
        GetTimetableParams(
 | 
			
		||||
            options: GetTimetableParamsOptions(
 | 
			
		||||
                element: GetTimetableParamsOptionsElement(
 | 
			
		||||
@@ -39,4 +36,3 @@ class GetTimetableCache extends RequestCache<GetTimetableResponse> {
 | 
			
		||||
    )
 | 
			
		||||
    ).run();
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -20,13 +20,13 @@ abstract class WebuntisApi extends ApiRequest {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  Future<String> query(WebuntisApi untis) async {
 | 
			
		||||
    String query = '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}';
 | 
			
		||||
    var query = '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}';
 | 
			
		||||
 | 
			
		||||
    String sessionId = '0';
 | 
			
		||||
    var sessionId = '0';
 | 
			
		||||
    if(authenticatedResponse) {
 | 
			
		||||
      sessionId = (await Authenticate.getSession()).sessionId;
 | 
			
		||||
    }
 | 
			
		||||
    http.Response data = await post(query, {'Cookie': 'JSESSIONID=$sessionId'});
 | 
			
		||||
    var data = await post(query, {'Cookie': 'JSESSIONID=$sessionId'});
 | 
			
		||||
    response = data;
 | 
			
		||||
 | 
			
		||||
    dynamic jsonData = jsonDecode(data.body);
 | 
			
		||||
@@ -48,16 +48,12 @@ abstract class WebuntisApi extends ApiRequest {
 | 
			
		||||
 | 
			
		||||
  Future<ApiResponse> run();
 | 
			
		||||
 | 
			
		||||
  String _body() {
 | 
			
		||||
    return genericParam == null ? '{}' : jsonEncode(genericParam);
 | 
			
		||||
  }
 | 
			
		||||
  String _body() => genericParam == null ? '{}' : jsonEncode(genericParam);
 | 
			
		||||
 | 
			
		||||
  Future<http.Response> post(String data, Map<String, String>? headers) async {
 | 
			
		||||
    return await http
 | 
			
		||||
  Future<http.Response> post(String data, Map<String, String>? headers) async => await http
 | 
			
		||||
        .post(endpoint, body: data, headers: headers)
 | 
			
		||||
        .timeout(
 | 
			
		||||
        const Duration(seconds: 10),
 | 
			
		||||
        onTimeout: () => throw WebuntisError('Timeout', 1)
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -5,7 +5,5 @@ class WebuntisError implements Exception {
 | 
			
		||||
  WebuntisError(this.message, this.code);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'WebUntis ($code): $message';
 | 
			
		||||
  }
 | 
			
		||||
  String toString() => 'WebUntis ($code): $message';
 | 
			
		||||
}
 | 
			
		||||
@@ -94,10 +94,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return PersistentTabView(
 | 
			
		||||
  Widget build(BuildContext context) => PersistentTabView(
 | 
			
		||||
      controller: App.bottomNavigator,
 | 
			
		||||
      gestureNavigationEnabled: true,
 | 
			
		||||
      navBarOverlap: const NavBarOverlap.none(),
 | 
			
		||||
      backgroundColor: Theme.of(context).colorScheme.primary,
 | 
			
		||||
 | 
			
		||||
@@ -120,7 +118,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
 | 
			
		||||
            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);
 | 
			
		||||
                var 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),
 | 
			
		||||
@@ -165,7 +163,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,20 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
extension IsSameDay on DateTime {
 | 
			
		||||
  bool isSameDay(DateTime other) {
 | 
			
		||||
    return year == other.year && month == other.month && day == other.day;
 | 
			
		||||
  }
 | 
			
		||||
  bool isSameDay(DateTime other) => year == other.year && month == other.month && day == other.day;
 | 
			
		||||
 | 
			
		||||
  DateTime nextWeekday(int day) {
 | 
			
		||||
    return add(Duration(days: (day - weekday) % DateTime.daysPerWeek));
 | 
			
		||||
  }
 | 
			
		||||
  DateTime nextWeekday(int day) => add(Duration(days: (day - weekday) % DateTime.daysPerWeek));
 | 
			
		||||
 | 
			
		||||
  DateTime withTime(TimeOfDay time) {
 | 
			
		||||
    return copyWith(hour: time.hour, minute: time.minute);
 | 
			
		||||
  }
 | 
			
		||||
  DateTime withTime(TimeOfDay time) => copyWith(hour: time.hour, minute: time.minute);
 | 
			
		||||
 | 
			
		||||
  TimeOfDay toTimeOfDay() {
 | 
			
		||||
    return TimeOfDay(hour: hour, minute: minute);
 | 
			
		||||
  }
 | 
			
		||||
  TimeOfDay toTimeOfDay() => TimeOfDay(hour: hour, minute: minute);
 | 
			
		||||
 | 
			
		||||
  bool isSameDateTime(DateTime other) {
 | 
			
		||||
    bool isSameDay = this.isSameDay(other);
 | 
			
		||||
    bool isSameTimeOfDay = (toTimeOfDay() == other.toTimeOfDay());
 | 
			
		||||
    var isSameDay = this.isSameDay(other);
 | 
			
		||||
    var isSameTimeOfDay = (toTimeOfDay() == other.toTimeOfDay());
 | 
			
		||||
 | 
			
		||||
    return isSameDay && isSameTimeOfDay;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool isSameOrAfter(DateTime other) {
 | 
			
		||||
    return isSameDateTime(other) || isAfter(other);
 | 
			
		||||
  }
 | 
			
		||||
  bool isSameOrAfter(DateTime other) => isSameDateTime(other) || isAfter(other);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
extension RenderNotNullExt<T> on T? {
 | 
			
		||||
  R? wrapNullable<R>(R Function(T data) defaultValueCallback) {
 | 
			
		||||
    return this != null ? defaultValueCallback(this as T) : null;
 | 
			
		||||
  }
 | 
			
		||||
  R? wrapNullable<R>(R Function(T data) defaultValueCallback) => this != null ? defaultValueCallback(this as T) : null;
 | 
			
		||||
}
 | 
			
		||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
extension TextExt on Text {
 | 
			
		||||
  Size get size {
 | 
			
		||||
    final TextPainter textPainter = TextPainter(
 | 
			
		||||
    final textPainter = TextPainter(
 | 
			
		||||
        text: TextSpan(text: data, style: style),
 | 
			
		||||
        maxLines: 1,
 | 
			
		||||
        textDirection: TextDirection.ltr
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,9 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
extension TimeOfDayExt on TimeOfDay {
 | 
			
		||||
  bool isBefore(TimeOfDay other) {
 | 
			
		||||
    return hour < other.hour && minute < other.minute;
 | 
			
		||||
  }
 | 
			
		||||
  bool isBefore(TimeOfDay other) => hour < other.hour && minute < other.minute;
 | 
			
		||||
 | 
			
		||||
  bool isAfter(TimeOfDay other) {
 | 
			
		||||
    return hour > other.hour && minute > other.minute;
 | 
			
		||||
  }
 | 
			
		||||
  bool isAfter(TimeOfDay other) => hour > other.hour && minute > other.minute;
 | 
			
		||||
 | 
			
		||||
  TimeOfDay add({int hours = 0, int minutes = 0}) {
 | 
			
		||||
    return replacing(hour: hour + hours, minute: minute + minutes);
 | 
			
		||||
  }
 | 
			
		||||
  TimeOfDay add({int hours = 0, int minutes = 0}) => replacing(hour: hour + hours, minute: minute + minutes);
 | 
			
		||||
}
 | 
			
		||||
@@ -41,16 +41,14 @@ Future<void> main() async {
 | 
			
		||||
    log('Error initializing Firebase app!');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ByteData data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
 | 
			
		||||
  var data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
 | 
			
		||||
  SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List());
 | 
			
		||||
 | 
			
		||||
  if(kReleaseMode) {
 | 
			
		||||
    ErrorWidget.builder = (error) {
 | 
			
		||||
      return PlaceholderView(
 | 
			
		||||
    ErrorWidget.builder = (error) => PlaceholderView(
 | 
			
		||||
        icon: Icons.phonelink_erase_rounded,
 | 
			
		||||
        text: error.toStringShort(),
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  runApp(
 | 
			
		||||
@@ -101,8 +99,7 @@ class _MainState extends State<Main> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Directionality(
 | 
			
		||||
  Widget build(BuildContext context) => Directionality(
 | 
			
		||||
      textDirection: TextDirection.ltr,
 | 
			
		||||
      child: Consumer<SettingsProvider>(
 | 
			
		||||
        builder: (context, settings, child) {
 | 
			
		||||
@@ -146,7 +143,6 @@ class _MainState extends State<Main> {
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,7 @@ class AccountData {
 | 
			
		||||
  final Future<SharedPreferences> _storage = SharedPreferences.getInstance();
 | 
			
		||||
  Completer<void> _populated = Completer();
 | 
			
		||||
 | 
			
		||||
  factory AccountData() {
 | 
			
		||||
    return _instance;
 | 
			
		||||
  }
 | 
			
		||||
  factory AccountData() => _instance;
 | 
			
		||||
 | 
			
		||||
  AccountData._construct() {
 | 
			
		||||
    _updateFromStorage();
 | 
			
		||||
@@ -38,16 +36,12 @@ class AccountData {
 | 
			
		||||
    return _password!;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String getUserSecret() {
 | 
			
		||||
    return sha512.convert(utf8.encode('${AccountData().getUsername()}:${AccountData().getPassword()}')).toString();
 | 
			
		||||
  }
 | 
			
		||||
  String getUserSecret() => sha512.convert(utf8.encode('${AccountData().getUsername()}:${AccountData().getPassword()}')).toString();
 | 
			
		||||
 | 
			
		||||
  Future<String> getDeviceId() async {
 | 
			
		||||
    return sha512.convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}')).toString();
 | 
			
		||||
  }
 | 
			
		||||
  Future<String> getDeviceId() async => sha512.convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}')).toString();
 | 
			
		||||
 | 
			
		||||
  Future<void> setData(String username, String password) async {
 | 
			
		||||
    SharedPreferences storage = await _storage;
 | 
			
		||||
    var storage = await _storage;
 | 
			
		||||
 | 
			
		||||
    storage.setString(_usernameField, username);
 | 
			
		||||
    storage.setString(_passwordField, password);
 | 
			
		||||
@@ -59,13 +53,13 @@ class AccountData {
 | 
			
		||||
 | 
			
		||||
    if(context != null) Provider.of<AccountModel>(context, listen: false).setState(AccountModelState.loggedOut);
 | 
			
		||||
 | 
			
		||||
    SharedPreferences storage = await _storage;
 | 
			
		||||
    var storage = await _storage;
 | 
			
		||||
    await storage.remove(_usernameField);
 | 
			
		||||
    await storage.remove(_passwordField);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _updateFromStorage() async {
 | 
			
		||||
    SharedPreferences storage = await _storage;
 | 
			
		||||
    var storage = await _storage;
 | 
			
		||||
    //await storage.reload(); // This line was the cause of the first rejected google play upload :(
 | 
			
		||||
    if(storage.containsKey(_usernameField) && storage.containsKey(_passwordField)) {
 | 
			
		||||
      _username = storage.getString(_usernameField);
 | 
			
		||||
@@ -79,9 +73,7 @@ class AccountData {
 | 
			
		||||
    return isPopulated();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool isPopulated() {
 | 
			
		||||
    return _username != null && _password != null;
 | 
			
		||||
  }
 | 
			
		||||
  bool isPopulated() => _username != null && _password != null;
 | 
			
		||||
 | 
			
		||||
  String buildHttpAuthString() {
 | 
			
		||||
    if(!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!');
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,9 @@ class Breaker extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _BreakerState extends State<Breaker> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Consumer<BreakerProps>(
 | 
			
		||||
  Widget build(BuildContext context) => Consumer<BreakerProps>(
 | 
			
		||||
      builder: (context, value, child) {
 | 
			
		||||
        String? blocked = value.isBlocked(widget.breaker);
 | 
			
		||||
        var blocked = value.isBlocked(widget.breaker);
 | 
			
		||||
        if(blocked != null) {
 | 
			
		||||
          return PlaceholderView(icon: Icons.security_outlined, text: "Die App/ Dieser Bereich wurde als Schutzmaßnahme deaktiviert!\n\n${blocked.isEmpty ? "Es wurde vom Server kein Grund übermittelt." : blocked}");
 | 
			
		||||
        }
 | 
			
		||||
@@ -31,4 +30,3 @@ class _BreakerState extends State<Breaker> {
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,13 +18,13 @@ class BreakerProps extends DataHolder {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(primaryLoading()) return null;
 | 
			
		||||
    GetBreakersResponse breakers = _getBreakersResponse!;
 | 
			
		||||
    var breakers = _getBreakersResponse!;
 | 
			
		||||
 | 
			
		||||
    if(breakers.global.areas.contains(type)) return breakers.global.message;
 | 
			
		||||
 | 
			
		||||
    int selfVersion = int.parse(packageInfo!.buildNumber);
 | 
			
		||||
    var selfVersion = int.parse(packageInfo!.buildNumber);
 | 
			
		||||
    for(var key in breakers.regional.keys) {
 | 
			
		||||
      GetBreakersReponseObject value = breakers.regional[key]!;
 | 
			
		||||
      var value = breakers.regional[key]!;
 | 
			
		||||
 | 
			
		||||
      if(int.parse(key.split('b')[1]) >= selfVersion) {
 | 
			
		||||
        if(value.areas.contains(type)) return value.message;
 | 
			
		||||
@@ -35,9 +35,7 @@ class BreakerProps extends DataHolder {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<ApiResponse?> properties() {
 | 
			
		||||
    return [_getBreakersResponse];
 | 
			
		||||
  }
 | 
			
		||||
  List<ApiResponse?> properties() => [_getBreakersResponse];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void run() {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,7 @@ class ChatListProps extends DataHolder {
 | 
			
		||||
  GetRoomResponse get getRoomsResponse => _getRoomResponse!;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<ApiResponse?> properties() {
 | 
			
		||||
    return [_getRoomResponse];
 | 
			
		||||
  }
 | 
			
		||||
  List<ApiResponse?> properties() => [_getRoomResponse];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void run({renew}) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,15 +11,13 @@ class ChatProps extends DataHolder {
 | 
			
		||||
  GetChatResponse get getChatResponse => _getChatResponse!;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<ApiResponse?> properties() {
 | 
			
		||||
    return [_getChatResponse];
 | 
			
		||||
  }
 | 
			
		||||
  List<ApiResponse?> properties() => [_getChatResponse];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void run() {
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
    if(_queryToken.isEmpty) return;
 | 
			
		||||
    DateTime requestStart = DateTime.now();
 | 
			
		||||
    var requestStart = DateTime.now();
 | 
			
		||||
 | 
			
		||||
    GetChatCache(
 | 
			
		||||
      chatToken: _queryToken,
 | 
			
		||||
@@ -39,7 +37,5 @@ class ChatProps extends DataHolder {
 | 
			
		||||
    run();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String currentToken() {
 | 
			
		||||
    return _queryToken;
 | 
			
		||||
  }
 | 
			
		||||
  String currentToken() => _queryToken;
 | 
			
		||||
}
 | 
			
		||||
@@ -3,10 +3,10 @@ import 'package:localstore/localstore.dart';
 | 
			
		||||
import '../widget/debug/cacheView.dart';
 | 
			
		||||
 | 
			
		||||
class DataCleaner {
 | 
			
		||||
  static void cleanOldCache() async {
 | 
			
		||||
  static Future<void> cleanOldCache() async {
 | 
			
		||||
    var cacheData = await Localstore.instance.collection(CacheView.collection).get();
 | 
			
		||||
    cacheData?.forEach((key, value) async {
 | 
			
		||||
      DateTime lastUpdate = DateTime.fromMillisecondsSinceEpoch(value['lastupdate']);
 | 
			
		||||
      var lastUpdate = DateTime.fromMillisecondsSinceEpoch(value['lastupdate']);
 | 
			
		||||
      if(DateTime.now().subtract(const Duration(days: 200)).isAfter(lastUpdate)) {
 | 
			
		||||
        await Localstore.instance.collection(CacheView.collection).doc(key.split('/').last).delete();
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,16 +6,14 @@ import '../api/apiResponse.dart';
 | 
			
		||||
 | 
			
		||||
abstract class DataHolder extends ChangeNotifier {
 | 
			
		||||
 | 
			
		||||
  CollectionRef storage(String path) {
 | 
			
		||||
    return Localstore.instance.collection(path);
 | 
			
		||||
  }
 | 
			
		||||
  CollectionRef storage(String path) => Localstore.instance.collection(path);
 | 
			
		||||
 | 
			
		||||
  void run();
 | 
			
		||||
  List<ApiResponse?> properties();
 | 
			
		||||
 | 
			
		||||
  bool primaryLoading() {
 | 
			
		||||
    // log("${toString()} ${properties().map((e) => e != null ? "1" : "0").join(", ")}");
 | 
			
		||||
    for(ApiResponse? element in properties()) {
 | 
			
		||||
    for(var element in properties()) {
 | 
			
		||||
      if(element == null) return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -23,17 +23,13 @@ class Endpoint {
 | 
			
		||||
 | 
			
		||||
  Endpoint({required this.domain, this.path = ''});
 | 
			
		||||
 | 
			
		||||
  String full() {
 | 
			
		||||
    return domain + path;
 | 
			
		||||
  }
 | 
			
		||||
  String full() => domain + path;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class EndpointData {
 | 
			
		||||
  static final EndpointData _instance = EndpointData._construct();
 | 
			
		||||
 | 
			
		||||
  factory EndpointData() {
 | 
			
		||||
    return _instance;
 | 
			
		||||
  }
 | 
			
		||||
  factory EndpointData() => _instance;
 | 
			
		||||
 | 
			
		||||
  EndpointData._construct();
 | 
			
		||||
 | 
			
		||||
@@ -43,8 +39,7 @@ class EndpointData {
 | 
			
		||||
    return existingName.startsWith('google') ? EndpointMode.stage : EndpointMode.live;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Endpoint webuntis() {
 | 
			
		||||
    return EndpointOptions(
 | 
			
		||||
  Endpoint webuntis() => EndpointOptions(
 | 
			
		||||
      live: Endpoint(
 | 
			
		||||
        domain: 'peleus.webuntis.com',
 | 
			
		||||
      ),
 | 
			
		||||
@@ -53,10 +48,8 @@ class EndpointData {
 | 
			
		||||
        path: '/marianum/marianummobile/webuntis/public/index.php/api'
 | 
			
		||||
      ),
 | 
			
		||||
    ).get(getEndpointMode());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Endpoint nextcloud() {
 | 
			
		||||
    return EndpointOptions(
 | 
			
		||||
  Endpoint nextcloud() => EndpointOptions(
 | 
			
		||||
      live: Endpoint(
 | 
			
		||||
        domain: 'cloud.marianum-fulda.de',
 | 
			
		||||
      ),
 | 
			
		||||
@@ -65,6 +58,5 @@ class EndpointData {
 | 
			
		||||
        path: '/marianum/marianummobile/cloud',
 | 
			
		||||
      )
 | 
			
		||||
    ).get(getEndpointMode());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -23,9 +23,7 @@ class FilesProps extends DataHolder {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<ApiResponse?> properties() {
 | 
			
		||||
    return [_listFilesResponse];
 | 
			
		||||
  }
 | 
			
		||||
  List<ApiResponse?> properties() => [_listFilesResponse];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void run() {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,7 @@ class HolidaysProps extends DataHolder {
 | 
			
		||||
  GetHolidaysResponse get getHolidaysResponse => _getHolidaysResponse!;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<ApiResponse?> properties() {
 | 
			
		||||
    return [_getHolidaysResponse];
 | 
			
		||||
  }
 | 
			
		||||
  List<ApiResponse?> properties() => [_getHolidaysResponse];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void run() {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,7 @@ class MessageProps extends DataHolder {
 | 
			
		||||
  GetMessagesResponse get getMessagesResponse => _getMessagesResponse!;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<ApiResponse?> properties() {
 | 
			
		||||
    return [_getMessagesResponse];
 | 
			
		||||
  }
 | 
			
		||||
  List<ApiResponse?> properties() => [_getMessagesResponse];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void run({renew}) {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,9 +42,7 @@ class TimetableProps extends DataHolder {
 | 
			
		||||
  bool get hasError => error != null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<ApiResponse?> properties() {
 | 
			
		||||
    return [_getTimetableResponse, _getRoomsResponse, _getSubjectsResponse, _getHolidaysResponse, _getCustomTimetableEventResponse];
 | 
			
		||||
  }
 | 
			
		||||
  List<ApiResponse?> properties() => [_getTimetableResponse, _getRoomsResponse, _getSubjectsResponse, _getHolidaysResponse, _getCustomTimetableEventResponse];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void run({renew}) {
 | 
			
		||||
@@ -101,9 +99,7 @@ class TimetableProps extends DataHolder {
 | 
			
		||||
 | 
			
		||||
  DateTime getDate(DateTime d) => DateTime(d.year, d.month, d.day);
 | 
			
		||||
 | 
			
		||||
  bool isWeekend(DateTime queryDate) {
 | 
			
		||||
    return queryDate.weekday == DateTime.saturday || queryDate.weekday == DateTime.sunday;
 | 
			
		||||
  }
 | 
			
		||||
  bool isWeekend(DateTime queryDate) => queryDate.weekday == DateTime.saturday || queryDate.weekday == DateTime.sunday;
 | 
			
		||||
 | 
			
		||||
  void updateWeek(DateTime start, DateTime end) {
 | 
			
		||||
    properties().forEach((element) => element = null);
 | 
			
		||||
@@ -123,7 +119,7 @@ class TimetableProps extends DataHolder {
 | 
			
		||||
    error = null;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
 | 
			
		||||
    DateTime queryWeek = DateTime.now().add(const Duration(days: 2));
 | 
			
		||||
    var queryWeek = DateTime.now().add(const Duration(days: 2));
 | 
			
		||||
 | 
			
		||||
    startDate = getDate(queryWeek.subtract(Duration(days: queryWeek.weekday - 1)));
 | 
			
		||||
    endDate = getDate(queryWeek.add(Duration(days: DateTime.daysPerWeek - queryWeek.weekday)));
 | 
			
		||||
 
 | 
			
		||||
@@ -3,27 +3,25 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
 | 
			
		||||
class NotificationService {
 | 
			
		||||
  static final NotificationService _instance = NotificationService._internal();
 | 
			
		||||
 | 
			
		||||
  factory NotificationService() {
 | 
			
		||||
    return _instance;
 | 
			
		||||
  }
 | 
			
		||||
  factory NotificationService() => _instance;
 | 
			
		||||
 | 
			
		||||
  NotificationService._internal();
 | 
			
		||||
 | 
			
		||||
  FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
 | 
			
		||||
 | 
			
		||||
  Future<void> initializeNotifications() async {
 | 
			
		||||
    const AndroidInitializationSettings androidSettings = AndroidInitializationSettings(
 | 
			
		||||
    const androidSettings = AndroidInitializationSettings(
 | 
			
		||||
        '@mipmap/ic_launcher'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
 | 
			
		||||
    final iosSettings = DarwinInitializationSettings(
 | 
			
		||||
      onDidReceiveLocalNotification: (id, title, body, payload) {
 | 
			
		||||
        // TODO Navigate to Talk section (This runs when an Notification is tapped)
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    final InitializationSettings initializationSettings = InitializationSettings(
 | 
			
		||||
    final initializationSettings = InitializationSettings(
 | 
			
		||||
      android: androidSettings,
 | 
			
		||||
      iOS: iosSettings,
 | 
			
		||||
    );
 | 
			
		||||
@@ -34,7 +32,7 @@ class NotificationService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> showNotification({required String title, required String body, required int badgeCount}) async {
 | 
			
		||||
    const AndroidNotificationDetails androidPlatformChannelSpecifics =
 | 
			
		||||
    const androidPlatformChannelSpecifics =
 | 
			
		||||
    AndroidNotificationDetails(
 | 
			
		||||
      'marmobile',
 | 
			
		||||
      'Marianum Fulda',
 | 
			
		||||
@@ -43,9 +41,9 @@ class NotificationService {
 | 
			
		||||
      ticker: 'Marianum Fulda',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const DarwinNotificationDetails iosPlatformChannelSpecifics = DarwinNotificationDetails();
 | 
			
		||||
    const iosPlatformChannelSpecifics = DarwinNotificationDetails();
 | 
			
		||||
 | 
			
		||||
    const NotificationDetails platformChannelSpecifics = NotificationDetails(
 | 
			
		||||
    const platformChannelSpecifics = NotificationDetails(
 | 
			
		||||
      android: androidPlatformChannelSpecifics,
 | 
			
		||||
      iOS: iosPlatformChannelSpecifics
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,7 @@ import '../storage/base/settingsProvider.dart';
 | 
			
		||||
import '../widget/confirmDialog.dart';
 | 
			
		||||
 | 
			
		||||
class NotifyUpdater {
 | 
			
		||||
  static ConfirmDialog enableAfterDisclaimer(SettingsProvider settings) {
 | 
			
		||||
    return ConfirmDialog(
 | 
			
		||||
  static ConfirmDialog enableAfterDisclaimer(SettingsProvider settings) => ConfirmDialog(
 | 
			
		||||
      title: 'Warnung',
 | 
			
		||||
      icon: Icons.warning_amber,
 | 
			
		||||
      content: ''
 | 
			
		||||
@@ -25,9 +24,8 @@ class NotifyUpdater {
 | 
			
		||||
        NotifyUpdater.registerToServer();
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  static void registerToServer() async {
 | 
			
		||||
    String? fcmToken = await FirebaseMessaging.instance.getToken();
 | 
			
		||||
  static Future<void> registerToServer() async {
 | 
			
		||||
    var fcmToken = await FirebaseMessaging.instance.getToken();
 | 
			
		||||
 | 
			
		||||
    if(fcmToken == null) throw Exception('Failed to register push notification because there is no FBC token!');
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ class SettingsProvider extends ChangeNotifier {
 | 
			
		||||
    _readFromStorage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void reset() async {
 | 
			
		||||
  Future<void> reset() async {
 | 
			
		||||
    _storage = await SharedPreferences.getInstance();
 | 
			
		||||
    _storage.remove(_fieldName);
 | 
			
		||||
    _settings = DefaultSettings.get();
 | 
			
		||||
@@ -38,7 +38,7 @@ class SettingsProvider extends ChangeNotifier {
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _readFromStorage() async {
 | 
			
		||||
  Future<void> _readFromStorage() async {
 | 
			
		||||
    _storage = await SharedPreferences.getInstance();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
@@ -63,7 +63,7 @@ class SettingsProvider extends ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> _mergeSettings(Map<String, dynamic> oldMap, Map<String, dynamic> newMap) {
 | 
			
		||||
    Map<String, dynamic> mergedMap = Map.from(newMap);
 | 
			
		||||
    var mergedMap = Map<String, dynamic>.from(newMap);
 | 
			
		||||
 | 
			
		||||
    oldMap.forEach((key, value) {
 | 
			
		||||
      if (mergedMap.containsKey(key)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,7 @@ class AppTheme {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static bool isDarkMode(BuildContext context) {
 | 
			
		||||
    return Theme.of(context).brightness == Brightness.dark;
 | 
			
		||||
  }
 | 
			
		||||
  static bool isDarkMode(BuildContext context) => Theme.of(context).brightness == Brightness.dark;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ThemeModeDisplay {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,9 +20,7 @@ class Login extends StatefulWidget {
 | 
			
		||||
class _LoginState extends State<Login> {
 | 
			
		||||
  bool displayDisclaimerText = true;
 | 
			
		||||
 | 
			
		||||
  String? _checkInput(value){
 | 
			
		||||
    return (value ?? '').length == 0 ? 'Eingabe erforderlich' : null;
 | 
			
		||||
  }
 | 
			
		||||
  String? _checkInput(value)=> (value ?? '').length == 0 ? 'Eingabe erforderlich' : null;
 | 
			
		||||
 | 
			
		||||
  Future<String?> _login(LoginData data) async {
 | 
			
		||||
    await AccountData().removeData();
 | 
			
		||||
@@ -49,15 +47,10 @@ class _LoginState extends State<Login> {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String> _resetPassword(String name) {
 | 
			
		||||
    return Future.delayed(Duration.zero).then((_) {
 | 
			
		||||
      return 'Diese Funktion steht nicht zur Verfügung!';
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  Future<String> _resetPassword(String name) => Future.delayed(Duration.zero).then((_) => 'Diese Funktion steht nicht zur Verfügung!');
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return FlutterLogin(
 | 
			
		||||
  Widget build(BuildContext context) => FlutterLogin(
 | 
			
		||||
      logo: Image.asset('assets/logo/icon.png').image,
 | 
			
		||||
 | 
			
		||||
      userValidator: _checkInput,
 | 
			
		||||
@@ -110,4 +103,3 @@ class _LoginState extends State<Login> {
 | 
			
		||||
      userType: LoginUserType.name,
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,14 +26,14 @@ class FileElement extends StatefulWidget {
 | 
			
		||||
  const FileElement(this.file, this.path, this.refetch, {super.key});
 | 
			
		||||
 | 
			
		||||
  static Future<DownloaderCore> download(BuildContext context, String remotePath, String name, Function(double) onProgress, Function(OpenResult) onDone) async {
 | 
			
		||||
    Directory paths = await getTemporaryDirectory();
 | 
			
		||||
    var paths = await getTemporaryDirectory();
 | 
			
		||||
    
 | 
			
		||||
    var encodedPath = Uri.encodeComponent(remotePath);
 | 
			
		||||
    encodedPath = encodedPath.replaceAll('%2F', '/');
 | 
			
		||||
 | 
			
		||||
    String local = paths.path + Platform.pathSeparator + name;
 | 
			
		||||
    var local = paths.path + Platform.pathSeparator + name;
 | 
			
		||||
 | 
			
		||||
    DownloaderUtils options = DownloaderUtils(
 | 
			
		||||
    var options = DownloaderUtils(
 | 
			
		||||
      progressCallback: (current, total) {
 | 
			
		||||
        final progress = (current / total) * 100;
 | 
			
		||||
        onProgress(progress);
 | 
			
		||||
@@ -52,7 +52,7 @@ class FileElement extends StatefulWidget {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return await Flowder.download(
 | 
			
		||||
      "${await WebdavApi.webdavConnectString}$encodedPath",
 | 
			
		||||
      '${await WebdavApi.webdavConnectString}$encodedPath',
 | 
			
		||||
      options,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -89,8 +89,7 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return ListTile(
 | 
			
		||||
  Widget build(BuildContext context) => ListTile(
 | 
			
		||||
      leading: CenteredLeading(
 | 
			
		||||
          Icon(widget.file.isDirectory ? Icons.folder : Icons.description_outlined)
 | 
			
		||||
      ),
 | 
			
		||||
@@ -100,9 +99,7 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
      onTap: () {
 | 
			
		||||
        if(widget.file.isDirectory) {
 | 
			
		||||
          Navigator.of(context).push(MaterialPageRoute(
 | 
			
		||||
            builder: (context) {
 | 
			
		||||
              return Files(widget.path.toList()..add(widget.file.name));
 | 
			
		||||
            },
 | 
			
		||||
            builder: (context) => Files(widget.path.toList()..add(widget.file.name)),
 | 
			
		||||
          ));
 | 
			
		||||
        } else {
 | 
			
		||||
          if(EndpointData().getEndpointMode() == EndpointMode.stage) {
 | 
			
		||||
@@ -141,12 +138,10 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
            setState(() => percent = progress);
 | 
			
		||||
          }, (result) {
 | 
			
		||||
            if(result.type != ResultType.done) {
 | 
			
		||||
              showDialog(context: context, builder: (context) {
 | 
			
		||||
                return AlertDialog(
 | 
			
		||||
              showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
                  title: const Text('Download'),
 | 
			
		||||
                  content: Text(result.message),
 | 
			
		||||
                );
 | 
			
		||||
              });
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setState(() {
 | 
			
		||||
@@ -158,8 +153,7 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onLongPress: () {
 | 
			
		||||
        showDialog(context: context, builder: (context) {
 | 
			
		||||
          return SimpleDialog(
 | 
			
		||||
        showDialog(context: context, builder: (context) => SimpleDialog(
 | 
			
		||||
            children: [
 | 
			
		||||
              ListTile(
 | 
			
		||||
                leading: const Icon(Icons.delete_outline),
 | 
			
		||||
@@ -189,9 +183,7 @@ class _FileElementState extends State<FileElement> {
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
          ));
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,233 +0,0 @@
 | 
			
		||||
import 'dart:developer';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:async/async.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:nextcloud/nextcloud.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../api/marianumcloud/webdav/webdavApi.dart';
 | 
			
		||||
 | 
			
		||||
class FileUploadDialog extends StatefulWidget {
 | 
			
		||||
  final String localPath;
 | 
			
		||||
  final List<String> remotePath;
 | 
			
		||||
  final String fileName;
 | 
			
		||||
  final void Function() onUploadFinished;
 | 
			
		||||
 | 
			
		||||
  final bool doShowFinish;
 | 
			
		||||
 | 
			
		||||
  const FileUploadDialog({super.key, required this.localPath, required this.remotePath, required this.fileName, required this.onUploadFinished, this.doShowFinish = true});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<FileUploadDialog> createState() => _FileUploadDialogState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _FileUploadDialogState extends State<FileUploadDialog> {
 | 
			
		||||
  FileUploadState state = FileUploadState.naming;
 | 
			
		||||
  CancelableOperation? cancelableOperation;
 | 
			
		||||
  late String targetFileName;
 | 
			
		||||
  late String remoteFolderName;
 | 
			
		||||
  late String fullRemotePath = "${widget.remotePath.join("/")}/$targetFileName";
 | 
			
		||||
  String? lastError;
 | 
			
		||||
 | 
			
		||||
  TextEditingController fileNameController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  void upload({bool override = false}) async {
 | 
			
		||||
    setState(() {
 | 
			
		||||
      state = FileUploadState.upload;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    WebDavClient webdavClient = await WebdavApi.webdav;
 | 
			
		||||
 | 
			
		||||
    if(!override) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        state = FileUploadState.checkConflict;
 | 
			
		||||
      });
 | 
			
		||||
      List<WebDavResponse> result = (await webdavClient.propfind(PathUri.parse(widget.remotePath.join('/')))).responses;
 | 
			
		||||
      if(result.any((element) => element.href!.endsWith('/$targetFileName'))) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          state = FileUploadState.conflict;
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      } else {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          state = FileUploadState.upload;
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Future<HttpClientResponse> uploadTask = webdavClient.putFile(File(widget.localPath), FileStat.statSync(widget.localPath), PathUri.parse(fullRemotePath)); // TODO use onProgress from putFile
 | 
			
		||||
    uploadTask.then(Future<HttpClientResponse?>.value).catchError((e) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        state = FileUploadState.error;
 | 
			
		||||
      });
 | 
			
		||||
      return null;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    cancelableOperation = CancelableOperation<HttpClientResponse>.fromFuture(
 | 
			
		||||
      uploadTask,
 | 
			
		||||
      onCancel: () => log('Upload cancelled'),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    cancelableOperation!.then((value) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        state = FileUploadState.done;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void cancel() {
 | 
			
		||||
    cancelableOperation?.cancel();
 | 
			
		||||
    setState(() {
 | 
			
		||||
      state = FileUploadState.naming;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    targetFileName = widget.fileName;
 | 
			
		||||
    remoteFolderName = widget.remotePath.isNotEmpty ? widget.remotePath.last : '/';
 | 
			
		||||
    fileNameController.text = widget.fileName;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if(state == FileUploadState.naming) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        title: const Text('Datei hochladen'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            TextField(
 | 
			
		||||
              controller: fileNameController,
 | 
			
		||||
              onChanged: (input) {
 | 
			
		||||
                targetFileName = input;
 | 
			
		||||
              },
 | 
			
		||||
              autocorrect: false,
 | 
			
		||||
              decoration: const InputDecoration(
 | 
			
		||||
                labelText: 'Dateiname',
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text('Abbrechen')),
 | 
			
		||||
          TextButton(onPressed: () async {
 | 
			
		||||
            upload();
 | 
			
		||||
          }, child: const Text('Hochladen')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(state == FileUploadState.conflict) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.error_outline),
 | 
			
		||||
        title: const Text('Datei konflikt'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text("Es gibt bereits eine Datei mit dem Namen $targetFileName in dem ausgewählten Ordner '$remoteFolderName'", textAlign: TextAlign.center),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            setState(() {
 | 
			
		||||
              state = FileUploadState.naming;
 | 
			
		||||
            });
 | 
			
		||||
          }, child: const Text('Datei umbenennen')),
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            upload(override: true);
 | 
			
		||||
          }, child: const Text('Datei überschreiben')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(state == FileUploadState.upload || state == FileUploadState.checkConflict) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.upload),
 | 
			
		||||
        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),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(height: 30),
 | 
			
		||||
            const CircularProgressIndicator()
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: const [
 | 
			
		||||
          // TODO implement working upload cancelling
 | 
			
		||||
          // TextButton(onPressed: () {
 | 
			
		||||
          //   cancel();
 | 
			
		||||
          // }, child: const Text("Abbrechen")),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(state == FileUploadState.done) {
 | 
			
		||||
      widget.onUploadFinished();
 | 
			
		||||
      if(!widget.doShowFinish) {
 | 
			
		||||
        Navigator.of(context).pop();
 | 
			
		||||
        return const SizedBox.shrink();
 | 
			
		||||
      }
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.done),
 | 
			
		||||
        title: const Text('Upload fertig'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text("Die Datei wurde erfolgreich nach '$remoteFolderName' hochgeladen!", textAlign: TextAlign.center),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text('Fertig')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(state == FileUploadState.error) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
        icon: const Icon(Icons.error_outline),
 | 
			
		||||
        title: const Text('Fehler'),
 | 
			
		||||
        content: const Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text('Es ist ein Fehler aufgetreten!', textAlign: TextAlign.center),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          }, child: const Text('Schlißen')),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw UnimplementedError('Invalid state');
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum FileUploadState {
 | 
			
		||||
  naming,
 | 
			
		||||
  checkConflict,
 | 
			
		||||
  conflict,
 | 
			
		||||
  upload,
 | 
			
		||||
  done,
 | 
			
		||||
  error
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'dart:io';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:loader_overlay/loader_overlay.dart';
 | 
			
		||||
import 'package:nextcloud/nextcloud.dart';
 | 
			
		||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
 | 
			
		||||
@@ -15,8 +16,8 @@ import '../../../storage/base/settingsProvider.dart';
 | 
			
		||||
import '../../../widget/loadingSpinner.dart';
 | 
			
		||||
import '../../../widget/placeholderView.dart';
 | 
			
		||||
import '../../../widget/filePick.dart';
 | 
			
		||||
import 'fileUploadDialog.dart';
 | 
			
		||||
import 'fileElement.dart';
 | 
			
		||||
import 'filesUploadDialog.dart';
 | 
			
		||||
 | 
			
		||||
class Files extends StatefulWidget {
 | 
			
		||||
  final List<String> path;
 | 
			
		||||
@@ -64,9 +65,7 @@ class SortOptions {
 | 
			
		||||
    )
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static BetterSortOption getOption(SortOption option) {
 | 
			
		||||
    return options[option]!;
 | 
			
		||||
  }
 | 
			
		||||
  static BetterSortOption getOption(SortOption option) => options[option]!;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _FilesState extends State<Files> {
 | 
			
		||||
@@ -90,7 +89,6 @@ class _FilesState extends State<Files> {
 | 
			
		||||
    ListFilesCache(
 | 
			
		||||
        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());
 | 
			
		||||
          setState(() {
 | 
			
		||||
            data = d;
 | 
			
		||||
@@ -99,9 +97,21 @@ class _FilesState extends State<Files> {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> mediaUpload(List<String>? paths) async {
 | 
			
		||||
    if(paths == null) return;
 | 
			
		||||
 | 
			
		||||
    pushScreen(
 | 
			
		||||
      context,
 | 
			
		||||
      withNavBar: false,
 | 
			
		||||
      screen: FilesUploadDialog(filePaths: paths, remotePath: widget.path.join('/'), onUploadFinished: (uploadedFilePaths) => _query()),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    List<CacheableFile> files = data?.sortBy(
 | 
			
		||||
    var files = data?.sortBy(
 | 
			
		||||
      sortOption: currentSort,
 | 
			
		||||
      foldersToTop: Provider.of<SettingsProvider>(context).val().fileSettings.sortFoldersToTop,
 | 
			
		||||
      reversed: currentSortDirection
 | 
			
		||||
@@ -119,8 +129,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
          // ),
 | 
			
		||||
          PopupMenuButton<bool>(
 | 
			
		||||
            icon: Icon(currentSortDirection ? Icons.text_rotate_up : Icons.text_rotation_down),
 | 
			
		||||
            itemBuilder: (context) {
 | 
			
		||||
              return [true, false].map((e) => PopupMenuItem<bool>(
 | 
			
		||||
            itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
 | 
			
		||||
                  value: e,
 | 
			
		||||
                  enabled: e != currentSortDirection,
 | 
			
		||||
                  child: Row(
 | 
			
		||||
@@ -130,8 +139,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                      Text(e ? 'Aufsteigend' : 'Absteigend')
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
              )).toList();
 | 
			
		||||
            },
 | 
			
		||||
              )).toList(),
 | 
			
		||||
            onSelected: (e) {
 | 
			
		||||
              setState(() {
 | 
			
		||||
                currentSortDirection = e;
 | 
			
		||||
@@ -141,19 +149,17 @@ class _FilesState extends State<Files> {
 | 
			
		||||
          ),
 | 
			
		||||
          PopupMenuButton<SortOption>(
 | 
			
		||||
            icon: const Icon(Icons.sort),
 | 
			
		||||
            itemBuilder: (context) {
 | 
			
		||||
              return SortOptions.options.keys.map((key) => PopupMenuItem<SortOption>(
 | 
			
		||||
            itemBuilder: (context) => SortOptions.options.keys.map((key) => PopupMenuItem<SortOption>(
 | 
			
		||||
                  value: key,
 | 
			
		||||
                  enabled: key != currentSort,
 | 
			
		||||
                  child: Row(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Icon(SortOptions.getOption(key).icon, color: Theme.of(context).colorScheme.onSurface),
 | 
			
		||||
                      const SizedBox(width: 15),
 | 
			
		||||
                      Text(SortOptions.getOption(key).displayName)
 | 
			
		||||
                      Text(SortOptions.getOption(key).displayName),
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
              )).toList();
 | 
			
		||||
            },
 | 
			
		||||
              )).toList(),
 | 
			
		||||
            onSelected: (e) {
 | 
			
		||||
              setState(() {
 | 
			
		||||
                currentSort = e;
 | 
			
		||||
@@ -167,8 +173,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
        heroTag: 'uploadFile',
 | 
			
		||||
        backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          showDialog(context: context, builder: (context) {
 | 
			
		||||
            return SimpleDialog(
 | 
			
		||||
          showDialog(context: context, builder: (context) => SimpleDialog(
 | 
			
		||||
              children: [
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  leading: const Icon(Icons.create_new_folder_outlined),
 | 
			
		||||
@@ -204,7 +209,6 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                  leading: const Icon(Icons.upload_file),
 | 
			
		||||
                  title: const Text('Aus Dateien hochladen'),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    context.loaderOverlay.show();
 | 
			
		||||
                    FilePick.documentPick().then(mediaUpload);
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
                  },
 | 
			
		||||
@@ -215,17 +219,15 @@ class _FilesState extends State<Files> {
 | 
			
		||||
                    leading: const Icon(Icons.add_a_photo_outlined),
 | 
			
		||||
                    title: const Text('Aus Gallerie hochladen'),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      context.loaderOverlay.show();
 | 
			
		||||
                      FilePick.galleryPick().then((value) {
 | 
			
		||||
                        mediaUpload(value?.path);
 | 
			
		||||
                      FilePick.multipleGalleryPick().then((value) {
 | 
			
		||||
                        if(value != null) mediaUpload(value.map((e) => e.path).toList());
 | 
			
		||||
                      });
 | 
			
		||||
                      Navigator.of(context).pop();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
            ));
 | 
			
		||||
        },
 | 
			
		||||
        child: const Icon(Icons.add),
 | 
			
		||||
      ),
 | 
			
		||||
@@ -239,7 +241,7 @@ class _FilesState extends State<Files> {
 | 
			
		||||
            padding: EdgeInsets.zero,
 | 
			
		||||
            itemCount: files.length,
 | 
			
		||||
            itemBuilder: (context, index) {
 | 
			
		||||
              CacheableFile file = files.toList()[index];
 | 
			
		||||
              var file = files.toList()[index];
 | 
			
		||||
              return FileElement(file, widget.path, _query);
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
@@ -247,15 +249,4 @@ class _FilesState extends State<Files> {
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void mediaUpload(String? path) async {
 | 
			
		||||
    context.loaderOverlay.hide();
 | 
			
		||||
 | 
			
		||||
    if(path == null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var fileName = path.split(Platform.pathSeparator).last;
 | 
			
		||||
    showDialog(context: context, builder: (context) => FileUploadDialog(localPath: path, remotePath: widget.path, fileName: fileName, onUploadFinished: _query), barrierDismissible: false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										311
									
								
								lib/view/pages/files/filesUploadDialog.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								lib/view/pages/files/filesUploadDialog.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,311 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:loader_overlay/loader_overlay.dart';
 | 
			
		||||
import 'package:nextcloud/nextcloud.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../api/marianumcloud/webdav/webdavApi.dart';
 | 
			
		||||
import '../../../widget/confirmDialog.dart';
 | 
			
		||||
import '../../../widget/focusBehaviour.dart';
 | 
			
		||||
 | 
			
		||||
class FilesUploadDialog extends StatefulWidget {
 | 
			
		||||
  final List<String> filePaths;
 | 
			
		||||
  final String remotePath;
 | 
			
		||||
  final void Function(List<String> uploadedFilePaths) onUploadFinished;
 | 
			
		||||
  final bool uniqueNames;
 | 
			
		||||
 | 
			
		||||
  const FilesUploadDialog({super.key, required this.filePaths, required this.remotePath, required this.onUploadFinished, this.uniqueNames = false});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<FilesUploadDialog> createState() => _FilesUploadDialogState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class UploadableFile {
 | 
			
		||||
  TextEditingController fileNameController = TextEditingController();
 | 
			
		||||
  String filePath;
 | 
			
		||||
  String fileName;
 | 
			
		||||
  double? _uploadProgress;
 | 
			
		||||
  bool isConflicting = false;
 | 
			
		||||
 | 
			
		||||
  UploadableFile(this.filePath, this.fileName);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class _FilesUploadDialogState extends State<FilesUploadDialog> {
 | 
			
		||||
  late List<UploadableFile> _uploadableFiles;
 | 
			
		||||
  bool _isUploading = false;
 | 
			
		||||
  double _overallProgressValue = 0.0;
 | 
			
		||||
  String _infoText = '';
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
 | 
			
		||||
    _uploadableFiles = widget.filePaths.map((filePath) {
 | 
			
		||||
      var fileName = filePath.split(Platform.pathSeparator).last;
 | 
			
		||||
      return UploadableFile(filePath, fileName);
 | 
			
		||||
    }).toList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void showHttpErrorCode(int httpErrorCode){
 | 
			
		||||
    showDialog(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (BuildContext context) => AlertDialog(
 | 
			
		||||
          title: const Text('Ein Fehler ist aufgetreten'),
 | 
			
		||||
          contentPadding: const EdgeInsets.all(10),
 | 
			
		||||
          content: Text('Error code: $httpErrorCode'),
 | 
			
		||||
          actions: [
 | 
			
		||||
            TextButton(
 | 
			
		||||
              onPressed: () => Navigator.of(context).pop(),
 | 
			
		||||
              child: const Text('Schließen', textAlign: TextAlign.center),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> uploadFiles({bool override = false}) async {
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _isUploading = true;
 | 
			
		||||
      _infoText = 'Vorbereiten';
 | 
			
		||||
      for (var file in _uploadableFiles) {
 | 
			
		||||
        file.isConflicting = false;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    var webdavClient = await WebdavApi.webdav;
 | 
			
		||||
 | 
			
		||||
    if (!override) {
 | 
			
		||||
      var result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses;
 | 
			
		||||
      var conflictingFiles = _uploadableFiles.where((file) {
 | 
			
		||||
        var fileName = file.fileName;
 | 
			
		||||
        return result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName'));
 | 
			
		||||
      }).toList();
 | 
			
		||||
 | 
			
		||||
      if(conflictingFiles.isNotEmpty) {
 | 
			
		||||
        bool replaceFiles = await showDialog(
 | 
			
		||||
          context: context,
 | 
			
		||||
          barrierDismissible: false,
 | 
			
		||||
          builder: (context) => AlertDialog(
 | 
			
		||||
              contentPadding: const EdgeInsets.all(10),
 | 
			
		||||
              title: const Text('Konflikt', textAlign: TextAlign.center),
 | 
			
		||||
              content: conflictingFiles.length == 1 ?
 | 
			
		||||
              Text(
 | 
			
		||||
                'Eine Datei mit dem Namen "${conflictingFiles.map((e) => e.fileName).first}" existiert bereits.',
 | 
			
		||||
                textAlign: TextAlign.left,
 | 
			
		||||
              ) :
 | 
			
		||||
              SingleChildScrollView(
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  '${conflictingFiles.length} Dateien mit folgenden Namen existieren bereits: \n${conflictingFiles.map((e) => '\n -  ${e.fileName}').join('')}',
 | 
			
		||||
                  textAlign: TextAlign.left,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              actions: [
 | 
			
		||||
                TextButton(
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    Navigator.pop(context, false);
 | 
			
		||||
                  },
 | 
			
		||||
                  child: const Text('Bearbeiten', textAlign: TextAlign.center),
 | 
			
		||||
                ),
 | 
			
		||||
                TextButton(
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    showDialog(
 | 
			
		||||
                      context: context,
 | 
			
		||||
                      builder: (context) => ConfirmDialog(
 | 
			
		||||
                        title: 'Bestätigen?',
 | 
			
		||||
                        content: 'Bist du sicher, dass du ${conflictingFiles.length} Dateien überschreiben möchtest?',
 | 
			
		||||
                        onConfirm: () {
 | 
			
		||||
                          Navigator.pop(context, true);
 | 
			
		||||
                        },
 | 
			
		||||
                        confirmButton: 'Ja',
 | 
			
		||||
                        cancelButton: 'Nein',
 | 
			
		||||
                        ),
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                  },
 | 
			
		||||
                  child: const Text('Überschreiben', textAlign: TextAlign.center),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if(!replaceFiles) {
 | 
			
		||||
          setState(() {
 | 
			
		||||
            _isUploading = false;
 | 
			
		||||
            _overallProgressValue = 0.0;
 | 
			
		||||
            _infoText = '';
 | 
			
		||||
            for (var element in conflictingFiles) {
 | 
			
		||||
              element.isConflicting = true;
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var uploadetFilePaths = <String>[];
 | 
			
		||||
    for (var file in _uploadableFiles) {
 | 
			
		||||
      var fileName = file.fileName;
 | 
			
		||||
      var filePath = file.filePath;
 | 
			
		||||
 | 
			
		||||
      if(widget.uniqueNames) fileName = '${fileName.split('.').first}-${const Uuid().v4()}.${fileName.split('.').last}';
 | 
			
		||||
 | 
			
		||||
      var fullRemotePath = '${widget.remotePath}/$fileName';
 | 
			
		||||
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}';
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      var uploadTask = await webdavClient.putFile(
 | 
			
		||||
        File(filePath),
 | 
			
		||||
        FileStat.statSync(filePath),
 | 
			
		||||
        PathUri.parse(fullRemotePath),
 | 
			
		||||
        onProgress: (progress) {
 | 
			
		||||
          setState(() {
 | 
			
		||||
            file._uploadProgress = progress;
 | 
			
		||||
            _overallProgressValue = ((progress + _uploadableFiles.indexOf(file)) / _uploadableFiles.length).toDouble();
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if(uploadTask.statusCode < 200 || uploadTask.statusCode > 299) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          _isUploading = false;
 | 
			
		||||
          _overallProgressValue = 0.0;
 | 
			
		||||
          _infoText = '';
 | 
			
		||||
        });
 | 
			
		||||
        Navigator.of(context).pop();
 | 
			
		||||
        showHttpErrorCode(uploadTask.statusCode);
 | 
			
		||||
      } else {
 | 
			
		||||
        uploadetFilePaths.add(fullRemotePath);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _isUploading = false;
 | 
			
		||||
      _overallProgressValue = 0.0;
 | 
			
		||||
      _infoText = '';
 | 
			
		||||
    });
 | 
			
		||||
    Navigator.of(context).pop();
 | 
			
		||||
    widget.onUploadFinished(uploadetFilePaths);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('Dateien hochladen'),
 | 
			
		||||
        automaticallyImplyLeading: false,
 | 
			
		||||
      ),
 | 
			
		||||
      body: LoaderOverlay(
 | 
			
		||||
        overlayWholeScreen: true,
 | 
			
		||||
        child: Column(
 | 
			
		||||
          children: [
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: ListView.builder(
 | 
			
		||||
                itemCount: _uploadableFiles.length,
 | 
			
		||||
                itemBuilder: (context, index) {
 | 
			
		||||
                  final currentFile = _uploadableFiles[index];
 | 
			
		||||
                  currentFile.fileNameController.text = currentFile.fileName;
 | 
			
		||||
                  return ListTile(
 | 
			
		||||
                    title: TextField(
 | 
			
		||||
                      readOnly: _isUploading,
 | 
			
		||||
                      controller: currentFile.fileNameController,
 | 
			
		||||
                      decoration: InputDecoration(
 | 
			
		||||
                        border: const UnderlineInputBorder(),
 | 
			
		||||
                        label: Text('Datei ${index+1}'),
 | 
			
		||||
                        errorText: currentFile.isConflicting ? 'existiert bereits' : null,
 | 
			
		||||
                        errorStyle: const TextStyle(color: Colors.red),
 | 
			
		||||
                      ),
 | 
			
		||||
                      onChanged: (input) {
 | 
			
		||||
                        currentFile.fileName = input;
 | 
			
		||||
                      },
 | 
			
		||||
                      onTapOutside: (PointerDownEvent event) {
 | 
			
		||||
                        FocusBehaviour.textFieldTapOutside(context);
 | 
			
		||||
                        if(currentFile.isConflicting){
 | 
			
		||||
                          setState(() {
 | 
			
		||||
                            currentFile.isConflicting = false;
 | 
			
		||||
                          });
 | 
			
		||||
                        }
 | 
			
		||||
                      },
 | 
			
		||||
                      onEditingComplete: () {
 | 
			
		||||
                        if(currentFile.isConflicting){
 | 
			
		||||
                          setState(() {
 | 
			
		||||
                            currentFile.isConflicting = false;
 | 
			
		||||
                          });
 | 
			
		||||
                        }
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    subtitle: _isUploading && (currentFile._uploadProgress ?? 0) < 1 ? LinearProgressIndicator(
 | 
			
		||||
                      value: currentFile._uploadProgress,
 | 
			
		||||
                      borderRadius: const BorderRadius.all(Radius.circular(2)),
 | 
			
		||||
                    ) : null,
 | 
			
		||||
                    trailing: Container(
 | 
			
		||||
                      width: 24,
 | 
			
		||||
                      height: 24,
 | 
			
		||||
                      padding: EdgeInsets.zero,
 | 
			
		||||
                      child: IconButton(
 | 
			
		||||
                        tooltip: 'Datei entfernen',
 | 
			
		||||
                        padding: EdgeInsets.zero,
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          if(!_isUploading) {
 | 
			
		||||
                            if(_uploadableFiles.length-1 <= 0) Navigator.of(context).pop();
 | 
			
		||||
                            setState(() {
 | 
			
		||||
                              _uploadableFiles.removeAt(index);
 | 
			
		||||
                            });
 | 
			
		||||
                          }
 | 
			
		||||
                        },
 | 
			
		||||
                        icon: const Icon(Icons.delete_outlined),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(left: 15, right: 15, bottom: 15, top: 5),
 | 
			
		||||
              child: Row(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
                children: [
 | 
			
		||||
                  Visibility(
 | 
			
		||||
                    visible: !_isUploading,
 | 
			
		||||
                    child: TextButton(
 | 
			
		||||
                      onPressed: () => Navigator.of(context).pop(),
 | 
			
		||||
                      child: const Text('Abbrechen'),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const Expanded(child: SizedBox.shrink()),
 | 
			
		||||
                  Visibility(
 | 
			
		||||
                    visible: _isUploading,
 | 
			
		||||
                    replacement: TextButton(
 | 
			
		||||
                      onPressed: () => uploadFiles(override: widget.uniqueNames),
 | 
			
		||||
                      child: const Text('Hochladen'),
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: Visibility(
 | 
			
		||||
                      visible: _infoText.length < 5,
 | 
			
		||||
                      replacement: Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(_infoText),
 | 
			
		||||
                          const SizedBox(width: 15),
 | 
			
		||||
                          CircularProgressIndicator(value: _overallProgressValue),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                      child: Stack(
 | 
			
		||||
                        alignment: Alignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          CircularProgressIndicator(value: _overallProgressValue),
 | 
			
		||||
                          Center(child: Text(_infoText)),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -44,8 +44,7 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('Feedback'),
 | 
			
		||||
      ),
 | 
			
		||||
@@ -125,7 +124,7 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
 | 
			
		||||
                    onPressed: () async {
 | 
			
		||||
                      context.loaderOverlay.show();
 | 
			
		||||
                      var imageData = await (await FilePick.galleryPick())?.readAsBytes();
 | 
			
		||||
                      context.loaderOverlay.hide();
 | 
			
		||||
                      if(context.mounted) context.loaderOverlay.hide();
 | 
			
		||||
                      setState(() {
 | 
			
		||||
                        _image = imageData;
 | 
			
		||||
                      });
 | 
			
		||||
@@ -170,6 +169,4 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:jiffy/jiffy.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../api/holidays/getHolidaysResponse.dart';
 | 
			
		||||
import '../../../../model/holidays/holidaysProps.dart';
 | 
			
		||||
import '../../../../storage/base/settingsProvider.dart';
 | 
			
		||||
import '../../../../widget/centeredLeading.dart';
 | 
			
		||||
@@ -22,9 +21,7 @@ class Holidays extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension StringExtension on String {
 | 
			
		||||
  String capitalize() {
 | 
			
		||||
    return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
 | 
			
		||||
  }
 | 
			
		||||
  String capitalize() => '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _HolidaysState extends State<Holidays> {
 | 
			
		||||
@@ -41,13 +38,10 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
    super.initState();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String parseString(String enDate) {
 | 
			
		||||
    return Jiffy.parse(enDate).format(pattern: 'dd.MM.yyyy');
 | 
			
		||||
  }
 | 
			
		||||
  String parseString(String enDate) => Jiffy.parse(enDate).format(pattern: 'dd.MM.yyyy');
 | 
			
		||||
 | 
			
		||||
  void showDisclaimer() {
 | 
			
		||||
    showDialog(context: context, builder: (context) {
 | 
			
		||||
      return AlertDialog(
 | 
			
		||||
    showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
        title: const Text('Richtigkeit und Bereitstellung der Daten'),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
@@ -70,13 +64,11 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
          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()),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
      ));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('Schulferien in Hessen'),
 | 
			
		||||
        actions: [
 | 
			
		||||
@@ -87,8 +79,7 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
          PopupMenuButton<bool>(
 | 
			
		||||
            initialValue: settings.val().holidaysSettings.showPastEvents,
 | 
			
		||||
            icon: const Icon(Icons.manage_history_outlined),
 | 
			
		||||
            itemBuilder: (context) {
 | 
			
		||||
              return [true, false].map((e) => PopupMenuItem<bool>(
 | 
			
		||||
            itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
 | 
			
		||||
                  value: e,
 | 
			
		||||
                  enabled: e != showPastEvents,
 | 
			
		||||
                  child: Row(
 | 
			
		||||
@@ -98,8 +89,7 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
                      Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen')
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
              )).toList();
 | 
			
		||||
            },
 | 
			
		||||
              )).toList(),
 | 
			
		||||
            onSelected: (e) {
 | 
			
		||||
              setState(() {
 | 
			
		||||
                showPastEvents = e;
 | 
			
		||||
@@ -112,7 +102,7 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
      body: Consumer<HolidaysProps>(builder: (context, value, child) {
 | 
			
		||||
        if(value.primaryLoading()) return const LoadingSpinner();
 | 
			
		||||
 | 
			
		||||
        List<GetHolidaysResponseObject> holidays = value.getHolidaysResponse.data;
 | 
			
		||||
        var 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!');
 | 
			
		||||
@@ -120,8 +110,8 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
        return ListView.builder(
 | 
			
		||||
            itemCount: holidays.length,
 | 
			
		||||
            itemBuilder: (context, index) {
 | 
			
		||||
              GetHolidaysResponseObject holiday = holidays[index];
 | 
			
		||||
              String holidayType = holiday.name.split(' ').first.capitalize();
 | 
			
		||||
              var holiday = holidays[index];
 | 
			
		||||
              var holidayType = holiday.name.split(' ').first.capitalize();
 | 
			
		||||
              return ListTile(
 | 
			
		||||
                leading: const CenteredLeading(Icon(Icons.calendar_month)),
 | 
			
		||||
                title: Text('$holidayType ab ${parseString(holiday.start)}'),
 | 
			
		||||
@@ -165,4 +155,3 @@ class _HolidaysState extends State<Holidays> {
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../api/mhsl/message/getMessages/getMessagesResponse.dart';
 | 
			
		||||
import '../../../../model/message/messageProps.dart';
 | 
			
		||||
import '../../../../widget/loadingSpinner.dart';
 | 
			
		||||
import 'messageView.dart';
 | 
			
		||||
@@ -24,8 +23,7 @@ class _MessageState extends State<Message> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('Marianum Message'),
 | 
			
		||||
      ),
 | 
			
		||||
@@ -36,7 +34,7 @@ class _MessageState extends State<Message> {
 | 
			
		||||
          child: ListView.builder(
 | 
			
		||||
              itemCount: value.getMessagesResponse.messages.length,
 | 
			
		||||
              itemBuilder: (context, index) {
 | 
			
		||||
                GetMessagesResponseObject message = value.getMessagesResponse.messages.toList()[index];
 | 
			
		||||
                var message = value.getMessagesResponse.messages.toList()[index];
 | 
			
		||||
                return ListTile(
 | 
			
		||||
                  leading: const Column(
 | 
			
		||||
                    mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
@@ -59,4 +57,3 @@ class _MessageState extends State<Message> {
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,7 @@ class MessageView extends StatefulWidget {
 | 
			
		||||
class _MessageViewState extends State<MessageView> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(widget.message.name),
 | 
			
		||||
      ),
 | 
			
		||||
@@ -27,8 +26,7 @@ class _MessageViewState extends State<MessageView> {
 | 
			
		||||
        enableHyperlinkNavigation: true,
 | 
			
		||||
        onDocumentLoadFailed: (PdfDocumentLoadFailedDetails e) {
 | 
			
		||||
          Navigator.of(context).pop();
 | 
			
		||||
          showDialog(context: context, builder: (context) {
 | 
			
		||||
            return AlertDialog(
 | 
			
		||||
          showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
              title: const Text('Fehler beim öffnen'),
 | 
			
		||||
              content: Text("Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}"),
 | 
			
		||||
              actions: [
 | 
			
		||||
@@ -36,8 +34,7 @@ class _MessageViewState extends State<MessageView> {
 | 
			
		||||
                  Navigator.of(context).pop();
 | 
			
		||||
                }, child: const Text('Ok'))
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
            ));
 | 
			
		||||
        },
 | 
			
		||||
        onHyperlinkClicked: (PdfHyperlinkClickedDetails e) {
 | 
			
		||||
          showDialog(
 | 
			
		||||
@@ -53,4 +50,3 @@ class _MessageViewState extends State<MessageView> {
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,7 @@ class Roomplan extends StatelessWidget {
 | 
			
		||||
  const Roomplan({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('Raumplan'),
 | 
			
		||||
      ),
 | 
			
		||||
@@ -18,4 +17,3 @@ class Roomplan extends StatelessWidget {
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ class AppSharePlatformView extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    Color foregroundColor = Theme.of(context).colorScheme.onBackground;
 | 
			
		||||
    var foregroundColor = Theme.of(context).colorScheme.onBackground;
 | 
			
		||||
    return Column(
 | 
			
		||||
      mainAxisSize: MainAxisSize.min,
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,7 @@ class QrShareView extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _QrShareViewState extends State<QrShareView> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return DefaultTabController(
 | 
			
		||||
  Widget build(BuildContext context) => DefaultTabController(
 | 
			
		||||
      length: 2,
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
@@ -33,4 +32,3 @@ class _QrShareViewState extends State<QrShareView> {
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,7 @@ class SelectShareTypeDialog extends StatelessWidget {
 | 
			
		||||
  const SelectShareTypeDialog({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return SimpleDialog(
 | 
			
		||||
  Widget build(BuildContext context) => SimpleDialog(
 | 
			
		||||
      children: [
 | 
			
		||||
        ListTile(
 | 
			
		||||
          leading: const Icon(Icons.qr_code_2_outlined),
 | 
			
		||||
@@ -37,4 +36,3 @@ class SelectShareTypeDialog extends StatelessWidget {
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,7 @@ class Overhang extends StatelessWidget {
 | 
			
		||||
  const Overhang({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('Mehr'),
 | 
			
		||||
        actions: [
 | 
			
		||||
@@ -80,4 +78,3 @@ class Overhang extends StatelessWidget {
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ class _ChatInfoState extends State<ChatInfo> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    bool isGroup = widget.room.type != GetRoomResponseObjectConversationType.oneToOne;
 | 
			
		||||
    var isGroup = widget.room.type != GetRoomResponseObjectConversationType.oneToOne;
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(widget.room.displayName),
 | 
			
		||||
 
 | 
			
		||||
@@ -13,20 +13,16 @@ class ParticipantsListView extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _ParticipantsListViewState extends State<ParticipantsListView> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('Teilnehmende'),
 | 
			
		||||
      ),
 | 
			
		||||
      body: ListView(
 | 
			
		||||
        children: widget.participantsResponse.data.map((participant) {
 | 
			
		||||
          return ListTile(
 | 
			
		||||
        children: widget.participantsResponse.data.map((participant) => ListTile(
 | 
			
		||||
            leading: UserAvatar(id: participant.actorId),
 | 
			
		||||
            title: Text(participant.displayName),
 | 
			
		||||
            subtitle: participant.statusMessage != null ? Text(participant.statusMessage!) : null,
 | 
			
		||||
          );
 | 
			
		||||
        }).toList(),
 | 
			
		||||
          )).toList(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -119,14 +119,14 @@ class _ChatListState extends State<ChatList> {
 | 
			
		||||
 | 
			
		||||
            if(data.primaryLoading()) return const LoadingSpinner();
 | 
			
		||||
            latestData = data;
 | 
			
		||||
            List<ChatTile> chats = [];
 | 
			
		||||
            var chats = <ChatTile>[];
 | 
			
		||||
            for (var chatRoom in data.getRoomsResponse.sortBy(
 | 
			
		||||
              lastActivity: true,
 | 
			
		||||
              favoritesToTop: Provider.of<SettingsProvider>(context).val().talkSettings.sortFavoritesToTop,
 | 
			
		||||
              unreadToTop: Provider.of<SettingsProvider>(context).val().talkSettings.sortUnreadToTop,
 | 
			
		||||
            )
 | 
			
		||||
            ) {
 | 
			
		||||
              bool hasDraft = settings.val().talkSettings.drafts.containsKey(chatRoom.token);
 | 
			
		||||
              var hasDraft = settings.val().talkSettings.drafts.containsKey(chatRoom.token);
 | 
			
		||||
              chats.add(ChatTile(data: chatRoom, query: _query, hasDraft: hasDraft));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,19 +40,18 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Consumer<ChatProps>(
 | 
			
		||||
  Widget build(BuildContext context) => Consumer<ChatProps>(
 | 
			
		||||
      builder: (context, data, child) {
 | 
			
		||||
        List<Widget> messages = List<Widget>.empty(growable: true);
 | 
			
		||||
        var messages = List<Widget>.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
        if(!data.primaryLoading()) {
 | 
			
		||||
 | 
			
		||||
          DateTime lastDate = DateTime.now();
 | 
			
		||||
          var lastDate = DateTime.now();
 | 
			
		||||
          data.getChatResponse.sortByTimestamp().forEach((element) {
 | 
			
		||||
            DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
 | 
			
		||||
            var 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');
 | 
			
		||||
            var commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? '0');
 | 
			
		||||
 | 
			
		||||
            if(!elementDate.isSameDay(lastDate)) {
 | 
			
		||||
              lastDate = elementDate;
 | 
			
		||||
@@ -141,4 +140,3 @@ class _ChatViewState extends State<ChatView> {
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,15 +51,13 @@ class ChatBubble extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
 | 
			
		||||
  BubbleStyle getSystemStyle() {
 | 
			
		||||
    return BubbleStyle(
 | 
			
		||||
  BubbleStyle getSystemStyle() => BubbleStyle(
 | 
			
		||||
      color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white,
 | 
			
		||||
      borderWidth: 1,
 | 
			
		||||
      elevation: 2,
 | 
			
		||||
      margin: const BubbleEdges.only(bottom: 20, top: 10),
 | 
			
		||||
      alignment: Alignment.center,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  BubbleStyle getRemoteStyle(bool seamless) {
 | 
			
		||||
    var color = AppTheme.isDarkMode(context) ? const Color(0xff202c33) : Colors.white;
 | 
			
		||||
@@ -101,10 +99,93 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void showOptionsDialog() {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters);
 | 
			
		||||
    var showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
 | 
			
		||||
    var showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system;
 | 
			
		||||
 | 
			
		||||
    var actorText = Text(
 | 
			
		||||
      widget.bubbleData.actorDisplayName,
 | 
			
		||||
      textAlign: TextAlign.start,
 | 
			
		||||
      overflow: TextOverflow.ellipsis,
 | 
			
		||||
      style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    var timeText = Text(
 | 
			
		||||
      Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: 'HH:mm'),
 | 
			
		||||
      textAlign: TextAlign.end,
 | 
			
		||||
      style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      mainAxisSize: MainAxisSize.min,
 | 
			
		||||
      mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
      textDirection: TextDirection.ltr,
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.end,
 | 
			
		||||
 | 
			
		||||
      children: [
 | 
			
		||||
        GestureDetector(
 | 
			
		||||
          child: Bubble(
 | 
			
		||||
            style: getStyle(),
 | 
			
		||||
            child: Container(
 | 
			
		||||
              constraints: BoxConstraints(
 | 
			
		||||
                maxWidth: MediaQuery.of(context).size.width * 0.9,
 | 
			
		||||
                minWidth: showActorDisplayName
 | 
			
		||||
                    ? actorText.size.width
 | 
			
		||||
                    : timeText.size.width + (widget.isSender ? widget.spacing + widget.timeIconSize : 0) + 3,
 | 
			
		||||
              ),
 | 
			
		||||
              child: Stack(
 | 
			
		||||
                children: [
 | 
			
		||||
                  Padding(
 | 
			
		||||
                      padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0),
 | 
			
		||||
                      child: message.getWidget()
 | 
			
		||||
                  ),
 | 
			
		||||
                  Visibility(
 | 
			
		||||
                    visible: showActorDisplayName,
 | 
			
		||||
                    child: Positioned(
 | 
			
		||||
                      top: 0,
 | 
			
		||||
                      left: 0,
 | 
			
		||||
                      child: actorText
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Visibility(
 | 
			
		||||
                    visible: showBubbleTime,
 | 
			
		||||
                    child: Positioned(
 | 
			
		||||
                      bottom: 0,
 | 
			
		||||
                      right: 0,
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          timeText,
 | 
			
		||||
                          if(widget.isSender) ...[
 | 
			
		||||
                            SizedBox(width: widget.spacing),
 | 
			
		||||
                            if(widget.isRead)
 | 
			
		||||
                              Icon(Icons.done_all_outlined, size: widget.timeIconSize, color: widget.timeIconColor)
 | 
			
		||||
                            else
 | 
			
		||||
                              Icon(Icons.done_outlined, size: widget.timeIconSize, color: widget.timeIconColor)
 | 
			
		||||
                          ]
 | 
			
		||||
                        ],
 | 
			
		||||
                      )
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Visibility(
 | 
			
		||||
                    visible: downloadProgress > 0,
 | 
			
		||||
                    child: Positioned(
 | 
			
		||||
                      bottom: 0,
 | 
			
		||||
                      right: 0,
 | 
			
		||||
                      left: 0,
 | 
			
		||||
                      child: LinearProgressIndicator(value: downloadProgress/100),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          onLongPress: () {
 | 
			
		||||
            showDialog(context: context, builder: (context) {
 | 
			
		||||
      List<String> commonReactions = ['👍', '👎', '😆', '❤️', '👀'];
 | 
			
		||||
      bool canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
 | 
			
		||||
              var commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
 | 
			
		||||
              var canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
 | 
			
		||||
              return SimpleDialog(
 | 
			
		||||
                children: [
 | 
			
		||||
                  Visibility(
 | 
			
		||||
@@ -134,8 +215,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                            ),
 | 
			
		||||
                            IconButton(
 | 
			
		||||
                              onPressed: () {
 | 
			
		||||
                        showDialog(context: context, builder: (context) {
 | 
			
		||||
                          return AlertDialog(
 | 
			
		||||
                                showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
                                    contentPadding: const EdgeInsets.all(15),
 | 
			
		||||
                                    titlePadding: const EdgeInsets.only(left: 6, top: 15),
 | 
			
		||||
                                    title: Row(
 | 
			
		||||
@@ -195,8 +275,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                                        ],
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                          );
 | 
			
		||||
                        });
 | 
			
		||||
                                  ));
 | 
			
		||||
                              },
 | 
			
		||||
                              style: IconButton.styleFrom(
 | 
			
		||||
                                padding: EdgeInsets.zero,
 | 
			
		||||
@@ -262,44 +341,12 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                ],
 | 
			
		||||
              );
 | 
			
		||||
            });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters);
 | 
			
		||||
    bool showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
 | 
			
		||||
    bool showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system;
 | 
			
		||||
 | 
			
		||||
    Text actorText = Text(
 | 
			
		||||
      widget.bubbleData.actorDisplayName,
 | 
			
		||||
      textAlign: TextAlign.start,
 | 
			
		||||
      overflow: TextOverflow.ellipsis,
 | 
			
		||||
      style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Text timeText = Text(
 | 
			
		||||
      Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: 'HH:mm'),
 | 
			
		||||
      textAlign: TextAlign.end,
 | 
			
		||||
      style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      mainAxisSize: MainAxisSize.min,
 | 
			
		||||
      mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
      textDirection: TextDirection.ltr,
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.end,
 | 
			
		||||
 | 
			
		||||
      children: [
 | 
			
		||||
        GestureDetector(
 | 
			
		||||
          onDoubleTap: showOptionsDialog,
 | 
			
		||||
          onLongPress: showOptionsDialog,
 | 
			
		||||
          },
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            if(message.file == null) return;
 | 
			
		||||
 | 
			
		||||
            if(downloadProgress > 0) {
 | 
			
		||||
              showDialog(context: context, builder: (context) {
 | 
			
		||||
                return AlertDialog(
 | 
			
		||||
              showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
                  title: const Text('Download abbrechen?'),
 | 
			
		||||
                  content: const Text('Möchtest du den Download abbrechen?'),
 | 
			
		||||
                  actions: [
 | 
			
		||||
@@ -317,8 +364,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                      });
 | 
			
		||||
                    }, child: const Text('Ja, Abbrechen'))
 | 
			
		||||
                  ],
 | 
			
		||||
                );
 | 
			
		||||
              });
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
@@ -336,69 +382,12 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
              });
 | 
			
		||||
 | 
			
		||||
              if(result.type != ResultType.done) {
 | 
			
		||||
                showDialog(context: context, builder: (context) {
 | 
			
		||||
                  return AlertDialog(
 | 
			
		||||
                showDialog(context: context, builder: (context) => AlertDialog(
 | 
			
		||||
                    content: Text(result.message),
 | 
			
		||||
                  );
 | 
			
		||||
                });
 | 
			
		||||
                  ));
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
          child: Bubble(
 | 
			
		||||
            style: getStyle(),
 | 
			
		||||
            child: Container(
 | 
			
		||||
              constraints: BoxConstraints(
 | 
			
		||||
                maxWidth: MediaQuery.of(context).size.width * 0.9,
 | 
			
		||||
                minWidth: showActorDisplayName
 | 
			
		||||
                    ? actorText.size.width
 | 
			
		||||
                    : timeText.size.width + (widget.isSender ? widget.spacing + widget.timeIconSize : 0) + 3,
 | 
			
		||||
              ),
 | 
			
		||||
              child: Stack(
 | 
			
		||||
                children: [
 | 
			
		||||
                  Padding(
 | 
			
		||||
                      padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0),
 | 
			
		||||
                      child: message.getWidget()
 | 
			
		||||
                  ),
 | 
			
		||||
                  Visibility(
 | 
			
		||||
                    visible: showActorDisplayName,
 | 
			
		||||
                    child: Positioned(
 | 
			
		||||
                      top: 0,
 | 
			
		||||
                      left: 0,
 | 
			
		||||
                      child: actorText
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Visibility(
 | 
			
		||||
                    visible: showBubbleTime,
 | 
			
		||||
                    child: Positioned(
 | 
			
		||||
                      bottom: 0,
 | 
			
		||||
                      right: 0,
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          timeText,
 | 
			
		||||
                          if(widget.isSender) ...[
 | 
			
		||||
                            SizedBox(width: widget.spacing),
 | 
			
		||||
                            if(widget.isRead)
 | 
			
		||||
                              Icon(Icons.done_all_outlined, size: widget.timeIconSize, color: widget.timeIconColor)
 | 
			
		||||
                            else
 | 
			
		||||
                              Icon(Icons.done_outlined, size: widget.timeIconSize, color: widget.timeIconColor)
 | 
			
		||||
                          ]
 | 
			
		||||
                        ],
 | 
			
		||||
                      )
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Visibility(
 | 
			
		||||
                    visible: downloadProgress > 0,
 | 
			
		||||
                    child: Positioned(
 | 
			
		||||
                      bottom: 0,
 | 
			
		||||
                      right: 0,
 | 
			
		||||
                      left: 0,
 | 
			
		||||
                      child: LinearProgressIndicator(value: downloadProgress/100),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        Visibility(
 | 
			
		||||
          visible: widget.bubbleData.reactions != null,
 | 
			
		||||
@@ -411,7 +400,7 @@ class _ChatBubbleState extends State<ChatBubble> {
 | 
			
		||||
                alignment: widget.isSender ? WrapAlignment.end : WrapAlignment.start,
 | 
			
		||||
                crossAxisAlignment: WrapCrossAlignment.start,
 | 
			
		||||
                children: widget.bubbleData.reactions?.entries.map<Widget>((e) {
 | 
			
		||||
                  bool hasSelfReacted = widget.bubbleData.reactionsSelf?.contains(e.key) ?? false;
 | 
			
		||||
                  var hasSelfReacted = widget.bubbleData.reactionsSelf?.contains(e.key) ?? false;
 | 
			
		||||
                  return Container(
 | 
			
		||||
                    margin: const EdgeInsets.only(right: 2.5, left: 2.5),
 | 
			
		||||
                    child: ActionChip(
 | 
			
		||||
 
 | 
			
		||||
@@ -37,8 +37,7 @@ class ChatMessage {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return CachedNetworkImage(
 | 
			
		||||
      errorWidget: (context, url, error) {
 | 
			
		||||
        return Padding(padding: const EdgeInsets.only(top: 10), child: Row(
 | 
			
		||||
      errorWidget: (context, url, error) => Padding(padding: const EdgeInsets.only(top: 10), child: Row(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -47,12 +46,9 @@ class ChatMessage {
 | 
			
		||||
            Flexible(child: Text(file!.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold))),
 | 
			
		||||
            const SizedBox(width: 10),
 | 
			
		||||
          ],
 | 
			
		||||
        ));
 | 
			
		||||
      },
 | 
			
		||||
        )),
 | 
			
		||||
      alignment: Alignment.center,
 | 
			
		||||
      placeholder: (context, url) {
 | 
			
		||||
        return const Padding(padding: EdgeInsets.all(10), child: CircularProgressIndicator());
 | 
			
		||||
      },
 | 
			
		||||
      placeholder: (context, url) => const Padding(padding: EdgeInsets.all(10), child: CircularProgressIndicator()),
 | 
			
		||||
      fadeInDuration: Duration.zero,
 | 
			
		||||
      fadeOutDuration: Duration.zero,
 | 
			
		||||
      errorListener: (value) {},
 | 
			
		||||
@@ -60,7 +56,7 @@ class ChatMessage {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void onOpen(LinkableElement link) async {
 | 
			
		||||
  Future<void> onOpen(LinkableElement link) async {
 | 
			
		||||
    if(await canLaunchUrlString(link.url)) {
 | 
			
		||||
      await launchUrlString(link.url);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:loader_overlay/loader_overlay.dart';
 | 
			
		||||
import 'package:nextcloud/nextcloud.dart';
 | 
			
		||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart';
 | 
			
		||||
import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart';
 | 
			
		||||
@@ -15,7 +14,7 @@ import '../../../../model/chatList/chatProps.dart';
 | 
			
		||||
import '../../../../storage/base/settingsProvider.dart';
 | 
			
		||||
import '../../../../widget/filePick.dart';
 | 
			
		||||
import '../../../../widget/focusBehaviour.dart';
 | 
			
		||||
import '../../files/fileUploadDialog.dart';
 | 
			
		||||
import '../../files/filesUploadDialog.dart';
 | 
			
		||||
 | 
			
		||||
class ChatTextfield extends StatefulWidget {
 | 
			
		||||
  final String sendToToken;
 | 
			
		||||
@@ -34,32 +33,37 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
    Provider.of<ChatProps>(context, listen: false).run();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void mediaUpload(String? path) async {
 | 
			
		||||
    context.loaderOverlay.hide();
 | 
			
		||||
 | 
			
		||||
    if(path == null) {
 | 
			
		||||
      return;
 | 
			
		||||
  void share(String shareFolder, List<String> filePaths) {
 | 
			
		||||
    for (var element in filePaths) {
 | 
			
		||||
      var fileName = element.split(Platform.pathSeparator).last;
 | 
			
		||||
      FileSharingApi().share(FileSharingApiParams(
 | 
			
		||||
          shareType: 10,
 | 
			
		||||
          shareWith: widget.sendToToken,
 | 
			
		||||
          path: '$shareFolder/$fileName',
 | 
			
		||||
      )).then((value) => _query());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    String filename = "${path.split("/").last.split(".").first}-${const Uuid().v4()}.${path.split(".").last}";
 | 
			
		||||
    String shareFolder = 'MarianumMobile';
 | 
			
		||||
  Future<void> mediaUpload(List<String>? paths) async {
 | 
			
		||||
    if (paths == null) return;
 | 
			
		||||
 | 
			
		||||
    var shareFolder = 'MarianumMobile';
 | 
			
		||||
    WebdavApi.webdav.then((webdav) {
 | 
			
		||||
      webdav.mkcol(PathUri.parse('/$shareFolder'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    showDialog(context: context, builder: (context) => FileUploadDialog(
 | 
			
		||||
      doShowFinish: false,
 | 
			
		||||
      fileName: filename,
 | 
			
		||||
      localPath: path,
 | 
			
		||||
      remotePath: [shareFolder],
 | 
			
		||||
      onUploadFinished: () {
 | 
			
		||||
        FileSharingApi().share(FileSharingApiParams(
 | 
			
		||||
          shareType: 10,
 | 
			
		||||
          shareWith: widget.sendToToken,
 | 
			
		||||
          path: '$shareFolder/$filename',
 | 
			
		||||
        )).then((value) => _query());
 | 
			
		||||
    pushScreen(
 | 
			
		||||
      context,
 | 
			
		||||
      withNavBar: false,
 | 
			
		||||
      screen: FilesUploadDialog(
 | 
			
		||||
        filePaths: paths,
 | 
			
		||||
        remotePath: shareFolder,
 | 
			
		||||
        onUploadFinished: (uploadedFilePaths) {
 | 
			
		||||
          share(shareFolder, uploadedFilePaths);
 | 
			
		||||
        },
 | 
			
		||||
    ), barrierDismissible: false);
 | 
			
		||||
        uniqueNames: true,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setDraft(String text) {
 | 
			
		||||
@@ -91,14 +95,12 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
              children: <Widget>[
 | 
			
		||||
                GestureDetector(
 | 
			
		||||
                  onTap: (){
 | 
			
		||||
                    showDialog(context: context, builder: (context) {
 | 
			
		||||
                      return SimpleDialog(
 | 
			
		||||
                    showDialog(context: context, builder: (context) => SimpleDialog(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.file_open),
 | 
			
		||||
                            title: const Text('Aus Dateien auswählen'),
 | 
			
		||||
                            onTap: () {
 | 
			
		||||
                              context.loaderOverlay.show();
 | 
			
		||||
                              FilePick.documentPick().then(mediaUpload);
 | 
			
		||||
                              Navigator.of(context).pop();
 | 
			
		||||
                            },
 | 
			
		||||
@@ -109,17 +111,15 @@ class _ChatTextfieldState extends State<ChatTextfield> {
 | 
			
		||||
                              leading: const Icon(Icons.image),
 | 
			
		||||
                              title: const Text('Aus Gallerie auswählen'),
 | 
			
		||||
                              onTap: () {
 | 
			
		||||
                                context.loaderOverlay.show();
 | 
			
		||||
                                FilePick.galleryPick().then((value) {
 | 
			
		||||
                                  mediaUpload(value?.path);
 | 
			
		||||
                                FilePick.multipleGalleryPick().then((value) {
 | 
			
		||||
                                  if(value != null) mediaUpload(value.map((e) => e.path).toList());
 | 
			
		||||
                                });
 | 
			
		||||
                                Navigator.of(context).pop();
 | 
			
		||||
                              },
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      );
 | 
			
		||||
                    });
 | 
			
		||||
                      ));
 | 
			
		||||
                  },
 | 
			
		||||
                  child: Material(
 | 
			
		||||
                    elevation: 5,
 | 
			
		||||
 
 | 
			
		||||
@@ -30,18 +30,14 @@ class ChatTile extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
  late String username;
 | 
			
		||||
  late UserAvatar circleAvatar;
 | 
			
		||||
  late String selfUsername;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    SharedPreferences.getInstance().then((value) => {
 | 
			
		||||
      username = value.getString('username')!
 | 
			
		||||
      selfUsername = value.getString('username')!
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    bool isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
 | 
			
		||||
    circleAvatar = UserAvatar(id: isGroup ? widget.data.token : widget.data.name, isGroup: isGroup);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setCurrentAsRead() {
 | 
			
		||||
@@ -54,11 +50,10 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
    ).run().then((value) => widget.query(renew: true));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
 | 
			
		||||
    return Consumer<ChatProps>(builder: (context, chatData, child) {
 | 
			
		||||
  Widget build(BuildContext context) => Consumer<ChatProps>(builder: (context, chatData, child) {
 | 
			
		||||
    var isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
 | 
			
		||||
    var circleAvatar = UserAvatar(id: isGroup ? widget.data.token : widget.data.name, isGroup: isGroup);
 | 
			
		||||
    return ListTile(
 | 
			
		||||
        style: ListTileStyle.list,
 | 
			
		||||
        tileColor: chatData.currentToken() == widget.data.token && TalkNavigator.isSecondaryVisible(context)
 | 
			
		||||
@@ -120,7 +115,7 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
        ),
 | 
			
		||||
        onTap: () async {
 | 
			
		||||
          setCurrentAsRead();
 | 
			
		||||
          ChatView view = ChatView(room: widget.data, selfId: username, avatar: circleAvatar);
 | 
			
		||||
          var view = ChatView(room: widget.data, selfId: selfUsername, avatar: circleAvatar);
 | 
			
		||||
          TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
 | 
			
		||||
          Provider.of<ChatProps>(context, listen: false).setQueryToken(widget.data.token);
 | 
			
		||||
        },
 | 
			
		||||
@@ -188,4 +183,3 @@ class _ChatTileState extends State<ChatTile> {
 | 
			
		||||
      );
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,7 @@ class SplitViewPlaceholder extends StatelessWidget {
 | 
			
		||||
  const SplitViewPlaceholder({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(),
 | 
			
		||||
      body: Center(
 | 
			
		||||
        child: Column(
 | 
			
		||||
@@ -26,4 +25,3 @@ class SplitViewPlaceholder extends StatelessWidget {
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,7 @@ class JoinChat extends SearchDelegate<String> {
 | 
			
		||||
  CancelableOperation<AutocompleteResponse>? future;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<Widget>? buildActions(BuildContext context) {
 | 
			
		||||
    return [
 | 
			
		||||
  List<Widget>? buildActions(BuildContext context) => [
 | 
			
		||||
      if(future != null && query.isNotEmpty) FutureBuilder(
 | 
			
		||||
        future: future!.value,
 | 
			
		||||
        builder: (context, snapshot) {
 | 
			
		||||
@@ -35,12 +34,9 @@ class JoinChat extends SearchDelegate<String> {
 | 
			
		||||
      ),
 | 
			
		||||
      if(query.isNotEmpty) IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget? buildLeading(BuildContext context) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  Widget? buildLeading(BuildContext context) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget buildResults(BuildContext context) {
 | 
			
		||||
@@ -61,8 +57,8 @@ class JoinChat extends SearchDelegate<String> {
 | 
			
		||||
          return ListView.builder(
 | 
			
		||||
            itemCount: snapshot.data!.data.length,
 | 
			
		||||
            itemBuilder: (context, index) {
 | 
			
		||||
              AutocompleteResponseObject object = snapshot.data!.data[index];
 | 
			
		||||
              CircleAvatar circleAvatar = CircleAvatar(
 | 
			
		||||
              var object = snapshot.data!.data[index];
 | 
			
		||||
              var circleAvatar = CircleAvatar(
 | 
			
		||||
                foregroundImage: Image.network('https://${EndpointData().nextcloud().full()}/avatar/${object.id}/128').image,
 | 
			
		||||
                backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
                foregroundColor: Colors.white,
 | 
			
		||||
@@ -89,8 +85,6 @@ class JoinChat extends SearchDelegate<String> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget buildSuggestions(BuildContext context) {
 | 
			
		||||
    return buildResults(context);
 | 
			
		||||
  }
 | 
			
		||||
  Widget buildSuggestions(BuildContext context) => buildResults(context);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -30,8 +30,7 @@ class _MessageReactionsState extends State<MessageReactions> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
  Widget build(BuildContext context) => Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('Reaktionen'),
 | 
			
		||||
      ),
 | 
			
		||||
@@ -42,8 +41,7 @@ class _MessageReactionsState extends State<MessageReactions> {
 | 
			
		||||
          if(snapshot.data == null) return const PlaceholderView(icon: Icons.search_off_outlined, text: 'Keine Reaktionen gefunden!');
 | 
			
		||||
          return ListView(
 | 
			
		||||
            children: [
 | 
			
		||||
              ...snapshot.data!.data.entries.map<Widget>((entry) {
 | 
			
		||||
                return ExpansionTile(
 | 
			
		||||
              ...snapshot.data!.data.entries.map<Widget>((entry) => ExpansionTile(
 | 
			
		||||
                  textColor: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                  collapsedTextColor: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                  iconColor: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
@@ -53,7 +51,7 @@ class _MessageReactionsState extends State<MessageReactions> {
 | 
			
		||||
                  leading: CenteredLeading(Text(entry.key)),
 | 
			
		||||
                  title: Text('${entry.value.length} mal reagiert'),
 | 
			
		||||
                  children: entry.value.map((e) {
 | 
			
		||||
                    bool isSelf = AccountData().getUsername() == e.actorId;
 | 
			
		||||
                    var isSelf = AccountData().getUsername() == e.actorId;
 | 
			
		||||
                    return ListTile(
 | 
			
		||||
                      leading: UserAvatar(id: e.actorId, isGroup: false),
 | 
			
		||||
                      title: Text(e.actorDisplayName),
 | 
			
		||||
@@ -71,12 +69,10 @@ class _MessageReactionsState extends State<MessageReactions> {
 | 
			
		||||
                        ),
 | 
			
		||||
                    );
 | 
			
		||||
                  }).toList(),
 | 
			
		||||
                );
 | 
			
		||||
              })
 | 
			
		||||
                ))
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,16 +9,12 @@ class SearchChat extends SearchDelegate {
 | 
			
		||||
  SearchChat(this.chats);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<Widget>? buildActions(BuildContext context) {
 | 
			
		||||
    return [
 | 
			
		||||
  List<Widget>? buildActions(BuildContext context) => [
 | 
			
		||||
      if(query.isNotEmpty) IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget? buildLeading(BuildContext context) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  Widget? buildLeading(BuildContext context) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget buildResults(BuildContext context) {
 | 
			
		||||
@@ -35,7 +31,5 @@ class SearchChat extends SearchDelegate {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget buildSuggestions(BuildContext context) {
 | 
			
		||||
    return buildResults(context);
 | 
			
		||||
  }
 | 
			
		||||
  Widget buildSuggestions(BuildContext context) => buildResults(context);
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user