diff --git a/lib/api/api_params.dart b/lib/api/api_params.dart index b679813..5e9f07f 100644 --- a/lib/api/api_params.dart +++ b/lib/api/api_params.dart @@ -1,3 +1 @@ -class ApiParams { - -} +class ApiParams {} diff --git a/lib/api/api_request.dart b/lib/api/api_request.dart index 705ccbc..2cdf97c 100644 --- a/lib/api/api_request.dart +++ b/lib/api/api_request.dart @@ -1,5 +1 @@ - - -class ApiRequest { - -} +class ApiRequest {} diff --git a/lib/api/api_response.dart b/lib/api/api_response.dart index bcbc91c..cef5697 100644 --- a/lib/api/api_response.dart +++ b/lib/api/api_response.dart @@ -1,5 +1,6 @@ import 'package:http/http.dart' as http; import 'package:json_annotation/json_annotation.dart'; + abstract class ApiResponse { @JsonKey(includeFromJson: false, includeToJson: false) late http.Response rawResponse; diff --git a/lib/api/errors/auth_exception.dart b/lib/api/errors/auth_exception.dart index 60a70f7..495bafd 100644 --- a/lib/api/errors/auth_exception.dart +++ b/lib/api/errors/auth_exception.dart @@ -9,15 +9,16 @@ class AuthException extends AppException { super.technicalDetails, }) : super(allowRetry: false); - factory AuthException.unauthorized({String? technicalDetails}) => AuthException( + factory AuthException.unauthorized({String? technicalDetails}) => + AuthException( statusCode: 401, userMessage: 'Bitte melde dich erneut an, um fortzufahren.', technicalDetails: technicalDetails, ); factory AuthException.forbidden({String? technicalDetails}) => AuthException( - statusCode: 403, - userMessage: 'Du hast keine Berechtigung für diese Aktion.', - technicalDetails: technicalDetails, - ); + statusCode: 403, + userMessage: 'Du hast keine Berechtigung für diese Aktion.', + technicalDetails: technicalDetails, + ); } diff --git a/lib/api/errors/error_mapper.dart b/lib/api/errors/error_mapper.dart index 1807359..a5db580 100644 --- a/lib/api/errors/error_mapper.dart +++ b/lib/api/errors/error_mapper.dart @@ -14,7 +14,8 @@ import 'server_exception.dart'; import 'talk_exception.dart'; import 'webuntis_exception.dart'; -const String _defaultFallback = 'Etwas ist schiefgelaufen. Bitte versuche es erneut.'; +const String _defaultFallback = + 'Etwas ist schiefgelaufen. Bitte versuche es erneut.'; const String _tlsErrorMessage = 'Die sichere Verbindung zum Server wurde abgelehnt (Zertifikat oder TLS-Fehler). ' 'Häufige Ursachen: falsche Geräte-Uhrzeit oder ein WLAN mit Anmeldeseite (z.B. Café/Hotel).'; @@ -28,9 +29,7 @@ AppException? _dioToAppException(DioException error) { case DioExceptionType.connectionError: return NetworkException(technicalDetails: error.message); case DioExceptionType.badCertificate: - return const NetworkException( - userMessage: _tlsErrorMessage, - ); + return const NetworkException(userMessage: _tlsErrorMessage); case DioExceptionType.badResponse: final status = error.response?.statusCode; return ServerException( @@ -40,13 +39,15 @@ AppException? _dioToAppException(DioException error) { case DioExceptionType.cancel: case DioExceptionType.unknown: final inner = error.error; - if (inner is SocketException) return NetworkException(technicalDetails: inner.message); - if (inner is HandshakeException) { - return const NetworkException( - userMessage: _tlsErrorMessage, - ); + if (inner is SocketException) { + return NetworkException(technicalDetails: inner.message); + } + if (inner is HandshakeException) { + return const NetworkException(userMessage: _tlsErrorMessage); + } + if (inner is FormatException) { + return ParseException(technicalDetails: inner.message); } - if (inner is FormatException) return ParseException(technicalDetails: inner.message); return null; } } diff --git a/lib/api/errors/network_exception.dart b/lib/api/errors/network_exception.dart index 10fbb56..06b38df 100644 --- a/lib/api/errors/network_exception.dart +++ b/lib/api/errors/network_exception.dart @@ -2,12 +2,15 @@ import 'app_exception.dart'; class NetworkException extends AppException { const NetworkException({ - super.userMessage = 'Keine Internetverbindung. Bitte prüfe dein Netzwerk und versuche es erneut.', + super.userMessage = + 'Keine Internetverbindung. Bitte prüfe dein Netzwerk und versuche es erneut.', super.technicalDetails, }) : super(allowRetry: true); - factory NetworkException.timeout({String? technicalDetails}) => NetworkException( - userMessage: 'Der Server hat zu lange gebraucht. Bitte versuche es erneut.', + factory NetworkException.timeout({String? technicalDetails}) => + NetworkException( + userMessage: + 'Der Server hat zu lange gebraucht. Bitte versuche es erneut.', technicalDetails: technicalDetails, ); } diff --git a/lib/api/errors/server_exception.dart b/lib/api/errors/server_exception.dart index efabca2..9d5aab1 100644 --- a/lib/api/errors/server_exception.dart +++ b/lib/api/errors/server_exception.dart @@ -8,7 +8,9 @@ class ServerException extends AppException { String? userMessage, super.technicalDetails, }) : super( - userMessage: userMessage ?? 'Der Server hat gerade Probleme (Status $statusCode). Bitte später erneut versuchen.', - allowRetry: true, - ); + userMessage: + userMessage ?? + 'Der Server hat gerade Probleme (Status $statusCode). Bitte später erneut versuchen.', + allowRetry: true, + ); } diff --git a/lib/api/errors/talk_exception.dart b/lib/api/errors/talk_exception.dart index 534d1b2..52190c2 100644 --- a/lib/api/errors/talk_exception.dart +++ b/lib/api/errors/talk_exception.dart @@ -5,11 +5,12 @@ class TalkException extends AppException { final TalkError source; TalkException(this.source) - : super( - userMessage: _mapMessage(source), - technicalDetails: 'Talk ${source.status} (${source.code}): ${source.message}', - allowRetry: source.code >= 500, - ); + : super( + userMessage: _mapMessage(source), + technicalDetails: + 'Talk ${source.status} (${source.code}): ${source.message}', + allowRetry: source.code >= 500, + ); static String _mapMessage(TalkError e) { switch (e.code) { @@ -27,7 +28,9 @@ class TalkException extends AppException { if (e.code >= 500) { return 'Talk-Server hat gerade Probleme (${e.code}).'; } - return e.message.isNotEmpty ? e.message : 'Talk meldet einen Fehler (${e.code}).'; + return e.message.isNotEmpty + ? e.message + : 'Talk meldet einen Fehler (${e.code}).'; } } } diff --git a/lib/api/errors/webuntis_exception.dart b/lib/api/errors/webuntis_exception.dart index 211f5ae..c09f48a 100644 --- a/lib/api/errors/webuntis_exception.dart +++ b/lib/api/errors/webuntis_exception.dart @@ -5,11 +5,11 @@ class WebuntisException extends AppException { final WebuntisError source; WebuntisException(this.source) - : super( - userMessage: _mapMessage(source), - technicalDetails: 'WebUntis (${source.code}): ${source.message}', - allowRetry: true, - ); + : super( + userMessage: _mapMessage(source), + technicalDetails: 'WebUntis (${source.code}): ${source.message}', + allowRetry: true, + ); static String _mapMessage(WebuntisError e) { switch (e.code) { diff --git a/lib/api/marianumcloud/autocomplete/autocomplete_api.dart b/lib/api/marianumcloud/autocomplete/autocomplete_api.dart index e1bb9e3..c18e4d9 100644 --- a/lib/api/marianumcloud/autocomplete/autocomplete_api.dart +++ b/lib/api/marianumcloud/autocomplete/autocomplete_api.dart @@ -20,9 +20,13 @@ class AutocompleteApi { ); final response = await http.get(endpoint, headers: NextcloudOcs.headers()); if (response.statusCode != HttpStatus.ok) { - throw Exception('Api call failed with ${response.statusCode}: ${response.body}'); + throw Exception( + 'Api call failed with ${response.statusCode}: ${response.body}', + ); } final decoded = jsonDecode(response.body) as Map; - return AutocompleteResponse.fromJson(decoded['ocs'] as Map); + return AutocompleteResponse.fromJson( + decoded['ocs'] as Map, + ); } } diff --git a/lib/api/marianumcloud/autocomplete/autocomplete_response.dart b/lib/api/marianumcloud/autocomplete/autocomplete_response.dart index 60b4e7b..d3e938d 100644 --- a/lib/api/marianumcloud/autocomplete/autocomplete_response.dart +++ b/lib/api/marianumcloud/autocomplete/autocomplete_response.dart @@ -8,7 +8,8 @@ class AutocompleteResponse { AutocompleteResponse(this.data); - factory AutocompleteResponse.fromJson(Map json) => _$AutocompleteResponseFromJson(json); + factory AutocompleteResponse.fromJson(Map json) => + _$AutocompleteResponseFromJson(json); Map toJson() => _$AutocompleteResponseToJson(this); } @@ -22,9 +23,17 @@ class AutocompleteResponseObject { String? subline; String? shareWithDisplayNameUniqe; - AutocompleteResponseObject(this.id, this.label, this.icon, this.source, this.status, - this.subline, this.shareWithDisplayNameUniqe); + AutocompleteResponseObject( + this.id, + this.label, + this.icon, + this.source, + this.status, + this.subline, + this.shareWithDisplayNameUniqe, + ); - factory AutocompleteResponseObject.fromJson(Map json) => _$AutocompleteResponseObjectFromJson(json); + factory AutocompleteResponseObject.fromJson(Map json) => + _$AutocompleteResponseObjectFromJson(json); Map toJson() => _$AutocompleteResponseObjectToJson(this); } diff --git a/lib/api/marianumcloud/files_sharing/file_sharing_api.dart b/lib/api/marianumcloud/files_sharing/file_sharing_api.dart index 1551ada..c78ecaf 100644 --- a/lib/api/marianumcloud/files_sharing/file_sharing_api.dart +++ b/lib/api/marianumcloud/files_sharing/file_sharing_api.dart @@ -13,7 +13,9 @@ class FileSharingApi { ); final response = await http.post(endpoint, headers: NextcloudOcs.headers()); if (response.statusCode != HttpStatus.ok) { - throw Exception('Api call failed with ${response.statusCode}: ${response.body}'); + throw Exception( + 'Api call failed with ${response.statusCode}: ${response.body}', + ); } } } diff --git a/lib/api/marianumcloud/files_sharing/file_sharing_api_params.dart b/lib/api/marianumcloud/files_sharing/file_sharing_api_params.dart index 4078d29..7f70f86 100644 --- a/lib/api/marianumcloud/files_sharing/file_sharing_api_params.dart +++ b/lib/api/marianumcloud/files_sharing/file_sharing_api_params.dart @@ -15,9 +15,10 @@ class FileSharingApiParams { required this.shareWith, required this.path, this.referenceId, - this.talkMetaData + this.talkMetaData, }); - factory FileSharingApiParams.fromJson(Map json) => _$FileSharingApiParamsFromJson(json); + factory FileSharingApiParams.fromJson(Map json) => + _$FileSharingApiParamsFromJson(json); Map toJson() => _$FileSharingApiParamsToJson(this); } diff --git a/lib/api/marianumcloud/nextcloud_ocs.dart b/lib/api/marianumcloud/nextcloud_ocs.dart index b04d770..c7086a0 100644 --- a/lib/api/marianumcloud/nextcloud_ocs.dart +++ b/lib/api/marianumcloud/nextcloud_ocs.dart @@ -7,10 +7,10 @@ class NextcloudOcs { NextcloudOcs._(); static Map headers() => { - 'Accept': 'application/json', - 'OCS-APIRequest': 'true', - 'Authorization': AccountData().getBasicAuthHeader(), - }; + 'Accept': 'application/json', + 'OCS-APIRequest': 'true', + 'Authorization': AccountData().getBasicAuthHeader(), + }; static Uri uri(String pathSuffix, {Map? queryParameters}) { final endpoint = EndpointData().nextcloud(); diff --git a/lib/api/marianumcloud/talk/actions/talk_actions.dart b/lib/api/marianumcloud/talk/actions/talk_actions.dart index 59272cb..57a10d0 100644 --- a/lib/api/marianumcloud/talk/actions/talk_actions.dart +++ b/lib/api/marianumcloud/talk/actions/talk_actions.dart @@ -12,39 +12,53 @@ class SetFavorite extends TalkApi { final String chatToken; final bool favoriteState; - SetFavorite(this.chatToken, this.favoriteState) : super('v4/room/$chatToken/favorite', null); + SetFavorite(this.chatToken, this.favoriteState) + : super('v4/room/$chatToken/favorite', null); @override ApiResponse? assemble(String raw) => null; @override - Future request(Uri uri, ApiParams? body, Map? headers) => - favoriteState ? http.post(uri, headers: headers) : http.delete(uri, headers: headers); + Future request( + Uri uri, + ApiParams? body, + Map? headers, + ) => favoriteState + ? http.post(uri, headers: headers) + : http.delete(uri, headers: headers); } class LeaveRoom extends TalkApi { final String chatToken; - LeaveRoom(this.chatToken) : super('v4/room/$chatToken/participants/self', null); + LeaveRoom(this.chatToken) + : super('v4/room/$chatToken/participants/self', null); @override ApiResponse? assemble(String raw) => null; @override - Future request(Uri uri, ApiParams? body, Map? headers) => - http.delete(uri, headers: headers); + Future request( + Uri uri, + ApiParams? body, + Map? headers, + ) => http.delete(uri, headers: headers); } class DeleteMessage extends TalkApi { final String chatToken; final int messageId; - DeleteMessage(this.chatToken, this.messageId) : super('v1/chat/$chatToken/$messageId', null); + DeleteMessage(this.chatToken, this.messageId) + : super('v1/chat/$chatToken/$messageId', null); @override ApiResponse? assemble(String raw) => null; @override - Future request(Uri uri, ApiParams? body, Map? headers) => - http.delete(uri, headers: headers); + Future request( + Uri uri, + ApiParams? body, + Map? headers, + ) => http.delete(uri, headers: headers); } diff --git a/lib/api/marianumcloud/talk/chat/get_chat.dart b/lib/api/marianumcloud/talk/chat/get_chat.dart index 9009744..ff13903 100644 --- a/lib/api/marianumcloud/talk/chat/get_chat.dart +++ b/lib/api/marianumcloud/talk/chat/get_chat.dart @@ -11,7 +11,8 @@ class GetChat extends TalkApi { String chatToken; GetChatParams params; - GetChat(this.chatToken, this.params) : super('v1/chat/$chatToken', null, getParameters: params.toJson()); + GetChat(this.chatToken, this.params) + : super('v1/chat/$chatToken', null, getParameters: params.toJson()); @override GetChatResponse assemble(String raw) { @@ -20,6 +21,9 @@ class GetChat extends TalkApi { } @override - Future request(Uri uri, Object? body, Map? headers) => http.get(uri, headers: headers); - + Future request( + Uri uri, + Object? body, + Map? headers, + ) => http.get(uri, headers: headers); } diff --git a/lib/api/marianumcloud/talk/chat/get_chat_cache.dart b/lib/api/marianumcloud/talk/chat/get_chat_cache.dart index b15a886..01ee56a 100644 --- a/lib/api/marianumcloud/talk/chat/get_chat_cache.dart +++ b/lib/api/marianumcloud/talk/chat/get_chat_cache.dart @@ -10,17 +10,17 @@ class GetChatCache extends SimpleCache { super.onError, required String chatToken, }) : super( - cacheTime: RequestCache.cacheNothing, - loader: () => GetChat( - chatToken, - GetChatParams( - lookIntoFuture: GetChatParamsSwitch.off, - setReadMarker: GetChatParamsSwitch.on, - limit: 200, - ), - ).run(), - fromJson: GetChatResponse.fromJson, - ) { + cacheTime: RequestCache.cacheNothing, + loader: () => GetChat( + chatToken, + GetChatParams( + lookIntoFuture: GetChatParamsSwitch.off, + setReadMarker: GetChatParamsSwitch.on, + limit: 200, + ), + ).run(), + fromJson: GetChatResponse.fromJson, + ) { start('nc-chat-$chatToken'); } } diff --git a/lib/api/marianumcloud/talk/chat/get_chat_params.dart b/lib/api/marianumcloud/talk/chat/get_chat_params.dart index 5287a3b..88b4c3a 100644 --- a/lib/api/marianumcloud/talk/chat/get_chat_params.dart +++ b/lib/api/marianumcloud/talk/chat/get_chat_params.dart @@ -15,20 +15,23 @@ class GetChatParams extends ApiParams { GetChatParamsSwitch? includeLastKnown; GetChatParams({ - required this.lookIntoFuture, - this.limit, - this.lastKnownMessageId, - this.lastCommonReadId, - this.timeout, - this.setReadMarker, - this.includeLastKnown + required this.lookIntoFuture, + this.limit, + this.lastKnownMessageId, + this.lastCommonReadId, + this.timeout, + this.setReadMarker, + this.includeLastKnown, }); - factory GetChatParams.fromJson(Map json) => _$GetChatParamsFromJson(json); + factory GetChatParams.fromJson(Map json) => + _$GetChatParamsFromJson(json); Map toJson() => _$GetChatParamsToJson(this); } enum GetChatParamsSwitch { - @JsonValue(1) on, - @JsonValue(0) off, + @JsonValue(1) + on, + @JsonValue(0) + off, } diff --git a/lib/api/marianumcloud/talk/chat/get_chat_response.dart b/lib/api/marianumcloud/talk/chat/get_chat_response.dart index 3c6416d..9b54111 100644 --- a/lib/api/marianumcloud/talk/chat/get_chat_response.dart +++ b/lib/api/marianumcloud/talk/chat/get_chat_response.dart @@ -12,7 +12,8 @@ class GetChatResponse extends ApiResponse { GetChatResponse(this.data); - factory GetChatResponse.fromJson(Map json) => _$GetChatResponseFromJson(json); + factory GetChatResponse.fromJson(Map json) => + _$GetChatResponseFromJson(json); Map toJson() => _$GetChatResponseToJson(this); List sortByTimestamp() { @@ -37,28 +38,30 @@ class GetChatResponseObject { String message; Map? reactions; List? reactionsSelf; - @JsonKey(fromJson: _fromJson) Map? messageParameters; + @JsonKey(fromJson: _fromJson) + Map? messageParameters; GetChatResponseObject? parent; GetChatResponseObject( - this.id, - this.token, - this.actorType, - this.actorId, - this.actorDisplayName, - this.timestamp, - this.systemMessage, - this.messageType, - this.isReplyable, - this.referenceId, - this.message, - this.messageParameters, - this.reactions, - this.reactionsSelf, - this.parent, + this.id, + this.token, + this.actorType, + this.actorId, + this.actorDisplayName, + this.timestamp, + this.systemMessage, + this.messageType, + this.isReplyable, + this.referenceId, + this.message, + this.messageParameters, + this.reactions, + this.reactionsSelf, + this.parent, ); - factory GetChatResponseObject.fromJson(Map json) => _$GetChatResponseObjectFromJson(json); + factory GetChatResponseObject.fromJson(Map json) => + _$GetChatResponseObjectFromJson(json); Map toJson() => _$GetChatResponseObjectToJson(this); static GetChatResponseObject getDateDummy(int timestamp) { @@ -66,7 +69,8 @@ class GetChatResponseObject { return getTextDummy(elementDate.formatDate()); } - static GetChatResponseObject getTextDummy(String text) => GetChatResponseObject( + static GetChatResponseObject getTextDummy(String text) => + GetChatResponseObject( 0, '', GetRoomResponseObjectMessageActorType.user, @@ -82,15 +86,17 @@ class GetChatResponseObject { null, null, null, - ); - + ); } Map? _fromJson(dynamic json) { if (json is Map) { final data = {}; for (final element in json.keys) { - data.putIfAbsent(element, () => RichObjectString.fromJson(json[element] as Map)); + data.putIfAbsent( + element, + () => RichObjectString.fromJson(json[element] as Map), + ); } return data; } @@ -109,17 +115,26 @@ class RichObjectString { RichObjectString(this.type, this.id, this.name, this.path, this.link); - factory RichObjectString.fromJson(Map json) => _$RichObjectStringFromJson(json); + factory RichObjectString.fromJson(Map json) => + _$RichObjectStringFromJson(json); Map toJson() => _$RichObjectStringToJson(this); } enum RichObjectStringObjectType { - @JsonValue('user') user, - @JsonValue('group') group, - @JsonValue('file') file, - @JsonValue('guest') guest, - @JsonValue('highlight') highlight, - @JsonValue('talk-poll') talkPoll, - @JsonValue('geo-location') geoLocation, - @JsonValue('call') call, + @JsonValue('user') + user, + @JsonValue('group') + group, + @JsonValue('file') + file, + @JsonValue('guest') + guest, + @JsonValue('highlight') + highlight, + @JsonValue('talk-poll') + talkPoll, + @JsonValue('geo-location') + geoLocation, + @JsonValue('call') + call, } diff --git a/lib/api/marianumcloud/talk/chat/rich_object_string_processor.dart b/lib/api/marianumcloud/talk/chat/rich_object_string_processor.dart index af03502..071a51e 100644 --- a/lib/api/marianumcloud/talk/chat/rich_object_string_processor.dart +++ b/lib/api/marianumcloud/talk/chat/rich_object_string_processor.dart @@ -1,9 +1,11 @@ - import 'get_chat_response.dart'; class RichObjectStringProcessor { - static String parseToString(String message, Map? data) { - if(data == null) return message; + static String parseToString( + String message, + Map? data, + ) { + if (data == null) return message; data.forEach((key, value) { message = message.replaceAll(RegExp('{$key}'), value.name); diff --git a/lib/api/marianumcloud/talk/create_room/create_room.dart b/lib/api/marianumcloud/talk/create_room/create_room.dart index e2183b6..626fd11 100644 --- a/lib/api/marianumcloud/talk/create_room/create_room.dart +++ b/lib/api/marianumcloud/talk/create_room/create_room.dart @@ -1,9 +1,9 @@ - import 'package:http/http.dart' as http; import 'package:http/http.dart'; import '../talk_api.dart'; import 'create_room_params.dart'; + class CreateRoom extends TalkApi { CreateRoomParams params; @@ -13,9 +13,19 @@ class CreateRoom extends TalkApi { Null assemble(String raw) => null; @override - Future? request(Uri uri, Object? body, Map? headers) { - if(body is CreateRoomParams) { - return http.post(uri, headers: headers, body: body.toJson().map((key, value) => MapEntry(key, value.toString()))); + Future? request( + Uri uri, + Object? body, + Map? headers, + ) { + if (body is CreateRoomParams) { + return http.post( + uri, + headers: headers, + body: body.toJson().map( + (key, value) => MapEntry(key, value.toString()), + ), + ); } return null; diff --git a/lib/api/marianumcloud/talk/create_room/create_room_params.dart b/lib/api/marianumcloud/talk/create_room/create_room_params.dart index 56ffe1d..69aa024 100644 --- a/lib/api/marianumcloud/talk/create_room/create_room_params.dart +++ b/lib/api/marianumcloud/talk/create_room/create_room_params.dart @@ -19,9 +19,10 @@ class CreateRoomParams extends ApiParams { this.source, this.roomName, this.objectType, - this.objectId + this.objectId, }); - factory CreateRoomParams.fromJson(Map json) => _$CreateRoomParamsFromJson(json); + factory CreateRoomParams.fromJson(Map json) => + _$CreateRoomParamsFromJson(json); Map toJson() => _$CreateRoomParamsToJson(this); } diff --git a/lib/api/marianumcloud/talk/delete_react_message/delete_react_message.dart b/lib/api/marianumcloud/talk/delete_react_message/delete_react_message.dart index 9dc886c..2b365a7 100644 --- a/lib/api/marianumcloud/talk/delete_react_message/delete_react_message.dart +++ b/lib/api/marianumcloud/talk/delete_react_message/delete_react_message.dart @@ -8,17 +8,24 @@ import 'delete_react_message_params.dart'; class DeleteReactMessage extends TalkApi { String chatToken; int messageId; - DeleteReactMessage({required this.chatToken, required this.messageId, required DeleteReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params); + DeleteReactMessage({ + required this.chatToken, + required this.messageId, + required DeleteReactMessageParams params, + }) : super('v1/reaction/$chatToken/$messageId', params); @override Null assemble(String raw) => null; @override - Future? request(Uri uri, ApiParams? body, Map? headers) { - if(body is DeleteReactMessageParams) { + Future? request( + Uri uri, + ApiParams? body, + Map? headers, + ) { + if (body is DeleteReactMessageParams) { return http.delete(uri, headers: headers, body: body.toJson()); } return null; } - } diff --git a/lib/api/marianumcloud/talk/delete_react_message/delete_react_message_params.dart b/lib/api/marianumcloud/talk/delete_react_message/delete_react_message_params.dart index d17bebc..181bec6 100644 --- a/lib/api/marianumcloud/talk/delete_react_message/delete_react_message_params.dart +++ b/lib/api/marianumcloud/talk/delete_react_message/delete_react_message_params.dart @@ -10,6 +10,7 @@ class DeleteReactMessageParams extends ApiParams { DeleteReactMessageParams(this.reaction); - factory DeleteReactMessageParams.fromJson(Map json) => _$DeleteReactMessageParamsFromJson(json); + factory DeleteReactMessageParams.fromJson(Map json) => + _$DeleteReactMessageParamsFromJson(json); Map toJson() => _$DeleteReactMessageParamsToJson(this); } diff --git a/lib/api/marianumcloud/talk/get_participants/get_participants.dart b/lib/api/marianumcloud/talk/get_participants/get_participants.dart index 03b302a..c37d7ce 100644 --- a/lib/api/marianumcloud/talk/get_participants/get_participants.dart +++ b/lib/api/marianumcloud/talk/get_participants/get_participants.dart @@ -12,10 +12,15 @@ class GetParticipants extends TalkApi { @override GetParticipantsResponse assemble(String raw) { final decoded = jsonDecode(raw) as Map; - return GetParticipantsResponse.fromJson(decoded['ocs'] as Map); + return GetParticipantsResponse.fromJson( + decoded['ocs'] as Map, + ); } @override - Future request(Uri uri, Object? body, Map? headers) => http.get(uri, headers: headers); - + Future request( + Uri uri, + Object? body, + Map? headers, + ) => http.get(uri, headers: headers); } diff --git a/lib/api/marianumcloud/talk/get_participants/get_participants_cache.dart b/lib/api/marianumcloud/talk/get_participants/get_participants_cache.dart index f40b017..560ba70 100644 --- a/lib/api/marianumcloud/talk/get_participants/get_participants_cache.dart +++ b/lib/api/marianumcloud/talk/get_participants/get_participants_cache.dart @@ -7,11 +7,11 @@ class GetParticipantsCache extends SimpleCache { required void Function(GetParticipantsResponse) onUpdate, required String chatToken, }) : super( - cacheTime: RequestCache.cacheNothing, - loader: () => GetParticipants(chatToken).run(), - fromJson: GetParticipantsResponse.fromJson, - onUpdate: onUpdate, - ) { + cacheTime: RequestCache.cacheNothing, + loader: () => GetParticipants(chatToken).run(), + fromJson: GetParticipantsResponse.fromJson, + onUpdate: onUpdate, + ) { start('nc-chat-participants-$chatToken'); } } diff --git a/lib/api/marianumcloud/talk/get_participants/get_participants_response.dart b/lib/api/marianumcloud/talk/get_participants/get_participants_response.dart index 5f97086..12a362b 100644 --- a/lib/api/marianumcloud/talk/get_participants/get_participants_response.dart +++ b/lib/api/marianumcloud/talk/get_participants/get_participants_response.dart @@ -1,4 +1,3 @@ - import 'package:json_annotation/json_annotation.dart'; import '../../../api_response.dart'; @@ -11,7 +10,8 @@ class GetParticipantsResponse extends ApiResponse { GetParticipantsResponse(this.data); - factory GetParticipantsResponse.fromJson(Map json) => _$GetParticipantsResponseFromJson(json); + factory GetParticipantsResponse.fromJson(Map json) => + _$GetParticipantsResponseFromJson(json); Map toJson() => _$GetParticipantsResponseToJson(this); } @@ -34,42 +34,55 @@ class GetParticipantsResponseObject { String? roomToken; GetParticipantsResponseObject( - this.attendeeId, - this.actorType, - this.actorId, - this.displayName, - this.participantType, - this.lastPing, - this.inCall, - this.permissions, - this.attendeePermissions, - this.sessionId, - this.sessionIds, - this.status, - this.statusIcon, - this.statusMessage, - this.roomToken); + this.attendeeId, + this.actorType, + this.actorId, + this.displayName, + this.participantType, + this.lastPing, + this.inCall, + this.permissions, + this.attendeePermissions, + this.sessionId, + this.sessionIds, + this.status, + this.statusIcon, + this.statusMessage, + this.roomToken, + ); - factory GetParticipantsResponseObject.fromJson(Map json) => _$GetParticipantsResponseObjectFromJson(json); + factory GetParticipantsResponseObject.fromJson(Map json) => + _$GetParticipantsResponseObjectFromJson(json); Map toJson() => _$GetParticipantsResponseObjectToJson(this); } enum GetParticipantsResponseObjectParticipantType { - @JsonValue(1) owner('Besitzer'), - @JsonValue(2) moderator('Moderator'), - @JsonValue(3) user('Teilnehmer'), - @JsonValue(4) guest('Gast'), - @JsonValue(5) userFollowingPublicLink('Teilnehmer über Link'), - @JsonValue(6) guestWithModeratorPermissions('Gast Moderator'); + @JsonValue(1) + owner('Besitzer'), + @JsonValue(2) + moderator('Moderator'), + @JsonValue(3) + user('Teilnehmer'), + @JsonValue(4) + guest('Gast'), + @JsonValue(5) + userFollowingPublicLink('Teilnehmer über Link'), + @JsonValue(6) + guestWithModeratorPermissions('Gast Moderator'); const GetParticipantsResponseObjectParticipantType(this.prettyName); final String prettyName; } enum GetParticipantsResponseObjectParticipantsInCallFlags { - @JsonValue(0) disconnected, - @JsonValue(1) inCall, - @JsonValue(2) providesAudio, - @JsonValue(3) providesVideo, - @JsonValue(4) usesSipDialIn + @JsonValue(0) + disconnected, + @JsonValue(1) + inCall, + @JsonValue(2) + providesAudio, + @JsonValue(3) + providesVideo, + @JsonValue(4) + usesSipDialIn, } diff --git a/lib/api/marianumcloud/talk/get_poll/get_poll_state.dart b/lib/api/marianumcloud/talk/get_poll/get_poll_state.dart index c4c37b7..3b6ccd2 100644 --- a/lib/api/marianumcloud/talk/get_poll/get_poll_state.dart +++ b/lib/api/marianumcloud/talk/get_poll/get_poll_state.dart @@ -8,14 +8,21 @@ import 'get_poll_state_response.dart'; class GetPollState extends TalkApi { String token; int pollId; - GetPollState({required this.token, required this.pollId}) : super('v1/poll/$token/$pollId', null); + GetPollState({required this.token, required this.pollId}) + : super('v1/poll/$token/$pollId', null); @override GetPollStateResponse assemble(String raw) { final decoded = jsonDecode(raw) as Map; - return GetPollStateResponse.fromJson(decoded['ocs'] as Map); + return GetPollStateResponse.fromJson( + decoded['ocs'] as Map, + ); } @override - Future request(Uri uri, Object? body, Map? headers) => http.get(uri, headers: headers); + Future request( + Uri uri, + Object? body, + Map? headers, + ) => http.get(uri, headers: headers); } diff --git a/lib/api/marianumcloud/talk/get_poll/get_poll_state_response.dart b/lib/api/marianumcloud/talk/get_poll/get_poll_state_response.dart index 5c43a38..928dba0 100644 --- a/lib/api/marianumcloud/talk/get_poll/get_poll_state_response.dart +++ b/lib/api/marianumcloud/talk/get_poll/get_poll_state_response.dart @@ -10,7 +10,8 @@ class GetPollStateResponse extends ApiResponse { GetPollStateResponse(this.data); - factory GetPollStateResponse.fromJson(Map json) => _$GetPollStateResponseFromJson(json); + factory GetPollStateResponse.fromJson(Map json) => + _$GetPollStateResponseFromJson(json); Map toJson() => _$GetPollStateResponseToJson(this); } @@ -31,20 +32,22 @@ class GetPollStateResponseObject { List? details; GetPollStateResponseObject( - this.id, - this.question, - this.options, - this.votes, - this.actorType, - this.actorId, - this.actorDisplayName, - this.status, - this.resultMode, - this.maxVotes, - this.votedSelf, - this.numVoters, - this.details); + this.id, + this.question, + this.options, + this.votes, + this.actorType, + this.actorId, + this.actorDisplayName, + this.status, + this.resultMode, + this.maxVotes, + this.votedSelf, + this.numVoters, + this.details, + ); - factory GetPollStateResponseObject.fromJson(Map json) => _$GetPollStateResponseObjectFromJson(json); + factory GetPollStateResponseObject.fromJson(Map json) => + _$GetPollStateResponseObjectFromJson(json); Map toJson() => _$GetPollStateResponseObjectToJson(this); } diff --git a/lib/api/marianumcloud/talk/get_reactions/get_reactions.dart b/lib/api/marianumcloud/talk/get_reactions/get_reactions.dart index 549c788..882eb29 100644 --- a/lib/api/marianumcloud/talk/get_reactions/get_reactions.dart +++ b/lib/api/marianumcloud/talk/get_reactions/get_reactions.dart @@ -10,15 +10,21 @@ import 'get_reactions_response.dart'; class GetReactions extends TalkApi { String chatToken; int messageId; - GetReactions({required this.chatToken, required this.messageId}) : super('v1/reaction/$chatToken/$messageId', null); + GetReactions({required this.chatToken, required this.messageId}) + : super('v1/reaction/$chatToken/$messageId', null); @override GetReactionsResponse assemble(String raw) { final decoded = jsonDecode(raw) as Map; - return GetReactionsResponse.fromJson(decoded['ocs'] as Map); + return GetReactionsResponse.fromJson( + decoded['ocs'] as Map, + ); } @override - Future? request(Uri uri, ApiParams? body, Map? headers) => http.get(uri, headers: headers); - + Future? request( + Uri uri, + ApiParams? body, + Map? headers, + ) => http.get(uri, headers: headers); } diff --git a/lib/api/marianumcloud/talk/get_reactions/get_reactions_response.dart b/lib/api/marianumcloud/talk/get_reactions/get_reactions_response.dart index 052b03a..1f58dfc 100644 --- a/lib/api/marianumcloud/talk/get_reactions/get_reactions_response.dart +++ b/lib/api/marianumcloud/talk/get_reactions/get_reactions_response.dart @@ -10,7 +10,8 @@ class GetReactionsResponse extends ApiResponse { GetReactionsResponse(this.data); - factory GetReactionsResponse.fromJson(Map json) => _$GetReactionsResponseFromJson(json); + factory GetReactionsResponse.fromJson(Map json) => + _$GetReactionsResponseFromJson(json); Map toJson() => _$GetReactionsResponseToJson(this); } @@ -21,13 +22,21 @@ class GetReactionsResponseObject { String actorDisplayName; int timestamp; - GetReactionsResponseObject(this.actorType, this.actorId, this.actorDisplayName, this.timestamp); + GetReactionsResponseObject( + this.actorType, + this.actorId, + this.actorDisplayName, + this.timestamp, + ); - factory GetReactionsResponseObject.fromJson(Map json) => _$GetReactionsResponseObjectFromJson(json); + factory GetReactionsResponseObject.fromJson(Map json) => + _$GetReactionsResponseObjectFromJson(json); Map toJson() => _$GetReactionsResponseObjectToJson(this); } enum GetReactionsResponseObjectActorType { - @JsonValue('guests') guests, - @JsonValue('users') users, + @JsonValue('guests') + guests, + @JsonValue('users') + users, } diff --git a/lib/api/marianumcloud/talk/react_message/react_message.dart b/lib/api/marianumcloud/talk/react_message/react_message.dart index c1e93b1..01c78dd 100644 --- a/lib/api/marianumcloud/talk/react_message/react_message.dart +++ b/lib/api/marianumcloud/talk/react_message/react_message.dart @@ -8,17 +8,24 @@ import 'react_message_params.dart'; class ReactMessage extends TalkApi { String chatToken; int messageId; - ReactMessage({required this.chatToken, required this.messageId, required ReactMessageParams params}) : super('v1/reaction/$chatToken/$messageId', params); + ReactMessage({ + required this.chatToken, + required this.messageId, + required ReactMessageParams params, + }) : super('v1/reaction/$chatToken/$messageId', params); @override Null assemble(String raw) => null; @override - Future? request(Uri uri, ApiParams? body, Map? headers) { - if(body is ReactMessageParams) { + Future? request( + Uri uri, + ApiParams? body, + Map? headers, + ) { + if (body is ReactMessageParams) { return http.post(uri, headers: headers, body: body.toJson()); } return null; } - } diff --git a/lib/api/marianumcloud/talk/react_message/react_message_params.dart b/lib/api/marianumcloud/talk/react_message/react_message_params.dart index 22b8845..1315898 100644 --- a/lib/api/marianumcloud/talk/react_message/react_message_params.dart +++ b/lib/api/marianumcloud/talk/react_message/react_message_params.dart @@ -10,6 +10,7 @@ class ReactMessageParams extends ApiParams { ReactMessageParams(this.reaction); - factory ReactMessageParams.fromJson(Map json) => _$ReactMessageParamsFromJson(json); + factory ReactMessageParams.fromJson(Map json) => + _$ReactMessageParamsFromJson(json); Map toJson() => _$ReactMessageParamsToJson(this); } diff --git a/lib/api/marianumcloud/talk/room/get_room.dart b/lib/api/marianumcloud/talk/room/get_room.dart index bb7d68e..c2049ef 100644 --- a/lib/api/marianumcloud/talk/room/get_room.dart +++ b/lib/api/marianumcloud/talk/room/get_room.dart @@ -6,13 +6,10 @@ import '../talk_api.dart'; import 'get_room_params.dart'; import 'get_room_response.dart'; - class GetRoom extends TalkApi { GetRoomParams params; GetRoom(this.params) : super('v4/room', null, getParameters: params.toJson()); - - @override GetRoomResponse assemble(String raw) { final decoded = jsonDecode(raw) as Map; @@ -20,6 +17,9 @@ class GetRoom extends TalkApi { } @override - Future request(Uri uri, Object? body, Map? headers) => http.get(uri, headers: headers); - + Future request( + Uri uri, + Object? body, + Map? headers, + ) => http.get(uri, headers: headers); } diff --git a/lib/api/marianumcloud/talk/room/get_room_cache.dart b/lib/api/marianumcloud/talk/room/get_room_cache.dart index 107a58b..8bcbd55 100644 --- a/lib/api/marianumcloud/talk/room/get_room_cache.dart +++ b/lib/api/marianumcloud/talk/room/get_room_cache.dart @@ -5,11 +5,11 @@ import 'get_room_response.dart'; class GetRoomCache extends SimpleCache { GetRoomCache({super.onUpdate, super.onError, super.renew}) - : super( - cacheTime: RequestCache.cacheMinute, - loader: () => GetRoom(GetRoomParams(includeStatus: true)).run(), - fromJson: GetRoomResponse.fromJson, - ) { + : super( + cacheTime: RequestCache.cacheMinute, + loader: () => GetRoom(GetRoomParams(includeStatus: true)).run(), + fromJson: GetRoomResponse.fromJson, + ) { start('nc-rooms'); } } diff --git a/lib/api/marianumcloud/talk/room/get_room_params.dart b/lib/api/marianumcloud/talk/room/get_room_params.dart index 09e397e..35dc4b8 100644 --- a/lib/api/marianumcloud/talk/room/get_room_params.dart +++ b/lib/api/marianumcloud/talk/room/get_room_params.dart @@ -1,4 +1,3 @@ - import 'package:json_annotation/json_annotation.dart'; import '../../../api_params.dart'; @@ -8,18 +7,22 @@ part 'get_room_params.g.dart'; @JsonSerializable(explicitToJson: true) class GetRoomParams extends ApiParams { GetRoomParamsStatusUpdate? noStatusUpdate; - @JsonKey(toJson: _format) bool? includeStatus; + @JsonKey(toJson: _format) + bool? includeStatus; int? modifiedSince; GetRoomParams({this.noStatusUpdate, this.includeStatus, this.modifiedSince}); - factory GetRoomParams.fromJson(Map json) => _$GetRoomParamsFromJson(json); + factory GetRoomParams.fromJson(Map json) => + _$GetRoomParamsFromJson(json); Map toJson() => _$GetRoomParamsToJson(this); - + static String _format(bool? v) => v.toString(); } enum GetRoomParamsStatusUpdate { - @JsonValue(0) defaults, - @JsonValue(1) keepAlive, + @JsonValue(0) + defaults, + @JsonValue(1) + keepAlive, } diff --git a/lib/api/marianumcloud/talk/room/get_room_response.dart b/lib/api/marianumcloud/talk/room/get_room_response.dart index da278d1..c18e668 100644 --- a/lib/api/marianumcloud/talk/room/get_room_response.dart +++ b/lib/api/marianumcloud/talk/room/get_room_response.dart @@ -11,17 +11,22 @@ class GetRoomResponse extends ApiResponse { GetRoomResponse(this.data); - factory GetRoomResponse.fromJson(Map json) => _$GetRoomResponseFromJson(json); + factory GetRoomResponse.fromJson(Map json) => + _$GetRoomResponseFromJson(json); Map toJson() => _$GetRoomResponseToJson(this); - List sortBy({bool lastActivity = true, required bool favoritesToTop, required bool unreadToTop}) { + List sortBy({ + bool lastActivity = true, + required bool favoritesToTop, + required bool unreadToTop, + }) { for (var chat in data) { final buffer = StringBuffer(); - if(favoritesToTop) { + if (favoritesToTop) { buffer.write(chat.isFavorite ? 'b' : 'a'); } - if(unreadToTop) { + if (unreadToTop) { buffer.write(chat.unreadMessages > 0 ? 'b' : 'a'); } @@ -69,69 +74,91 @@ class GetRoomResponseObject { String? sort; GetRoomResponseObject( - this.id, - this.token, - this.type, - this.name, - this.displayName, - this.description, - this.participantType, - this.participantFlags, - this.readOnly, - this.listable, - this.lastPing, - this.sessionId, - this.hasPassword, - this.hasCall, - this.callFlag, - this.canStartCall, - this.canDeleteConversation, - this.canLeaveConversation, - this.lastActivity, - this.isFavorite, - this.notificationLevel, - this.unreadMessages, - this.unreadMention, - this.unreadMentionDirect, - this.lastReadMessage, - this.lastCommonReadMessage, - this.lastMessage, - this.status, - this.statusIcon, - this.statusMessage); + this.id, + this.token, + this.type, + this.name, + this.displayName, + this.description, + this.participantType, + this.participantFlags, + this.readOnly, + this.listable, + this.lastPing, + this.sessionId, + this.hasPassword, + this.hasCall, + this.callFlag, + this.canStartCall, + this.canDeleteConversation, + this.canLeaveConversation, + this.lastActivity, + this.isFavorite, + this.notificationLevel, + this.unreadMessages, + this.unreadMention, + this.unreadMentionDirect, + this.lastReadMessage, + this.lastCommonReadMessage, + this.lastMessage, + this.status, + this.statusIcon, + this.statusMessage, + ); - factory GetRoomResponseObject.fromJson(Map json) => _$GetRoomResponseObjectFromJson(json); + factory GetRoomResponseObject.fromJson(Map json) => + _$GetRoomResponseObjectFromJson(json); Map toJson() => _$GetRoomResponseObjectToJson(this); } enum GetRoomResponseObjectConversationType { - @JsonValue(1) oneToOne, - @JsonValue(2) group, - @JsonValue(3) public, - @JsonValue(4) changelog, - @JsonValue(5) deleted, - @JsonValue(6) noteToSelf, + @JsonValue(1) + oneToOne, + @JsonValue(2) + group, + @JsonValue(3) + public, + @JsonValue(4) + changelog, + @JsonValue(5) + deleted, + @JsonValue(6) + noteToSelf, } enum GetRoomResponseObjectParticipantNotificationLevel { - @JsonValue(0) defaultLevel, - @JsonValue(1) alwaysNotify, - @JsonValue(2) notifyOnMention, - @JsonValue(3) neverNotify, + @JsonValue(0) + defaultLevel, + @JsonValue(1) + alwaysNotify, + @JsonValue(2) + notifyOnMention, + @JsonValue(3) + neverNotify, } enum GetRoomResponseObjectMessageActorType { - @JsonValue('deleted_users') deletedUsers, - @JsonValue('users') user, - @JsonValue('guests') guest, - @JsonValue('bots') bot, - @JsonValue('bridged') bridge, + @JsonValue('deleted_users') + deletedUsers, + @JsonValue('users') + user, + @JsonValue('guests') + guest, + @JsonValue('bots') + bot, + @JsonValue('bridged') + bridge, } enum GetRoomResponseObjectMessageType { - @JsonValue('comment') comment, - @JsonValue('voice-message') voiceMessage, - @JsonValue('comment_deleted') deletedComment, - @JsonValue('system') system, - @JsonValue('command') command, + @JsonValue('comment') + comment, + @JsonValue('voice-message') + voiceMessage, + @JsonValue('comment_deleted') + deletedComment, + @JsonValue('system') + system, + @JsonValue('command') + command, } diff --git a/lib/api/marianumcloud/talk/send_message/send_message.dart b/lib/api/marianumcloud/talk/send_message/send_message.dart index af3a012..b2849ad 100644 --- a/lib/api/marianumcloud/talk/send_message/send_message.dart +++ b/lib/api/marianumcloud/talk/send_message/send_message.dart @@ -7,17 +7,21 @@ import 'send_message_params.dart'; class SendMessage extends TalkApi { String chatToken; - SendMessage(this.chatToken, SendMessageParams params) : super('v1/chat/$chatToken', params); + SendMessage(this.chatToken, SendMessageParams params) + : super('v1/chat/$chatToken', params); @override Null assemble(String raw) => null; @override - Future? request(Uri uri, ApiParams? body, Map? headers) { - if(body is SendMessageParams) { + Future? request( + Uri uri, + ApiParams? body, + Map? headers, + ) { + if (body is SendMessageParams) { return http.post(uri, headers: headers, body: body.toJson()); } return null; } - } diff --git a/lib/api/marianumcloud/talk/send_message/send_message_params.dart b/lib/api/marianumcloud/talk/send_message/send_message_params.dart index 8ded2e2..a84adea 100644 --- a/lib/api/marianumcloud/talk/send_message/send_message_params.dart +++ b/lib/api/marianumcloud/talk/send_message/send_message_params.dart @@ -11,6 +11,7 @@ class SendMessageParams extends ApiParams { SendMessageParams(this.message, {this.replyTo}); - factory SendMessageParams.fromJson(Map json) => _$SendMessageParamsFromJson(json); + factory SendMessageParams.fromJson(Map json) => + _$SendMessageParamsFromJson(json); Map toJson() => _$SendMessageParamsToJson(this); } diff --git a/lib/api/marianumcloud/talk/set_read_marker/set_read_marker.dart b/lib/api/marianumcloud/talk/set_read_marker/set_read_marker.dart index 24389ef..847a96d 100644 --- a/lib/api/marianumcloud/talk/set_read_marker/set_read_marker.dart +++ b/lib/api/marianumcloud/talk/set_read_marker/set_read_marker.dart @@ -1,4 +1,3 @@ - import 'package:http/http.dart' as http; import 'package:http/http.dart'; @@ -10,21 +9,28 @@ class SetReadMarker extends TalkApi { bool readState; SetReadMarkerParams? setReadMarkerParams; - SetReadMarker(this.chatToken, this.readState, {this.setReadMarkerParams}) : super('v1/chat/$chatToken/read', null, getParameters: setReadMarkerParams?.toJson()) { - if(readState) assert(setReadMarkerParams?.lastReadMessage != null); + SetReadMarker(this.chatToken, this.readState, {this.setReadMarkerParams}) + : super( + 'v1/chat/$chatToken/read', + null, + getParameters: setReadMarkerParams?.toJson(), + ) { + if (readState) assert(setReadMarkerParams?.lastReadMessage != null); } @override Null assemble(String raw) => null; @override - Future request(Uri uri, Object? body, Map? headers) { - if(readState) { - + Future request( + Uri uri, + Object? body, + Map? headers, + ) { + if (readState) { return http.post(uri, headers: headers); } else { return http.delete(uri, headers: headers); } } - } diff --git a/lib/api/marianumcloud/talk/set_read_marker/set_read_marker_params.dart b/lib/api/marianumcloud/talk/set_read_marker/set_read_marker_params.dart index 50edee7..62c3278 100644 --- a/lib/api/marianumcloud/talk/set_read_marker/set_read_marker_params.dart +++ b/lib/api/marianumcloud/talk/set_read_marker/set_read_marker_params.dart @@ -8,10 +8,9 @@ part 'set_read_marker_params.g.dart'; class SetReadMarkerParams extends ApiParams { int? lastReadMessage; - SetReadMarkerParams({ - this.lastReadMessage - }); + SetReadMarkerParams({this.lastReadMessage}); - factory SetReadMarkerParams.fromJson(Map json) => _$SetReadMarkerParamsFromJson(json); + factory SetReadMarkerParams.fromJson(Map json) => + _$SetReadMarkerParamsFromJson(json); Map toJson() => _$SetReadMarkerParamsToJson(this); } diff --git a/lib/api/marianumcloud/talk/talk_api.dart b/lib/api/marianumcloud/talk/talk_api.dart index 9e63d35..b461221 100644 --- a/lib/api/marianumcloud/talk/talk_api.dart +++ b/lib/api/marianumcloud/talk/talk_api.dart @@ -14,12 +14,7 @@ import '../../errors/parse_exception.dart'; import '../../errors/server_exception.dart'; import '../nextcloud_ocs.dart'; -enum TalkApiMethod { - get, - post, - put, - delete, -} +enum TalkApiMethod { get, post, put, delete } abstract class TalkApi extends ApiRequest { String path; @@ -31,11 +26,18 @@ abstract class TalkApi extends ApiRequest { TalkApi(this.path, this.body, {this.headers, this.getParameters}); - Future? request(Uri uri, ApiParams? body, Map? headers); + Future? request( + Uri uri, + ApiParams? body, + Map? headers, + ); T assemble(String raw); Future run() async { - final endpoint = NextcloudOcs.uri('apps/spreed/api/$path', queryParameters: getParameters); + final endpoint = NextcloudOcs.uri( + 'apps/spreed/api/$path', + queryParameters: getParameters, + ); final mergedHeaders = {...NextcloudOcs.headers(), ...?headers}; final http.Response data; @@ -60,8 +62,12 @@ abstract class TalkApi extends ApiRequest { if (status < 200 || status >= 300) { final detail = 'Talk $endpoint -> HTTP $status'; log(detail); - if (status == 401) throw AuthException.unauthorized(technicalDetails: detail); - if (status == 403) throw AuthException.forbidden(technicalDetails: detail); + if (status == 401) { + throw AuthException.unauthorized(technicalDetails: detail); + } + if (status == 403) { + throw AuthException.forbidden(technicalDetails: detail); + } if (status == 404) throw NotFoundException(technicalDetails: detail); throw ServerException(statusCode: status, technicalDetails: detail); } diff --git a/lib/api/marianumcloud/webdav/queries/download_file/download_file.dart b/lib/api/marianumcloud/webdav/queries/download_file/download_file.dart index 5a4f164..160b4a1 100644 --- a/lib/api/marianumcloud/webdav/queries/download_file/download_file.dart +++ b/lib/api/marianumcloud/webdav/queries/download_file/download_file.dart @@ -1,5 +1,3 @@ - - import '../../../../api_response.dart'; import '../../webdav_api.dart'; import 'download_file_params.dart'; diff --git a/lib/api/marianumcloud/webdav/queries/download_file/download_file_params.dart b/lib/api/marianumcloud/webdav/queries/download_file/download_file_params.dart index ba8b075..d7763b5 100644 --- a/lib/api/marianumcloud/webdav/queries/download_file/download_file_params.dart +++ b/lib/api/marianumcloud/webdav/queries/download_file/download_file_params.dart @@ -10,8 +10,13 @@ class DownloadFileParams extends ApiParams { String localTargetPath; String filename; - DownloadFileParams(this.webdavSourcePath, this.localTargetPath, this.filename); + DownloadFileParams( + this.webdavSourcePath, + this.localTargetPath, + this.filename, + ); - factory DownloadFileParams.fromJson(Map json) => _$DownloadFileParamsFromJson(json); + factory DownloadFileParams.fromJson(Map json) => + _$DownloadFileParamsFromJson(json); Map toJson() => _$DownloadFileParamsToJson(this); } diff --git a/lib/api/marianumcloud/webdav/queries/download_file/download_file_response.dart b/lib/api/marianumcloud/webdav/queries/download_file/download_file_response.dart index 76ff712..ca91def 100644 --- a/lib/api/marianumcloud/webdav/queries/download_file/download_file_response.dart +++ b/lib/api/marianumcloud/webdav/queries/download_file/download_file_response.dart @@ -1,4 +1,3 @@ - import 'package:json_annotation/json_annotation.dart'; part 'download_file_response.g.dart'; @@ -9,6 +8,7 @@ class DownloadFileResponse { DownloadFileResponse(this.path); - factory DownloadFileResponse.fromJson(Map json) => _$DownloadFileResponseFromJson(json); + factory DownloadFileResponse.fromJson(Map json) => + _$DownloadFileResponseFromJson(json); Map toJson() => _$DownloadFileResponseToJson(this); } diff --git a/lib/api/marianumcloud/webdav/queries/list_files/cacheable_file.dart b/lib/api/marianumcloud/webdav/queries/list_files/cacheable_file.dart index c716dfb..bf23d1e 100644 --- a/lib/api/marianumcloud/webdav/queries/list_files/cacheable_file.dart +++ b/lib/api/marianumcloud/webdav/queries/list_files/cacheable_file.dart @@ -15,7 +15,16 @@ class CacheableFile { DateTime? modifiedAt; String? sort; - CacheableFile({required this.path, required this.isDirectory, required this.name, this.mimeType, this.size, this.eTag, this.createdAt, this.modifiedAt}); + CacheableFile({ + required this.path, + required this.isDirectory, + required this.name, + this.mimeType, + this.size, + this.eTag, + this.createdAt, + this.modifiedAt, + }); CacheableFile.fromDavFile(WebDavFile file) { path = file.path.path; @@ -28,6 +37,7 @@ class CacheableFile { modifiedAt = file.lastModified; } - factory CacheableFile.fromJson(Map json) => _$CacheableFileFromJson(json); + factory CacheableFile.fromJson(Map json) => + _$CacheableFileFromJson(json); Map toJson() => _$CacheableFileToJson(this); } diff --git a/lib/api/marianumcloud/webdav/queries/list_files/list_files.dart b/lib/api/marianumcloud/webdav/queries/list_files/list_files.dart index 6bebff9..d9b9745 100644 --- a/lib/api/marianumcloud/webdav/queries/list_files/list_files.dart +++ b/lib/api/marianumcloud/webdav/queries/list_files/list_files.dart @@ -1,4 +1,3 @@ - import 'package:nextcloud/nextcloud.dart'; import '../../webdav_api.dart'; @@ -26,12 +25,15 @@ class ListFiles extends WebdavApi { Future run() async { final webdav = await WebdavApi.webdav; final timeout = _isRoot ? _rootTimeout : _subfolderTimeout; - final davFiles = (await webdav.propfind(PathUri.parse(params.path)).timeout(timeout)).toWebDavFiles(); + final davFiles = + (await webdav.propfind(PathUri.parse(params.path)).timeout(timeout)) + .toWebDavFiles(); final files = davFiles.map(CacheableFile.fromDavFile).toSet(); - // somehow the current working folder is also listed, it is filtered here. - files.removeWhere((element) => element.path == '/${params.path}/' || element.path == '/'); + files.removeWhere( + (element) => element.path == '/${params.path}/' || element.path == '/', + ); return ListFilesResponse(files); } diff --git a/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart b/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart index 4f17e6e..b9a4cd1 100644 --- a/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart +++ b/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart @@ -17,16 +17,18 @@ class ListFilesCache extends SimpleCache { super.onError, required String path, }) : super( - cacheTime: RequestCache.cacheNothing, - loader: () => ListFiles(ListFilesParams(path)).run(), - fromJson: ListFilesResponse.fromJson, - onUpdate: onUpdate, - ) { + cacheTime: RequestCache.cacheNothing, + loader: () => ListFiles(ListFilesParams(path)).run(), + fromJson: ListFilesResponse.fromJson, + onUpdate: onUpdate, + ) { start(_documentId(path)); } static String _documentId(String path) { - final cacheName = md5.convert(utf8.encode('MarianumMobile-$path')).toString(); + final cacheName = md5 + .convert(utf8.encode('MarianumMobile-$path')) + .toString(); return 'wd-folder-$cacheName'; } @@ -35,7 +37,10 @@ class ListFilesCache extends SimpleCache { /// `_FilesView` for that path via [CacheInvalidationBus] so it refetches /// even while it is sitting in the background of the navigation stack. static Future invalidate(String path) async { - await Localstore.instance.collection(RequestCache.collection).doc(_documentId(path)).delete(); + await Localstore.instance + .collection(RequestCache.collection) + .doc(_documentId(path)) + .delete(); CacheInvalidationBus.notifyListFiles(path); } } diff --git a/lib/api/marianumcloud/webdav/queries/list_files/list_files_params.dart b/lib/api/marianumcloud/webdav/queries/list_files/list_files_params.dart index c18a539..ccd6a05 100644 --- a/lib/api/marianumcloud/webdav/queries/list_files/list_files_params.dart +++ b/lib/api/marianumcloud/webdav/queries/list_files/list_files_params.dart @@ -10,6 +10,7 @@ class ListFilesParams extends ApiParams { ListFilesParams(this.path); - factory ListFilesParams.fromJson(Map json) => _$ListFilesParamsFromJson(json); + factory ListFilesParams.fromJson(Map json) => + _$ListFilesParamsFromJson(json); Map toJson() => _$ListFilesParamsToJson(this); } diff --git a/lib/api/marianumcloud/webdav/queries/list_files/list_files_response.dart b/lib/api/marianumcloud/webdav/queries/list_files/list_files_response.dart index 8614f3e..3c73cd8 100644 --- a/lib/api/marianumcloud/webdav/queries/list_files/list_files_response.dart +++ b/lib/api/marianumcloud/webdav/queries/list_files/list_files_response.dart @@ -9,49 +9,73 @@ part 'list_files_response.g.dart'; @JsonSerializable(explicitToJson: true) class ListFilesResponse extends ApiResponse { - Set files; + Set files; - ListFilesResponse(this.files); + ListFilesResponse(this.files); - factory ListFilesResponse.fromJson(Map json) => _$ListFilesResponseFromJson(json); - Map toJson() => _$ListFilesResponseToJson(this); + factory ListFilesResponse.fromJson(Map json) => + _$ListFilesResponseFromJson(json); + Map toJson() => _$ListFilesResponseToJson(this); - List sortBy({bool foldersToTop = true, SortOption sortOption = SortOption.name, bool reversed = false}) { - var list = List.empty(growable: true); + List sortBy({ + bool foldersToTop = true, + SortOption sortOption = SortOption.name, + bool reversed = false, + }) { + var list = List.empty(growable: true); - if(foldersToTop) { - list.addAll(_sort(files.where((element) => element.isDirectory).toSet(), reversed: reversed, sortOption: sortOption)); - list.addAll(_sort(files.where((element) => !element.isDirectory).toSet(), reversed: reversed, sortOption: sortOption)); - } else { - list.addAll(_sort(files, reversed: reversed, sortOption: sortOption)); - } - - return list; + if (foldersToTop) { + list.addAll( + _sort( + files.where((element) => element.isDirectory).toSet(), + reversed: reversed, + sortOption: sortOption, + ), + ); + list.addAll( + _sort( + files.where((element) => !element.isDirectory).toSet(), + reversed: reversed, + sortOption: sortOption, + ), + ); + } else { + list.addAll(_sort(files, reversed: reversed, sortOption: sortOption)); } - List _sort(Set files, {SortOption sortOption = SortOption.name, bool reversed = false}) { - for (var file in files) { - final buffer = StringBuffer(); + return list; + } - switch(sortOption) { - case SortOption.date: - buffer.write(Jiffy.parseFromMillisecondsSinceEpoch(file.modifiedAt?.millisecondsSinceEpoch ?? 0).format(pattern: 'yyyyMMddhhmmss')); - break; + List _sort( + Set files, { + SortOption sortOption = SortOption.name, + bool reversed = false, + }) { + for (var file in files) { + final buffer = StringBuffer(); - case SortOption.name: - buffer.write(file.name.toLowerCase()); - break; + switch (sortOption) { + case SortOption.date: + buffer.write( + Jiffy.parseFromMillisecondsSinceEpoch( + file.modifiedAt?.millisecondsSinceEpoch ?? 0, + ).format(pattern: 'yyyyMMddhhmmss'), + ); + break; - case SortOption.size: - buffer.write(file.size); - break; - } + case SortOption.name: + buffer.write(file.name.toLowerCase()); + break; - file.sort = buffer.toString(); - } + case SortOption.size: + buffer.write(file.size); + break; + } - - var list = files.toList()..sort((a, b) => b.sort!.compareTo(a.sort!)); - return reversed ? list.reversed.toList() : list; + file.sort = buffer.toString(); } + + var list = files.toList()..sort((a, b) => b.sort!.compareTo(a.sort!)); + return reversed ? list.reversed.toList() : list; + } } diff --git a/lib/api/marianumcloud/webdav/webdav_api.dart b/lib/api/marianumcloud/webdav/webdav_api.dart index 3327b62..5916761 100644 --- a/lib/api/marianumcloud/webdav/webdav_api.dart +++ b/lib/api/marianumcloud/webdav/webdav_api.dart @@ -16,7 +16,12 @@ abstract class WebdavApi extends ApiRequest { static Future webdav = establishWebdavConnection(); - static Future establishWebdavConnection() async => NextcloudClient(Uri.parse('https://${EndpointData().nextcloud().full()}'), password: AccountData().getPassword(), loginName: AccountData().getUsername()).webdav; + static Future establishWebdavConnection() async => + NextcloudClient( + Uri.parse('https://${EndpointData().nextcloud().full()}'), + password: AccountData().getPassword(), + loginName: AccountData().getUsername(), + ).webdav; /// Builds the WebDAV download URL without embedded credentials. Callers must /// authenticate via the [AccountData.authHeaders] header instead. diff --git a/lib/api/mhsl/breaker/get_breakers/get_breakers.dart b/lib/api/mhsl/breaker/get_breakers/get_breakers.dart index b8f5c93..35d70b0 100644 --- a/lib/api/mhsl/breaker/get_breakers/get_breakers.dart +++ b/lib/api/mhsl/breaker/get_breakers/get_breakers.dart @@ -9,9 +9,9 @@ class GetBreakers extends MhslApi { GetBreakers() : super('breaker/'); @override - GetBreakersResponse assemble(String raw) => GetBreakersResponse.fromJson(jsonDecode(raw) as Map); + GetBreakersResponse assemble(String raw) => + GetBreakersResponse.fromJson(jsonDecode(raw) as Map); @override Future? request(Uri uri) => http.get(uri); - } diff --git a/lib/api/mhsl/breaker/get_breakers/get_breakers_cache.dart b/lib/api/mhsl/breaker/get_breakers/get_breakers_cache.dart index 8f3c180..1937df5 100644 --- a/lib/api/mhsl/breaker/get_breakers/get_breakers_cache.dart +++ b/lib/api/mhsl/breaker/get_breakers/get_breakers_cache.dart @@ -4,11 +4,11 @@ import 'get_breakers_response.dart'; class GetBreakersCache extends SimpleCache { GetBreakersCache({super.onUpdate, super.renew}) - : super( - cacheTime: RequestCache.cacheMinute, - loader: () => GetBreakers().run(), - fromJson: GetBreakersResponse.fromJson, - ) { + : super( + cacheTime: RequestCache.cacheMinute, + loader: () => GetBreakers().run(), + fromJson: GetBreakersResponse.fromJson, + ) { start('breakers'); } } diff --git a/lib/api/mhsl/breaker/get_breakers/get_breakers_response.dart b/lib/api/mhsl/breaker/get_breakers/get_breakers_response.dart index aa0f3b1..c0cb39f 100644 --- a/lib/api/mhsl/breaker/get_breakers/get_breakers_response.dart +++ b/lib/api/mhsl/breaker/get_breakers/get_breakers_response.dart @@ -1,4 +1,3 @@ - import 'package:json_annotation/json_annotation.dart'; import '../../../api_response.dart'; @@ -12,7 +11,8 @@ class GetBreakersResponse extends ApiResponse { GetBreakersResponse(this.global, this.regional); - factory GetBreakersResponse.fromJson(Map json) => _$GetBreakersResponseFromJson(json); + factory GetBreakersResponse.fromJson(Map json) => + _$GetBreakersResponseFromJson(json); Map toJson() => _$GetBreakersResponseToJson(this); } @@ -23,14 +23,20 @@ class GetBreakersReponseObject { GetBreakersReponseObject(this.areas, this.message); - factory GetBreakersReponseObject.fromJson(Map json) => _$GetBreakersReponseObjectFromJson(json); + factory GetBreakersReponseObject.fromJson(Map json) => + _$GetBreakersReponseObjectFromJson(json); Map toJson() => _$GetBreakersReponseObjectToJson(this); } enum BreakerArea { - @JsonValue('GLOBAL') global, - @JsonValue('TIMETABLE') timetable, - @JsonValue('TALK') talk, - @JsonValue('FILES') files, - @JsonValue('MORE') more, + @JsonValue('GLOBAL') + global, + @JsonValue('TIMETABLE') + timetable, + @JsonValue('TALK') + talk, + @JsonValue('FILES') + files, + @JsonValue('MORE') + more, } diff --git a/lib/api/mhsl/custom_timetable_event/add/add_custom_timetable_event.dart b/lib/api/mhsl/custom_timetable_event/add/add_custom_timetable_event.dart index c7fe3fc..abd6683 100644 --- a/lib/api/mhsl/custom_timetable_event/add/add_custom_timetable_event.dart +++ b/lib/api/mhsl/custom_timetable_event/add/add_custom_timetable_event.dart @@ -8,7 +8,7 @@ import 'add_custom_timetable_event_params.dart'; class AddCustomTimetableEvent extends MhslApi { AddCustomTimetableEventParams params; - + AddCustomTimetableEvent(this.params) : super('server/timetable/customEvents'); @override diff --git a/lib/api/mhsl/custom_timetable_event/add/add_custom_timetable_event_params.dart b/lib/api/mhsl/custom_timetable_event/add/add_custom_timetable_event_params.dart index a1d3b74..fab722d 100644 --- a/lib/api/mhsl/custom_timetable_event/add/add_custom_timetable_event_params.dart +++ b/lib/api/mhsl/custom_timetable_event/add/add_custom_timetable_event_params.dart @@ -11,6 +11,7 @@ class AddCustomTimetableEventParams { AddCustomTimetableEventParams(this.user, this.event); - factory AddCustomTimetableEventParams.fromJson(Map json) => _$AddCustomTimetableEventParamsFromJson(json); + factory AddCustomTimetableEventParams.fromJson(Map json) => + _$AddCustomTimetableEventParamsFromJson(json); Map toJson() => _$AddCustomTimetableEventParamsToJson(this); } diff --git a/lib/api/mhsl/custom_timetable_event/custom_timetable_event.dart b/lib/api/mhsl/custom_timetable_event/custom_timetable_event.dart index be9e4a6..eb8eab9 100644 --- a/lib/api/mhsl/custom_timetable_event/custom_timetable_event.dart +++ b/lib/api/mhsl/custom_timetable_event/custom_timetable_event.dart @@ -20,9 +20,19 @@ class CustomTimetableEvent { @JsonKey(toJson: MhslApi.dateTimeToJson, fromJson: MhslApi.dateTimeFromJson) DateTime updatedAt; - CustomTimetableEvent({required this.id, required this.title, required this.description, required this.startDate, - required this.endDate, required this.color, required this.rrule, required this.createdAt, required this.updatedAt}); + CustomTimetableEvent({ + required this.id, + required this.title, + required this.description, + required this.startDate, + required this.endDate, + required this.color, + required this.rrule, + required this.createdAt, + required this.updatedAt, + }); - factory CustomTimetableEvent.fromJson(Map json) => _$CustomTimetableEventFromJson(json); + factory CustomTimetableEvent.fromJson(Map json) => + _$CustomTimetableEventFromJson(json); Map toJson() => _$CustomTimetableEventToJson(this); } diff --git a/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event.dart b/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event.dart index dbdf476..b222705 100644 --- a/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event.dart +++ b/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event.dart @@ -9,10 +9,12 @@ import 'get_custom_timetable_event_response.dart'; class GetCustomTimetableEvent extends MhslApi { GetCustomTimetableEventParams params; - GetCustomTimetableEvent(this.params) : super('server/timetable/customEvents?user=${params.user}'); + GetCustomTimetableEvent(this.params) + : super('server/timetable/customEvents?user=${params.user}'); @override - GetCustomTimetableEventResponse assemble(String raw) => GetCustomTimetableEventResponse.fromJson({'events': jsonDecode(raw)}); + GetCustomTimetableEventResponse assemble(String raw) => + GetCustomTimetableEventResponse.fromJson({'events': jsonDecode(raw)}); @override Future? request(Uri uri) => http.get(uri); diff --git a/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_cache.dart b/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_cache.dart index ba49152..d644acc 100644 --- a/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_cache.dart +++ b/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_cache.dart @@ -3,17 +3,18 @@ import 'get_custom_timetable_event.dart'; import 'get_custom_timetable_event_params.dart'; import 'get_custom_timetable_event_response.dart'; -class GetCustomTimetableEventCache extends SimpleCache { +class GetCustomTimetableEventCache + extends SimpleCache { GetCustomTimetableEventCache( GetCustomTimetableEventParams params, { super.onUpdate, super.onError, super.renew, }) : super( - cacheTime: RequestCache.cacheMinute, - loader: () => GetCustomTimetableEvent(params).run(), - fromJson: GetCustomTimetableEventResponse.fromJson, - ) { + cacheTime: RequestCache.cacheMinute, + loader: () => GetCustomTimetableEvent(params).run(), + fromJson: GetCustomTimetableEventResponse.fromJson, + ) { start('customTimetableEvents'); } } diff --git a/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_params.dart b/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_params.dart index 58a9103..98d22ad 100644 --- a/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_params.dart +++ b/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_params.dart @@ -8,6 +8,7 @@ class GetCustomTimetableEventParams { GetCustomTimetableEventParams(this.user); - factory GetCustomTimetableEventParams.fromJson(Map json) => _$GetCustomTimetableEventParamsFromJson(json); + factory GetCustomTimetableEventParams.fromJson(Map json) => + _$GetCustomTimetableEventParamsFromJson(json); Map toJson() => _$GetCustomTimetableEventParamsToJson(this); } diff --git a/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart b/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart index 99684a2..43d2dc0 100644 --- a/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart +++ b/lib/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart @@ -11,6 +11,8 @@ class GetCustomTimetableEventResponse extends ApiResponse { GetCustomTimetableEventResponse(this.events); - factory GetCustomTimetableEventResponse.fromJson(Map json) => _$GetCustomTimetableEventResponseFromJson(json); - Map toJson() => _$GetCustomTimetableEventResponseToJson(this); + factory GetCustomTimetableEventResponse.fromJson(Map json) => + _$GetCustomTimetableEventResponseFromJson(json); + Map toJson() => + _$GetCustomTimetableEventResponseToJson(this); } diff --git a/lib/api/mhsl/custom_timetable_event/remove/remove_custom_timetable_event.dart b/lib/api/mhsl/custom_timetable_event/remove/remove_custom_timetable_event.dart index 436395e..ca466f6 100644 --- a/lib/api/mhsl/custom_timetable_event/remove/remove_custom_timetable_event.dart +++ b/lib/api/mhsl/custom_timetable_event/remove/remove_custom_timetable_event.dart @@ -9,11 +9,13 @@ import 'remove_custom_timetable_event_params.dart'; class RemoveCustomTimetableEvent extends MhslApi { RemoveCustomTimetableEventParams params; - RemoveCustomTimetableEvent(this.params) : super('server/timetable/customEvents'); + RemoveCustomTimetableEvent(this.params) + : super('server/timetable/customEvents'); @override void assemble(String raw) {} @override - Future? request(Uri uri) => http.delete(uri, body: jsonEncode(params.toJson())); + Future? request(Uri uri) => + http.delete(uri, body: jsonEncode(params.toJson())); } diff --git a/lib/api/mhsl/custom_timetable_event/remove/remove_custom_timetable_event_params.dart b/lib/api/mhsl/custom_timetable_event/remove/remove_custom_timetable_event_params.dart index a84ba07..2f99426 100644 --- a/lib/api/mhsl/custom_timetable_event/remove/remove_custom_timetable_event_params.dart +++ b/lib/api/mhsl/custom_timetable_event/remove/remove_custom_timetable_event_params.dart @@ -8,6 +8,9 @@ class RemoveCustomTimetableEventParams { RemoveCustomTimetableEventParams(this.id); - factory RemoveCustomTimetableEventParams.fromJson(Map json) => _$RemoveCustomTimetableEventParamsFromJson(json); - Map toJson() => _$RemoveCustomTimetableEventParamsToJson(this); + factory RemoveCustomTimetableEventParams.fromJson( + Map json, + ) => _$RemoveCustomTimetableEventParamsFromJson(json); + Map toJson() => + _$RemoveCustomTimetableEventParamsToJson(this); } diff --git a/lib/api/mhsl/custom_timetable_event/update/update_custom_timetable_event.dart b/lib/api/mhsl/custom_timetable_event/update/update_custom_timetable_event.dart index 4ae91d4..ab537e6 100644 --- a/lib/api/mhsl/custom_timetable_event/update/update_custom_timetable_event.dart +++ b/lib/api/mhsl/custom_timetable_event/update/update_custom_timetable_event.dart @@ -9,11 +9,13 @@ import 'update_custom_timetable_event_params.dart'; class UpdateCustomTimetableEvent extends MhslApi { UpdateCustomTimetableEventParams params; - UpdateCustomTimetableEvent(this.params) : super('server/timetable/customEvents'); + UpdateCustomTimetableEvent(this.params) + : super('server/timetable/customEvents'); @override void assemble(String raw) {} @override - Future? request(Uri uri) => http.patch(uri, body: jsonEncode(params.toJson())); + Future? request(Uri uri) => + http.patch(uri, body: jsonEncode(params.toJson())); } diff --git a/lib/api/mhsl/custom_timetable_event/update/update_custom_timetable_event_params.dart b/lib/api/mhsl/custom_timetable_event/update/update_custom_timetable_event_params.dart index 75f4dae..f4e16f4 100644 --- a/lib/api/mhsl/custom_timetable_event/update/update_custom_timetable_event_params.dart +++ b/lib/api/mhsl/custom_timetable_event/update/update_custom_timetable_event_params.dart @@ -1,4 +1,3 @@ - import 'package:json_annotation/json_annotation.dart'; import '../custom_timetable_event.dart'; @@ -12,6 +11,9 @@ class UpdateCustomTimetableEventParams { UpdateCustomTimetableEventParams(this.id, this.event); - factory UpdateCustomTimetableEventParams.fromJson(Map json) => _$UpdateCustomTimetableEventParamsFromJson(json); - Map toJson() => _$UpdateCustomTimetableEventParamsToJson(this); + factory UpdateCustomTimetableEventParams.fromJson( + Map json, + ) => _$UpdateCustomTimetableEventParamsFromJson(json); + Map toJson() => + _$UpdateCustomTimetableEventParamsToJson(this); } diff --git a/lib/api/mhsl/mhsl_api.dart b/lib/api/mhsl/mhsl_api.dart index da380f3..80b79bb 100644 --- a/lib/api/mhsl/mhsl_api.dart +++ b/lib/api/mhsl/mhsl_api.dart @@ -20,7 +20,9 @@ abstract class MhslApi extends ApiRequest { T assemble(String raw); Future run() async { - final endpoint = Uri.parse('https://mhsl.eu/marianum/marianummobile/$subpath'); + final endpoint = Uri.parse( + 'https://mhsl.eu/marianum/marianummobile/$subpath', + ); final http.Response data; try { @@ -54,6 +56,7 @@ abstract class MhslApi extends ApiRequest { } } - static String dateTimeToJson(DateTime time) => Jiffy.parseFromDateTime(time).format(pattern: 'yyyy-MM-dd HH:mm:ss'); + static String dateTimeToJson(DateTime time) => + Jiffy.parseFromDateTime(time).format(pattern: 'yyyy-MM-dd HH:mm:ss'); static DateTime dateTimeFromJson(String time) => DateTime.parse(time); } diff --git a/lib/api/mhsl/notify/register/notify_register.dart b/lib/api/mhsl/notify/register/notify_register.dart index b28c3dc..2f9f4b0 100644 --- a/lib/api/mhsl/notify/register/notify_register.dart +++ b/lib/api/mhsl/notify/register/notify_register.dart @@ -1,4 +1,3 @@ - import 'dart:convert'; import 'dart:developer'; @@ -11,11 +10,8 @@ class NotifyRegister extends MhslApi { NotifyRegisterParams params; NotifyRegister(this.params) : super('notify/register/'); - @override - void assemble(String raw) { - - } + void assemble(String raw) {} @override Future request(Uri uri) { diff --git a/lib/api/mhsl/notify/register/notify_register_params.dart b/lib/api/mhsl/notify/register/notify_register_params.dart index 1c92c46..243904e 100644 --- a/lib/api/mhsl/notify/register/notify_register_params.dart +++ b/lib/api/mhsl/notify/register/notify_register_params.dart @@ -11,9 +11,10 @@ class NotifyRegisterParams { NotifyRegisterParams({ required this.username, required this.password, - required this.fcmToken + required this.fcmToken, }); - factory NotifyRegisterParams.fromJson(Map json) => _$NotifyRegisterParamsFromJson(json); + factory NotifyRegisterParams.fromJson(Map json) => + _$NotifyRegisterParamsFromJson(json); Map toJson() => _$NotifyRegisterParamsToJson(this); } diff --git a/lib/api/mhsl/server/feedback/add_feedback.dart b/lib/api/mhsl/server/feedback/add_feedback.dart index 7f69978..7b8e0ff 100644 --- a/lib/api/mhsl/server/feedback/add_feedback.dart +++ b/lib/api/mhsl/server/feedback/add_feedback.dart @@ -6,7 +6,6 @@ import 'package:http/http.dart' as http; import '../../mhsl_api.dart'; import 'add_feedback_params.dart'; - class AddFeedback extends MhslApi { AddFeedbackParams params; AddFeedback(this.params) : super('server/feedback'); @@ -15,5 +14,6 @@ class AddFeedback extends MhslApi { void assemble(String raw) {} @override - Future? request(Uri uri) => http.post(uri, body: jsonEncode(params.toJson())); + Future? request(Uri uri) => + http.post(uri, body: jsonEncode(params.toJson())); } diff --git a/lib/api/mhsl/server/feedback/add_feedback_params.dart b/lib/api/mhsl/server/feedback/add_feedback_params.dart index ecf9adb..6e7df0e 100644 --- a/lib/api/mhsl/server/feedback/add_feedback_params.dart +++ b/lib/api/mhsl/server/feedback/add_feedback_params.dart @@ -9,7 +9,6 @@ class AddFeedbackParams { String? screenshot; int appVersion; - AddFeedbackParams({ required this.user, required this.feedback, @@ -17,6 +16,7 @@ class AddFeedbackParams { required this.appVersion, }); - factory AddFeedbackParams.fromJson(Map json) => _$AddFeedbackParamsFromJson(json); + factory AddFeedbackParams.fromJson(Map json) => + _$AddFeedbackParamsFromJson(json); Map toJson() => _$AddFeedbackParamsToJson(this); } diff --git a/lib/api/mhsl/server/user_index/update/update_user_index_params.dart b/lib/api/mhsl/server/user_index/update/update_user_index_params.dart index 7fd07f4..9fead47 100644 --- a/lib/api/mhsl/server/user_index/update/update_user_index_params.dart +++ b/lib/api/mhsl/server/user_index/update/update_user_index_params.dart @@ -10,15 +10,15 @@ class UpdateUserIndexParams { int appVersion; String deviceInfo; - UpdateUserIndexParams({ required this.user, required this.username, required this.device, required this.appVersion, - required this.deviceInfo + required this.deviceInfo, }); - factory UpdateUserIndexParams.fromJson(Map json) => _$UpdateUserIndexParamsFromJson(json); + factory UpdateUserIndexParams.fromJson(Map json) => + _$UpdateUserIndexParamsFromJson(json); Map toJson() => _$UpdateUserIndexParamsToJson(this); } diff --git a/lib/api/mhsl/server/user_index/update/update_userindex.dart b/lib/api/mhsl/server/user_index/update/update_userindex.dart index 9b728b6..ed4776f 100644 --- a/lib/api/mhsl/server/user_index/update/update_userindex.dart +++ b/lib/api/mhsl/server/user_index/update/update_userindex.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'dart:convert'; import 'dart:developer'; @@ -26,14 +25,18 @@ class UpdateUserIndex extends MhslApi { } static Future index() async { - unawaited(UpdateUserIndex( - UpdateUserIndexParams( - username: AccountData().getUsername(), - user: AccountData().getUserSecret(), - device: await AccountData().getDeviceId(), - appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber), - deviceInfo: jsonEncode((await DeviceInfoPlugin().deviceInfo).data).toString(), - ), - ).run()); + unawaited( + UpdateUserIndex( + UpdateUserIndexParams( + username: AccountData().getUsername(), + user: AccountData().getUserSecret(), + device: await AccountData().getDeviceId(), + appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber), + deviceInfo: jsonEncode( + (await DeviceInfoPlugin().deviceInfo).data, + ).toString(), + ), + ).run(), + ); } } diff --git a/lib/api/request_cache.dart b/lib/api/request_cache.dart index a5614e2..86d705b 100644 --- a/lib/api/request_cache.dart +++ b/lib/api/request_cache.dart @@ -49,7 +49,10 @@ abstract class RequestCache { Future start(String document) async { try { - final tableData = await Localstore.instance.collection(collection).doc(document).get(); + final tableData = await Localstore.instance + .collection(collection) + .doc(document) + .get(); if (tableData != null) { final cached = onLocalData(tableData['json'] as String); onUpdate?.call(cached); @@ -57,7 +60,8 @@ abstract class RequestCache { } final lastUpdate = (tableData?['lastupdate'] as num?) ?? 0; - if (DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) < lastUpdate) { + if (DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) < + lastUpdate) { if (renew == null || !renew!) return; } @@ -65,10 +69,12 @@ abstract class RequestCache { final newValue = await onLoad(); onUpdate?.call(newValue); onNetworkData?.call(newValue); - unawaited(Localstore.instance.collection(collection).doc(document).set({ - 'json': jsonEncode(newValue), - 'lastupdate': DateTime.now().millisecondsSinceEpoch, - })); + unawaited( + Localstore.instance.collection(collection).doc(document).set({ + 'json': jsonEncode(newValue), + 'lastupdate': DateTime.now().millisecondsSinceEpoch, + }), + ); } on Exception catch (e) { onError(e); } @@ -79,7 +85,6 @@ abstract class RequestCache { T onLocalData(String json); Future onLoad(); - } /// Concrete [RequestCache] that takes the two overrides as constructor @@ -97,22 +102,23 @@ class SimpleCache extends RequestCache { void Function(T)? onNetworkData, void Function(Exception)? onError, bool? renew, - }) : _loader = loader, - _fromJson = fromJson, - super( - cacheTime, - onUpdate, - onError: onError ?? RequestCache.ignore, - renew: renew, - onCacheData: onCacheData, - onNetworkData: onNetworkData, - ); + }) : _loader = loader, + _fromJson = fromJson, + super( + cacheTime, + onUpdate, + onError: onError ?? RequestCache.ignore, + renew: renew, + onCacheData: onCacheData, + onNetworkData: onNetworkData, + ); @override Future onLoad() => _loader(); @override - T onLocalData(String json) => _fromJson(jsonDecode(json) as Map); + T onLocalData(String json) => + _fromJson(jsonDecode(json) as Map); } /// Captures the latest cache payload (cached or network) and rethrows the @@ -120,24 +126,27 @@ class SimpleCache extends RequestCache { /// `latest`/`capturedError`/`await ready` boilerplate that DataProviders /// otherwise repeat per endpoint. Future resolveFromCache( - RequestCache Function(void Function(T) onUpdate, void Function(Exception) onError) build, { + RequestCache Function( + void Function(T) onUpdate, + void Function(Exception) onError, + ) + build, { void Function(Object)? onError, String? operationName, }) async { T? latest; Object? capturedError; - final cache = build( - (data) => latest = data, - (e) { - capturedError = e; - onError?.call(e); - }, - ); + final cache = build((data) => latest = data, (e) { + capturedError = e; + onError?.call(e); + }); await cache.ready; if (latest != null) return latest as T; final err = capturedError; if (err != null) throw err; throw ParseException( - technicalDetails: operationName != null ? 'No data and no error from $operationName' : null, + technicalDetails: operationName != null + ? 'No data and no error from $operationName' + : null, ); } diff --git a/lib/api/webuntis/queries/authenticate/authenticate.dart b/lib/api/webuntis/queries/authenticate/authenticate.dart index 551d815..183b513 100644 --- a/lib/api/webuntis/queries/authenticate/authenticate.dart +++ b/lib/api/webuntis/queries/authenticate/authenticate.dart @@ -9,7 +9,8 @@ import 'authenticate_response.dart'; class Authenticate extends WebuntisApi { AuthenticateParams param; - Authenticate(this.param) : super('authenticate', param, authenticatedResponse: false); + Authenticate(this.param) + : super('authenticate', param, authenticatedResponse: false); @override Future run() async { @@ -17,7 +18,11 @@ class Authenticate extends WebuntisApi { try { final rawAnswer = await query(this); final decoded = jsonDecode(rawAnswer) as Map; - final response = finalize(AuthenticateResponse.fromJson(decoded['result'] as Map)); + final response = finalize( + AuthenticateResponse.fromJson( + decoded['result'] as Map, + ), + ); _lastResponse = response; if (!awaitedResponse.isCompleted) awaitedResponse.complete(); return response; @@ -40,23 +45,22 @@ class Authenticate extends WebuntisApi { static Future createSession() async { _lastResponse = await Authenticate( - AuthenticateParams( - user: AccountData().getUsername(), - password: AccountData().getPassword(), - ) + AuthenticateParams( + user: AccountData().getUsername(), + password: AccountData().getPassword(), + ), ).run(); } static Future getSession() async { - if(awaitingResponse) { + if (awaitingResponse) { await awaitedResponse.future; } - if(_lastResponse == null) { + if (_lastResponse == null) { awaitingResponse = true; await createSession(); } return _lastResponse!; - } } diff --git a/lib/api/webuntis/queries/authenticate/authenticate_params.dart b/lib/api/webuntis/queries/authenticate/authenticate_params.dart index bf3b23e..4af2cec 100644 --- a/lib/api/webuntis/queries/authenticate/authenticate_params.dart +++ b/lib/api/webuntis/queries/authenticate/authenticate_params.dart @@ -6,12 +6,12 @@ part 'authenticate_params.g.dart'; @JsonSerializable() class AuthenticateParams extends ApiParams { - String user; String password; AuthenticateParams({required this.user, required this.password}); - factory AuthenticateParams.fromJson(Map json) => _$AuthenticateParamsFromJson(json); + factory AuthenticateParams.fromJson(Map json) => + _$AuthenticateParamsFromJson(json); Map toJson() => _$AuthenticateParamsToJson(this); } diff --git a/lib/api/webuntis/queries/authenticate/authenticate_response.dart b/lib/api/webuntis/queries/authenticate/authenticate_response.dart index 0ca87db..b9c661e 100644 --- a/lib/api/webuntis/queries/authenticate/authenticate_response.dart +++ b/lib/api/webuntis/queries/authenticate/authenticate_response.dart @@ -6,14 +6,19 @@ part 'authenticate_response.g.dart'; @JsonSerializable() class AuthenticateResponse extends ApiResponse { - String sessionId; int personType; int personId; int klasseId; - AuthenticateResponse(this.sessionId, this.personType, this.personId, this.klasseId); + AuthenticateResponse( + this.sessionId, + this.personType, + this.personId, + this.klasseId, + ); - factory AuthenticateResponse.fromJson(Map json) => _$AuthenticateResponseFromJson(json); + factory AuthenticateResponse.fromJson(Map json) => + _$AuthenticateResponseFromJson(json); Map toJson() => _$AuthenticateResponseToJson(this); } diff --git a/lib/api/webuntis/queries/get_holidays/get_holidays.dart b/lib/api/webuntis/queries/get_holidays/get_holidays.dart index 68031ec..d314004 100644 --- a/lib/api/webuntis/queries/get_holidays/get_holidays.dart +++ b/lib/api/webuntis/queries/get_holidays/get_holidays.dart @@ -9,10 +9,17 @@ class GetHolidays extends WebuntisApi { @override Future run() async { final rawAnswer = await query(this); - return finalize(GetHolidaysResponse.fromJson(jsonDecode(rawAnswer) as Map)); + return finalize( + GetHolidaysResponse.fromJson( + jsonDecode(rawAnswer) as Map, + ), + ); } - static GetHolidaysResponseObject? find(GetHolidaysResponse holidaysResponse, {DateTime? time}) { + static GetHolidaysResponseObject? find( + GetHolidaysResponse holidaysResponse, { + DateTime? time, + }) { time ??= DateTime.now(); time = DateTime(time.year, time.month, time.day, 0, 0, 0, 0, 0); @@ -20,9 +27,8 @@ class GetHolidays extends WebuntisApi { var start = DateTime.parse(element.startDate.toString()); var end = DateTime.parse(element.endDate.toString()); - if(!start.isAfter(time) && !end.isBefore(time)) return element; + if (!start.isAfter(time) && !end.isBefore(time)) return element; } return null; } - } diff --git a/lib/api/webuntis/queries/get_holidays/get_holidays_cache.dart b/lib/api/webuntis/queries/get_holidays/get_holidays_cache.dart index a974eb1..1a9393d 100644 --- a/lib/api/webuntis/queries/get_holidays/get_holidays_cache.dart +++ b/lib/api/webuntis/queries/get_holidays/get_holidays_cache.dart @@ -4,11 +4,11 @@ import 'get_holidays_response.dart'; class GetHolidaysCache extends SimpleCache { GetHolidaysCache({super.onUpdate, super.onError, super.renew}) - : super( - cacheTime: RequestCache.cacheDay, - loader: () => GetHolidays().run(), - fromJson: GetHolidaysResponse.fromJson, - ) { + : super( + cacheTime: RequestCache.cacheDay, + loader: () => GetHolidays().run(), + fromJson: GetHolidaysResponse.fromJson, + ) { start('wu-holidays'); } } diff --git a/lib/api/webuntis/queries/get_holidays/get_holidays_response.dart b/lib/api/webuntis/queries/get_holidays/get_holidays_response.dart index 8fa2624..019603e 100644 --- a/lib/api/webuntis/queries/get_holidays/get_holidays_response.dart +++ b/lib/api/webuntis/queries/get_holidays/get_holidays_response.dart @@ -10,7 +10,8 @@ class GetHolidaysResponse extends ApiResponse { GetHolidaysResponse(this.result); - factory GetHolidaysResponse.fromJson(Map json) => _$GetHolidaysResponseFromJson(json); + factory GetHolidaysResponse.fromJson(Map json) => + _$GetHolidaysResponseFromJson(json); Map toJson() => _$GetHolidaysResponseToJson(this); } @@ -22,8 +23,15 @@ class GetHolidaysResponseObject { int startDate; int endDate; - GetHolidaysResponseObject(this.id, this.name, this.longName, this.startDate, this.endDate); + GetHolidaysResponseObject( + this.id, + this.name, + this.longName, + this.startDate, + this.endDate, + ); - factory GetHolidaysResponseObject.fromJson(Map json) => _$GetHolidaysResponseObjectFromJson(json); + factory GetHolidaysResponseObject.fromJson(Map json) => + _$GetHolidaysResponseObjectFromJson(json); Map toJson() => _$GetHolidaysResponseObjectToJson(this); } diff --git a/lib/api/webuntis/queries/get_rooms/get_rooms.dart b/lib/api/webuntis/queries/get_rooms/get_rooms.dart index 4b7bf86..d7d32b3 100644 --- a/lib/api/webuntis/queries/get_rooms/get_rooms.dart +++ b/lib/api/webuntis/queries/get_rooms/get_rooms.dart @@ -11,7 +11,11 @@ class GetRooms extends WebuntisApi { Future run() async { final rawAnswer = await query(this); try { - return finalize(GetRoomsResponse.fromJson(jsonDecode(rawAnswer) as Map)); + return finalize( + GetRoomsResponse.fromJson( + jsonDecode(rawAnswer) as Map, + ), + ); } catch (e, trace) { log(trace.toString()); log('Failed to parse getRoom data with server response: $rawAnswer'); @@ -19,5 +23,4 @@ class GetRooms extends WebuntisApi { throw Exception('Failed to parse getRoom server response: $rawAnswer'); } - } diff --git a/lib/api/webuntis/queries/get_rooms/get_rooms_cache.dart b/lib/api/webuntis/queries/get_rooms/get_rooms_cache.dart index a07a449..df62f87 100644 --- a/lib/api/webuntis/queries/get_rooms/get_rooms_cache.dart +++ b/lib/api/webuntis/queries/get_rooms/get_rooms_cache.dart @@ -4,11 +4,11 @@ import 'get_rooms_response.dart'; class GetRoomsCache extends SimpleCache { GetRoomsCache({super.onUpdate, super.onError, super.renew}) - : super( - cacheTime: RequestCache.cacheHour, - loader: () => GetRooms().run(), - fromJson: GetRoomsResponse.fromJson, - ) { + : super( + cacheTime: RequestCache.cacheHour, + loader: () => GetRooms().run(), + fromJson: GetRoomsResponse.fromJson, + ) { start('wu-rooms'); } } diff --git a/lib/api/webuntis/queries/get_rooms/get_rooms_response.dart b/lib/api/webuntis/queries/get_rooms/get_rooms_response.dart index 614406d..83bff1d 100644 --- a/lib/api/webuntis/queries/get_rooms/get_rooms_response.dart +++ b/lib/api/webuntis/queries/get_rooms/get_rooms_response.dart @@ -10,7 +10,8 @@ class GetRoomsResponse extends ApiResponse { GetRoomsResponse(this.result); - factory GetRoomsResponse.fromJson(Map json) => _$GetRoomsResponseFromJson(json); + factory GetRoomsResponse.fromJson(Map json) => + _$GetRoomsResponseFromJson(json); Map toJson() => _$GetRoomsResponseToJson(this); } @@ -22,8 +23,15 @@ class GetRoomsResponseObject { bool active; String building; - GetRoomsResponseObject(this.id, this.name, this.longName, this.active, this.building); + GetRoomsResponseObject( + this.id, + this.name, + this.longName, + this.active, + this.building, + ); - factory GetRoomsResponseObject.fromJson(Map json) => _$GetRoomsResponseObjectFromJson(json); + factory GetRoomsResponseObject.fromJson(Map json) => + _$GetRoomsResponseObjectFromJson(json); Map toJson() => _$GetRoomsResponseObjectToJson(this); } diff --git a/lib/api/webuntis/queries/get_subjects/get_subjects.dart b/lib/api/webuntis/queries/get_subjects/get_subjects.dart index 75a4f1b..736de80 100644 --- a/lib/api/webuntis/queries/get_subjects/get_subjects.dart +++ b/lib/api/webuntis/queries/get_subjects/get_subjects.dart @@ -9,6 +9,10 @@ class GetSubjects extends WebuntisApi { @override Future run() async { final rawAnswer = await query(this); - return finalize(GetSubjectsResponse.fromJson(jsonDecode(rawAnswer) as Map)); + return finalize( + GetSubjectsResponse.fromJson( + jsonDecode(rawAnswer) as Map, + ), + ); } } diff --git a/lib/api/webuntis/queries/get_subjects/get_subjects_cache.dart b/lib/api/webuntis/queries/get_subjects/get_subjects_cache.dart index c513054..0064607 100644 --- a/lib/api/webuntis/queries/get_subjects/get_subjects_cache.dart +++ b/lib/api/webuntis/queries/get_subjects/get_subjects_cache.dart @@ -4,11 +4,11 @@ import 'get_subjects_response.dart'; class GetSubjectsCache extends SimpleCache { GetSubjectsCache({super.onUpdate, super.onError, super.renew}) - : super( - cacheTime: RequestCache.cacheHour, - loader: () => GetSubjects().run(), - fromJson: GetSubjectsResponse.fromJson, - ) { + : super( + cacheTime: RequestCache.cacheHour, + loader: () => GetSubjects().run(), + fromJson: GetSubjectsResponse.fromJson, + ) { start('wu-subjects'); } } diff --git a/lib/api/webuntis/queries/get_subjects/get_subjects_response.dart b/lib/api/webuntis/queries/get_subjects/get_subjects_response.dart index 255b5ad..386933e 100644 --- a/lib/api/webuntis/queries/get_subjects/get_subjects_response.dart +++ b/lib/api/webuntis/queries/get_subjects/get_subjects_response.dart @@ -10,7 +10,8 @@ class GetSubjectsResponse extends ApiResponse { GetSubjectsResponse(this.result); - factory GetSubjectsResponse.fromJson(Map json) => _$GetSubjectsResponseFromJson(json); + factory GetSubjectsResponse.fromJson(Map json) => + _$GetSubjectsResponseFromJson(json); Map toJson() => _$GetSubjectsResponseToJson(this); } @@ -22,8 +23,15 @@ class GetSubjectsResponseObject { String alternateName; bool active; - GetSubjectsResponseObject(this.id, this.name, this.longName, this.alternateName, this.active); + GetSubjectsResponseObject( + this.id, + this.name, + this.longName, + this.alternateName, + this.active, + ); - factory GetSubjectsResponseObject.fromJson(Map json) => _$GetSubjectsResponseObjectFromJson(json); + factory GetSubjectsResponseObject.fromJson(Map json) => + _$GetSubjectsResponseObjectFromJson(json); Map toJson() => _$GetSubjectsResponseObjectToJson(this); } diff --git a/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units.dart b/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units.dart index 9f910e1..a872c5c 100644 --- a/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units.dart +++ b/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units.dart @@ -11,12 +11,20 @@ class GetTimegridUnits extends WebuntisApi { Future run() async { final rawAnswer = await query(this); try { - return finalize(GetTimegridUnitsResponse.fromJson(jsonDecode(rawAnswer) as Map)); + return finalize( + GetTimegridUnitsResponse.fromJson( + jsonDecode(rawAnswer) as Map, + ), + ); } catch (e, trace) { log(trace.toString()); - log('Failed to parse getTimegridUnits data with server response: $rawAnswer'); + log( + 'Failed to parse getTimegridUnits data with server response: $rawAnswer', + ); } - throw Exception('Failed to parse getTimegridUnits server response: $rawAnswer'); + throw Exception( + 'Failed to parse getTimegridUnits server response: $rawAnswer', + ); } } diff --git a/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units_cache.dart b/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units_cache.dart index 811ed86..45ba202 100644 --- a/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units_cache.dart +++ b/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units_cache.dart @@ -4,11 +4,11 @@ import 'get_timegrid_units_response.dart'; class GetTimegridUnitsCache extends SimpleCache { GetTimegridUnitsCache({super.onUpdate, super.renew}) - : super( - cacheTime: RequestCache.cacheDay, - loader: () => GetTimegridUnits().run(), - fromJson: GetTimegridUnitsResponse.fromJson, - ) { + : super( + cacheTime: RequestCache.cacheDay, + loader: () => GetTimegridUnits().run(), + fromJson: GetTimegridUnitsResponse.fromJson, + ) { start('wu-timegrid'); } } diff --git a/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart b/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart index 5b458aa..b2cfc43 100644 --- a/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart +++ b/lib/api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart @@ -10,7 +10,8 @@ class GetTimegridUnitsResponse extends ApiResponse { GetTimegridUnitsResponse(this.result); - factory GetTimegridUnitsResponse.fromJson(Map json) => _$GetTimegridUnitsResponseFromJson(json); + factory GetTimegridUnitsResponse.fromJson(Map json) => + _$GetTimegridUnitsResponseFromJson(json); Map toJson() => _$GetTimegridUnitsResponseToJson(this); } @@ -21,7 +22,8 @@ class GetTimegridUnitsResponseDay { GetTimegridUnitsResponseDay(this.day, this.timeUnits); - factory GetTimegridUnitsResponseDay.fromJson(Map json) => _$GetTimegridUnitsResponseDayFromJson(json); + factory GetTimegridUnitsResponseDay.fromJson(Map json) => + _$GetTimegridUnitsResponseDayFromJson(json); Map toJson() => _$GetTimegridUnitsResponseDayToJson(this); } @@ -33,6 +35,7 @@ class GetTimegridUnitsResponseUnit { GetTimegridUnitsResponseUnit(this.name, this.startTime, this.endTime); - factory GetTimegridUnitsResponseUnit.fromJson(Map json) => _$GetTimegridUnitsResponseUnitFromJson(json); + factory GetTimegridUnitsResponseUnit.fromJson(Map json) => + _$GetTimegridUnitsResponseUnitFromJson(json); Map toJson() => _$GetTimegridUnitsResponseUnitToJson(this); } diff --git a/lib/api/webuntis/queries/get_timetable/get_timetable.dart b/lib/api/webuntis/queries/get_timetable/get_timetable.dart index d451d3c..0c60f88 100644 --- a/lib/api/webuntis/queries/get_timetable/get_timetable.dart +++ b/lib/api/webuntis/queries/get_timetable/get_timetable.dart @@ -12,7 +12,10 @@ class GetTimetable extends WebuntisApi { @override Future run() async { final rawAnswer = await query(this); - return finalize(GetTimetableResponse.fromJson(jsonDecode(rawAnswer) as Map)); + return finalize( + GetTimetableResponse.fromJson( + jsonDecode(rawAnswer) as Map, + ), + ); } - } diff --git a/lib/api/webuntis/queries/get_timetable/get_timetable_cache.dart b/lib/api/webuntis/queries/get_timetable/get_timetable_cache.dart index 56a73c9..440e1bb 100644 --- a/lib/api/webuntis/queries/get_timetable/get_timetable_cache.dart +++ b/lib/api/webuntis/queries/get_timetable/get_timetable_cache.dart @@ -12,11 +12,11 @@ class GetTimetableCache extends SimpleCache { required int enddate, super.renew, }) : super( - cacheTime: RequestCache.cacheMinute, - loader: () => _load(startdate, enddate), - fromJson: GetTimetableResponse.fromJson, - onUpdate: onUpdate, - ) { + cacheTime: RequestCache.cacheMinute, + loader: () => _load(startdate, enddate), + fromJson: GetTimetableResponse.fromJson, + onUpdate: onUpdate, + ) { start('wu-timetable-$startdate-$enddate'); } diff --git a/lib/api/webuntis/queries/get_timetable/get_timetable_params.dart b/lib/api/webuntis/queries/get_timetable/get_timetable_params.dart index 9286863..727ba9f 100644 --- a/lib/api/webuntis/queries/get_timetable/get_timetable_params.dart +++ b/lib/api/webuntis/queries/get_timetable/get_timetable_params.dart @@ -10,11 +10,11 @@ class GetTimetableParams extends ApiParams { GetTimetableParams({required this.options}); - factory GetTimetableParams.fromJson(Map json) => _$GetTimetableParamsFromJson(json); + factory GetTimetableParams.fromJson(Map json) => + _$GetTimetableParamsFromJson(json); Map toJson() => _$GetTimetableParamsToJson(this); } - @JsonSerializable(explicitToJson: true) class GetTimetableParamsOptions { GetTimetableParamsOptionsElement element; @@ -59,20 +59,30 @@ class GetTimetableParamsOptions { this.klasseFields, this.roomFields, this.subjectFields, - this.teacherFields + this.teacherFields, }); - factory GetTimetableParamsOptions.fromJson(Map json) => _$GetTimetableParamsOptionsFromJson(json); + factory GetTimetableParamsOptions.fromJson(Map json) => + _$GetTimetableParamsOptionsFromJson(json); Map toJson() => _$GetTimetableParamsOptionsToJson(this); } enum GetTimetableParamsOptionsFields { - @JsonValue('id') id, - @JsonValue('name') name, - @JsonValue('longname') longname, - @JsonValue('externalkey') externalkey; + @JsonValue('id') + id, + @JsonValue('name') + name, + @JsonValue('longname') + longname, + @JsonValue('externalkey') + externalkey; - static List all = [id, name, longname, externalkey]; + static List all = [ + id, + name, + longname, + externalkey, + ]; } @JsonSerializable() @@ -82,13 +92,23 @@ class GetTimetableParamsOptionsElement { @JsonKey(includeIfNull: false) GetTimetableParamsOptionsElementKeyType? keyType; - GetTimetableParamsOptionsElement({required this.id, required this.type, this.keyType}); - factory GetTimetableParamsOptionsElement.fromJson(Map json) => _$GetTimetableParamsOptionsElementFromJson(json); - Map toJson() => _$GetTimetableParamsOptionsElementToJson(this); + GetTimetableParamsOptionsElement({ + required this.id, + required this.type, + this.keyType, + }); + factory GetTimetableParamsOptionsElement.fromJson( + Map json, + ) => _$GetTimetableParamsOptionsElementFromJson(json); + Map toJson() => + _$GetTimetableParamsOptionsElementToJson(this); } enum GetTimetableParamsOptionsElementKeyType { - @JsonValue('id') id, - @JsonValue('name') name, - @JsonValue('externalkey') externalkey + @JsonValue('id') + id, + @JsonValue('name') + name, + @JsonValue('externalkey') + externalkey, } diff --git a/lib/api/webuntis/queries/get_timetable/get_timetable_response.dart b/lib/api/webuntis/queries/get_timetable/get_timetable_response.dart index 05e1ea1..76ff345 100644 --- a/lib/api/webuntis/queries/get_timetable/get_timetable_response.dart +++ b/lib/api/webuntis/queries/get_timetable/get_timetable_response.dart @@ -10,9 +10,9 @@ class GetTimetableResponse extends ApiResponse { GetTimetableResponse(this.result); - factory GetTimetableResponse.fromJson(Map json) => _$GetTimetableResponseFromJson(json); + factory GetTimetableResponse.fromJson(Map json) => + _$GetTimetableResponseFromJson(json); Map toJson() => _$GetTimetableResponseToJson(this); - } @JsonSerializable(explicitToJson: true) @@ -55,10 +55,11 @@ class GetTimetableResponseObject { required this.kl, required this.te, required this.su, - required this.ro + required this.ro, }); - factory GetTimetableResponseObject.fromJson(Map json) => _$GetTimetableResponseObjectFromJson(json); + factory GetTimetableResponseObject.fromJson(Map json) => + _$GetTimetableResponseObjectFromJson(json); Map toJson() => _$GetTimetableResponseObjectToJson(this); } @@ -68,8 +69,11 @@ class GetTimetableResponseObjectFields { GetTimetableResponseObjectFields(this.te); - factory GetTimetableResponseObjectFields.fromJson(Map json) => _$GetTimetableResponseObjectFieldsFromJson(json); - Map toJson() => _$GetTimetableResponseObjectFieldsToJson(this); + factory GetTimetableResponseObjectFields.fromJson( + Map json, + ) => _$GetTimetableResponseObjectFieldsFromJson(json); + Map toJson() => + _$GetTimetableResponseObjectFieldsToJson(this); } @JsonSerializable() @@ -79,10 +83,18 @@ class GetTimetableResponseObjectFieldsObject { String? longname; String? externalkey; - GetTimetableResponseObjectFieldsObject({this.id, this.name, this.longname, this.externalkey}); + GetTimetableResponseObjectFieldsObject({ + this.id, + this.name, + this.longname, + this.externalkey, + }); - factory GetTimetableResponseObjectFieldsObject.fromJson(Map json) => _$GetTimetableResponseObjectFieldsObjectFromJson(json); - Map toJson() => _$GetTimetableResponseObjectFieldsObjectToJson(this); + factory GetTimetableResponseObjectFieldsObject.fromJson( + Map json, + ) => _$GetTimetableResponseObjectFieldsObjectFromJson(json); + Map toJson() => + _$GetTimetableResponseObjectFieldsObjectToJson(this); } @JsonSerializable() @@ -92,10 +104,17 @@ class GetTimetableResponseObjectClass { String longname; String? externalkey; - GetTimetableResponseObjectClass(this.id, this.name, this.longname, this.externalkey); + GetTimetableResponseObjectClass( + this.id, + this.name, + this.longname, + this.externalkey, + ); - factory GetTimetableResponseObjectClass.fromJson(Map json) => _$GetTimetableResponseObjectClassFromJson(json); - Map toJson() => _$GetTimetableResponseObjectClassToJson(this); + factory GetTimetableResponseObjectClass.fromJson(Map json) => + _$GetTimetableResponseObjectClassFromJson(json); + Map toJson() => + _$GetTimetableResponseObjectClassToJson(this); } @JsonSerializable() @@ -107,11 +126,20 @@ class GetTimetableResponseObjectTeacher { String? orgname; String? externalkey; + GetTimetableResponseObjectTeacher( + this.id, + this.name, + this.longname, + this.orgid, + this.orgname, + this.externalkey, + ); - GetTimetableResponseObjectTeacher(this.id, this.name, this.longname, this.orgid, this.orgname, this.externalkey); - - factory GetTimetableResponseObjectTeacher.fromJson(Map json) => _$GetTimetableResponseObjectTeacherFromJson(json); - Map toJson() => _$GetTimetableResponseObjectTeacherToJson(this); + factory GetTimetableResponseObjectTeacher.fromJson( + Map json, + ) => _$GetTimetableResponseObjectTeacherFromJson(json); + Map toJson() => + _$GetTimetableResponseObjectTeacherToJson(this); } @JsonSerializable() @@ -122,8 +150,11 @@ class GetTimetableResponseObjectSubject { GetTimetableResponseObjectSubject(this.id, this.name, this.longname); - factory GetTimetableResponseObjectSubject.fromJson(Map json) => _$GetTimetableResponseObjectSubjectFromJson(json); - Map toJson() => _$GetTimetableResponseObjectSubjectToJson(this); + factory GetTimetableResponseObjectSubject.fromJson( + Map json, + ) => _$GetTimetableResponseObjectSubjectFromJson(json); + Map toJson() => + _$GetTimetableResponseObjectSubjectToJson(this); } @JsonSerializable() @@ -134,6 +165,7 @@ class GetTimetableResponseObjectRoom { GetTimetableResponseObjectRoom(this.id, this.name, this.longname); - factory GetTimetableResponseObjectRoom.fromJson(Map json) => _$GetTimetableResponseObjectRoomFromJson(json); + factory GetTimetableResponseObjectRoom.fromJson(Map json) => + _$GetTimetableResponseObjectRoomFromJson(json); Map toJson() => _$GetTimetableResponseObjectRoomToJson(this); } diff --git a/lib/api/webuntis/services/lesson_resolver.dart b/lib/api/webuntis/services/lesson_resolver.dart index 77128e5..5403001 100644 --- a/lib/api/webuntis/services/lesson_resolver.dart +++ b/lib/api/webuntis/services/lesson_resolver.dart @@ -9,10 +9,14 @@ import '../queries/get_subjects/get_subjects_response.dart'; /// When a record is missing the resolver returns a placeholder fallback /// instead of `null` so call sites stay branch-free. class LessonResolver { - static GetSubjectsResponseObject resolveSubject(TimetableState state, int? id) { + static GetSubjectsResponseObject resolveSubject( + TimetableState state, + int? id, + ) { final fallback = GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true); if (id == null) return fallback; - return state.subjects?.result.firstWhereOrNull((s) => s.id == id) ?? fallback; + return state.subjects?.result.firstWhereOrNull((s) => s.id == id) ?? + fallback; } static GetRoomsResponseObject resolveRoom(TimetableState state, int? id) { @@ -61,9 +65,7 @@ class LessonFormatter { /// optional longname (rendered in parentheses if it differs from `name`), /// and optional extra info (joined with `·`). static String formatLine(String name, {String? longname, String? extra}) { - final parts = [ - if (name.isNotEmpty) name else '?', - ]; + final parts = [if (name.isNotEmpty) name else '?']; final ln = (longname ?? '').trim(); if (ln.isNotEmpty && ln != name) parts.add('($ln)'); final ex = (extra ?? '').trim(); diff --git a/lib/api/webuntis/webuntis_api.dart b/lib/api/webuntis/webuntis_api.dart index 690bf94..93d7d87 100644 --- a/lib/api/webuntis/webuntis_api.dart +++ b/lib/api/webuntis/webuntis_api.dart @@ -14,18 +14,24 @@ import 'queries/authenticate/authenticate.dart'; import 'webuntis_error.dart'; abstract class WebuntisApi extends ApiRequest { - Uri endpoint = Uri.parse('https://${EndpointData().webuntis().full()}/WebUntis/jsonrpc.do?school=marianum-fulda'); + Uri endpoint = Uri.parse( + 'https://${EndpointData().webuntis().full()}/WebUntis/jsonrpc.do?school=marianum-fulda', + ); String method; ApiParams? genericParam; http.Response? response; bool authenticatedResponse; - WebuntisApi(this.method, this.genericParam, {this.authenticatedResponse = true}); - + WebuntisApi( + this.method, + this.genericParam, { + this.authenticatedResponse = true, + }); Future query(WebuntisApi untis, {bool retry = false}) async { - final body = '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}'; + final body = + '{"id":"ID","method":"$method","params":${untis._body()},"jsonrpc":"2.0"}'; var sessionId = '0'; if (authenticatedResponse) { @@ -38,13 +44,20 @@ abstract class WebuntisApi extends ApiRequest { try { jsonData = jsonDecode(data.body) as Map; } on FormatException catch (e) { - throw ParseException(technicalDetails: 'WebUntis JSON decode: ${e.message}'); + throw ParseException( + technicalDetails: 'WebUntis JSON decode: ${e.message}', + ); } final error = jsonData['error'] as Map?; if (error != null) { final code = error['code'] as int; if (code == -8520) { - if (retry) throw WebuntisError('Authentication was tried (probably session timeout), but was not successful!', -8520); + if (retry) { + throw WebuntisError( + 'Authentication was tried (probably session timeout), but was not successful!', + -8520, + ); + } await Authenticate.createSession(); return query(untis, retry: true); } else { @@ -65,14 +78,22 @@ abstract class WebuntisApi extends ApiRequest { Future post(String data, Map? headers) async { try { - return await http.post(endpoint, body: data, headers: headers).timeout( + return await http + .post(endpoint, body: data, headers: headers) + .timeout( const Duration(seconds: 10), - onTimeout: () => throw NetworkException.timeout(technicalDetails: 'WebUntis $method timed out after 10s'), + onTimeout: () => throw NetworkException.timeout( + technicalDetails: 'WebUntis $method timed out after 10s', + ), ); } on SocketException catch (e) { - throw NetworkException(technicalDetails: 'WebUntis $method: ${e.message}'); + throw NetworkException( + technicalDetails: 'WebUntis $method: ${e.message}', + ); } on http.ClientException catch (e) { - throw NetworkException(technicalDetails: 'WebUntis $method: ${e.message}'); + throw NetworkException( + technicalDetails: 'WebUntis $method: ${e.message}', + ); } } } diff --git a/lib/app.dart b/lib/app.dart index b33a5e5..6ca7924 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -95,7 +95,9 @@ class _AppState extends State with WidgetsBindingObserver { if (!mounted) return; NotificationController.onForegroundMessageHandler(message, context); }); - FirebaseMessaging.onBackgroundMessage(NotificationController.onBackgroundMessageHandler); + FirebaseMessaging.onBackgroundMessage( + NotificationController.onBackgroundMessageHandler, + ); FirebaseMessaging.onMessageOpenedApp.listen((message) { if (!mounted) return; @@ -119,83 +121,89 @@ class _AppState extends State with WidgetsBindingObserver { } @override - Widget build(BuildContext context) => BlocBuilder( - builder: (context, _) { - final bottomBarModules = AppModule.getBottomBarModules(context); - final totalTabs = bottomBarModules.length + 1; - final currentIndex = Main.bottomNavigator.index; + Widget build( + BuildContext context, + ) => BlocBuilder( + builder: (context, _) { + final bottomBarModules = AppModule.getBottomBarModules(context); + final totalTabs = bottomBarModules.length + 1; + final currentIndex = Main.bottomNavigator.index; - // The bottom-bar layout is identified by the ordered list of module - // names plus the trailing 'more' slot. Whenever this layout changes - // — slot count, reordering, or hiding a module — we recreate the - // entire PersistentTabView via the [layoutKey] below. The package - // caches per-tab navigator state by index in `_navigatorKeys`, and - // its internal `alignLength` only ever appends or trims at the end. - // So when the module sitting at e.g. index 3 changes, the navigator - // at that index still serves the old screen's route stack and the - // user sees stale content. Re-mounting clears those stacks; the - // trade-off (losing in-tab pushed routes on a settings change) is - // acceptable since the user explicitly re-shaped the bar. - final layoutKey = ValueKey('${bottomBarModules.map((m) => m.module.name).join('|')}|more'); - - if (totalTabs != _knownTotalTabs) { - var targetIndex = currentIndex; - if (_userOnLastTab) { - targetIndex = totalTabs - 1; - } else if (currentIndex >= totalTabs) { - targetIndex = totalTabs - 1; - } - // Re-mounting PTV with a new key constructs fresh internals from - // its controller's current index. If the controller still points - // past the new tab list, Style6BottomNavBar (and others) crash on - // out-of-range access during initState. Replace the controller - // atomically with one initialised at the safe target index so the - // new PTV mounts cleanly. - if (targetIndex != currentIndex) { - Main.bottomNavigator.removeListener(_onTabControllerChanged); - Main.bottomNavigator = PersistentTabController(initialIndex: targetIndex); - Main.bottomNavigator.addListener(_onTabControllerChanged); - _userOnLastTab = targetIndex == totalTabs - 1; - } - } - - _knownTotalTabs = totalTabs; - - return PersistentTabView( - key: layoutKey, - controller: Main.bottomNavigator, - navBarOverlap: const NavBarOverlap.none(), - backgroundColor: Theme.of(context).colorScheme.primary, - handleAndroidBackButtonPress: true, - screenTransitionAnimation: const ScreenTransitionAnimation( - curve: Curves.easeOutQuad, - duration: Duration(milliseconds: 200), - ), - tabs: [ - ...bottomBarModules.map((e) => e.toBottomTab(context)), - PersistentTabConfig( - screen: const Breaker(breaker: BreakerArea.more, child: Overhang()), - item: ItemConfig( - activeForegroundColor: Theme.of(context).primaryColor, - inactiveForegroundColor: Theme.of(context).colorScheme.secondary, - icon: const Icon(Icons.apps), - title: 'Mehr', - ), - ), - ], - navBarBuilder: (config) => Style6BottomNavBar( - // Style6BottomNavBar builds its internal animation controller list - // in initState and never grows it on didUpdateWidget. Keying by the - // item count forces a fresh State whenever the slot count changes, - // which avoids a RangeError when more tabs slide in. - key: ValueKey(config.items.length), - navBarConfig: config, - navBarDecoration: NavBarDecoration( - border: const Border(top: BorderSide(width: 1, color: Colors.grey)), - color: Theme.of(context).colorScheme.surface, - ), - ), - ); - }, + // The bottom-bar layout is identified by the ordered list of module + // names plus the trailing 'more' slot. Whenever this layout changes + // — slot count, reordering, or hiding a module — we recreate the + // entire PersistentTabView via the [layoutKey] below. The package + // caches per-tab navigator state by index in `_navigatorKeys`, and + // its internal `alignLength` only ever appends or trims at the end. + // So when the module sitting at e.g. index 3 changes, the navigator + // at that index still serves the old screen's route stack and the + // user sees stale content. Re-mounting clears those stacks; the + // trade-off (losing in-tab pushed routes on a settings change) is + // acceptable since the user explicitly re-shaped the bar. + final layoutKey = ValueKey( + '${bottomBarModules.map((m) => m.module.name).join('|')}|more', ); + + if (totalTabs != _knownTotalTabs) { + var targetIndex = currentIndex; + if (_userOnLastTab) { + targetIndex = totalTabs - 1; + } else if (currentIndex >= totalTabs) { + targetIndex = totalTabs - 1; + } + // Re-mounting PTV with a new key constructs fresh internals from + // its controller's current index. If the controller still points + // past the new tab list, Style6BottomNavBar (and others) crash on + // out-of-range access during initState. Replace the controller + // atomically with one initialised at the safe target index so the + // new PTV mounts cleanly. + if (targetIndex != currentIndex) { + Main.bottomNavigator.removeListener(_onTabControllerChanged); + Main.bottomNavigator = PersistentTabController( + initialIndex: targetIndex, + ); + Main.bottomNavigator.addListener(_onTabControllerChanged); + _userOnLastTab = targetIndex == totalTabs - 1; + } + } + + _knownTotalTabs = totalTabs; + + return PersistentTabView( + key: layoutKey, + controller: Main.bottomNavigator, + navBarOverlap: const NavBarOverlap.none(), + backgroundColor: Theme.of(context).colorScheme.primary, + handleAndroidBackButtonPress: true, + screenTransitionAnimation: const ScreenTransitionAnimation( + curve: Curves.easeOutQuad, + duration: Duration(milliseconds: 200), + ), + tabs: [ + ...bottomBarModules.map((e) => e.toBottomTab(context)), + PersistentTabConfig( + screen: const Breaker(breaker: BreakerArea.more, child: Overhang()), + item: ItemConfig( + activeForegroundColor: Theme.of(context).primaryColor, + inactiveForegroundColor: Theme.of(context).colorScheme.secondary, + icon: const Icon(Icons.apps), + title: 'Mehr', + ), + ), + ], + navBarBuilder: (config) => Style6BottomNavBar( + // Style6BottomNavBar builds its internal animation controller list + // in initState and never grows it on didUpdateWidget. Keying by the + // item count forces a fresh State whenever the slot count changes, + // which avoids a RangeError when more tabs slide in. + key: ValueKey(config.items.length), + navBarConfig: config, + navBarDecoration: NavBarDecoration( + border: const Border(top: BorderSide(width: 1, color: Colors.grey)), + color: Theme.of(context).colorScheme.surface, + ), + ), + ); + }, + ); } diff --git a/lib/extensions/date_time.dart b/lib/extensions/date_time.dart index 7dc0d52..830f2b7 100644 --- a/lib/extensions/date_time.dart +++ b/lib/extensions/date_time.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; extension IsSameDay on DateTime { - bool isSameDay(DateTime other) => year == other.year && month == other.month && day == other.day; + bool isSameDay(DateTime other) => + year == other.year && month == other.month && day == other.day; - DateTime nextWeekday(int day) => add(Duration(days: (day - weekday) % DateTime.daysPerWeek)); + DateTime nextWeekday(int day) => + add(Duration(days: (day - weekday) % DateTime.daysPerWeek)); - DateTime withTime(TimeOfDay time) => copyWith(hour: time.hour, minute: time.minute); + DateTime withTime(TimeOfDay time) => + copyWith(hour: time.hour, minute: time.minute); TimeOfDay toTimeOfDay() => TimeOfDay(hour: hour, minute: minute); @@ -25,15 +28,20 @@ extension IsSameDay on DateTime { extension DateTimeFormatting on DateTime { String formatHm() => Jiffy.parseFromDateTime(this).format(pattern: 'HH:mm'); - String formatDate() => Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.yyyy'); + String formatDate() => + Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.yyyy'); - String formatDateTime() => Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.yyyy HH:mm'); + String formatDateTime() => + Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.yyyy HH:mm'); - String formatDateShort() => Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.'); + String formatDateShort() => + Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM.'); - String formatDateShortHm() => Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM. HH:mm'); + String formatDateShortHm() => + Jiffy.parseFromDateTime(this).format(pattern: 'dd.MM. HH:mm'); - String formatMonthYear() => Jiffy.parseFromDateTime(this).format(pattern: 'MMMM yyyy'); + String formatMonthYear() => + Jiffy.parseFromDateTime(this).format(pattern: 'MMMM yyyy'); String formatRelative() => Jiffy.parseFromDateTime(this).fromNow(); diff --git a/lib/extensions/render_not_null.dart b/lib/extensions/render_not_null.dart index 3d267a0..b7be60f 100644 --- a/lib/extensions/render_not_null.dart +++ b/lib/extensions/render_not_null.dart @@ -1,3 +1,4 @@ extension RenderNotNullExt on T? { - R? wrapNullable(R Function(T data) defaultValueCallback) => this != null ? defaultValueCallback(this as T) : null; + R? wrapNullable(R Function(T data) defaultValueCallback) => + this != null ? defaultValueCallback(this as T) : null; } diff --git a/lib/extensions/text.dart b/lib/extensions/text.dart index 735a860..879e7ec 100644 --- a/lib/extensions/text.dart +++ b/lib/extensions/text.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; extension TextExt on Text { Size get size { final textPainter = TextPainter( - text: TextSpan(text: data, style: style), - maxLines: 1, - textDirection: TextDirection.ltr + text: TextSpan(text: data, style: style), + maxLines: 1, + textDirection: TextDirection.ltr, )..layout(minWidth: 0, maxWidth: double.infinity); return textPainter.size; } diff --git a/lib/extensions/time_of_day.dart b/lib/extensions/time_of_day.dart index c99a47e..b2c39e0 100644 --- a/lib/extensions/time_of_day.dart +++ b/lib/extensions/time_of_day.dart @@ -5,5 +5,6 @@ extension TimeOfDayExt on TimeOfDay { bool isAfter(TimeOfDay other) => hour > other.hour && minute > other.minute; - TimeOfDay add({int hours = 0, int minutes = 0}) => replacing(hour: hour + hours, minute: minute + minutes); + TimeOfDay add({int hours = 0, int minutes = 0}) => + replacing(hour: hour + hours, minute: minute + minutes); } diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index e389256..9521cbc 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -63,7 +63,8 @@ class DefaultFirebaseOptions { messagingSenderId: '522850592536', projectId: 'marmobile-33b10', storageBucket: 'marmobile-33b10.appspot.com', - iosClientId: '522850592536-edj90sbbnkjqe3aqui37j8enu93v4fk8.apps.googleusercontent.com', + iosClientId: + '522850592536-edj90sbbnkjqe3aqui37j8enu93v4fk8.apps.googleusercontent.com', iosBundleId: 'eu.mhsl.marianum.mobile.client', ); } diff --git a/lib/main.dart b/lib/main.dart index e392be5..3d02fec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,19 +40,28 @@ Future main() async { log('MarianumMobile started'); WidgetsFlutterBinding.ensureInitialized(); - void addCertificateAsTrusted(ByteData certificate) => - SecurityContext.defaultContext.setTrustedCertificatesBytes(certificate.buffer.asUint8List()); + void addCertificateAsTrusted(ByteData certificate) => SecurityContext + .defaultContext + .setTrustedCertificatesBytes(certificate.buffer.asUint8List()); final initialisationTasks = [ Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) .then((_) {}) .onError((error, _) => log('Error initializing Firebase: $error')), - PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem').then(addCertificateAsTrusted), - PlatformAssetBundle().load('assets/ca/lets-encrypt-r10.pem').then(addCertificateAsTrusted), - PlatformAssetBundle().load('assets/ca/lets-encrypt-r13.pem').then(addCertificateAsTrusted), + PlatformAssetBundle() + .load('assets/ca/lets-encrypt-r3.pem') + .then(addCertificateAsTrusted), + PlatformAssetBundle() + .load('assets/ca/lets-encrypt-r10.pem') + .then(addCertificateAsTrusted), + PlatformAssetBundle() + .load('assets/ca/lets-encrypt-r13.pem') + .then(addCertificateAsTrusted), Future(() async { final storage = await HydratedStorage.build( - storageDirectory: HydratedStorageDirectory((await getTemporaryDirectory()).path), + storageDirectory: HydratedStorageDirectory( + (await getTemporaryDirectory()).path, + ), ); HydratedBloc.storage = storage; }), @@ -71,27 +80,30 @@ Future main() async { if (kReleaseMode) { ErrorWidget.builder = (error) => Material( - color: Colors.white, - child: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.phonelink_erase_rounded, size: 40), - const SizedBox(height: 12), - Text(error.toStringShort(), textAlign: TextAlign.center), - ], - ), - ), + color: Colors.white, + child: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.phonelink_erase_rounded, size: 40), + const SizedBox(height: 12), + Text(error.toStringShort(), textAlign: TextAlign.center), + ], ), - ); + ), + ), + ); } // Capture uncaught Flutter and platform errors so they show up in logs // instead of being silently swallowed. FlutterError.onError = (details) { - log('Uncaught Flutter error: ${details.exception}', stackTrace: details.stack); + log( + 'Uncaught Flutter error: ${details.exception}', + stackTrace: details.stack, + ); FlutterError.presentError(details); }; PlatformDispatcher.instance.onError = (error, stack) { @@ -104,9 +116,13 @@ Future main() async { MultiBlocProvider( providers: [ BlocProvider(create: (_) => SettingsCubit()), - BlocProvider(create: (_) => AccountBloc( - initialStatus: AccountData().isPopulated() ? AccountStatus.loggedIn : AccountStatus.loggedOut, - )), + BlocProvider( + create: (_) => AccountBloc( + initialStatus: AccountData().isPopulated() + ? AccountStatus.loggedIn + : AccountStatus.loggedOut, + ), + ), BlocProvider(create: (_) => BreakerBloc()), BlocProvider(create: (_) => ChatListBloc()), BlocProvider(create: (_) => ChatBloc()), @@ -120,7 +136,9 @@ Future main() async { class Main extends StatefulWidget { const Main({super.key}); - static PersistentTabController bottomNavigator = PersistentTabController(initialIndex: 0); + static PersistentTabController bottomNavigator = PersistentTabController( + initialIndex: 0, + ); @override State
createState() => _MainState(); @@ -134,107 +152,116 @@ class _MainState extends State
{ AccountData().waitForPopulation().then((value) { if (!mounted) return; - context.read().setStatus(value ? AccountStatus.loggedIn : AccountStatus.loggedOut); + context.read().setStatus( + value ? AccountStatus.loggedIn : AccountStatus.loggedOut, + ); }); } @override Widget build(BuildContext context) => Directionality( - textDirection: TextDirection.ltr, - child: BlocBuilder( - builder: (context, settings) { - final devToolsSettings = settings.devToolsSettings; - return MaterialApp( - showPerformanceOverlay: devToolsSettings.showPerformanceOverlay, - checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers, - checkerboardRasterCacheImages: devToolsSettings.checkerboardRasterCacheImages, - debugShowCheckedModeBanner: false, - localizationsDelegates: const [ - ...GlobalMaterialLocalizations.delegates, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: const [Locale('de'), Locale('en')], - locale: const Locale('de'), - title: 'Marianum Fulda', - themeMode: settings.appTheme, - theme: LightAppTheme.theme, - darkTheme: DarkAppTheme.theme, - // Brand-colored backdrop behind every route. During the logout - // home-swap and route pop animations the framework can briefly - // expose the layer below the topmost Scaffold; without this - // the dark Material default shows through and the user sees a - // black flash. - builder: (context, child) => ColoredBox( - color: LightAppTheme.marianumRed, - child: child ?? const SizedBox.shrink(), - ), - home: LoaderOverlay( - child: Breaker( - breaker: BreakerArea.global, - child: BlocConsumer( - listenWhen: (previous, current) => previous.status != current.status, - listener: (context, accountState) { - if (accountState.status != AccountStatus.loggedOut) return; - // Routes pushed via AppRoutes (e.g. Settings) live on the - // root navigator and survive the home swap below, so they - // would still cover the Login screen after logout. Pop - // them here so the user immediately sees Login. - final navigator = Navigator.of(context); - if (navigator.canPop()) { - navigator.popUntil((route) => route.isFirst); - } - // Capture bloc references before the post-frame callback - // — by the time it runs the dialog/Settings context is - // gone but this listener context is still valid. - final settingsCubit = context.read(); - final timetableBloc = context.read(); - final chatListBloc = context.read(); - final chatBloc = context.read(); - final breakerBloc = context.read(); - // Defer the actual wipe until after this frame so the - // App tree (TimetableBloc/ChatListBloc watchers etc.) - // is already torn down. Resetting blocs while App is - // still in front caused a black-frame race. - WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(_wipeUserState( - settingsCubit: settingsCubit, - timetableBloc: timetableBloc, - chatListBloc: chatListBloc, - chatBloc: chatBloc, - breakerBloc: breakerBloc, - )); - }); - }, - builder: (context, accountState) { - switch (accountState.status) { - case AccountStatus.loggedIn: - return const App(); - case AccountStatus.loggedOut: - return const Login(); - case AccountStatus.undefined: - return Scaffold( - backgroundColor: LightAppTheme.marianumRed, - body: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AppProgressIndicator.large(color: Colors.white), - SizedBox(height: 16), - Text('Konto wird geladen…', - style: TextStyle(color: Colors.white)), - ], + textDirection: TextDirection.ltr, + child: BlocBuilder( + builder: (context, settings) { + final devToolsSettings = settings.devToolsSettings; + return MaterialApp( + showPerformanceOverlay: devToolsSettings.showPerformanceOverlay, + checkerboardOffscreenLayers: + devToolsSettings.checkerboardOffscreenLayers, + checkerboardRasterCacheImages: + devToolsSettings.checkerboardRasterCacheImages, + debugShowCheckedModeBanner: false, + localizationsDelegates: const [ + ...GlobalMaterialLocalizations.delegates, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: const [Locale('de'), Locale('en')], + locale: const Locale('de'), + title: 'Marianum Fulda', + themeMode: settings.appTheme, + theme: LightAppTheme.theme, + darkTheme: DarkAppTheme.theme, + // Brand-colored backdrop behind every route. During the logout + // home-swap and route pop animations the framework can briefly + // expose the layer below the topmost Scaffold; without this + // the dark Material default shows through and the user sees a + // black flash. + builder: (context, child) => ColoredBox( + color: LightAppTheme.marianumRed, + child: child ?? const SizedBox.shrink(), + ), + home: LoaderOverlay( + child: Breaker( + breaker: BreakerArea.global, + child: BlocConsumer( + listenWhen: (previous, current) => + previous.status != current.status, + listener: (context, accountState) { + if (accountState.status != AccountStatus.loggedOut) return; + // Routes pushed via AppRoutes (e.g. Settings) live on the + // root navigator and survive the home swap below, so they + // would still cover the Login screen after logout. Pop + // them here so the user immediately sees Login. + final navigator = Navigator.of(context); + if (navigator.canPop()) { + navigator.popUntil((route) => route.isFirst); + } + // Capture bloc references before the post-frame callback + // — by the time it runs the dialog/Settings context is + // gone but this listener context is still valid. + final settingsCubit = context.read(); + final timetableBloc = context.read(); + final chatListBloc = context.read(); + final chatBloc = context.read(); + final breakerBloc = context.read(); + // Defer the actual wipe until after this frame so the + // App tree (TimetableBloc/ChatListBloc watchers etc.) + // is already torn down. Resetting blocs while App is + // still in front caused a black-frame race. + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited( + _wipeUserState( + settingsCubit: settingsCubit, + timetableBloc: timetableBloc, + chatListBloc: chatListBloc, + chatBloc: chatBloc, + breakerBloc: breakerBloc, + ), + ); + }); + }, + builder: (context, accountState) { + switch (accountState.status) { + case AccountStatus.loggedIn: + return const App(); + case AccountStatus.loggedOut: + return const Login(); + case AccountStatus.undefined: + return Scaffold( + backgroundColor: LightAppTheme.marianumRed, + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppProgressIndicator.large(color: Colors.white), + SizedBox(height: 16), + Text( + 'Konto wird geladen…', + style: TextStyle(color: Colors.white), ), - ), - ); - } - }, - ), - ), + ], + ), + ), + ); + } + }, ), - ); - }, - ), - ); + ), + ), + ); + }, + ), + ); } Future _wipeUserState({ diff --git a/lib/model/account_data.dart b/lib/model/account_data.dart index 35680da..6ec006a 100644 --- a/lib/model/account_data.dart +++ b/lib/model/account_data.dart @@ -34,11 +34,16 @@ class AccountData { return _password!; } - String getUserSecret() => - sha512.convert(utf8.encode('${getUsername()}:${getPassword()}')).toString(); + String getUserSecret() => sha512 + .convert(utf8.encode('${getUsername()}:${getPassword()}')) + .toString(); Future getDeviceId() async => sha512 - .convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}')) + .convert( + utf8.encode( + '${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}', + ), + ) .toString(); Future setData(String username, String password) async { @@ -92,7 +97,11 @@ class AccountData { /// Prefer this over embedding credentials in URLs — error logs and crash /// reports often capture the URL but not headers. String getBasicAuthHeader() { - if (!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!'); + if (!isPopulated()) { + throw Exception( + 'AccountData (e.g. username or password) is not initialized!', + ); + } return 'Basic ${base64Encode(utf8.encode('$_username:$_password'))}'; } diff --git a/lib/model/data_cleaner.dart b/lib/model/data_cleaner.dart index 4094023..ed88004 100644 --- a/lib/model/data_cleaner.dart +++ b/lib/model/data_cleaner.dart @@ -4,11 +4,20 @@ import '../api/request_cache.dart'; class DataCleaner { static Future cleanOldCache() async { - final cacheData = await Localstore.instance.collection(RequestCache.collection).get(); + final cacheData = await Localstore.instance + .collection(RequestCache.collection) + .get(); cacheData?.forEach((key, value) async { - final lastUpdate = DateTime.fromMillisecondsSinceEpoch(value['lastupdate'] as int); - if (DateTime.now().subtract(const Duration(days: 200)).isAfter(lastUpdate)) { - await Localstore.instance.collection(RequestCache.collection).doc(key.split('/').last).delete(); + final lastUpdate = DateTime.fromMillisecondsSinceEpoch( + value['lastupdate'] as int, + ); + if (DateTime.now() + .subtract(const Duration(days: 200)) + .isAfter(lastUpdate)) { + await Localstore.instance + .collection(RequestCache.collection) + .doc(key.split('/').last) + .delete(); } }); } diff --git a/lib/model/endpoint_data.dart b/lib/model/endpoint_data.dart index 6bdd4f4..6bbe016 100644 --- a/lib/model/endpoint_data.dart +++ b/lib/model/endpoint_data.dart @@ -1,10 +1,6 @@ - import 'account_data.dart'; -enum EndpointMode { - live, - stage, -} +enum EndpointMode { live, stage } class EndpointOptions { Endpoint live; @@ -12,7 +8,7 @@ class EndpointOptions { EndpointOptions({required this.live, required this.staged}); Endpoint get(EndpointMode mode) { - if(staged == null || mode == EndpointMode.live) return live; + if (staged == null || mode == EndpointMode.live) return live; return staged!; } } @@ -36,27 +32,21 @@ class EndpointData { EndpointMode getEndpointMode() { late String existingName; existingName = AccountData().getUsername(); - return existingName.startsWith('google') ? EndpointMode.stage : EndpointMode.live; + return existingName.startsWith('google') + ? EndpointMode.stage + : EndpointMode.live; } Endpoint webuntis() => EndpointOptions( - live: Endpoint( - domain: 'marianum-fulda.webuntis.com', - ), - staged: Endpoint( - domain: 'mhsl.eu', - path: '/marianum/marianummobile/webuntis/public/index.php/api' - ), - ).get(getEndpointMode()); + live: Endpoint(domain: 'marianum-fulda.webuntis.com'), + staged: Endpoint( + domain: 'mhsl.eu', + path: '/marianum/marianummobile/webuntis/public/index.php/api', + ), + ).get(getEndpointMode()); Endpoint nextcloud() => EndpointOptions( - live: Endpoint( - domain: 'cloud.marianum-fulda.de', - ), - staged: Endpoint( - domain: 'mhsl.eu', - path: '/marianum/marianummobile/cloud', - ) - ).get(getEndpointMode()); - + live: Endpoint(domain: 'cloud.marianum-fulda.de'), + staged: Endpoint(domain: 'mhsl.eu', path: '/marianum/marianummobile/cloud'), + ).get(getEndpointMode()); } diff --git a/lib/notification/notification_controller.dart b/lib/notification/notification_controller.dart index 162ff28..88cf348 100644 --- a/lib/notification/notification_controller.dart +++ b/lib/notification/notification_controller.dart @@ -13,20 +13,29 @@ class NotificationController { NotificationTasks.updateBadgeCount(message); } - static Future onForegroundMessageHandler(RemoteMessage message, BuildContext context) async { + static Future onForegroundMessageHandler( + RemoteMessage message, + BuildContext context, + ) async { NotificationTasks.updateProviders(context); NotificationTasks.updateBadgeCount(message); } - static Future onAppOpenedByNotification(RemoteMessage message, BuildContext context) async { - NotificationTasks.navigateToTalk(context, chatToken: _extractChatToken(message)); + static Future onAppOpenedByNotification( + RemoteMessage message, + BuildContext context, + ) async { + NotificationTasks.navigateToTalk( + context, + chatToken: _extractChatToken(message), + ); NotificationTasks.updateProviders(context); DebugTile(context).run(() { InfoDialog.show( context, 'Dieser Bericht wird angezeigt, da du den Entwicklermodus aktiviert hast und die App über eine Benachrichtigung geöffnet wurde.\n\n' - '${JsonViewer.format(message.data)}', + '${JsonViewer.format(message.data)}', copyable: true, title: 'Notification report', ); diff --git a/lib/notification/notification_service.dart b/lib/notification/notification_service.dart index 5c5ae6c..7ee3d86 100644 --- a/lib/notification/notification_service.dart +++ b/lib/notification/notification_service.dart @@ -7,16 +7,15 @@ class NotificationService { NotificationService._internal(); - FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); Future initializeNotifications() async { const androidSettings = AndroidInitializationSettings( - '@mipmap/ic_launcher' - ); - - final iosSettings = DarwinInitializationSettings( + '@mipmap/ic_launcher', ); + final iosSettings = DarwinInitializationSettings(); final initializationSettings = InitializationSettings( android: androidSettings, @@ -24,13 +23,16 @@ class NotificationService { ); await flutterLocalNotificationsPlugin.initialize( - settings: initializationSettings + settings: initializationSettings, ); } - Future showNotification({required String title, required String body, required int badgeCount}) async { - const androidPlatformChannelSpecifics = - AndroidNotificationDetails( + Future showNotification({ + required String title, + required String body, + required int badgeCount, + }) async { + const androidPlatformChannelSpecifics = AndroidNotificationDetails( 'marmobile', 'Marianum Fulda', importance: Importance.defaultImportance, @@ -42,14 +44,14 @@ class NotificationService { const platformChannelSpecifics = NotificationDetails( android: androidPlatformChannelSpecifics, - iOS: iosPlatformChannelSpecifics + iOS: iosPlatformChannelSpecifics, ); await flutterLocalNotificationsPlugin.show( id: 0, title: title, body: body, - notificationDetails: platformChannelSpecifics + notificationDetails: platformChannelSpecifics, ); } } diff --git a/lib/notification/notification_tasks.dart b/lib/notification/notification_tasks.dart index 42c310e..9a2f167 100644 --- a/lib/notification/notification_tasks.dart +++ b/lib/notification/notification_tasks.dart @@ -9,7 +9,9 @@ import '../state/app/modules/chat_list/bloc/chat_list_bloc.dart'; class NotificationTasks { static void updateBadgeCount(RemoteMessage notification) { - FlutterAppBadge.count(int.parse((notification.data['unreadCount'] as String?) ?? '0')); + FlutterAppBadge.count( + int.parse((notification.data['unreadCount'] as String?) ?? '0'), + ); } static void updateProviders(BuildContext context) { diff --git a/lib/notification/notify_updater.dart b/lib/notification/notify_updater.dart index dc3ae15..260c884 100644 --- a/lib/notification/notify_updater.dart +++ b/lib/notification/notify_updater.dart @@ -10,33 +10,42 @@ import '../state/app/modules/settings/bloc/settings_cubit.dart'; import '../widget/confirm_dialog.dart'; class NotifyUpdater { - static ConfirmDialog enableAfterDisclaimer(SettingsCubit settings) => ConfirmDialog( - title: 'Warnung', - icon: Icons.warning_amber, - content: '' - 'Die Push-Benachrichtigungen werden durch mhsl.eu versendet.\n\n' - 'Durch das aktivieren dieser Funktion wird dein Nutzername, dein Password und eine Geräte-ID von mhsl dauerhaft gespeichert und verarbeitet.\n\n' - 'Für mehr Informationen drücke lange auf die Einstellungsoption!', - confirmButton: 'Aktivieren', - onConfirm: () { - unawaited(FirebaseMessaging.instance.requestPermission(provisional: false)); - settings.val(write: true).notificationSettings.enabled = true; - unawaited(NotifyUpdater.registerToServer()); - }, + static ConfirmDialog enableAfterDisclaimer( + SettingsCubit settings, + ) => ConfirmDialog( + title: 'Warnung', + icon: Icons.warning_amber, + content: + '' + 'Die Push-Benachrichtigungen werden durch mhsl.eu versendet.\n\n' + 'Durch das aktivieren dieser Funktion wird dein Nutzername, dein Password und eine Geräte-ID von mhsl dauerhaft gespeichert und verarbeitet.\n\n' + 'Für mehr Informationen drücke lange auf die Einstellungsoption!', + confirmButton: 'Aktivieren', + onConfirm: () { + unawaited( + FirebaseMessaging.instance.requestPermission(provisional: false), ); + settings.val(write: true).notificationSettings.enabled = true; + unawaited(NotifyUpdater.registerToServer()); + }, + ); static Future registerToServer() async { final fcmToken = await FirebaseMessaging.instance.getToken(); if (fcmToken == null) { - throw Exception('Failed to register push notification because there is no FBC token!'); + throw Exception( + 'Failed to register push notification because there is no FBC token!', + ); } - unawaited(NotifyRegister( - NotifyRegisterParams( - username: AccountData().getUsername(), - password: AccountData().getPassword(), - fcmToken: fcmToken, - ), - ).run()); + unawaited( + NotifyRegister( + NotifyRegisterParams( + username: AccountData().getUsername(), + password: AccountData().getPassword(), + fcmToken: fcmToken, + ), + ).run(), + ); } } diff --git a/lib/routing/app_routes.dart b/lib/routing/app_routes.dart index dd7bb97..6f79929 100644 --- a/lib/routing/app_routes.dart +++ b/lib/routing/app_routes.dart @@ -38,7 +38,11 @@ class AppRoutes { pushScreen(context, withNavBar: false, screen: Files(path: path)); } - static void openFileViewer(BuildContext context, String localPath, {bool openExternal = false}) { + static void openFileViewer( + BuildContext context, + String localPath, { + bool openExternal = false, + }) { pushScreen( context, withNavBar: false, @@ -50,7 +54,11 @@ class AppRoutes { pushScreen(context, withNavBar: false, screen: const CustomEventsView()); } - static void openMarianumMessage(BuildContext context, String basePath, MarianumMessage message) { + static void openMarianumMessage( + BuildContext context, + String basePath, + MarianumMessage message, + ) { pushScreen( context, withNavBar: false, @@ -82,7 +90,11 @@ class AppRoutes { pushScreen(context, withNavBar: false, screen: const Roomplan()); } - static void openMessageReactions(BuildContext context, String token, int messageId) { + static void openMessageReactions( + BuildContext context, + String token, + int messageId, + ) { pushScreen( context, withNavBar: false, @@ -113,7 +125,9 @@ class AppRoutes { try { context.read().refresh(); } catch (e) { - if (kDebugMode) debugPrint('openChatByToken: ChatListBloc refresh failed: $e'); + if (kDebugMode) { + debugPrint('openChatByToken: ChatListBloc refresh failed: $e'); + } } } @@ -130,7 +144,10 @@ class AppRoutes { if (room == null) return null; final isGroup = room.type != GetRoomResponseObjectConversationType.oneToOne; - final avatar = UserAvatar(id: isGroup ? room.token : room.name, isGroup: isGroup); + final avatar = UserAvatar( + id: isGroup ? room.token : room.name, + isGroup: isGroup, + ); return ResolvedPendingChat( room: room, selfId: AccountData().getUsername(), @@ -138,7 +155,10 @@ class AppRoutes { ); } - static GetRoomResponseObject? _findRoomByToken(GetRoomResponse? rooms, String token) { + static GetRoomResponseObject? _findRoomByToken( + GetRoomResponse? rooms, + String token, + ) { if (rooms == null) return null; for (final room in rooms.data) { if (room.token == token) return room; @@ -155,10 +175,9 @@ class AppRoutes { /// Switches the bottom navigation to [module]. Returns false when the /// module is not currently in the bottom bar. static bool goToTab(BuildContext context, Modules module) { - final index = AppModule.getBottomBarModules(context) - .map((e) => e.module) - .toList() - .indexOf(module); + final index = AppModule.getBottomBarModules( + context, + ).map((e) => e.module).toList().indexOf(module); if (index == -1) return false; Main.bottomNavigator.jumpToTab(index); return true; diff --git a/lib/state/app/basis/dataloader/holiday_data_loader.dart b/lib/state/app/basis/dataloader/holiday_data_loader.dart index e384f00..b148ebe 100644 --- a/lib/state/app/basis/dataloader/holiday_data_loader.dart +++ b/lib/state/app/basis/dataloader/holiday_data_loader.dart @@ -3,7 +3,6 @@ import 'package:dio/dio.dart'; import '../../infrastructure/data_loader/data_loader.dart'; abstract class HolidayDataLoader extends DataLoader { - HolidayDataLoader() : super(Dio(BaseOptions( - baseUrl: 'https://ferien-api.de/api/v1/', - ))); + HolidayDataLoader() + : super(Dio(BaseOptions(baseUrl: 'https://ferien-api.de/api/v1/'))); } diff --git a/lib/state/app/basis/dataloader/mhsl_data_loader.dart b/lib/state/app/basis/dataloader/mhsl_data_loader.dart index fa29cf4..737dc7b 100644 --- a/lib/state/app/basis/dataloader/mhsl_data_loader.dart +++ b/lib/state/app/basis/dataloader/mhsl_data_loader.dart @@ -3,7 +3,8 @@ import 'package:dio/dio.dart'; import '../../infrastructure/data_loader/data_loader.dart'; abstract class MhslDataLoader extends DataLoader { - MhslDataLoader() : super(Dio(BaseOptions( - baseUrl: 'https://mhsl.eu/marianum/marianummobile/' - ))); + MhslDataLoader() + : super( + Dio(BaseOptions(baseUrl: 'https://mhsl.eu/marianum/marianummobile/')), + ); } diff --git a/lib/state/app/infrastructure/data_loader/data_loader.dart b/lib/state/app/infrastructure/data_loader/data_loader.dart index fabb054..8b1dc93 100644 --- a/lib/state/app/infrastructure/data_loader/data_loader.dart +++ b/lib/state/app/infrastructure/data_loader/data_loader.dart @@ -14,10 +14,14 @@ abstract class DataLoader { Future run() async { final response = await fetch(); try { - return assemble(DataLoaderResult( - json: jsonDecode(response.data!), - headers: response.headers.map.map((key, value) => MapEntry(key, value.join(';'))), - )); + return assemble( + DataLoaderResult( + json: jsonDecode(response.data!), + headers: response.headers.map.map( + (key, value) => MapEntry(key, value.join(';')), + ), + ), + ); } catch (e, stack) { log('DataLoader assemble failed', error: e, stackTrace: stack); rethrow; @@ -34,7 +38,8 @@ class DataLoaderResult { Map asMap() => json as Map; List asList() => json as List; - List> asListOfMaps() => asList().map((e) => e as Map).toList(); + List> asListOfMaps() => + asList().map((e) => e as Map).toList(); DataLoaderResult({required this.json, required this.headers}); } diff --git a/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_bloc.dart b/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_bloc.dart index daaca67..a17c85f 100644 --- a/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_bloc.dart +++ b/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_bloc.dart @@ -21,16 +21,19 @@ class LoadableStateBloc extends Bloc LoadableStateBloc() : super(const LoadableStateState(connections: null)) { on((event, emit) { emit(event.state); - if(connectivityStatusKnown() && isConnected()) { - if(reFetch == null) return; + if (connectivityStatusKnown() && isConnected()) { + if (reFetch == null) return; reFetch!(); } }); - void emitConnectivity(List result) => add(ConnectivityChanged(LoadableStateState(connections: result))); + void emitConnectivity(List result) => + add(ConnectivityChanged(LoadableStateState(connections: result))); Connectivity().checkConnectivity().then(emitConnectivity); - _updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity); + _updateStream = Connectivity().onConnectivityChanged.listen( + emitConnectivity, + ); WidgetsBinding.instance.addObserver(this); } @@ -38,42 +41,51 @@ class LoadableStateBloc extends Bloc void didChangeAppLifecycleState(AppLifecycleState state) { if (state != AppLifecycleState.resumed) return; final now = DateTime.now(); - if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) return; + if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) { + return; + } _lastResumeRefetch = now; // Re-check connectivity. The resulting [ConnectivityChanged] event takes // it from there: its handler updates the offline/online indicator and // triggers [reFetch] when the device is connected, so a stale // "Verbindung fehlgeschlagen" bar from a suspend-time fetch clears as // soon as the network is reachable again. - unawaited(Connectivity().checkConnectivity().then( - (result) => add(ConnectivityChanged(LoadableStateState(connections: result))), - )); + unawaited( + Connectivity().checkConnectivity().then( + (result) => + add(ConnectivityChanged(LoadableStateState(connections: result))), + ), + ); } bool connectivityStatusKnown() => state.connections != null; - bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true); + bool isConnected() => + !(state.connections?.contains(ConnectivityResult.none) ?? true); bool allowRetry() => reFetch != null; IconData connectionIcon() => connectivityStatusKnown() ? isConnected() - ? Icons.nearby_error - : Icons.signal_wifi_connected_no_internet_4 + ? Icons.nearby_error + : Icons.signal_wifi_connected_no_internet_4 : Icons.device_unknown; - Color connectionColor(BuildContext context) => connectivityStatusKnown() && !isConnected() + Color connectionColor(BuildContext context) => + connectivityStatusKnown() && !isConnected() ? Colors.grey.shade600 : Theme.of(context).primaryColor; - Color connectionForegroundColor(BuildContext context) => connectivityStatusKnown() && !isConnected() + Color connectionForegroundColor(BuildContext context) => + connectivityStatusKnown() && !isConnected() ? Colors.white - : ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) == Brightness.dark - ? Colors.white - : Colors.black; + : ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) == + Brightness.dark + ? Colors.white + : Colors.black; String connectionText({int? lastUpdated}) => connectivityStatusKnown() ? isConnected() - ? 'Verbindung fehlgeschlagen' - : 'Offline${lastUpdated == null ? '' : ' - Stand von ${DateTime.fromMillisecondsSinceEpoch(lastUpdated).formatRelative()}'}' + ? 'Verbindung fehlgeschlagen' + : 'Offline${lastUpdated == null ? '' : ' - Stand von ${DateTime.fromMillisecondsSinceEpoch(lastUpdated).formatRelative()}'}' : 'Unbekannte Fehlerursache'; @override diff --git a/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_event.dart b/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_event.dart index 80f3791..40955b6 100644 --- a/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_event.dart +++ b/lib/state/app/infrastructure/loadable_state/bloc/loadable_state_event.dart @@ -1,6 +1,7 @@ import 'loadable_state_state.dart'; sealed class LoadableStateEvent {} + final class ConnectivityChanged extends LoadableStateEvent { final LoadableStateState state; ConnectivityChanged(this.state); diff --git a/lib/state/app/infrastructure/loadable_state/view/loadable_state_background_loading.dart b/lib/state/app/infrastructure/loadable_state/view/loadable_state_background_loading.dart index 5147e45..b4f6d49 100644 --- a/lib/state/app/infrastructure/loadable_state/view/loadable_state_background_loading.dart +++ b/lib/state/app/infrastructure/loadable_state/view/loadable_state_background_loading.dart @@ -8,14 +8,15 @@ class LoadableStateBackgroundLoading extends StatelessWidget { @override Widget build(BuildContext context) => AnimatedSwitcher( - duration: LoadableStateConsumer.animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(), - ); + duration: LoadableStateConsumer.animationDuration, + transitionBuilder: (Widget child, Animation animation) => + SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(), + ); } diff --git a/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart index 8867571..16d3c7f 100644 --- a/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart +++ b/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart @@ -12,7 +12,12 @@ import 'loadable_state_error_bar.dart'; import 'loadable_state_error_screen.dart'; import 'loadable_state_primary_loading.dart'; -class LoadableStateConsumer, LoadableState>, TState> extends StatelessWidget { +class LoadableStateConsumer< + TController + extends Bloc, LoadableState>, + TState +> + extends StatelessWidget { final Widget Function(TState state, bool loading) child; final void Function(TState state)? onLoad; final bool wrapWithScrollView; @@ -39,12 +44,16 @@ class LoadableStateConsumer().state; final loadedData = loadableState.data; - if(!loadableState.isLoading && onLoad != null && loadedData is TState) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) => onLoad!(loadedData)); + if (!loadableState.isLoading && onLoad != null && loadedData is TState) { + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) => onLoad!(loadedData), + ); } final typedData = loadedData is TState ? loadedData : null; - final hasContent = typedData != null && (isReady?.call(typedData) ?? loadableState.showContent()); + final hasContent = + typedData != null && + (isReady?.call(typedData) ?? loadableState.showContent()); final hasError = loadableState.error != null; final isLoading = loadableState.isLoading; @@ -57,23 +66,23 @@ class LoadableStateConsumer RefreshIndicator( onRefresh: () { - if(loadableState.reFetch != null) loadableState.reFetch!(); + if (loadableState.reFetch != null) loadableState.reFetch!(); return Future.value(); }, child: ConditionalWrapper( condition: wrapWithScrollView, wrapper: (child) => SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), - child: child + child: child, ), child: child, - ) + ), ), child: SizedBox( height: MediaQuery.of(context).size.height, child: hasContent - ? child(typedData as TState, isLoading) - : const SizedBox.shrink(), + ? child(typedData as TState, isLoading) + : const SizedBox.shrink(), ), ); @@ -94,7 +103,9 @@ class LoadableStateConsumer(); - final isOfflineWithCache = hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected(); + final isOfflineWithCache = + hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected(); final shouldShow = visible || isOfflineWithCache; return AnimatedSize( duration: animationDuration, child: AnimatedSwitcher( - duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: Visibility( - key: Key(shouldShow.hashCode.toString()), - visible: shouldShow, - replacement: const SizedBox(width: double.infinity), - child: Builder( - builder: (context) { - var bloc = context.watch(); - return InkWell( - onTap: () { - if(!bloc.isConnected()) return; - final body = [ - if (message != null && message!.isNotEmpty) message!, - if (technicalDetails != null && technicalDetails!.isNotEmpty) technicalDetails!, - ].join('\n\n'); - if (body.isEmpty) return; - InfoDialog.show(context, body, copyable: true, title: 'Fehlerdetails'); - }, - child: Container( - height: 20, - decoration: BoxDecoration( - color: bloc.connectionColor(context), - ), - child: LoadableStateErrorBarText(lastUpdated: lastUpdated), - ), + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => + SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Visibility( + key: Key(shouldShow.hashCode.toString()), + visible: shouldShow, + replacement: const SizedBox(width: double.infinity), + child: Builder( + builder: (context) { + var bloc = context.watch(); + return InkWell( + onTap: () { + if (!bloc.isConnected()) return; + final body = [ + if (message != null && message!.isNotEmpty) message!, + if (technicalDetails != null && + technicalDetails!.isNotEmpty) + technicalDetails!, + ].join('\n\n'); + if (body.isEmpty) return; + InfoDialog.show( + context, + body, + copyable: true, + title: 'Fehlerdetails', ); }, - ) - ) + child: Container( + height: 20, + decoration: BoxDecoration( + color: bloc.connectionColor(context), + ), + child: LoadableStateErrorBarText(lastUpdated: lastUpdated), + ), + ); + }, + ), + ), ), ); } @@ -78,14 +87,18 @@ class LoadableStateErrorBarText extends StatefulWidget { const LoadableStateErrorBarText({required this.lastUpdated, super.key}); @override - State createState() => _LoadableStateErrorBarTextState(); + State createState() => + _LoadableStateErrorBarTextState(); } class _LoadableStateErrorBarTextState extends State { late Timer _rebuildTimer; @override void initState() { - _rebuildTimer = Timer.periodic(const Duration(seconds: 10), (timer) => setState(() {})); + _rebuildTimer = Timer.periodic( + const Duration(seconds: 10), + (timer) => setState(() {}), + ); super.initState(); } diff --git a/lib/state/app/infrastructure/loadable_state/view/loadable_state_error_screen.dart b/lib/state/app/infrastructure/loadable_state/view/loadable_state_error_screen.dart index dac7d3c..7f58eb5 100644 --- a/lib/state/app/infrastructure/loadable_state/view/loadable_state_error_screen.dart +++ b/lib/state/app/infrastructure/loadable_state/view/loadable_state_error_screen.dart @@ -20,52 +20,66 @@ class LoadableStateErrorScreen extends StatelessWidget { Widget build(BuildContext context) { final bloc = context.watch(); final isOffline = bloc.connectivityStatusKnown() && !bloc.isConnected(); - final headline = isOffline ? bloc.connectionText() : (message ?? bloc.connectionText()); + final headline = isOffline + ? bloc.connectionText() + : (message ?? bloc.connectionText()); return AnimatedOpacity( opacity: visible ? 1.0 : 0.0, duration: LoadableStateConsumer.animationDuration, curve: Curves.easeInOut, - child: !visible ? null : Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(bloc.connectionIcon(), size: 40), - const SizedBox(height: 12), - Text( - headline, - style: const TextStyle(fontSize: 20), - textAlign: TextAlign.center, + child: !visible + ? null + : Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(bloc.connectionIcon(), size: 40), + const SizedBox(height: 12), + Text( + headline, + style: const TextStyle(fontSize: 20), + textAlign: TextAlign.center, + ), + if (!isOffline && + message != null && + message != headline) ...[ + const SizedBox(height: 8), + Text( + message!, + style: TextStyle( + color: Theme.of(context).hintColor, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + if (bloc.allowRetry()) ...[ + const SizedBox(height: 16), + TextButton( + onPressed: () => bloc.reFetch!(), + child: const Text('Erneut versuchen'), + ), + ], + if (technicalDetails != null) ...[ + const SizedBox(height: 4), + TextButton( + onPressed: () => InfoDialog.show( + context, + technicalDetails!, + copyable: true, + title: 'Fehlerdetails', + ), + child: const Text('Details anzeigen'), + ), + ], + ], + ), ), - if (!isOffline && message != null && message != headline) ...[ - const SizedBox(height: 8), - Text( - message!, - style: TextStyle(color: Theme.of(context).hintColor, fontSize: 14), - textAlign: TextAlign.center, - ), - ], - if (bloc.allowRetry()) ...[ - const SizedBox(height: 16), - TextButton( - onPressed: () => bloc.reFetch!(), - child: const Text('Erneut versuchen'), - ), - ], - if (technicalDetails != null) ...[ - const SizedBox(height: 4), - TextButton( - onPressed: () => InfoDialog.show(context, technicalDetails!, copyable: true, title: 'Fehlerdetails'), - child: const Text('Details anzeigen'), - ), - ], - ], - ), - ), - ), + ), ); } } diff --git a/lib/state/app/infrastructure/loadable_state/view/loadable_state_primary_loading.dart b/lib/state/app/infrastructure/loadable_state/view/loadable_state_primary_loading.dart index 6e996a6..44d359b 100644 --- a/lib/state/app/infrastructure/loadable_state/view/loadable_state_primary_loading.dart +++ b/lib/state/app/infrastructure/loadable_state/view/loadable_state_primary_loading.dart @@ -9,9 +9,9 @@ class LoadableStatePrimaryLoading extends StatelessWidget { @override Widget build(BuildContext context) => AnimatedOpacity( - opacity: visible ? 1.0 : 0.0, - duration: LoadableStateConsumer.animationDuration, - curve: Curves.easeInOut, - child: const Center(child: AppProgressIndicator.large()), - ); + opacity: visible ? 1.0 : 0.0, + duration: LoadableStateConsumer.animationDuration, + curve: Curves.easeInOut, + child: const Center(child: AppProgressIndicator.large()), + ); } diff --git a/lib/state/app/infrastructure/utility_widgets/bloc_module.dart b/lib/state/app/infrastructure/utility_widgets/bloc_module.dart index d745e99..7b032d0 100644 --- a/lib/state/app/infrastructure/utility_widgets/bloc_module.dart +++ b/lib/state/app/infrastructure/utility_widgets/bloc_module.dart @@ -1,15 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class BlocModule, TState> extends StatelessWidget { +class BlocModule, TState> + extends StatelessWidget { final TBloc Function(BuildContext context) create; final Widget Function(BuildContext context, TBloc bloc, TState state) child; final bool autoRebuild; final void Function(BuildContext context, TBloc bloc)? onInitialisation; - const BlocModule({required this.create, required this.child, this.autoRebuild = false, this.onInitialisation, super.key}); + const BlocModule({ + required this.create, + required this.child, + this.autoRebuild = false, + this.onInitialisation, + super.key, + }); - Widget rebuildChild(BuildContext context) => child(context, context.watch(), context.watch().state); - Widget staticChild(BuildContext context) => child(context, context.read(), context.read().state); + Widget rebuildChild(BuildContext context) => + child(context, context.watch(), context.watch().state); + Widget staticChild(BuildContext context) => + child(context, context.read(), context.read().state); @override Widget build(BuildContext context) => BlocProvider( @@ -19,9 +28,8 @@ class BlocModule, TState> extends St return bloc; }, child: Builder( - builder: (context) => autoRebuild - ? rebuildChild(context) - : staticChild(context) - ) + builder: (context) => + autoRebuild ? rebuildChild(context) : staticChild(context), + ), ); } diff --git a/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart b/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart index ff5aaa3..8f23f7c 100644 --- a/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart +++ b/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart @@ -13,60 +13,79 @@ abstract class LoadableHydratedBloc< TEvent extends LoadableHydratedBlocEvent, TState, TRepository extends Repository -> extends HydratedBloc< - LoadableHydratedBlocEvent, - LoadableState -> { +> + extends + HydratedBloc, LoadableState> { late TRepository _repository; - LoadableHydratedBloc() : super(const LoadableState( - error: null, - data: null, - isLoading: true, - lastFetch: null, - reFetch: null, - )) { - + LoadableHydratedBloc() + : super( + const LoadableState( + error: null, + data: null, + isLoading: true, + lastFetch: null, + reFetch: null, + ), + ) { on>((event, emit) { - emit(LoadableState( - isLoading: state.isLoading, - data: event.state(innerState ?? fromNothing()), - lastFetch: state.lastFetch, - reFetch: retry, - error: state.error, - )); + emit( + LoadableState( + isLoading: state.isLoading, + data: event.state(innerState ?? fromNothing()), + lastFetch: state.lastFetch, + reFetch: retry, + error: state.error, + ), + ); }); - on>((event, emit) => emit(LoadableState( - isLoading: false, - data: event.state(innerState ?? fromNothing()), - lastFetch: DateTime.now().millisecondsSinceEpoch, - reFetch: retry, - error: null, - ))); + on>( + (event, emit) => emit( + LoadableState( + isLoading: false, + data: event.state(innerState ?? fromNothing()), + lastFetch: DateTime.now().millisecondsSinceEpoch, + reFetch: retry, + error: null, + ), + ), + ); - on>((event, emit) => emit(LoadableState( - isLoading: true, - data: innerState, - lastFetch: state.lastFetch, - reFetch: null, - error: null, - ))); + on>( + (event, emit) => emit( + LoadableState( + isLoading: true, + data: innerState, + lastFetch: state.lastFetch, + reFetch: null, + error: null, + ), + ), + ); - on>((event, emit) => emit(LoadableState( - isLoading: false, - data: innerState, - lastFetch: state.lastFetch, - reFetch: retry, - error: event.error - ))); + on>( + (event, emit) => emit( + LoadableState( + isLoading: false, + data: innerState, + lastFetch: state.lastFetch, + reFetch: retry, + error: event.error, + ), + ), + ); - on>((event, emit) => emit(const LoadableState( - isLoading: false, - data: null, - lastFetch: null, - reFetch: null, - error: null, - ))); + on>( + (event, emit) => emit( + const LoadableState( + isLoading: false, + data: null, + lastFetch: null, + reFetch: null, + error: null, + ), + ), + ); _repository = repository(); fetch(); @@ -92,23 +111,27 @@ abstract class LoadableHydratedBloc< void fetch() { log('Fetching data for ${TState.toString()}'); - gatherData().catchError( - (e) { - log('Error while fetching ${TState.toString()}: ${e.toString()}'); - // The bloc may have been closed before this async error landed (e.g. - // when its scoping widget tree was disposed mid-fetch). Adding to a - // closed bloc throws "Cannot add new events after calling close", - // so swallow that case quietly. - if (isClosed) return; - add(Error(LoadingError( - message: errorToUserMessage(e), - technicalDetails: errorToTechnicalDetails(e), - allowRetry: errorAllowsRetry(e), - ))); - }, - ).then((value) { - log('Fetch for ${TState.toString()} completed!'); - }); + gatherData() + .catchError((e) { + log('Error while fetching ${TState.toString()}: ${e.toString()}'); + // The bloc may have been closed before this async error landed (e.g. + // when its scoping widget tree was disposed mid-fetch). Adding to a + // closed bloc throws "Cannot add new events after calling close", + // so swallow that case quietly. + if (isClosed) return; + add( + Error( + LoadingError( + message: errorToUserMessage(e), + technicalDetails: errorToTechnicalDetails(e), + allowRetry: errorAllowsRetry(e), + ), + ), + ); + }) + .then((value) { + log('Fetch for ${TState.toString()} completed!'); + }); } @override @@ -129,13 +152,13 @@ abstract class LoadableHydratedBloc< try { final stateData = state.data; data = stateData is TState ? toStorage(stateData) : null; - } catch(e) { + } catch (e) { log('Failed to save state ${TState.toString()}: ${e.toString()}'); } return LoadableSaveContext.wrap( data, - state.lastFetch ?? DateTime.now().millisecondsSinceEpoch + state.lastFetch ?? DateTime.now().millisecondsSinceEpoch, ); } diff --git a/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart b/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart index c0b9934..8f3150e 100644 --- a/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart +++ b/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart @@ -1,17 +1,22 @@ import '../../loadable_state/loading_error.dart'; class LoadableHydratedBlocEvent {} + class Emit extends LoadableHydratedBlocEvent { final TState Function(TState state) state; Emit(this.state); } + class DataGathered extends LoadableHydratedBlocEvent { final TState Function(TState state) state; DataGathered(this.state); } + class Error extends LoadableHydratedBlocEvent { final LoadingError error; Error(this.error); } + class RefetchStarted extends LoadableHydratedBlocEvent {} + class Reset extends LoadableHydratedBlocEvent {} diff --git a/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_save_context.dart b/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_save_context.dart index 8d7dc2d..240582c 100644 --- a/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_save_context.dart +++ b/lib/state/app/infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_save_context.dart @@ -6,18 +6,25 @@ part 'loadable_save_context.g.dart'; @freezed abstract class LoadableSaveContext with _$LoadableSaveContext { const LoadableSaveContext._(); - const factory LoadableSaveContext({ - required int timestamp, - }) = _LoadableSaveContext; + const factory LoadableSaveContext({required int timestamp}) = + _LoadableSaveContext; - factory LoadableSaveContext.fromJson(Map json) => _$LoadableSaveContextFromJson(json); + factory LoadableSaveContext.fromJson(Map json) => + _$LoadableSaveContextFromJson(json); static String dataKey = 'data'; static String metaKey = 'meta'; static Map wrap(Map? data, int lastFetch) => - {dataKey: data, metaKey: LoadableSaveContext(timestamp: lastFetch).toJson()}; + { + dataKey: data, + metaKey: LoadableSaveContext(timestamp: lastFetch).toJson(), + }; - static ({Map data, LoadableSaveContext meta}) unwrap(Map data) => - (data: data[dataKey] as Map, meta: LoadableSaveContext.fromJson(data[metaKey] as Map)); + static ({Map data, LoadableSaveContext meta}) unwrap( + Map data, + ) => ( + data: data[dataKey] as Map, + meta: LoadableSaveContext.fromJson(data[metaKey] as Map), + ); } diff --git a/lib/state/app/modules/account/bloc/account_bloc.dart b/lib/state/app/modules/account/bloc/account_bloc.dart index 4ad0f5d..f1d2b6a 100644 --- a/lib/state/app/modules/account/bloc/account_bloc.dart +++ b/lib/state/app/modules/account/bloc/account_bloc.dart @@ -4,8 +4,11 @@ import 'account_event.dart'; import 'account_state.dart'; class AccountBloc extends Bloc { - AccountBloc({AccountStatus initialStatus = AccountStatus.undefined}) : super(AccountState(status: initialStatus)) { - on((event, emit) => emit(state.copyWith(status: event.status))); + AccountBloc({AccountStatus initialStatus = AccountStatus.undefined}) + : super(AccountState(status: initialStatus)) { + on( + (event, emit) => emit(state.copyWith(status: event.status)), + ); } void setStatus(AccountStatus status) => add(AccountStatusChanged(status)); diff --git a/lib/state/app/modules/account/bloc/account_state.dart b/lib/state/app/modules/account/bloc/account_state.dart index d0ab496..878407d 100644 --- a/lib/state/app/modules/account/bloc/account_state.dart +++ b/lib/state/app/modules/account/bloc/account_state.dart @@ -4,5 +4,6 @@ class AccountState { final AccountStatus status; const AccountState({this.status = AccountStatus.undefined}); - AccountState copyWith({AccountStatus? status}) => AccountState(status: status ?? this.status); + AccountState copyWith({AccountStatus? status}) => + AccountState(status: status ?? this.status); } diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index 07b64bf..aa05517 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -27,9 +27,18 @@ class AppModule { BreakerArea breakerArea; Widget Function() create; - AppModule(this.module, {required this.name, required this.icon, this.breakerArea = BreakerArea.global, required this.create}); + AppModule( + this.module, { + required this.name, + required this.icon, + this.breakerArea = BreakerArea.global, + required this.create, + }); - static Map modules(BuildContext context, {bool showFiltered = false}) { + static Map modules( + BuildContext context, { + bool showFiltered = false, + }) { final settings = context.read(); var available = { Modules.timetable: AppModule( @@ -45,8 +54,12 @@ class AppModule { icon: () => BlocBuilder>( builder: (context, state) { final rooms = state.data?.rooms; - if (rooms == null || rooms.data.isEmpty) return const Icon(Icons.chat); - final messages = rooms.data.map((e) => e.unreadMessages).reduce((a, b) => a + b); + if (rooms == null || rooms.data.isEmpty) { + return const Icon(Icons.chat); + } + final messages = rooms.data + .map((e) => e.unreadMessages) + .reduce((a, b) => a + b); return badges.Badge( showBadge: messages > 0, position: badges.BadgePosition.topEnd(top: -3, end: -3), @@ -56,7 +69,14 @@ class AppModule { badgeColor: Theme.of(context).primaryColor, elevation: 1, ), - badgeContent: Text('$messages', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), + badgeContent: Text( + '$messages', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), child: const Icon(Icons.chat), ); }, @@ -108,9 +128,19 @@ class AppModule { ), }; - if (!showFiltered) available.removeWhere((key, value) => settings.val().modulesSettings.hiddenModules.contains(key)); + if (!showFiltered) { + available.removeWhere( + (key, value) => + settings.val().modulesSettings.hiddenModules.contains(key), + ); + } - return { for (var element in settings.val().modulesSettings.moduleOrder.where((element) => available.containsKey(element))) element : available[element]! }; + return { + for (var element in settings.val().modulesSettings.moduleOrder.where( + (element) => available.containsKey(element), + )) + element: available[element]!, + }; } static const int minBottomBarSlots = 3; @@ -150,26 +180,45 @@ class AppModule { return all.skip(slots).toList(); } - Widget toListTile(BuildContext context, {Key? key, bool isReorder = false, Function()? onVisibleChange, bool isVisible = true}) => ListTile( + Widget toListTile( + BuildContext context, { + Key? key, + bool isReorder = false, + Function()? onVisibleChange, + bool isVisible = true, + }) => ListTile( key: key, leading: CenteredLeading(icon()), title: Text(name), onTap: isReorder ? null : () => AppRoutes.openModule(context, this), trailing: isReorder - ? Row(mainAxisSize: MainAxisSize.min, children: [ - IconButton(onPressed: onVisibleChange, icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined)), - Icon(Icons.drag_handle_outlined) - ]) + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: onVisibleChange, + icon: Icon( + isVisible + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + ), + Icon(Icons.drag_handle_outlined), + ], + ) : const Icon(Icons.arrow_right), ); - PersistentTabConfig toBottomTab(BuildContext context, {Widget Function(IconData icon)? iconBuilder}) => PersistentTabConfig( + PersistentTabConfig toBottomTab( + BuildContext context, { + Widget Function(IconData icon)? iconBuilder, + }) => PersistentTabConfig( screen: Breaker(breaker: breakerArea, child: create()), item: ItemConfig( - activeForegroundColor: Theme.of(context).primaryColor, - inactiveForegroundColor: Theme.of(context).colorScheme.secondary, - icon: icon(), - title: name + activeForegroundColor: Theme.of(context).primaryColor, + inactiveForegroundColor: Theme.of(context).colorScheme.secondary, + icon: icon(), + title: name, ), ); } diff --git a/lib/state/app/modules/breaker/bloc/breaker_bloc.dart b/lib/state/app/modules/breaker/bloc/breaker_bloc.dart index 86bb6fc..24af779 100644 --- a/lib/state/app/modules/breaker/bloc/breaker_bloc.dart +++ b/lib/state/app/modules/breaker/bloc/breaker_bloc.dart @@ -8,7 +8,9 @@ import '../repository/breaker_repository.dart'; import 'breaker_event.dart'; import 'breaker_state.dart'; -class BreakerBloc extends LoadableHydratedBloc { +class BreakerBloc + extends + LoadableHydratedBloc { PackageInfo? _packageInfo; @override @@ -18,7 +20,8 @@ class BreakerBloc extends LoadableHydratedBloc const BreakerState(); @override - BreakerState fromStorage(Map json) => BreakerState.fromJson(json); + BreakerState fromStorage(Map json) => + BreakerState.fromJson(json); @override Map? toStorage(BreakerState state) => state.toJson(); diff --git a/lib/state/app/modules/breaker/bloc/breaker_state.dart b/lib/state/app/modules/breaker/bloc/breaker_state.dart index 60c1685..367688f 100644 --- a/lib/state/app/modules/breaker/bloc/breaker_state.dart +++ b/lib/state/app/modules/breaker/bloc/breaker_state.dart @@ -7,9 +7,8 @@ part 'breaker_state.g.dart'; @freezed abstract class BreakerState with _$BreakerState { - const factory BreakerState({ - GetBreakersResponse? response, - }) = _BreakerState; + const factory BreakerState({GetBreakersResponse? response}) = _BreakerState; - factory BreakerState.fromJson(Map json) => _$BreakerStateFromJson(json); + factory BreakerState.fromJson(Map json) => + _$BreakerStateFromJson(json); } diff --git a/lib/state/app/modules/breaker/data_provider/breaker_data_provider.dart b/lib/state/app/modules/breaker/data_provider/breaker_data_provider.dart index d07fa83..1f8ed6b 100644 --- a/lib/state/app/modules/breaker/data_provider/breaker_data_provider.dart +++ b/lib/state/app/modules/breaker/data_provider/breaker_data_provider.dart @@ -6,9 +6,11 @@ import '../../../../../api/mhsl/breaker/get_breakers/get_breakers_response.dart' class BreakerDataProvider { Future getBreakers() { final completer = Completer(); - GetBreakersCache(onUpdate: (data) { - if (!completer.isCompleted) completer.complete(data); - }); + GetBreakersCache( + onUpdate: (data) { + if (!completer.isCompleted) completer.complete(data); + }, + ); return completer.future; } } diff --git a/lib/state/app/modules/breaker/repository/breaker_repository.dart b/lib/state/app/modules/breaker/repository/breaker_repository.dart index 7bc37ac..7a22aed 100644 --- a/lib/state/app/modules/breaker/repository/breaker_repository.dart +++ b/lib/state/app/modules/breaker/repository/breaker_repository.dart @@ -5,7 +5,8 @@ import '../data_provider/breaker_data_provider.dart'; class BreakerRepository extends Repository { final BreakerDataProvider _provider; - BreakerRepository([BreakerDataProvider? provider]) : _provider = provider ?? BreakerDataProvider(); + BreakerRepository([BreakerDataProvider? provider]) + : _provider = provider ?? BreakerDataProvider(); BreakerDataProvider get data => _provider; } diff --git a/lib/state/app/modules/chat/bloc/chat_bloc.dart b/lib/state/app/modules/chat/bloc/chat_bloc.dart index f02b80f..a79d169 100644 --- a/lib/state/app/modules/chat/bloc/chat_bloc.dart +++ b/lib/state/app/modules/chat/bloc/chat_bloc.dart @@ -6,7 +6,8 @@ import '../repository/chat_repository.dart'; import 'chat_event.dart'; import 'chat_state.dart'; -class ChatBloc extends LoadableHydratedBloc { +class ChatBloc + extends LoadableHydratedBloc { DateTime _lastTokenSet = DateTime.fromMillisecondsSinceEpoch(0); @override @@ -86,11 +87,15 @@ class ChatBloc extends LoadableHydratedBloc json) => _$ChatStateFromJson(json); + factory ChatState.fromJson(Map json) => + _$ChatStateFromJson(json); } diff --git a/lib/state/app/modules/chat/repository/chat_repository.dart b/lib/state/app/modules/chat/repository/chat_repository.dart index 38c3833..ba3edf8 100644 --- a/lib/state/app/modules/chat/repository/chat_repository.dart +++ b/lib/state/app/modules/chat/repository/chat_repository.dart @@ -5,7 +5,8 @@ import '../data_provider/chat_data_provider.dart'; class ChatRepository extends Repository { final ChatDataProvider _provider; - ChatRepository([ChatDataProvider? provider]) : _provider = provider ?? ChatDataProvider(); + ChatRepository([ChatDataProvider? provider]) + : _provider = provider ?? ChatDataProvider(); ChatDataProvider get data => _provider; } diff --git a/lib/state/app/modules/chat_list/bloc/chat_list_bloc.dart b/lib/state/app/modules/chat_list/bloc/chat_list_bloc.dart index b1687c5..d05895d 100644 --- a/lib/state/app/modules/chat_list/bloc/chat_list_bloc.dart +++ b/lib/state/app/modules/chat_list/bloc/chat_list_bloc.dart @@ -11,7 +11,9 @@ import '../repository/chat_list_repository.dart'; import 'chat_list_event.dart'; import 'chat_list_state.dart'; -class ChatListBloc extends LoadableHydratedBloc { +class ChatListBloc + extends + LoadableHydratedBloc { bool _forceRenew = false; @override @@ -27,7 +29,8 @@ class ChatListBloc extends LoadableHydratedBloc const ChatListState(); @override - ChatListState fromStorage(Map json) => ChatListState.fromJson(json); + ChatListState fromStorage(Map json) => + ChatListState.fromJson(json); @override Map? toStorage(ChatListState state) => state.toJson(); @@ -62,11 +65,15 @@ class ChatListBloc extends LoadableHydratedBloc(0, (a, room) => a + room.unreadMessages); + final unread = rooms.data.fold( + 0, + (a, room) => a + room.unreadMessages, + ); FlutterAppBadge.count(unread); } on Object catch (e) { log('Failed to update app badge: $e'); diff --git a/lib/state/app/modules/chat_list/bloc/chat_list_state.dart b/lib/state/app/modules/chat_list/bloc/chat_list_state.dart index 25210cf..adcfc62 100644 --- a/lib/state/app/modules/chat_list/bloc/chat_list_state.dart +++ b/lib/state/app/modules/chat_list/bloc/chat_list_state.dart @@ -7,9 +7,8 @@ part 'chat_list_state.g.dart'; @freezed abstract class ChatListState with _$ChatListState { - const factory ChatListState({ - GetRoomResponse? rooms, - }) = _ChatListState; + const factory ChatListState({GetRoomResponse? rooms}) = _ChatListState; - factory ChatListState.fromJson(Map json) => _$ChatListStateFromJson(json); + factory ChatListState.fromJson(Map json) => + _$ChatListStateFromJson(json); } diff --git a/lib/state/app/modules/chat_list/data_provider/chat_list_data_provider.dart b/lib/state/app/modules/chat_list/data_provider/chat_list_data_provider.dart index 6fe28b0..8786baa 100644 --- a/lib/state/app/modules/chat_list/data_provider/chat_list_data_provider.dart +++ b/lib/state/app/modules/chat_list/data_provider/chat_list_data_provider.dart @@ -8,16 +8,12 @@ class ChatListDataProvider { Future getRooms({ void Function(Object)? onError, bool renew = false, - }) => - resolveFromCache( - (onUpdate, onError) => GetRoomCache( - renew: renew, - onUpdate: onUpdate, - onError: onError, - ), - onError: onError, - operationName: 'getRooms', - ); + }) => resolveFromCache( + (onUpdate, onError) => + GetRoomCache(renew: renew, onUpdate: onUpdate, onError: onError), + onError: onError, + operationName: 'getRooms', + ); Future createDirectRoom(String invite) => CreateRoom(CreateRoomParams(roomType: 1, invite: invite)).run(); diff --git a/lib/state/app/modules/chat_list/repository/chat_list_repository.dart b/lib/state/app/modules/chat_list/repository/chat_list_repository.dart index 9589cb3..880d15f 100644 --- a/lib/state/app/modules/chat_list/repository/chat_list_repository.dart +++ b/lib/state/app/modules/chat_list/repository/chat_list_repository.dart @@ -5,7 +5,8 @@ import '../data_provider/chat_list_data_provider.dart'; class ChatListRepository extends Repository { final ChatListDataProvider _provider; - ChatListRepository([ChatListDataProvider? provider]) : _provider = provider ?? ChatListDataProvider(); + ChatListRepository([ChatListDataProvider? provider]) + : _provider = provider ?? ChatListDataProvider(); ChatListDataProvider get data => _provider; } diff --git a/lib/state/app/modules/files/bloc/files_bloc.dart b/lib/state/app/modules/files/bloc/files_bloc.dart index fbe72e3..27e5f5e 100644 --- a/lib/state/app/modules/files/bloc/files_bloc.dart +++ b/lib/state/app/modules/files/bloc/files_bloc.dart @@ -7,7 +7,8 @@ import '../repository/files_repository.dart'; import 'files_event.dart'; import 'files_state.dart'; -class FilesBloc extends LoadableHydratedBloc { +class FilesBloc + extends LoadableHydratedBloc { final List initialPath; FilesBloc({this.initialPath = const []}); @@ -19,7 +20,8 @@ class FilesBloc extends LoadableHydratedBloc FilesState(currentPath: initialPath); @override - FilesState fromStorage(Map json) => FilesState.fromJson(json); + FilesState fromStorage(Map json) => + FilesState.fromJson(json); @override Map? toStorage(FilesState state) => null; @@ -60,7 +62,9 @@ class FilesBloc extends LoadableHydratedBloc file.name.isEmpty || file.name == path.lastOrNull); + cached.files.removeWhere( + (file) => file.name.isEmpty || file.name == path.lastOrNull, + ); add(Emit((s) => s.copyWith(listing: cached))); }, onError: (e) => capturedError = e, @@ -70,15 +74,21 @@ class FilesBloc extends LoadableHydratedBloc file.name.isEmpty || file.name == path.lastOrNull); + listing.files.removeWhere( + (file) => file.name.isEmpty || file.name == path.lastOrNull, + ); add(DataGathered((s) => s.copyWith(listing: listing))); } if (capturedError != null) { - add(Error(LoadingError( - message: errorToUserMessage(capturedError), - technicalDetails: errorToTechnicalDetails(capturedError), - allowRetry: errorAllowsRetry(capturedError), - ))); + add( + Error( + LoadingError( + message: errorToUserMessage(capturedError), + technicalDetails: errorToTechnicalDetails(capturedError), + allowRetry: errorAllowsRetry(capturedError), + ), + ), + ); } } } diff --git a/lib/state/app/modules/files/bloc/files_state.dart b/lib/state/app/modules/files/bloc/files_state.dart index 448241f..de1920a 100644 --- a/lib/state/app/modules/files/bloc/files_state.dart +++ b/lib/state/app/modules/files/bloc/files_state.dart @@ -12,5 +12,6 @@ abstract class FilesState with _$FilesState { ListFilesResponse? listing, }) = _FilesState; - factory FilesState.fromJson(Map json) => _$FilesStateFromJson(json); + factory FilesState.fromJson(Map json) => + _$FilesStateFromJson(json); } diff --git a/lib/state/app/modules/files/data_provider/files_data_provider.dart b/lib/state/app/modules/files/data_provider/files_data_provider.dart index 39913b2..e721fbb 100644 --- a/lib/state/app/modules/files/data_provider/files_data_provider.dart +++ b/lib/state/app/modules/files/data_provider/files_data_provider.dart @@ -15,17 +15,16 @@ class FilesDataProvider { String path, { void Function(ListFilesResponse)? onCacheData, void Function(Object)? onError, - }) => - resolveFromCache( - (onUpdate, onError) => ListFilesCache( - path: path, - onUpdate: onUpdate, - onCacheData: onCacheData, - onError: onError, - ), - onError: onError, - operationName: 'listFiles', - ); + }) => resolveFromCache( + (onUpdate, onError) => ListFilesCache( + path: path, + onUpdate: onUpdate, + onCacheData: onCacheData, + onError: onError, + ), + onError: onError, + operationName: 'listFiles', + ); Future createFolder(String fullPath) async { final webdav = await WebdavApi.webdav; diff --git a/lib/state/app/modules/files/repository/files_repository.dart b/lib/state/app/modules/files/repository/files_repository.dart index c7e129c..35f316c 100644 --- a/lib/state/app/modules/files/repository/files_repository.dart +++ b/lib/state/app/modules/files/repository/files_repository.dart @@ -5,7 +5,8 @@ import '../data_provider/files_data_provider.dart'; class FilesRepository extends Repository { final FilesDataProvider _provider; - FilesRepository([FilesDataProvider? provider]) : _provider = provider ?? FilesDataProvider(); + FilesRepository([FilesDataProvider? provider]) + : _provider = provider ?? FilesDataProvider(); FilesDataProvider get data => _provider; } diff --git a/lib/state/app/modules/grade_averages/bloc/grade_averages_bloc.dart b/lib/state/app/modules/grade_averages/bloc/grade_averages_bloc.dart index 6a084ca..2d3b89b 100644 --- a/lib/state/app/modules/grade_averages/bloc/grade_averages_bloc.dart +++ b/lib/state/app/modules/grade_averages/bloc/grade_averages_bloc.dart @@ -3,17 +3,23 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'grade_averages_event.dart'; import 'grade_averages_state.dart'; -class GradeAveragesBloc extends HydratedBloc { - GradeAveragesBloc() : super(const GradeAveragesState(gradingSystem: GradeAveragesGradingSystem.middleSchool, grades: [])) { - +class GradeAveragesBloc + extends HydratedBloc { + GradeAveragesBloc() + : super( + const GradeAveragesState( + gradingSystem: GradeAveragesGradingSystem.middleSchool, + grades: [], + ), + ) { on((event, emit) { add(ResetAll()); emit( state.copyWith( gradingSystem: event.isMiddleSchool - ? GradeAveragesGradingSystem.middleSchool - : GradeAveragesGradingSystem.highSchool - ) + ? GradeAveragesGradingSystem.middleSchool + : GradeAveragesGradingSystem.highSchool, + ), ); }); @@ -22,7 +28,12 @@ class GradeAveragesBloc extends HydratedBloc((event, emit) { - emit(state.copyWith(grades: [...state.grades]..removeWhere((grade) => grade == event.grade))); + emit( + state.copyWith( + grades: [...state.grades] + ..removeWhere((grade) => grade == event.grade), + ), + ); }); on((event, emit) { @@ -30,20 +41,26 @@ class GradeAveragesBloc extends HydratedBloc((event, emit) { - emit(state.copyWith(grades: List.from(state.grades)..remove(event.grade))); + emit( + state.copyWith(grades: List.from(state.grades)..remove(event.grade)), + ); }); - } - double average() => state.grades.isEmpty ? 0 : state.grades.reduce((a, b) => a + b) / state.grades.length; - bool isMiddleSchool() => state.gradingSystem == GradeAveragesGradingSystem.middleSchool; + double average() => state.grades.isEmpty + ? 0 + : state.grades.reduce((a, b) => a + b) / state.grades.length; + bool isMiddleSchool() => + state.gradingSystem == GradeAveragesGradingSystem.middleSchool; bool canDecrementOrDelete(int grade) => state.grades.contains(grade); int countOfGrade(int grade) => state.grades.where((g) => g == grade).length; - int gradesInGradingSystem() => state.gradingSystem == GradeAveragesGradingSystem.middleSchool ? 6 : 16; + int gradesInGradingSystem() => + state.gradingSystem == GradeAveragesGradingSystem.middleSchool ? 6 : 16; int getGradeFromIndex(int index) => isMiddleSchool() ? index + 1 : 15 - index; @override - GradeAveragesState? fromJson(Map json) => GradeAveragesState.fromJson(json); + GradeAveragesState? fromJson(Map json) => + GradeAveragesState.fromJson(json); @override Map? toJson(GradeAveragesState state) => state.toJson(); } diff --git a/lib/state/app/modules/grade_averages/bloc/grade_averages_event.dart b/lib/state/app/modules/grade_averages/bloc/grade_averages_event.dart index 0be46eb..cbf3ba4 100644 --- a/lib/state/app/modules/grade_averages/bloc/grade_averages_event.dart +++ b/lib/state/app/modules/grade_averages/bloc/grade_averages_event.dart @@ -1,19 +1,22 @@ - sealed class GradeAveragesEvent {} final class GradingSystemChanged extends GradeAveragesEvent { final bool isMiddleSchool; GradingSystemChanged(this.isMiddleSchool); } + final class ResetAll extends GradeAveragesEvent {} + final class ResetGrade extends GradeAveragesEvent { final int grade; ResetGrade(this.grade); } + final class IncrementGrade extends GradeAveragesEvent { final int grade; IncrementGrade(this.grade); } + final class DecrementGrade extends GradeAveragesEvent { final int grade; DecrementGrade(this.grade); diff --git a/lib/state/app/modules/grade_averages/bloc/grade_averages_state.dart b/lib/state/app/modules/grade_averages/bloc/grade_averages_state.dart index 44e7f8f..6abb004 100644 --- a/lib/state/app/modules/grade_averages/bloc/grade_averages_state.dart +++ b/lib/state/app/modules/grade_averages/bloc/grade_averages_state.dart @@ -10,10 +10,8 @@ abstract class GradeAveragesState with _$GradeAveragesState { required List grades, }) = _GradeAveragesState; - factory GradeAveragesState.fromJson(Map json) => _$GradeAveragesStateFromJson(json); + factory GradeAveragesState.fromJson(Map json) => + _$GradeAveragesStateFromJson(json); } -enum GradeAveragesGradingSystem { - highSchool, - middleSchool, -} +enum GradeAveragesGradingSystem { highSchool, middleSchool } diff --git a/lib/state/app/modules/holidays/bloc/holidays_bloc.dart b/lib/state/app/modules/holidays/bloc/holidays_bloc.dart index 2e3b96b..bf2de75 100644 --- a/lib/state/app/modules/holidays/bloc/holidays_bloc.dart +++ b/lib/state/app/modules/holidays/bloc/holidays_bloc.dart @@ -4,32 +4,51 @@ import '../repository/holidays_repository.dart'; import 'holidays_event.dart'; import 'holidays_state.dart'; -class HolidaysBloc extends LoadableHydratedBloc { +class HolidaysBloc + extends + LoadableHydratedBloc { HolidaysBloc() { on((event, emit) { - add(Emit((state) => state.copyWith(showPastHolidays: event.shouldBeVisible))); + add( + Emit( + (state) => state.copyWith(showPastHolidays: event.shouldBeVisible), + ), + ); }); - on((event, emit) => add( - Emit((state) => state.copyWith(showDisclaimer: false)) - )); + on( + (event, emit) => + add(Emit((state) => state.copyWith(showDisclaimer: false))), + ); } bool showPastHolidays() => innerState?.showPastHolidays ?? false; bool showDisclaimerOnEntry() => innerState?.showDisclaimer ?? false; - List? getHolidays() => innerState?.holidays - .where((element) => showPastHolidays() || DateTime.parse(element.end).isAfter(DateTime.now())) - .toList() ?? []; + List? getHolidays() => + innerState?.holidays + .where( + (element) => + showPastHolidays() || + DateTime.parse(element.end).isAfter(DateTime.now()), + ) + .toList() ?? + []; @override - HolidaysState fromNothing() => const HolidaysState(showPastHolidays: false, holidays: [], showDisclaimer: true); + HolidaysState fromNothing() => const HolidaysState( + showPastHolidays: false, + holidays: [], + showDisclaimer: true, + ); @override - HolidaysState fromStorage(Map json) => HolidaysState.fromJson(json); + HolidaysState fromStorage(Map json) => + HolidaysState.fromJson(json); @override Future gatherData() async { var holidays = await repo.getHolidays(); add(DataGathered((state) => state.copyWith(holidays: holidays))); } + @override HolidaysRepository repository() => HolidaysRepository(); @override diff --git a/lib/state/app/modules/holidays/bloc/holidays_event.dart b/lib/state/app/modules/holidays/bloc/holidays_event.dart index 4be4a68..99d2574 100644 --- a/lib/state/app/modules/holidays/bloc/holidays_event.dart +++ b/lib/state/app/modules/holidays/bloc/holidays_event.dart @@ -2,8 +2,10 @@ import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_ import 'holidays_state.dart'; sealed class HolidaysEvent extends LoadableHydratedBlocEvent {} + class SetPastHolidaysVisible extends HolidaysEvent { final bool shouldBeVisible; SetPastHolidaysVisible(this.shouldBeVisible); } + class DisclaimerDismissed extends HolidaysEvent {} diff --git a/lib/state/app/modules/holidays/bloc/holidays_state.dart b/lib/state/app/modules/holidays/bloc/holidays_state.dart index eec02b2..d2755e4 100644 --- a/lib/state/app/modules/holidays/bloc/holidays_state.dart +++ b/lib/state/app/modules/holidays/bloc/holidays_state.dart @@ -12,7 +12,8 @@ abstract class HolidaysState with _$HolidaysState { required List holidays, }) = _HolidaysState; - factory HolidaysState.fromJson(Map json) => _$HolidaysStateFromJson(json); + factory HolidaysState.fromJson(Map json) => + _$HolidaysStateFromJson(json); } @freezed @@ -26,5 +27,6 @@ abstract class Holiday with _$Holiday { required String slug, }) = _Holiday; - factory Holiday.fromJson(Map json) => _$HolidayFromJson(json); + factory Holiday.fromJson(Map json) => + _$HolidayFromJson(json); } diff --git a/lib/state/app/modules/holidays/data_provider/holidays_get_holidays.dart b/lib/state/app/modules/holidays/data_provider/holidays_get_holidays.dart index ad663da..79c9c60 100644 --- a/lib/state/app/modules/holidays/data_provider/holidays_get_holidays.dart +++ b/lib/state/app/modules/holidays/data_provider/holidays_get_holidays.dart @@ -6,7 +6,8 @@ import '../bloc/holidays_state.dart'; class HolidaysGetHolidays extends HolidayDataLoader> { @override - List assemble(DataLoaderResult data) => data.asListOfMaps().map(Holiday.fromJson).toList(); + List assemble(DataLoaderResult data) => + data.asListOfMaps().map(Holiday.fromJson).toList(); @override Future> fetch() => dio.get('/holidays/HE'); diff --git a/lib/state/app/modules/marianum_dates/bloc/marianum_dates_bloc.dart b/lib/state/app/modules/marianum_dates/bloc/marianum_dates_bloc.dart index 241370d..a0ebeed 100644 --- a/lib/state/app/modules/marianum_dates/bloc/marianum_dates_bloc.dart +++ b/lib/state/app/modules/marianum_dates/bloc/marianum_dates_bloc.dart @@ -4,28 +4,41 @@ import '../repository/marianum_dates_repository.dart'; import 'marianum_dates_event.dart'; import 'marianum_dates_state.dart'; -class MarianumDatesBloc extends LoadableHydratedBloc { +class MarianumDatesBloc + extends + LoadableHydratedBloc< + MarianumDatesEvent, + MarianumDatesState, + MarianumDatesRepository + > { MarianumDatesBloc() { on((event, emit) { - add(Emit((state) => state.copyWith(showPastEvents: event.shouldBeVisible))); + add( + Emit((state) => state.copyWith(showPastEvents: event.shouldBeVisible)), + ); }); } bool showPastEvents() => innerState?.showPastEvents ?? false; - List? getEvents() => innerState?.events - .where((e) => showPastEvents() || e.end.isAfter(DateTime.now())) - .toList() ?? []; + List? getEvents() => + innerState?.events + .where((e) => showPastEvents() || e.end.isAfter(DateTime.now())) + .toList() ?? + []; @override - MarianumDatesState fromNothing() => const MarianumDatesState(showPastEvents: false, events: []); + MarianumDatesState fromNothing() => + const MarianumDatesState(showPastEvents: false, events: []); @override - MarianumDatesState fromStorage(Map json) => MarianumDatesState.fromJson(json); + MarianumDatesState fromStorage(Map json) => + MarianumDatesState.fromJson(json); @override Future gatherData() async { final events = await repo.getEvents(); add(DataGathered((state) => state.copyWith(events: events))); } + @override MarianumDatesRepository repository() => MarianumDatesRepository(); @override diff --git a/lib/state/app/modules/marianum_dates/bloc/marianum_dates_event.dart b/lib/state/app/modules/marianum_dates/bloc/marianum_dates_event.dart index 1bfcb88..b62b9f9 100644 --- a/lib/state/app/modules/marianum_dates/bloc/marianum_dates_event.dart +++ b/lib/state/app/modules/marianum_dates/bloc/marianum_dates_event.dart @@ -1,7 +1,8 @@ import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart'; import 'marianum_dates_state.dart'; -sealed class MarianumDatesEvent extends LoadableHydratedBlocEvent {} +sealed class MarianumDatesEvent + extends LoadableHydratedBlocEvent {} class SetPastEventsVisible extends MarianumDatesEvent { final bool shouldBeVisible; diff --git a/lib/state/app/modules/marianum_dates/bloc/marianum_dates_state.dart b/lib/state/app/modules/marianum_dates/bloc/marianum_dates_state.dart index 26eb18f..a45611e 100644 --- a/lib/state/app/modules/marianum_dates/bloc/marianum_dates_state.dart +++ b/lib/state/app/modules/marianum_dates/bloc/marianum_dates_state.dart @@ -11,7 +11,8 @@ abstract class MarianumDatesState with _$MarianumDatesState { required List events, }) = _MarianumDatesState; - factory MarianumDatesState.fromJson(Map json) => _$MarianumDatesStateFromJson(json); + factory MarianumDatesState.fromJson(Map json) => + _$MarianumDatesStateFromJson(json); } @freezed @@ -25,5 +26,6 @@ abstract class MarianumDate with _$MarianumDate { required bool isAllDay, }) = _MarianumDate; - factory MarianumDate.fromJson(Map json) => _$MarianumDateFromJson(json); + factory MarianumDate.fromJson(Map json) => + _$MarianumDateFromJson(json); } diff --git a/lib/state/app/modules/marianum_dates/data_provider/marianum_dates_get_events.dart b/lib/state/app/modules/marianum_dates/data_provider/marianum_dates_get_events.dart index fc0c177..63bcbbb 100644 --- a/lib/state/app/modules/marianum_dates/data_provider/marianum_dates_get_events.dart +++ b/lib/state/app/modules/marianum_dates/data_provider/marianum_dates_get_events.dart @@ -4,12 +4,15 @@ import 'package:enough_icalendar/enough_icalendar.dart'; import '../bloc/marianum_dates_state.dart'; class MarianumDatesGetEvents { - static const String url = 'https://public-cal.marianumlan.de/cal_public/ad4c5da8-7466-9c72-89cb-8b8d9a5cf26c'; + static const String url = + 'https://public-cal.marianumlan.de/cal_public/ad4c5da8-7466-9c72-89cb-8b8d9a5cf26c'; - final Dio _dio = Dio(BaseOptions( - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 30), - )); + final Dio _dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 30), + ), + ); Future> run() async { final response = await _dio.get(url); @@ -20,7 +23,11 @@ class MarianumDatesGetEvents { final calendar = root is VCalendar ? root : null; final source = calendar?.children ?? root.children; - final events = source.whereType().map(_toMarianumDate).whereType().toList(); + final events = source + .whereType() + .map(_toMarianumDate) + .whereType() + .toList(); events.sort((a, b) => a.start.compareTo(b.start)); return events; } @@ -41,8 +48,11 @@ class MarianumDatesGetEvents { } static bool _isAllDay(DateTime start, DateTime end) { - final startMidnight = start.hour == 0 && start.minute == 0 && start.second == 0; + final startMidnight = + start.hour == 0 && start.minute == 0 && start.second == 0; final endMidnight = end.hour == 0 && end.minute == 0 && end.second == 0; - return startMidnight && endMidnight && end.difference(start).inHours % 24 == 0; + return startMidnight && + endMidnight && + end.difference(start).inHours % 24 == 0; } } diff --git a/lib/state/app/modules/marianum_message/bloc/marianum_message_bloc.dart b/lib/state/app/modules/marianum_message/bloc/marianum_message_bloc.dart index daca62d..decc69f 100644 --- a/lib/state/app/modules/marianum_message/bloc/marianum_message_bloc.dart +++ b/lib/state/app/modules/marianum_message/bloc/marianum_message_bloc.dart @@ -4,7 +4,13 @@ import '../repository/marianum_message_repository.dart'; import 'marianum_message_event.dart'; import 'marianum_message_state.dart'; -class MarianumMessageBloc extends LoadableHydratedBloc { +class MarianumMessageBloc + extends + LoadableHydratedBloc< + MarianumMessageEvent, + MarianumMessageState, + MarianumMessageRepository + > { @override Future gatherData() async { var messages = await repo.getMessages(); @@ -15,10 +21,13 @@ class MarianumMessageBloc extends LoadableHydratedBloc MarianumMessageRepository(); @override - MarianumMessageState fromNothing() => const MarianumMessageState(messageList: MarianumMessageList(base: '', messages: [])); + MarianumMessageState fromNothing() => const MarianumMessageState( + messageList: MarianumMessageList(base: '', messages: []), + ); @override - MarianumMessageState fromStorage(Map json) => MarianumMessageState.fromJson(json); + MarianumMessageState fromStorage(Map json) => + MarianumMessageState.fromJson(json); @override Map? toStorage(MarianumMessageState state) => state.toJson(); } diff --git a/lib/state/app/modules/marianum_message/bloc/marianum_message_event.dart b/lib/state/app/modules/marianum_message/bloc/marianum_message_event.dart index f71d5c7..6ad6e1c 100644 --- a/lib/state/app/modules/marianum_message/bloc/marianum_message_event.dart +++ b/lib/state/app/modules/marianum_message/bloc/marianum_message_event.dart @@ -1,5 +1,7 @@ import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart'; import 'marianum_message_state.dart'; -sealed class MarianumMessageEvent extends LoadableHydratedBlocEvent {} +sealed class MarianumMessageEvent + extends LoadableHydratedBlocEvent {} + class MessageEvent extends MarianumMessageEvent {} diff --git a/lib/state/app/modules/marianum_message/bloc/marianum_message_state.dart b/lib/state/app/modules/marianum_message/bloc/marianum_message_state.dart index a119bb6..9421618 100644 --- a/lib/state/app/modules/marianum_message/bloc/marianum_message_state.dart +++ b/lib/state/app/modules/marianum_message/bloc/marianum_message_state.dart @@ -3,14 +3,14 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'marianum_message_state.freezed.dart'; part 'marianum_message_state.g.dart'; - @freezed abstract class MarianumMessageState with _$MarianumMessageState { const factory MarianumMessageState({ required MarianumMessageList messageList, }) = _MarianumMessageState; - factory MarianumMessageState.fromJson(Map json) => _$MarianumMessageStateFromJson(json); + factory MarianumMessageState.fromJson(Map json) => + _$MarianumMessageStateFromJson(json); } @freezed @@ -20,7 +20,8 @@ abstract class MarianumMessageList with _$MarianumMessageList { required List messages, }) = _MarianumMessageList; - factory MarianumMessageList.fromJson(Map json) => _$MarianumMessageListFromJson(json); + factory MarianumMessageList.fromJson(Map json) => + _$MarianumMessageListFromJson(json); } @freezed @@ -31,11 +32,8 @@ abstract class MarianumMessage with _$MarianumMessage { required String url, }) = _MarianumMessage; - factory MarianumMessage.fromJson(Map json) => _$MarianumMessageFromJson(json); + factory MarianumMessage.fromJson(Map json) => + _$MarianumMessageFromJson(json); } - -enum GradeAveragesGradingSystem { - highSchool, - middleSchool, -} +enum GradeAveragesGradingSystem { highSchool, middleSchool } diff --git a/lib/state/app/modules/marianum_message/data_provider/marianum_message_get_messages.dart b/lib/state/app/modules/marianum_message/data_provider/marianum_message_get_messages.dart index a74dda4..a8eb24a 100644 --- a/lib/state/app/modules/marianum_message/data_provider/marianum_message_get_messages.dart +++ b/lib/state/app/modules/marianum_message/data_provider/marianum_message_get_messages.dart @@ -8,5 +8,6 @@ class MarianumMessageGetMessages extends MhslDataLoader { @override Future> fetch() async => dio.get('/message/messages.json'); @override - MarianumMessageList assemble(DataLoaderResult data) => MarianumMessageList.fromJson(data.asMap()); + MarianumMessageList assemble(DataLoaderResult data) => + MarianumMessageList.fromJson(data.asMap()); } diff --git a/lib/state/app/modules/marianum_message/repository/marianum_message_repository.dart b/lib/state/app/modules/marianum_message/repository/marianum_message_repository.dart index 9a6d9bc..3d26cb4 100644 --- a/lib/state/app/modules/marianum_message/repository/marianum_message_repository.dart +++ b/lib/state/app/modules/marianum_message/repository/marianum_message_repository.dart @@ -3,5 +3,6 @@ import '../bloc/marianum_message_state.dart'; import '../data_provider/marianum_message_get_messages.dart'; class MarianumMessageRepository extends Repository { - Future getMessages() => MarianumMessageGetMessages().run(); + Future getMessages() => + MarianumMessageGetMessages().run(); } diff --git a/lib/state/app/modules/settings/bloc/settings_cubit.dart b/lib/state/app/modules/settings/bloc/settings_cubit.dart index ad38792..a785e22 100644 --- a/lib/state/app/modules/settings/bloc/settings_cubit.dart +++ b/lib/state/app/modules/settings/bloc/settings_cubit.dart @@ -27,7 +27,11 @@ class SettingsCubit extends HydratedCubit { _emitFreshInstance(); }); } - Debouncer.debounce(_debounceTag, const Duration(milliseconds: 500), _emitFreshInstance); + Debouncer.debounce( + _debounceTag, + const Duration(milliseconds: 500), + _emitFreshInstance, + ); } return state; } @@ -50,7 +54,11 @@ class SettingsCubit extends HydratedCubit { return _appendNewModules(Settings.fromJson(json)); } catch (_) { try { - return _appendNewModules(Settings.fromJson(_mergeSettings(json, DefaultSettings.get().toJson()))); + return _appendNewModules( + Settings.fromJson( + _mergeSettings(json, DefaultSettings.get().toJson()), + ), + ); } catch (_) { return DefaultSettings.get(); } @@ -63,7 +71,9 @@ class SettingsCubit extends HydratedCubit { Settings _appendNewModules(Settings s) { final order = s.modulesSettings.moduleOrder; final hidden = s.modulesSettings.hiddenModules; - final missing = Modules.values.where((m) => !order.contains(m) && !hidden.contains(m)); + final missing = Modules.values.where( + (m) => !order.contains(m) && !hidden.contains(m), + ); if (missing.isEmpty) return s; s.modulesSettings.moduleOrder = [...order, ...missing]; return s; @@ -72,12 +82,19 @@ class SettingsCubit extends HydratedCubit { @override Map? toJson(Settings state) => state.toJson(); - Map _mergeSettings(Map oldMap, Map newMap) { + Map _mergeSettings( + Map oldMap, + Map newMap, + ) { final merged = Map.from(newMap); oldMap.forEach((key, value) { if (merged.containsKey(key)) { - if (value is Map && merged[key] is Map) { - merged[key] = _mergeSettings(value, merged[key] as Map); + if (value is Map && + merged[key] is Map) { + merged[key] = _mergeSettings( + value, + merged[key] as Map, + ); } else { merged[key] = value; } diff --git a/lib/state/app/modules/timetable/bloc/timetable_bloc.dart b/lib/state/app/modules/timetable/bloc/timetable_bloc.dart index 1c7dc5b..ea0f985 100644 --- a/lib/state/app/modules/timetable/bloc/timetable_bloc.dart +++ b/lib/state/app/modules/timetable/bloc/timetable_bloc.dart @@ -8,7 +8,13 @@ import '../repository/timetable_repository.dart'; import 'timetable_event.dart'; import 'timetable_state.dart'; -class TimetableBloc extends LoadableHydratedBloc { +class TimetableBloc + extends + LoadableHydratedBloc< + TimetableEvent, + TimetableState, + TimetableRepository + > { static const Duration _weekSpan = Duration(days: 7); static final DateFormat _weekKeyFormat = DateFormat('yyyyMMdd'); @@ -37,7 +43,8 @@ class TimetableBloc extends LoadableHydratedBloc json) => TimetableState.fromJson(json); + TimetableState fromStorage(Map json) => + TimetableState.fromJson(json); @override Map? toStorage(TimetableState state) => state.toJson(); @@ -54,7 +61,12 @@ class TimetableBloc extends LoadableHydratedBloc s.copyWith( + add( + Emit( + (s) => s.copyWith( rooms: rooms, subjects: subjects, schoolHolidays: schoolHolidays, dataVersion: s.dataVersion + 1, - ))); + ), + ), + ); } catch (e) { onError?.call(e); } try { final timegrid = await repo.data.getTimegrid(renew: renew); - add(Emit((s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1))); + add( + Emit( + (s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1), + ), + ); } catch (_) { // Timegrid load failure falls back to a hardcoded schedule in the UI layer. } @@ -146,8 +171,16 @@ class TimetableBloc extends LoadableHydratedBloc s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1))); + final events = await repo.data.getCustomEvents( + renew: renew, + onError: onError, + ); + add( + Emit( + (s) => + s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1), + ), + ); } catch (e) { onError?.call(e); } @@ -155,7 +188,11 @@ class TimetableBloc extends LoadableHydratedBloc _refreshCustomEvents() async { final events = await repo.data.getCustomEvents(renew: true); - add(DataGathered((s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1))); + add( + DataGathered( + (s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1), + ), + ); } void _prefetchAdjacentWeeks(DateTime start, DateTime end) { @@ -164,16 +201,21 @@ class TimetableBloc extends LoadableHydratedBloc _writeWeekToCache(start, week)).catchError((_) {}); + repo.data + .getWeek(start, end) + .then((week) => _writeWeekToCache(start, week)) + .catchError((_) {}); } void _writeWeekToCache(DateTime weekStart, GetTimetableResponse week) { final key = _weekKeyFormat.format(weekStart); - add(Emit((s) { - final updated = Map.of(s.weekCache); - updated[key] = week; - return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1); - })); + add( + Emit((s) { + final updated = Map.of(s.weekCache); + updated[key] = week; + return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1); + }), + ); } static DateTime _startOfWeek(DateTime reference) { @@ -182,7 +224,9 @@ class TimetableBloc extends LoadableHydratedBloc{}) Map weekCache, + @Default({}) + Map weekCache, GetRoomsResponse? rooms, GetSubjectsResponse? subjects, GetHolidaysResponse? schoolHolidays, @@ -26,10 +27,15 @@ abstract class TimetableState with _$TimetableState { @Default(0) int dataVersion, }) = _TimetableState; - factory TimetableState.fromJson(Map json) => _$TimetableStateFromJson(json); + factory TimetableState.fromJson(Map json) => + _$TimetableStateFromJson(json); Iterable getAllKnownLessons() => weekCache.values.expand((response) => response.result); - bool get hasReferenceData => rooms != null && subjects != null && schoolHolidays != null && customEvents != null; + bool get hasReferenceData => + rooms != null && + subjects != null && + schoolHolidays != null && + customEvents != null; } diff --git a/lib/state/app/modules/timetable/data_provider/timetable_data_provider.dart b/lib/state/app/modules/timetable/data_provider/timetable_data_provider.dart index afd993f..2b029a2 100644 --- a/lib/state/app/modules/timetable/data_provider/timetable_data_provider.dart +++ b/lib/state/app/modules/timetable/data_provider/timetable_data_provider.dart @@ -31,90 +31,78 @@ class TimetableDataProvider { DateTime endDate, { void Function(Object)? onError, bool renew = false, - }) => - resolveFromCache( - (onUpdate, onError) => GetTimetableCache( - startdate: int.parse(_dateFormat.format(startDate)), - enddate: int.parse(_dateFormat.format(endDate)), - renew: renew, - onUpdate: onUpdate, - onError: onError, - ), - onError: onError, - operationName: 'getWeek', - ); + }) => resolveFromCache( + (onUpdate, onError) => GetTimetableCache( + startdate: int.parse(_dateFormat.format(startDate)), + enddate: int.parse(_dateFormat.format(endDate)), + renew: renew, + onUpdate: onUpdate, + onError: onError, + ), + onError: onError, + operationName: 'getWeek', + ); Future getRooms({ void Function(Object)? onError, bool renew = false, - }) => - resolveFromCache( - (onUpdate, onError) => GetRoomsCache( - renew: renew, - onUpdate: onUpdate, - onError: onError, - ), - onError: onError, - operationName: 'getRooms', - ); + }) => resolveFromCache( + (onUpdate, onError) => + GetRoomsCache(renew: renew, onUpdate: onUpdate, onError: onError), + onError: onError, + operationName: 'getRooms', + ); Future getSubjects({ void Function(Object)? onError, bool renew = false, - }) => - resolveFromCache( - (onUpdate, onError) => GetSubjectsCache( - renew: renew, - onUpdate: onUpdate, - onError: onError, - ), - onError: onError, - operationName: 'getSubjects', - ); + }) => resolveFromCache( + (onUpdate, onError) => + GetSubjectsCache(renew: renew, onUpdate: onUpdate, onError: onError), + onError: onError, + operationName: 'getSubjects', + ); Future getSchoolHolidays({ void Function(Object)? onError, bool renew = false, - }) => - resolveFromCache( - (onUpdate, onError) => GetHolidaysCache( - renew: renew, - onUpdate: onUpdate, - onError: onError, - ), - onError: onError, - operationName: 'getSchoolHolidays', - ); + }) => resolveFromCache( + (onUpdate, onError) => + GetHolidaysCache(renew: renew, onUpdate: onUpdate, onError: onError), + onError: onError, + operationName: 'getSchoolHolidays', + ); Future getTimegrid({bool renew = false}) => resolveFromCache( - (onUpdate, _) => GetTimegridUnitsCache( - renew: renew, - onUpdate: onUpdate, - ), + (onUpdate, _) => + GetTimegridUnitsCache(renew: renew, onUpdate: onUpdate), operationName: 'getTimegrid', ); Future getCustomEvents({ bool renew = false, void Function(Object)? onError, - }) => - resolveFromCache( - (onUpdate, onError) => GetCustomTimetableEventCache( - GetCustomTimetableEventParams(AccountData().getUserSecret()), - renew: renew, - onUpdate: onUpdate, - onError: onError, - ), - onError: onError, - operationName: 'getCustomEvents', - ); + }) => resolveFromCache( + (onUpdate, onError) => GetCustomTimetableEventCache( + GetCustomTimetableEventParams(AccountData().getUserSecret()), + renew: renew, + onUpdate: onUpdate, + onError: onError, + ), + onError: onError, + operationName: 'getCustomEvents', + ); Future addCustomEvent(CustomTimetableEvent event) => - AddCustomTimetableEvent(AddCustomTimetableEventParams(AccountData().getUserSecret(), event)).run(); + AddCustomTimetableEvent( + AddCustomTimetableEventParams(AccountData().getUserSecret(), event), + ).run(); Future updateCustomEvent(String id, CustomTimetableEvent event) => - UpdateCustomTimetableEvent(UpdateCustomTimetableEventParams(id, event)).run(); + UpdateCustomTimetableEvent( + UpdateCustomTimetableEventParams(id, event), + ).run(); Future removeCustomEvent(String id) => RemoveCustomTimetableEvent(RemoveCustomTimetableEventParams(id)).run(); diff --git a/lib/state/app/modules/timetable/repository/timetable_repository.dart b/lib/state/app/modules/timetable/repository/timetable_repository.dart index 36cb6e3..af88a9e 100644 --- a/lib/state/app/modules/timetable/repository/timetable_repository.dart +++ b/lib/state/app/modules/timetable/repository/timetable_repository.dart @@ -5,7 +5,8 @@ import '../data_provider/timetable_data_provider.dart'; class TimetableRepository extends Repository { final TimetableDataProvider _provider; - TimetableRepository([TimetableDataProvider? provider]) : _provider = provider ?? TimetableDataProvider(); + TimetableRepository([TimetableDataProvider? provider]) + : _provider = provider ?? TimetableDataProvider(); TimetableDataProvider get data => _provider; } diff --git a/lib/storage/dev_tools_settings.dart b/lib/storage/dev_tools_settings.dart index 03e2fac..d89ffe2 100644 --- a/lib/storage/dev_tools_settings.dart +++ b/lib/storage/dev_tools_settings.dart @@ -8,8 +8,13 @@ class DevToolsSettings { bool checkerboardOffscreenLayers; bool checkerboardRasterCacheImages; - DevToolsSettings({required this.showPerformanceOverlay, required this.checkerboardOffscreenLayers, required this.checkerboardRasterCacheImages}); + DevToolsSettings({ + required this.showPerformanceOverlay, + required this.checkerboardOffscreenLayers, + required this.checkerboardRasterCacheImages, + }); - factory DevToolsSettings.fromJson(Map json) => _$DevToolsSettingsFromJson(json); + factory DevToolsSettings.fromJson(Map json) => + _$DevToolsSettingsFromJson(json); Map toJson() => _$DevToolsSettingsToJson(this); } diff --git a/lib/storage/file_settings.dart b/lib/storage/file_settings.dart index 3b76ffa..c7dad77 100644 --- a/lib/storage/file_settings.dart +++ b/lib/storage/file_settings.dart @@ -11,8 +11,13 @@ class FileSettings { bool ascending; SortOption sortBy; - FileSettings({required this.sortFoldersToTop, required this.ascending, required this.sortBy}); + FileSettings({ + required this.sortFoldersToTop, + required this.ascending, + required this.sortBy, + }); - factory FileSettings.fromJson(Map json) => _$FileSettingsFromJson(json); + factory FileSettings.fromJson(Map json) => + _$FileSettingsFromJson(json); Map toJson() => _$FileSettingsToJson(this); } diff --git a/lib/storage/file_view_settings.dart b/lib/storage/file_view_settings.dart index 736377f..365f680 100644 --- a/lib/storage/file_view_settings.dart +++ b/lib/storage/file_view_settings.dart @@ -8,6 +8,7 @@ class FileViewSettings { FileViewSettings({required this.alwaysOpenExternally}); - factory FileViewSettings.fromJson(Map json) => _$FileViewSettingsFromJson(json); + factory FileViewSettings.fromJson(Map json) => + _$FileViewSettingsFromJson(json); Map toJson() => _$FileViewSettingsToJson(this); } diff --git a/lib/storage/holidays_settings.dart b/lib/storage/holidays_settings.dart index d4034e5..6f4709b 100644 --- a/lib/storage/holidays_settings.dart +++ b/lib/storage/holidays_settings.dart @@ -7,8 +7,12 @@ class HolidaysSettings { bool dismissedDisclaimer; bool showPastEvents; - HolidaysSettings({required this.dismissedDisclaimer, required this.showPastEvents}); + HolidaysSettings({ + required this.dismissedDisclaimer, + required this.showPastEvents, + }); - factory HolidaysSettings.fromJson(Map json) => _$HolidaysSettingsFromJson(json); + factory HolidaysSettings.fromJson(Map json) => + _$HolidaysSettingsFromJson(json); Map toJson() => _$HolidaysSettingsToJson(this); } diff --git a/lib/storage/modules_settings.dart b/lib/storage/modules_settings.dart index 117f354..0706b9c 100644 --- a/lib/storage/modules_settings.dart +++ b/lib/storage/modules_settings.dart @@ -18,6 +18,7 @@ class ModulesSettings { this.fixedBottomBarSlots = 3, }); - factory ModulesSettings.fromJson(Map json) => _$ModulesSettingsFromJson(json); + factory ModulesSettings.fromJson(Map json) => + _$ModulesSettingsFromJson(json); Map toJson() => _$ModulesSettingsToJson(this); } diff --git a/lib/storage/notification_settings.dart b/lib/storage/notification_settings.dart index ae08533..664cd2f 100644 --- a/lib/storage/notification_settings.dart +++ b/lib/storage/notification_settings.dart @@ -7,8 +7,12 @@ class NotificationSettings { bool askUsageDismissed; bool enabled; - NotificationSettings({required this.askUsageDismissed, required this.enabled}); + NotificationSettings({ + required this.askUsageDismissed, + required this.enabled, + }); - factory NotificationSettings.fromJson(Map json) => _$NotificationSettingsFromJson(json); + factory NotificationSettings.fromJson(Map json) => + _$NotificationSettingsFromJson(json); Map toJson() => _$NotificationSettingsToJson(this); } diff --git a/lib/storage/settings.dart b/lib/storage/settings.dart index 4e42830..e7fc579 100644 --- a/lib/storage/settings.dart +++ b/lib/storage/settings.dart @@ -14,10 +14,7 @@ part 'settings.g.dart'; @JsonSerializable(explicitToJson: true) class Settings { - @JsonKey( - toJson: _themeToJson, - fromJson: _themeFromJson, - ) + @JsonKey(toJson: _themeToJson, fromJson: _themeFromJson) ThemeMode appTheme; bool devToolsEnabled; @@ -44,8 +41,10 @@ class Settings { }); static String _themeToJson(ThemeMode m) => m.name; - static ThemeMode _themeFromJson(String m) => ThemeMode.values.firstWhere((element) => element.name == m); + static ThemeMode _themeFromJson(String m) => + ThemeMode.values.firstWhere((element) => element.name == m); - factory Settings.fromJson(Map json) => _$SettingsFromJson(json); + factory Settings.fromJson(Map json) => + _$SettingsFromJson(json); Map toJson() => _$SettingsToJson(this); } diff --git a/lib/storage/talk_settings.dart b/lib/storage/talk_settings.dart index 180dc45..77838bc 100644 --- a/lib/storage/talk_settings.dart +++ b/lib/storage/talk_settings.dart @@ -9,8 +9,14 @@ class TalkSettings { Map drafts; Map draftReplies; - TalkSettings({required this.sortFavoritesToTop, required this.sortUnreadToTop, required this.drafts, required this.draftReplies}); + TalkSettings({ + required this.sortFavoritesToTop, + required this.sortUnreadToTop, + required this.drafts, + required this.draftReplies, + }); - factory TalkSettings.fromJson(Map json) => _$TalkSettingsFromJson(json); + factory TalkSettings.fromJson(Map json) => + _$TalkSettingsFromJson(json); Map toJson() => _$TalkSettingsToJson(this); } diff --git a/lib/storage/timetable_settings.dart b/lib/storage/timetable_settings.dart index 75faf6a..feaa007 100644 --- a/lib/storage/timetable_settings.dart +++ b/lib/storage/timetable_settings.dart @@ -14,6 +14,7 @@ class TimetableSettings { required this.timetableNameMode, }); - factory TimetableSettings.fromJson(Map json) => _$TimetableSettingsFromJson(json); + factory TimetableSettings.fromJson(Map json) => + _$TimetableSettingsFromJson(json); Map toJson() => _$TimetableSettingsToJson(this); } diff --git a/lib/theming/app_theme.dart b/lib/theming/app_theme.dart index d39f459..4acb96f 100644 --- a/lib/theming/app_theme.dart +++ b/lib/theming/app_theme.dart @@ -15,18 +15,27 @@ TextStyle inputErrorStyle(BuildContext context) => class AppTheme { static DropdownDisplay getDisplayOptions(ThemeMode theme) { - switch(theme) { + switch (theme) { case ThemeMode.system: - return DropdownDisplay(icon: Icons.auto_fix_high_outlined, displayName: 'Systemvorgabe'); + return DropdownDisplay( + icon: Icons.auto_fix_high_outlined, + displayName: 'Systemvorgabe', + ); case ThemeMode.light: - return DropdownDisplay(icon: Icons.wb_sunny_outlined, displayName: 'Hell'); + return DropdownDisplay( + icon: Icons.wb_sunny_outlined, + displayName: 'Hell', + ); case ThemeMode.dark: - return DropdownDisplay(icon: Icons.dark_mode_outlined, displayName: 'Dunkel'); - + return DropdownDisplay( + icon: Icons.dark_mode_outlined, + displayName: 'Dunkel', + ); } } - static bool isDarkMode(BuildContext context) => Theme.of(context).brightness == Brightness.dark; + static bool isDarkMode(BuildContext context) => + Theme.of(context).brightness == Brightness.dark; } diff --git a/lib/theming/light_app_theme.dart b/lib/theming/light_app_theme.dart index 97e9130..5589bae 100644 --- a/lib/theming/light_app_theme.dart +++ b/lib/theming/light_app_theme.dart @@ -7,7 +7,7 @@ class LightAppTheme { brightness: Brightness.light, colorScheme: ColorScheme.fromSeed(seedColor: marianumRed), floatingActionButtonTheme: const FloatingActionButtonThemeData( - foregroundColor: Colors.white - ) + foregroundColor: Colors.white, + ), ); } diff --git a/lib/utils/cache_invalidation_bus.dart b/lib/utils/cache_invalidation_bus.dart index ee98c0d..9f4aa4e 100644 --- a/lib/utils/cache_invalidation_bus.dart +++ b/lib/utils/cache_invalidation_bus.dart @@ -8,7 +8,8 @@ import 'dart:async'; class CacheInvalidationBus { CacheInvalidationBus._(); - static final StreamController _listFiles = StreamController.broadcast(); + static final StreamController _listFiles = + StreamController.broadcast(); /// Emits the invalidated `pathString` (in `FilesBloc` format: relative, /// no leading or trailing slash; root is '/'). diff --git a/lib/utils/download_manager.dart b/lib/utils/download_manager.dart index ba9c0dd..8e68ab4 100644 --- a/lib/utils/download_manager.dart +++ b/lib/utils/download_manager.dart @@ -49,7 +49,9 @@ class DownloadJob { final String localPath; final FileDownloader _downloader; - final ValueNotifier status = ValueNotifier(const DownloadInProgress(0)); + final ValueNotifier status = ValueNotifier( + const DownloadInProgress(0), + ); bool _disposed = false; bool get isFinished => @@ -86,7 +88,10 @@ class DownloadManager { /// Returns the existing job if a download is in progress for [remotePath], /// otherwise starts a new one. Caller listens on [DownloadJob.status]. - Future start({required String remotePath, required String name}) async { + Future start({ + required String remotePath, + required String name, + }) async { final existing = _jobs[remotePath]; if (existing != null && !existing.isFinished) return existing; if (existing != null) { diff --git a/lib/utils/file_downloader.dart b/lib/utils/file_downloader.dart index d6b8152..34c1088 100644 --- a/lib/utils/file_downloader.dart +++ b/lib/utils/file_downloader.dart @@ -28,20 +28,24 @@ class FileDownloader { required void Function() onDone, required void Function(Object error) onError, }) { - client.download( - url, - savePath, - cancelToken: _cancelToken, - onReceiveProgress: (received, total) { - if (_cancelled || total <= 0) return; - onProgress((received / total) * 100); - }, - ).then((_) { - if (_cancelled) return; - onDone(); - }).catchError((Object error) { - if (_cancelled) return; - onError(error); - }).ignore(); + client + .download( + url, + savePath, + cancelToken: _cancelToken, + onReceiveProgress: (received, total) { + if (_cancelled || total <= 0) return; + onProgress((received / total) * 100); + }, + ) + .then((_) { + if (_cancelled) return; + onDone(); + }) + .catchError((Object error) { + if (_cancelled) return; + onError(error); + }) + .ignore(); } } diff --git a/lib/utils/url_opener.dart b/lib/utils/url_opener.dart index 450ed94..b88f8ab 100644 --- a/lib/utils/url_opener.dart +++ b/lib/utils/url_opener.dart @@ -3,7 +3,7 @@ import 'package:url_launcher/url_launcher_string.dart'; class UrlOpener { static Future onOpen(LinkableElement link) async { - if(await canLaunchUrlString(link.url)) { + if (await canLaunchUrlString(link.url)) { await launchUrlString(link.url); } } diff --git a/lib/view/login/login.dart b/lib/view/login/login.dart index 286f7e4..879cf0f 100644 --- a/lib/view/login/login.dart +++ b/lib/view/login/login.dart @@ -38,34 +38,37 @@ class _LoginState extends State { @override Widget build(BuildContext context) => Scaffold( - backgroundColor: _marianumRed, - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) => SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - maxWidth: 420, - ), - child: IntrinsicHeight( - child: Column( - children: [ - const LoginHeader(), - const SizedBox(height: 28), - LoginCard(controller: _controller, onSuccess: _onLoginSuccess), - const SizedBox(height: 18), - const LoginDisclaimer(), - const Spacer(), - const LoginFooter(), - ], + backgroundColor: _marianumRed, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + maxWidth: 420, + ), + child: IntrinsicHeight( + child: Column( + children: [ + const LoginHeader(), + const SizedBox(height: 28), + LoginCard( + controller: _controller, + onSuccess: _onLoginSuccess, ), - ), + const SizedBox(height: 18), + const LoginDisclaimer(), + const Spacer(), + const LoginFooter(), + ], ), ), ), ), ), - ); + ), + ), + ); } diff --git a/lib/view/login/widgets/login_branding.dart b/lib/view/login/widgets/login_branding.dart index 07475ce..04649c5 100644 --- a/lib/view/login/widgets/login_branding.dart +++ b/lib/view/login/widgets/login_branding.dart @@ -5,37 +5,37 @@ class LoginHeader extends StatelessWidget { @override Widget build(BuildContext context) => Column( - children: [ - const SizedBox(height: 40), - Image.asset( - 'assets/logo/icon.png', - height: 110, - fit: BoxFit.contain, - gaplessPlayback: true, - ), - const SizedBox(height: 20), - const Text( - 'Marianum Fulda', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 26, - fontWeight: FontWeight.w600, - letterSpacing: 0.3, - ), - ), - const SizedBox(height: 6), - Text( - 'Stundenplan, Talk & Dateien an einem Ort.', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.85), - fontSize: 14, - height: 1.3, - ), - ), - ], - ); + children: [ + const SizedBox(height: 40), + Image.asset( + 'assets/logo/icon.png', + height: 110, + fit: BoxFit.contain, + gaplessPlayback: true, + ), + const SizedBox(height: 20), + const Text( + 'Marianum Fulda', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 26, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + const SizedBox(height: 6), + Text( + 'Stundenplan, Talk & Dateien an einem Ort.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.85), + fontSize: 14, + height: 1.3, + ), + ), + ], + ); } class LoginDisclaimer extends StatelessWidget { @@ -43,17 +43,17 @@ class LoginDisclaimer extends StatelessWidget { @override Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - 'Inoffizieller Nextcloud & Webuntis Client. Wird nicht vom Marianum betrieben. Keine Gewähr für Vollständigkeit, Richtigkeit und Aktualität.', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.75), - fontSize: 11, - height: 1.4, - ), - ), - ); + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + 'Inoffizieller Nextcloud & Webuntis Client. Wird nicht vom Marianum betrieben. Keine Gewähr für Vollständigkeit, Richtigkeit und Aktualität.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.75), + fontSize: 11, + height: 1.4, + ), + ), + ); } class LoginFooter extends StatelessWidget { @@ -61,15 +61,15 @@ class LoginFooter extends StatelessWidget { @override Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.only(top: 16, bottom: 8), - child: Text( - 'Marianum Fulda. Die persönliche Schule.', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.7), - fontSize: 12, - fontStyle: FontStyle.italic, - ), - ), - ); + padding: const EdgeInsets.only(top: 16, bottom: 8), + child: Text( + 'Marianum Fulda. Die persönliche Schule.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + ); } diff --git a/lib/view/login/widgets/login_card.dart b/lib/view/login/widgets/login_card.dart index 61d1129..2ca2990 100644 --- a/lib/view/login/widgets/login_card.dart +++ b/lib/view/login/widgets/login_card.dart @@ -10,7 +10,11 @@ class LoginCard extends StatefulWidget { final LoginController controller; final VoidCallback onSuccess; - const LoginCard({required this.controller, required this.onSuccess, super.key}); + const LoginCard({ + required this.controller, + required this.onSuccess, + super.key, + }); @override State createState() => _LoginCardState(); @@ -59,7 +63,9 @@ class _LoginCardState extends State { labelText: label, prefixIcon: Icon(icon), filled: true, - fillColor: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4), + fillColor: theme.colorScheme.surfaceContainerHighest.withValues( + alpha: 0.4, + ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, @@ -92,7 +98,9 @@ class _LoginCardState extends State { children: [ Text( 'Anmelden', - style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600), + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: 6), Text( @@ -109,7 +117,11 @@ class _LoginCardState extends State { autocorrect: false, textInputAction: TextInputAction.next, onFieldSubmitted: (_) => _passwordFocus.requestFocus(), - decoration: _decoration(theme, 'Nutzername', Icons.person_outline), + decoration: _decoration( + theme, + 'Nutzername', + Icons.person_outline, + ), ), const SizedBox(height: 12), TextFormField( @@ -136,14 +148,22 @@ class _LoginCardState extends State { child: FilledButton( onPressed: loading ? null : _submit, style: FilledButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), ), child: loading ? const SizedBox( height: 22, width: 22, - child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white), + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: Colors.white, + ), ) : const Text('Anmelden'), ), diff --git a/lib/view/login/widgets/login_error_banner.dart b/lib/view/login/widgets/login_error_banner.dart index df6395f..87d2ff4 100644 --- a/lib/view/login/widgets/login_error_banner.dart +++ b/lib/view/login/widgets/login_error_banner.dart @@ -9,7 +9,11 @@ class LoginErrorBanner extends StatelessWidget { final String? message; final String? details; - const LoginErrorBanner({required this.message, required this.details, super.key}); + const LoginErrorBanner({ + required this.message, + required this.details, + super.key, + }); @override Widget build(BuildContext context) { @@ -26,14 +30,26 @@ class LoginErrorBanner extends StatelessWidget { borderRadius: BorderRadius.circular(12), child: InkWell( onTap: details != null - ? () => InfoDialog.show(context, details!, copyable: true, title: 'Fehlerdetails') + ? () => InfoDialog.show( + context, + details!, + copyable: true, + title: 'Fehlerdetails', + ) : null, borderRadius: BorderRadius.circular(12), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), child: Row( children: [ - Icon(Icons.error_outline, size: 20, color: theme.colorScheme.onErrorContainer), + Icon( + Icons.error_outline, + size: 20, + color: theme.colorScheme.onErrorContainer, + ), const SizedBox(width: 10), Expanded( child: Text( @@ -50,7 +66,8 @@ class LoginErrorBanner extends StatelessWidget { Icon( Icons.chevron_right, size: 20, - color: theme.colorScheme.onErrorContainer.withValues(alpha: 0.7), + color: theme.colorScheme.onErrorContainer + .withValues(alpha: 0.7), ), ], ], diff --git a/lib/view/pages/files/data/sort_options.dart b/lib/view/pages/files/data/sort_options.dart index 941e7d1..ab78fdf 100644 --- a/lib/view/pages/files/data/sort_options.dart +++ b/lib/view/pages/files/data/sort_options.dart @@ -9,7 +9,11 @@ class BetterSortOption { final int Function(CacheableFile, CacheableFile) compare; final IconData icon; - BetterSortOption({required this.displayName, required this.icon, required this.compare}); + BetterSortOption({ + required this.displayName, + required this.icon, + required this.compare, + }); } class SortOptions { diff --git a/lib/view/pages/files/files.dart b/lib/view/pages/files/files.dart index 73bed9d..b860b7e 100644 --- a/lib/view/pages/files/files.dart +++ b/lib/view/pages/files/files.dart @@ -25,7 +25,8 @@ class Files extends StatelessWidget { Files({List? path, super.key}) : path = path ?? []; @override - Widget build(BuildContext context) => BlocModule>( + Widget build(BuildContext context) => + BlocModule>( create: (_) => FilesBloc(initialPath: path), child: (context, _, _) => _FilesView(path: path), ); @@ -51,7 +52,8 @@ class _FilesViewState extends State<_FilesView> { // Relative folder path matching the WebDAV format used by `CacheableFile.path` // (no leading slash; trailing slash for non-root). Empty string means root. - String get _currentFolderPath => widget.path.isEmpty ? '' : '${widget.path.join('/')}/'; + String get _currentFolderPath => + widget.path.isEmpty ? '' : '${widget.path.join('/')}/'; @override void initState() { @@ -59,7 +61,9 @@ class _FilesViewState extends State<_FilesView> { settings = context.read(); currentSort = settings.val().fileSettings.sortBy; currentSortDirection = settings.val().fileSettings.ascending; - _invalidationSub = CacheInvalidationBus.listFilesStream.listen(_onInvalidation); + _invalidationSub = CacheInvalidationBus.listFilesStream.listen( + _onInvalidation, + ); } void _onInvalidation(String invalidatedPath) { @@ -77,15 +81,17 @@ class _FilesViewState extends State<_FilesView> { Future _mediaUpload(List? paths) async { if (paths == null) return; final bloc = context.read(); - unawaited(pushScreen( - context, - withNavBar: false, - screen: FilesUploadDialog( - filePaths: paths, - remotePath: widget.path.join('/'), - onUploadFinished: (_) => bloc.refresh(), + unawaited( + pushScreen( + context, + withNavBar: false, + screen: FilesUploadDialog( + filePaths: paths, + remotePath: widget.path.join('/'), + onUploadFinished: (_) => bloc.refresh(), + ), ), - )); + ); } @override @@ -116,29 +122,41 @@ class _FilesViewState extends State<_FilesView> { floatingActionButton: FloatingActionButton( heroTag: 'uploadFile', backgroundColor: Theme.of(context).primaryColor, - onPressed: () => showAddFileSheet(context, bloc: bloc, onPickedFiles: _mediaUpload), + onPressed: () => + showAddFileSheet(context, bloc: bloc, onPickedFiles: _mediaUpload), child: const Icon(Icons.add), ), body: Column( children: [ - ClipboardBanner(currentFolder: _currentFolderPath, onPasteDone: bloc.refresh), + ClipboardBanner( + currentFolder: _currentFolderPath, + onPasteDone: bloc.refresh, + ), Expanded( child: LoadableStateConsumer( isReady: (state) => state.listing != null, child: (state, _) { final listing = state.listing!; if (listing.files.isEmpty) { - return const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer'); + return const PlaceholderView( + icon: Icons.folder_off_rounded, + text: 'Der Ordner ist leer', + ); } final files = listing.sortBy( sortOption: currentSort, - foldersToTop: context.watch().val().fileSettings.sortFoldersToTop, + foldersToTop: context + .watch() + .val() + .fileSettings + .sortFoldersToTop, reversed: currentSortDirection, ); return ListView.builder( padding: EdgeInsets.zero, itemCount: files.length, - itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh), + itemBuilder: (context, index) => + FileElement(files[index], widget.path, bloc.refresh), ); }, ), diff --git a/lib/view/pages/files/files_upload_dialog.dart b/lib/view/pages/files/files_upload_dialog.dart index b7d72b1..273557c 100644 --- a/lib/view/pages/files/files_upload_dialog.dart +++ b/lib/view/pages/files/files_upload_dialog.dart @@ -15,7 +15,13 @@ class FilesUploadDialog extends StatefulWidget { final void Function(List uploadedFilePaths) onUploadFinished; final bool uniqueNames; - const FilesUploadDialog({super.key, required this.filePaths, required this.remotePath, required this.onUploadFinished, this.uniqueNames = false}); + const FilesUploadDialog({ + super.key, + required this.filePaths, + required this.remotePath, + required this.onUploadFinished, + this.uniqueNames = false, + }); @override State createState() => _FilesUploadDialogState(); @@ -31,7 +37,6 @@ class UploadableFile { UploadableFile(this.filePath, this.fileName); } - class _FilesUploadDialogState extends State { late List _uploadableFiles; bool _isUploading = false; @@ -63,7 +68,12 @@ class _FilesUploadDialogState extends State { _overallProgressValue = 0.0; _infoText = ''; }); - InfoDialog.show(context, message, title: 'Upload fehlgeschlagen', copyable: true); + InfoDialog.show( + context, + message, + title: 'Upload fehlgeschlagen', + copyable: true, + ); } Future uploadFiles({bool override = false}) async { @@ -80,7 +90,9 @@ class _FilesUploadDialogState extends State { if (!override) { List result; try { - result = (await webdavClient.propfind(PathUri.parse(widget.remotePath))).responses; + result = (await webdavClient.propfind( + PathUri.parse(widget.remotePath), + )).responses; } catch (e) { if (!mounted) return; _showUploadError('Verbindung fehlgeschlagen: $e'); @@ -88,7 +100,11 @@ class _FilesUploadDialogState extends State { } final conflictingFiles = _uploadableFiles.where((file) { final fileName = file.fileName; - return result.any((element) => Uri.decodeComponent((element as WebDavResponse).href!).endsWith('/$fileName')); + return result.any( + (element) => Uri.decodeComponent( + (element as WebDavResponse).href!, + ).endsWith('/$fileName'), + ); }).toList(); if (conflictingFiles.isNotEmpty) { @@ -97,46 +113,46 @@ class _FilesUploadDialogState extends State { 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, - ), + 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), ), - 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), - ), - ], - ) + 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 != true) { @@ -160,13 +176,15 @@ class _FilesUploadDialogState extends State { if (widget.uniqueNames) { final unique = DateTime.now().microsecondsSinceEpoch.toRadixString(36); - fileName = '${fileName.split('.').first}-$unique.${fileName.split('.').last}'; + fileName = + '${fileName.split('.').first}-$unique.${fileName.split('.').last}'; } var fullRemotePath = '${widget.remotePath}/$fileName'; setState(() { - _infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}'; + _infoText = + '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}'; }); final HttpClientResponse uploadTask; @@ -178,7 +196,10 @@ class _FilesUploadDialogState extends State { onProgress: (progress) { setState(() { file._uploadProgress = progress; - _overallProgressValue = ((progress + _uploadableFiles.indexOf(file)) / _uploadableFiles.length).toDouble(); + _overallProgressValue = + ((progress + _uploadableFiles.indexOf(file)) / + _uploadableFiles.length) + .toDouble(); }); }, ); @@ -188,7 +209,7 @@ class _FilesUploadDialogState extends State { return; } - if(uploadTask.statusCode < 200 || uploadTask.statusCode > 299) { + if (uploadTask.statusCode < 200 || uploadTask.statusCode > 299) { setState(() { _isUploading = false; _overallProgressValue = 0.0; @@ -214,119 +235,133 @@ class _FilesUploadDialogState extends State { @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: TextStyle(color: Theme.of(context).colorScheme.error), + 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: TextStyle( + color: Theme.of(context).colorScheme.error, ), - 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, + 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, - child: IconButton( - tooltip: 'Datei entfernen', - padding: EdgeInsets.zero, - onPressed: () { - if(!_isUploading) { - if(_uploadableFiles.length-1 <= 0) Navigator.of(context).pop(); - setState(() { - _uploadableFiles.removeAt(index); - }); + onPressed: () { + if (!_isUploading) { + if (_uploadableFiles.length - 1 <= 0) { + Navigator.of(context).pop(); } - }, - 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'), + setState(() { + _uploadableFiles.removeAt(index); + }); + } + }, + icon: const Icon(Icons.delete_outlined), ), ), - 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)), - ], - ), - ), - - - ), - ], - ), + ); + }, ), - ], - ), + ), + 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)), + ], + ), + ), + ), + ], + ), + ), + ], ), - ); + ), + ); } diff --git a/lib/view/pages/files/widgets/clipboard_banner.dart b/lib/view/pages/files/widgets/clipboard_banner.dart index ac73360..8b1a078 100644 --- a/lib/view/pages/files/widgets/clipboard_banner.dart +++ b/lib/view/pages/files/widgets/clipboard_banner.dart @@ -50,7 +50,11 @@ class _ClipboardBannerState extends State { final src = _normalised(f.path); if (dst == src || dst.startsWith(src)) return false; } - final destination = _joinPath(widget.currentFolder, f.name, isDirectory: f.isDirectory); + final destination = _joinPath( + widget.currentFolder, + f.name, + isDirectory: f.isDirectory, + ); if (destination != f.path) atLeastOneActionable = true; } return atLeastOneActionable; @@ -75,14 +79,24 @@ class _ClipboardBannerState extends State { try { final webdav = await WebdavApi.webdav; for (final file in cb.files) { - final destination = _joinPath(widget.currentFolder, file.name, isDirectory: file.isDirectory); + final destination = _joinPath( + widget.currentFolder, + file.name, + isDirectory: file.isDirectory, + ); if (destination == file.path) continue; try { if (operation == FileClipboardOperation.cut) { - await webdav.move(PathUri.parse(file.path), PathUri.parse(destination)); + await webdav.move( + PathUri.parse(file.path), + PathUri.parse(destination), + ); invalidatedSourceFolders.add(_parentCacheKey(file.path)); } else { - await webdav.copy(PathUri.parse(file.path), PathUri.parse(destination)); + await webdav.copy( + PathUri.parse(file.path), + PathUri.parse(destination), + ); } } on Object catch (e) { errors.add('${file.name}: $e'); @@ -111,42 +125,49 @@ class _ClipboardBannerState extends State { @override Widget build(BuildContext context) => ListenableBuilder( - listenable: FileClipboard.instance, - builder: (context, _) { - final cb = FileClipboard.instance; - if (cb.isEmpty) return const SizedBox.shrink(); - final cut = cb.operation == FileClipboardOperation.cut; - final count = cb.files.length; - final label = count == 1 ? '"${cb.files.first.name}"' : '$count Elemente'; - return Material( - color: Theme.of(context).colorScheme.secondaryContainer, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row( - children: [ - Icon(cut ? Icons.drive_file_move_outline : Icons.copy_outlined, size: 20), - const SizedBox(width: 12), - Expanded( - child: Text( - cut ? '$label verschieben' : '$label kopieren', - overflow: TextOverflow.ellipsis, - ), - ), - TextButton( - onPressed: _busy || !_canPaste ? null : _paste, - child: _busy - ? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2)) - : const Text('Hier einfügen'), - ), - IconButton( - tooltip: 'Verwerfen', - icon: const Icon(Icons.close, size: 20), - onPressed: _busy ? null : cb.clear, - ), - ], + listenable: FileClipboard.instance, + builder: (context, _) { + final cb = FileClipboard.instance; + if (cb.isEmpty) return const SizedBox.shrink(); + final cut = cb.operation == FileClipboardOperation.cut; + final count = cb.files.length; + final label = count == 1 ? '"${cb.files.first.name}"' : '$count Elemente'; + return Material( + color: Theme.of(context).colorScheme.secondaryContainer, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Icon( + cut ? Icons.drive_file_move_outline : Icons.copy_outlined, + size: 20, ), - ), - ); - }, + const SizedBox(width: 12), + Expanded( + child: Text( + cut ? '$label verschieben' : '$label kopieren', + overflow: TextOverflow.ellipsis, + ), + ), + TextButton( + onPressed: _busy || !_canPaste ? null : _paste, + child: _busy + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Hier einfügen'), + ), + IconButton( + tooltip: 'Verwerfen', + icon: const Icon(Icons.close, size: 20), + onPressed: _busy ? null : cb.clear, + ), + ], + ), + ), ); + }, + ); } diff --git a/lib/view/pages/files/widgets/file_details_sheet.dart b/lib/view/pages/files/widgets/file_details_sheet.dart index 2e59564..e2dee4b 100644 --- a/lib/view/pages/files/widgets/file_details_sheet.dart +++ b/lib/view/pages/files/widgets/file_details_sheet.dart @@ -12,49 +12,67 @@ void showFileDetailsSheet(BuildContext context, CacheableFile file) { showDetailsBottomSheet( context, header: ListTile( - leading: Icon(file.isDirectory ? Icons.folder : Icons.description_outlined, size: 32), - title: Text(file.name, style: const TextStyle(fontWeight: FontWeight.bold)), + leading: Icon( + file.isDirectory ? Icons.folder : Icons.description_outlined, + size: 32, + ), + title: Text( + file.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), subtitle: Text(file.isDirectory ? 'Ordner' : (file.mimeType ?? '–')), ), children: (_) => [ _DetailRow(label: 'Pfad', value: file.path, copyable: true), - if (!file.isDirectory) _DetailRow(label: 'Größe', value: filesize(file.size)), + if (!file.isDirectory) + _DetailRow(label: 'Größe', value: filesize(file.size)), if (file.modifiedAt != null) _DetailRow( label: 'Geändert', - value: '${file.modifiedAt!.formatDateTime()} (${file.modifiedAt!.formatRelative()})', + value: + '${file.modifiedAt!.formatDateTime()} (${file.modifiedAt!.formatRelative()})', ), if (file.createdAt != null) _DetailRow(label: 'Erstellt', value: file.createdAt!.formatDateTime()), - if (file.eTag != null) _DetailRow(label: 'ETag', value: file.eTag!, copyable: true), + if (file.eTag != null) + _DetailRow(label: 'ETag', value: file.eTag!, copyable: true), ], ); } class _DetailRow extends StatelessWidget { - const _DetailRow({required this.label, required this.value, this.copyable = false}); + const _DetailRow({ + required this.label, + required this.value, + this.copyable = false, + }); final String label; final String value; final bool copyable; @override Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 90, - child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 90, + child: Text( + label, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - Expanded(child: SelectableText(value)), - if (copyable) - IconButton( - tooltip: 'Kopieren', - icon: const Icon(Icons.copy, size: 18), - onPressed: () => copyToClipboard(context, value), - ), - ], + ), ), - ); + Expanded(child: SelectableText(value)), + if (copyable) + IconButton( + tooltip: 'Kopieren', + icon: const Icon(Icons.copy, size: 18), + onPressed: () => copyToClipboard(context, value), + ), + ], + ), + ); } diff --git a/lib/view/pages/files/widgets/file_element.dart b/lib/view/pages/files/widgets/file_element.dart index 82a8dc1..0e64864 100644 --- a/lib/view/pages/files/widgets/file_element.dart +++ b/lib/view/pages/files/widgets/file_element.dart @@ -138,11 +138,17 @@ class _FileElementState extends State { void _onTap() { if (widget.file.isDirectory) { - AppRoutes.openFolder(context, widget.path.toList()..add(widget.file.name)); + AppRoutes.openFolder( + context, + widget.path.toList()..add(widget.file.name), + ); return; } if (EndpointData().getEndpointMode() == EndpointMode.stage) { - InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!'); + InfoDialog.show( + context, + 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!', + ); return; } final status = _job?.status.value; @@ -178,21 +184,34 @@ class _FileElementState extends State { autofocus: true, ), actions: [ - TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Abbrechen')), TextButton( - onPressed: () => Navigator.of(dialogCtx).pop(controller.text.trim()), + onPressed: () => Navigator.of(dialogCtx).pop(), + child: const Text('Abbrechen'), + ), + TextButton( + onPressed: () => + Navigator.of(dialogCtx).pop(controller.text.trim()), child: const Text('Umbenennen'), ), ], ), ); - if (newName == null || newName.isEmpty || newName == widget.file.name) return; + if (newName == null || newName.isEmpty || newName == widget.file.name) { + return; + } final parent = _parentPathOf(widget.file.path); - final destination = _joinPath(parent, newName, isDirectory: widget.file.isDirectory); + final destination = _joinPath( + parent, + newName, + isDirectory: widget.file.isDirectory, + ); await _runWebdavOp(() async { final webdav = await WebdavApi.webdav; - await webdav.move(PathUri.parse(widget.file.path), PathUri.parse(destination)); + await webdav.move( + PathUri.parse(widget.file.path), + PathUri.parse(destination), + ); }, errorTitle: 'Umbenennen fehlgeschlagen'); } finally { controller.dispose(); @@ -205,10 +224,14 @@ class _FileElementState extends State { } else { FileClipboard.instance.cut([widget.file]); } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('"${widget.file.name}" zum ${copy ? "Kopieren" : "Verschieben"} bereitgelegt'), - duration: const Duration(seconds: 2), - )); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '"${widget.file.name}" zum ${copy ? "Kopieren" : "Verschieben"} bereitgelegt', + ), + duration: const Duration(seconds: 2), + ), + ); } Future _delete() async { @@ -227,7 +250,10 @@ class _FileElementState extends State { ); } - Future _runWebdavOp(Future Function() action, {required String errorTitle}) async { + Future _runWebdavOp( + Future Function() action, { + required String errorTitle, + }) async { try { await action(); widget.refetch(); @@ -287,13 +313,13 @@ class _FileElementState extends State { @override Widget build(BuildContext context) => ListTile( - leading: CenteredLeading( - Icon(widget.file.isDirectory ? Icons.folder : Icons.description_outlined), - ), - title: Text(widget.file.name, maxLines: 2, overflow: TextOverflow.ellipsis), - subtitle: _subtitle(), - trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null), - onTap: _onTap, - onLongPress: _showActionSheet, - ); + leading: CenteredLeading( + Icon(widget.file.isDirectory ? Icons.folder : Icons.description_outlined), + ), + title: Text(widget.file.name, maxLines: 2, overflow: TextOverflow.ellipsis), + subtitle: _subtitle(), + trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null), + onTap: _onTap, + onLongPress: _showActionSheet, + ); } diff --git a/lib/view/pages/files/widgets/files_sort_actions.dart b/lib/view/pages/files/widgets/files_sort_actions.dart index 269f725..77a3175 100644 --- a/lib/view/pages/files/widgets/files_sort_actions.dart +++ b/lib/view/pages/files/widgets/files_sort_actions.dart @@ -23,37 +23,48 @@ class FilesSortActions extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ PopupMenuButton( - icon: Icon(ascending ? Icons.text_rotate_up : Icons.text_rotation_down), + icon: Icon( + ascending ? Icons.text_rotate_up : Icons.text_rotation_down, + ), itemBuilder: (context) => [true, false] - .map((e) => PopupMenuItem( - value: e, - enabled: e != ascending, - child: Row( - children: [ - Icon(e ? Icons.text_rotate_up : Icons.text_rotation_down, - color: theme.colorScheme.onSurface), - const SizedBox(width: 15), - Text(e ? 'Aufsteigend' : 'Absteigend'), - ], - ), - )) + .map( + (e) => PopupMenuItem( + value: e, + enabled: e != ascending, + child: Row( + children: [ + Icon( + e ? Icons.text_rotate_up : Icons.text_rotation_down, + color: theme.colorScheme.onSurface, + ), + const SizedBox(width: 15), + Text(e ? 'Aufsteigend' : 'Absteigend'), + ], + ), + ), + ) .toList(), onSelected: onDirectionChanged, ), PopupMenuButton( icon: const Icon(Icons.sort), itemBuilder: (context) => SortOptions.options.keys - .map((key) => PopupMenuItem( - value: key, - enabled: key != currentSort, - child: Row( - children: [ - Icon(SortOptions.getOption(key).icon, color: theme.colorScheme.onSurface), - const SizedBox(width: 15), - Text(SortOptions.getOption(key).displayName), - ], - ), - )) + .map( + (key) => PopupMenuItem( + value: key, + enabled: key != currentSort, + child: Row( + children: [ + Icon( + SortOptions.getOption(key).icon, + color: theme.colorScheme.onSurface, + ), + const SizedBox(width: 15), + Text(SortOptions.getOption(key).displayName), + ], + ), + ), + ) .toList(), onSelected: onSortChanged, ), diff --git a/lib/view/pages/grade_averages/grade_averages_list_view.dart b/lib/view/pages/grade_averages/grade_averages_list_view.dart index 48c0def..d87625e 100644 --- a/lib/view/pages/grade_averages/grade_averages_list_view.dart +++ b/lib/view/pages/grade_averages/grade_averages_list_view.dart @@ -12,7 +12,7 @@ class GradeAveragesListView extends StatelessWidget { var bloc = context.watch(); String getGradeDisplay(int grade) { - if(bloc.isMiddleSchool()) { + if (bloc.isMiddleSchool()) { return 'Note $grade'; } else { return "$grade Punkt${grade > 1 ? "e" : ""}"; @@ -25,7 +25,9 @@ class GradeAveragesListView extends StatelessWidget { var grade = bloc.getGradeFromIndex(index); return Material( child: ListTile( - tileColor: grade.isEven ? Colors.transparent : Colors.transparent.withAlpha(50), + tileColor: grade.isEven + ? Colors.transparent + : Colors.transparent.withAlpha(50), title: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -39,7 +41,13 @@ class GradeAveragesListView extends StatelessWidget { icon: const Icon(Icons.remove), color: Theme.of(context).colorScheme.onSurface, ), - Text('${bloc.countOfGrade(grade)}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), + Text( + '${bloc.countOfGrade(grade)}', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), IconButton( onPressed: () { bloc.add(IncrementGrade(grade)); diff --git a/lib/view/pages/grade_averages/grade_averages_view.dart b/lib/view/pages/grade_averages/grade_averages_view.dart index 828536a..c7c5558 100644 --- a/lib/view/pages/grade_averages/grade_averages_view.dart +++ b/lib/view/pages/grade_averages/grade_averages_view.dart @@ -12,49 +12,56 @@ class GradeAveragesView extends StatelessWidget { @override Widget build(BuildContext context) => BlocProvider( - create: (context) => GradeAveragesBloc(), - child: BlocBuilder( - builder: (context, state) { - var bloc = context.watch(); + create: (context) => GradeAveragesBloc(), + child: BlocBuilder( + builder: (context, state) { + var bloc = context.watch(); - return Scaffold( + return Scaffold( appBar: AppBar( title: const Text('Notendurschnittsrechner'), actions: [ Visibility( visible: bloc.state.grades.isNotEmpty, child: IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => ConfirmDialog( - title: 'Zurücksetzen?', - content: 'Alle Einträge werden entfernt.', - confirmButton: 'Zurücksetzen', - onConfirm: () { - bloc.add(ResetAll()); - }, - ), - ); - }, - icon: const Icon(Icons.delete_forever)), + onPressed: () { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: 'Zurücksetzen?', + content: 'Alle Einträge werden entfernt.', + confirmButton: 'Zurücksetzen', + onConfirm: () { + bloc.add(ResetAll()); + }, + ), + ); + }, + icon: const Icon(Icons.delete_forever), + ), ), PopupMenuButton( initialValue: bloc.isMiddleSchool(), icon: const Icon(Icons.more_horiz), - itemBuilder: (context) => [true, false].map((isMiddleSchool) => PopupMenuItem( - value: isMiddleSchool, - child: Row( - children: [ - Icon( - isMiddleSchool ? Icons.calculate_outlined : Icons.school_outlined, - color: Theme.of(context).colorScheme.onSurface + itemBuilder: (context) => [true, false] + .map( + (isMiddleSchool) => PopupMenuItem( + value: isMiddleSchool, + child: Row( + children: [ + Icon( + isMiddleSchool + ? Icons.calculate_outlined + : Icons.school_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 15), + Text(isMiddleSchool ? 'Realschule' : 'Oberstufe'), + ], + ), ), - const SizedBox(width: 15), - Text(isMiddleSchool ? 'Realschule' : 'Oberstufe'), - ], - ), - )).toList(), + ) + .toList(), onSelected: (isMiddleSchool) { if (bloc.state.grades.isNotEmpty) { showDialog( @@ -62,9 +69,10 @@ class GradeAveragesView extends StatelessWidget { builder: (context) => ConfirmDialog( title: 'Notensystem wechseln', content: - 'Beim Wechsel des Notensystems werden alle Einträge zurückgesetzt.', + 'Beim Wechsel des Notensystems werden alle Einträge zurückgesetzt.', confirmButton: 'Fortfahren', - onConfirm: () => bloc.add(GradingSystemChanged(isMiddleSchool)), + onConfirm: () => + bloc.add(GradingSystemChanged(isMiddleSchool)), ), ); } else { @@ -84,23 +92,34 @@ class GradeAveragesView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Ø', style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold)), + Text( + 'Ø', + style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold), + ), SizedBox(width: 5), - Text(bloc.average().toStringAsFixed(2), style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold)) + Text( + bloc.average().toStringAsFixed(2), + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + ), + ), ], ), const SizedBox(height: 10), const Divider(), const SizedBox(height: 10), - Text(bloc.isMiddleSchool() ? 'Wähle die Anzahl deiner jeweiligen Noten aus' : 'Wähle die Anzahl deiner jeweiligen Punkte aus'), - const SizedBox(height: 10), - const Expanded( - child: GradeAveragesListView() + Text( + bloc.isMiddleSchool() + ? 'Wähle die Anzahl deiner jeweiligen Noten aus' + : 'Wähle die Anzahl deiner jeweiligen Punkte aus', ), + const SizedBox(height: 10), + const Expanded(child: GradeAveragesListView()), ], ), ); - }, - ), - ); + }, + ), + ); } diff --git a/lib/view/pages/holidays/holidays_view.dart b/lib/view/pages/holidays/holidays_view.dart index 21241b7..2018d2d 100644 --- a/lib/view/pages/holidays/holidays_view.dart +++ b/lib/view/pages/holidays/holidays_view.dart @@ -19,17 +19,19 @@ class HolidaysView extends StatelessWidget { const HolidaysView({super.key}); @override - Widget build(BuildContext context) => BlocModule>( + Widget build( + BuildContext context, + ) => BlocModule>( create: (context) => HolidaysBloc(), autoRebuild: true, child: (context, bloc, state) { void showDisclaimer() => InfoDialog.show( - context, - 'Sämtliche Datumsangaben sind ohne Gewähr.\n' - 'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n' - 'Die Daten stammen von https://ferien-api.de/', - title: 'Richtigkeit und Bereitstellung der Daten', - ); + context, + 'Sämtliche Datumsangaben sind ohne Gewähr.\n' + 'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n' + 'Die Daten stammen von https://ferien-api.de/', + title: 'Richtigkeit und Bereitstellung der Daten', + ); return Scaffold( appBar: AppBar( @@ -42,79 +44,110 @@ class HolidaysView extends StatelessWidget { PopupMenuButton( initialValue: bloc.showPastHolidays(), icon: const Icon(Icons.history), - itemBuilder: (context) => [true, false].map((e) => PopupMenuItem( - value: e, - enabled: e != bloc.showPastHolidays(), - child: Row( - children: [ - Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface), - const SizedBox(width: 15), - Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen') - ], + itemBuilder: (context) => [true, false] + .map( + (e) => PopupMenuItem( + value: e, + enabled: e != bloc.showPastHolidays(), + child: Row( + children: [ + Icon( + e + ? Icons.history_outlined + : Icons.history_toggle_off_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 15), + Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen'), + ], + ), + ), ) - )).toList(), + .toList(), onSelected: (e) => bloc.add(SetPastHolidaysVisible(e)), ), ], ), body: LoadableStateConsumer( onLoad: (state) { - if(state.showDisclaimer) showDisclaimer(); + if (state.showDisclaimer) showDisclaimer(); bloc.add(DisclaimerDismissed()); }, - child: (state, loading) => ListViewUtil.fromList(bloc.getHolidays(), (holiday) { - var holidayType = holiday.name.split(' ').first.capitalize(); - String formatDate(String date) => Jiffy.parse(date).format(pattern: 'dd.MM.yyyy'); - String getYear(String date, {String format = 'yyyy'}) => Jiffy.parse(date).format(pattern: format); + child: (state, loading) => ListViewUtil.fromList( + bloc.getHolidays(), + (holiday) { + var holidayType = holiday.name.split(' ').first.capitalize(); + String formatDate(String date) => + Jiffy.parse(date).format(pattern: 'dd.MM.yyyy'); + String getYear(String date, {String format = 'yyyy'}) => + Jiffy.parse(date).format(pattern: format); - String getHolidayYear(String startDate, String endDate) => getYear(startDate) == getYear(endDate) + String getHolidayYear(String startDate, String endDate) => + getYear(startDate) == getYear(endDate) ? getYear(startDate) : '${getYear(startDate)}/${getYear(endDate, format: 'yy')}'; - return ListTile( - leading: const CenteredLeading(Icon(Icons.calendar_month)), - title: Text('$holidayType ${getHolidayYear(holiday.start, holiday.end)}'), - subtitle: Text('${formatDate(holiday.start)} - ${formatDate(holiday.end)}'), - onTap: () => showDetailsBottomSheet( - context, - header: Padding( - padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), - child: Text( - '$holidayType ${holiday.year} in Hessen', - style: Theme.of(context).textTheme.titleLarge, - ), + return ListTile( + leading: const CenteredLeading(Icon(Icons.calendar_month)), + title: Text( + '$holidayType ${getHolidayYear(holiday.start, holiday.end)}', ), - children: (sheetCtx) => [ - ListTile( - leading: const CenteredLeading(Icon(Icons.signpost_outlined)), - title: Text(holiday.name.capitalize()), - subtitle: Text(holiday.slug.capitalize()), - ), - ListTile( - leading: const Icon(Icons.date_range_outlined), - title: Text('vom ${formatDate(holiday.start)}'), - ), - ListTile( - leading: const Icon(Icons.date_range_outlined), - title: Text('bis zum ${formatDate(holiday.end)}'), - ), - if (DateTime.parse(holiday.start).difference(DateTime.now()).isNegative) - ListTile( - leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)), - title: Text(Jiffy.parse(holiday.start).fromNow()), - ) - else - ListTile( - leading: const CenteredLeading(Icon(Icons.timer_outlined)), - title: AnimatedTime(callback: () => DateTime.parse(holiday.start).difference(DateTime.now())), - subtitle: Text(Jiffy.parse(holiday.start).fromNow()), + subtitle: Text( + '${formatDate(holiday.start)} - ${formatDate(holiday.end)}', + ), + onTap: () => showDetailsBottomSheet( + context, + header: Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), + child: Text( + '$holidayType ${holiday.year} in Hessen', + style: Theme.of(context).textTheme.titleLarge, ), - DebugTile(sheetCtx).jsonData(holiday.toJson()), - ], - ), - trailing: const Icon(Icons.arrow_right), - ); - }), + ), + children: (sheetCtx) => [ + ListTile( + leading: const CenteredLeading( + Icon(Icons.signpost_outlined), + ), + title: Text(holiday.name.capitalize()), + subtitle: Text(holiday.slug.capitalize()), + ), + ListTile( + leading: const Icon(Icons.date_range_outlined), + title: Text('vom ${formatDate(holiday.start)}'), + ), + ListTile( + leading: const Icon(Icons.date_range_outlined), + title: Text('bis zum ${formatDate(holiday.end)}'), + ), + if (DateTime.parse( + holiday.start, + ).difference(DateTime.now()).isNegative) + ListTile( + leading: const CenteredLeading( + Icon(Icons.content_paste_search_outlined), + ), + title: Text(Jiffy.parse(holiday.start).fromNow()), + ) + else + ListTile( + leading: const CenteredLeading( + Icon(Icons.timer_outlined), + ), + title: AnimatedTime( + callback: () => DateTime.parse( + holiday.start, + ).difference(DateTime.now()), + ), + subtitle: Text(Jiffy.parse(holiday.start).fromNow()), + ), + DebugTile(sheetCtx).jsonData(holiday.toJson()), + ], + ), + trailing: const Icon(Icons.arrow_right), + ); + }, + ), ), ); }, diff --git a/lib/view/pages/marianum_dates/marianum_dates_view.dart b/lib/view/pages/marianum_dates/marianum_dates_view.dart index 3574031..585457a 100644 --- a/lib/view/pages/marianum_dates/marianum_dates_view.dart +++ b/lib/view/pages/marianum_dates/marianum_dates_view.dart @@ -19,7 +19,8 @@ class MarianumDatesView extends StatelessWidget { static List<_MonthGroup> _groupByMonth(List events) { final byMonth = >{}; for (final e in events) { - final key = '${e.start.year.toString().padLeft(4, '0')}-${e.start.month.toString().padLeft(2, '0')}'; + final key = + '${e.start.year.toString().padLeft(4, '0')}-${e.start.month.toString().padLeft(2, '0')}'; byMonth.putIfAbsent(key, () => []).add(e); } final keys = byMonth.keys.toList()..sort(); @@ -31,7 +32,8 @@ class MarianumDatesView extends StatelessWidget { } @override - Widget build(BuildContext context) => BlocModule>( + Widget build(BuildContext context) => + BlocModule>( create: (context) => MarianumDatesBloc(), autoRebuild: true, child: (context, bloc, state) => Scaffold( @@ -42,18 +44,26 @@ class MarianumDatesView extends StatelessWidget { initialValue: bloc.showPastEvents(), icon: const Icon(Icons.history), itemBuilder: (context) => [true, false] - .map((e) => PopupMenuItem( - value: e, - enabled: e != bloc.showPastEvents(), - child: Row( - children: [ - Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, - color: Theme.of(context).colorScheme.onSurface), - const SizedBox(width: 15), - Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen'), - ], - ), - )) + .map( + (e) => PopupMenuItem( + value: e, + enabled: e != bloc.showPastEvents(), + child: Row( + children: [ + Icon( + e + ? Icons.history_outlined + : Icons.history_toggle_off_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 15), + Text( + e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen', + ), + ], + ), + ), + ) .toList(), onSelected: (e) => bloc.add(SetPastEventsVisible(e)), ), @@ -61,7 +71,10 @@ class MarianumDatesView extends StatelessWidget { icon: const Icon(Icons.search), onPressed: () { final events = bloc.getEvents() ?? const []; - showSearch(context: context, delegate: SearchMarianumDates(events)); + showSearch( + context: context, + delegate: SearchMarianumDates(events), + ); }, ), ], @@ -89,7 +102,8 @@ class MarianumDatesView extends StatelessWidget { ), SliverList.builder( itemCount: group.events.length, - itemBuilder: (_, i) => MarianumDateRow(event: group.events[i]), + itemBuilder: (_, i) => + MarianumDateRow(event: group.events[i]), ), ], ), diff --git a/lib/view/pages/marianum_dates/search_marianum_dates.dart b/lib/view/pages/marianum_dates/search_marianum_dates.dart index 8a76c98..293bbf9 100644 --- a/lib/view/pages/marianum_dates/search_marianum_dates.dart +++ b/lib/view/pages/marianum_dates/search_marianum_dates.dart @@ -21,15 +21,15 @@ class SearchMarianumDates extends SearchDelegate { @override List? buildActions(BuildContext context) => [ - if (query.isNotEmpty) - IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)), - ]; + if (query.isNotEmpty) + IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)), + ]; @override Widget? buildLeading(BuildContext context) => IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => close(context, null), - ); + icon: const Icon(Icons.arrow_back), + onPressed: () => close(context, null), + ); @override Widget buildResults(BuildContext context) { diff --git a/lib/view/pages/marianum_dates/widgets/event_details_sheet.dart b/lib/view/pages/marianum_dates/widgets/event_details_sheet.dart index cf14e48..11e0564 100644 --- a/lib/view/pages/marianum_dates/widgets/event_details_sheet.dart +++ b/lib/view/pages/marianum_dates/widgets/event_details_sheet.dart @@ -32,12 +32,16 @@ void showEventDetailsSheet(BuildContext context, MarianumDate event) { if (isUpcoming) ListTile( leading: const CenteredLeading(Icon(Icons.timer_outlined)), - title: AnimatedTime(callback: () => event.start.difference(DateTime.now())), + title: AnimatedTime( + callback: () => event.start.difference(DateTime.now()), + ), subtitle: Text(event.start.formatRelative()), ) else ListTile( - leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)), + leading: const CenteredLeading( + Icon(Icons.content_paste_search_outlined), + ), title: Text(event.start.formatRelative()), ), DebugTile(sheetContext).jsonData(event.toJson()), diff --git a/lib/view/pages/marianum_dates/widgets/event_list_tile.dart b/lib/view/pages/marianum_dates/widgets/event_list_tile.dart index ba4b2f2..8fb09f9 100644 --- a/lib/view/pages/marianum_dates/widgets/event_list_tile.dart +++ b/lib/view/pages/marianum_dates/widgets/event_list_tile.dart @@ -63,9 +63,12 @@ class MarianumDateRow extends StatelessWidget { event.title.isEmpty ? '(ohne Titel)' : event.title, maxLines: 2, overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurface), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), ), - if (event.description != null && event.description!.trim().isNotEmpty) ...[ + if (event.description != null && + event.description!.trim().isNotEmpty) ...[ const SizedBox(height: 2), Text( event.description!.trim(), @@ -88,7 +91,9 @@ class MarianumDateRow extends StatelessWidget { ), const SizedBox(width: 4), IconButton( - icon: _CalendarPlusIcon(color: theme.colorScheme.onSurfaceVariant), + icon: _CalendarPlusIcon( + color: theme.colorScheme.onSurfaceVariant, + ), tooltip: 'In Stundenplan übernehmen', onPressed: () => showDialog( context: context, @@ -117,25 +122,25 @@ class _CalendarPlusIcon extends StatelessWidget { @override Widget build(BuildContext context) => SizedBox( - width: 22, - height: 22, - child: Stack( - clipBehavior: Clip.none, - children: [ - Icon(Icons.event_outlined, size: 22, color: color), - Positioned( - right: -2, - bottom: -2, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - shape: BoxShape.circle, - ), - padding: const EdgeInsets.all(1), - child: Icon(Icons.add_circle, size: 12, color: color), - ), + width: 22, + height: 22, + child: Stack( + clipBehavior: Clip.none, + children: [ + Icon(Icons.event_outlined, size: 22, color: color), + Positioned( + right: -2, + bottom: -2, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + shape: BoxShape.circle, ), - ], + padding: const EdgeInsets.all(1), + child: Icon(Icons.add_circle, size: 12, color: color), + ), ), - ); + ], + ), + ); } diff --git a/lib/view/pages/marianum_dates/widgets/month_section_header.dart b/lib/view/pages/marianum_dates/widgets/month_section_header.dart index 944b44c..e274711 100644 --- a/lib/view/pages/marianum_dates/widgets/month_section_header.dart +++ b/lib/view/pages/marianum_dates/widgets/month_section_header.dart @@ -7,7 +7,11 @@ class MonthHeaderDelegate extends SliverPersistentHeaderDelegate { static const double _height = 38; @override - Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { final theme = Theme.of(context); return Container( height: _height, @@ -32,5 +36,6 @@ class MonthHeaderDelegate extends SliverPersistentHeaderDelegate { double get minExtent => _height; @override - bool shouldRebuild(covariant MonthHeaderDelegate oldDelegate) => oldDelegate.label != label; + bool shouldRebuild(covariant MonthHeaderDelegate oldDelegate) => + oldDelegate.label != label; } diff --git a/lib/view/pages/marianum_message/marianum_message_list_view.dart b/lib/view/pages/marianum_message/marianum_message_list_view.dart index 637e160..f49b2a4 100644 --- a/lib/view/pages/marianum_message/marianum_message_list_view.dart +++ b/lib/view/pages/marianum_message/marianum_message_list_view.dart @@ -11,32 +11,36 @@ class MarianumMessageListView extends StatelessWidget { const MarianumMessageListView({super.key}); @override - Widget build(BuildContext context) => BlocModule>( + Widget build( + BuildContext context, + ) => BlocModule>( create: (context) => MarianumMessageBloc(), child: (context, bloc, state) => Scaffold( - appBar: AppBar( - title: const Text('Marianum Message'), + appBar: AppBar(title: const Text('Marianum Message')), + body: LoadableStateConsumer( + child: (state, loading) => ListView.builder( + itemCount: state.messageList.messages.length, + itemBuilder: (context, index) { + var message = state.messageList.messages.toList()[index]; + return ListTile( + leading: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [Icon(Icons.newspaper)], + ), + title: Text(message.name, overflow: TextOverflow.ellipsis), + subtitle: Text('vom ${message.date}'), + trailing: const Icon(Icons.arrow_right), + onTap: () { + AppRoutes.openMarianumMessage( + context, + state.messageList.base, + message, + ); + }, + ); + }, ), - body: LoadableStateConsumer( - child: (state, loading) => ListView.builder( - itemCount: state.messageList.messages.length, - itemBuilder: (context, index) { - var message = state.messageList.messages.toList()[index]; - return ListTile( - leading: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [Icon(Icons.newspaper)], - ), - title: Text(message.name, overflow: TextOverflow.ellipsis), - subtitle: Text('vom ${message.date}'), - trailing: const Icon(Icons.arrow_right), - onTap: () { - AppRoutes.openMarianumMessage(context, state.messageList.base, message); - }, - ); - } - ), - ), - ) + ), + ), ); } diff --git a/lib/view/pages/marianum_message/marianum_message_view.dart b/lib/view/pages/marianum_message/marianum_message_view.dart index 6968e8c..1ce7b40 100644 --- a/lib/view/pages/marianum_message/marianum_message_view.dart +++ b/lib/view/pages/marianum_message/marianum_message_view.dart @@ -16,34 +16,34 @@ class MessageView extends StatefulWidget { } class _MessageViewState extends State { - @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: Text(widget.message.name), - ), - body: SfPdfViewer.network( - widget.basePath + widget.message.url, - enableHyperlinkNavigation: true, - onDocumentLoadFailed: (PdfDocumentLoadFailedDetails e) { - Navigator.of(context).pop(); - InfoDialog.show( - context, - "Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}", - title: 'Fehler beim öffnen', - ); - }, - onHyperlinkClicked: (PdfHyperlinkClickedDetails e) { - showDialog( - context: context, - builder: (context) => ConfirmDialog( - title: 'Link öffnen', - content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}', - confirmButton: 'Öffnen', - onConfirm: () => launchUrl(Uri.parse(e.uri), mode: LaunchMode.externalApplication), + appBar: AppBar(title: Text(widget.message.name)), + body: SfPdfViewer.network( + widget.basePath + widget.message.url, + enableHyperlinkNavigation: true, + onDocumentLoadFailed: (PdfDocumentLoadFailedDetails e) { + Navigator.of(context).pop(); + InfoDialog.show( + context, + "Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}", + title: 'Fehler beim öffnen', + ); + }, + onHyperlinkClicked: (PdfHyperlinkClickedDetails e) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: 'Link öffnen', + content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}', + confirmButton: 'Öffnen', + onConfirm: () => launchUrl( + Uri.parse(e.uri), + mode: LaunchMode.externalApplication, ), - ); - }, - ), - ); + ), + ); + }, + ), + ); } diff --git a/lib/view/pages/more/feedback/feedback_dialog.dart b/lib/view/pages/more/feedback/feedback_dialog.dart index 42d082d..3fcd528 100644 --- a/lib/view/pages/more/feedback/feedback_dialog.dart +++ b/lib/view/pages/more/feedback/feedback_dialog.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; @@ -46,40 +45,49 @@ class _FeedbackDialogState extends State { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Feedback'), - ), - body: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - const SizedBox(height: 5), - const Text('Feedback, Anregungen, Ideen, Fehler und Verbesserungen', textAlign: TextAlign.center), - const SizedBox(height: 15), - const Text('Bitte gib keine geheimen Daten wie z.B. Passwörter weiter.', textAlign: TextAlign.center, style: TextStyle(fontSize: 11)), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.all(10), - child: TextField( - onChanged: (value) { - if(value.trim().toLowerCase() == 'ranzig') { - _feedbackInput.text = 'selber'; - } - }, - controller: _feedbackInput, - autofocus: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - label: const Text('Feedback und Verbesserungen'), - errorText: _textFieldEmpty ? 'Bitte gib eine Beschreibung an!' : null, - ), - minLines: 4, - maxLines: 7, - onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context), + appBar: AppBar(title: const Text('Feedback')), + body: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: 5), + const Text( + 'Feedback, Anregungen, Ideen, Fehler und Verbesserungen', + textAlign: TextAlign.center, + ), + const SizedBox(height: 15), + const Text( + 'Bitte gib keine geheimen Daten wie z.B. Passwörter weiter.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.all(10), + child: TextField( + onChanged: (value) { + if (value.trim().toLowerCase() == 'ranzig') { + _feedbackInput.text = 'selber'; + } + }, + controller: _feedbackInput, + autofocus: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + label: const Text('Feedback und Verbesserungen'), + errorText: _textFieldEmpty + ? 'Bitte gib eine Beschreibung an!' + : null, ), + minLines: 4, + maxLines: 7, + onTapOutside: (PointerDownEvent event) => + FocusBehaviour.textFieldTapOutside(context), ), - const SizedBox(height: 10), - if(_image != null) Row( + ), + const SizedBox(height: 10), + if (_image != null) + Row( mainAxisAlignment: MainAxisAlignment.center, children: [ badges.Badge( @@ -109,76 +117,95 @@ class _FeedbackDialogState extends State { ), ], ), - Padding( - padding: const EdgeInsets.all(5), + Padding( + padding: const EdgeInsets.all(5), + child: Visibility( + visible: _error != null, child: Visibility( - visible: _error != null, - child: Visibility( - visible: context.read().val().devToolsEnabled, - replacement: const Text('Senden fehlgeschlagen, bitte überprüfe die Internetverbindung.', textAlign: TextAlign.center, style: TextStyle(color: Colors.red)), - child: Text('Senden fehlgeschlagen: \n $_error', textAlign: TextAlign.center, style: const TextStyle(color: Colors.red)), + visible: context.read().val().devToolsEnabled, + replacement: const Text( + 'Senden fehlgeschlagen, bitte überprüfe die Internetverbindung.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.red), + ), + child: Text( + 'Senden fehlgeschlagen: \n $_error', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.red), ), ), ), - Padding( - padding: const EdgeInsets.only(right: 20, left: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Visibility( - visible: _image == null, - child: IconButton( - onPressed: () async { - context.loaderOverlay.show(); - final picked = await FilePick.multipleGalleryPick(); - final imageData = await picked?.first.readAsBytes(); - if(context.mounted) context.loaderOverlay.hide(); - setState(() { - _image = imageData; - }); - }, - icon: const Icon(Icons.attach_file_outlined), - ), - ), - const Expanded(child: SizedBox.shrink()), - TextButton( - onPressed: () async { - if(_feedbackInput.text.isEmpty){ - setState(() { - _textFieldEmpty = true; - }); - return; - } - context.loaderOverlay.show(); - unawaited(AddFeedback( - AddFeedbackParams( - user: AccountData().getUserSecret(), - feedback: _feedbackInput.text, - screenshot: _image != null ? base64Encode(_image!) : null, - appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber), + ), + Padding( + padding: const EdgeInsets.only(right: 20, left: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Visibility( + visible: _image == null, + child: IconButton( + onPressed: () async { + context.loaderOverlay.show(); + final picked = await FilePick.multipleGalleryPick(); + final imageData = await picked?.first.readAsBytes(); + if (context.mounted) context.loaderOverlay.hide(); + setState(() { + _image = imageData; + }); + }, + icon: const Icon(Icons.attach_file_outlined), + ), + ), + const Expanded(child: SizedBox.shrink()), + TextButton( + onPressed: () async { + if (_feedbackInput.text.isEmpty) { + setState(() { + _textFieldEmpty = true; + }); + return; + } + context.loaderOverlay.show(); + unawaited( + AddFeedback( + AddFeedbackParams( + user: AccountData().getUserSecret(), + feedback: _feedbackInput.text, + screenshot: _image != null + ? base64Encode(_image!) + : null, + appVersion: int.parse( + (await PackageInfo.fromPlatform()).buildNumber, ), - ).run().then((value) { + ), + ) + .run() + .then((value) { if (!context.mounted) return; Navigator.of(context).pop(); - InfoDialog.show(context, 'Danke für dein Feedback!'); + InfoDialog.show( + context, + 'Danke für dein Feedback!', + ); context.loaderOverlay.hide(); - }).catchError((Object error, StackTrace trace) { + }) + .catchError((Object error, StackTrace trace) { if (!mounted) return; setState(() { _error = error.toString(); }); if (!context.mounted) return; context.loaderOverlay.hide(); - })); - }, - child: const Text('Senden'), - ) - ] - ) - ) - - ], - ), + }), + ); + }, + child: const Text('Senden'), + ), + ], + ), + ), + ], ), - ); + ), + ); } diff --git a/lib/view/pages/more/roomplan/roomplan.dart b/lib/view/pages/more/roomplan/roomplan.dart index 0b15a20..63f3d3e 100644 --- a/lib/view/pages/more/roomplan/roomplan.dart +++ b/lib/view/pages/more/roomplan/roomplan.dart @@ -6,14 +6,14 @@ class Roomplan extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Raumplan'), + appBar: AppBar(title: const Text('Raumplan')), + body: PhotoView( + imageProvider: Image.asset('assets/img/raumplan.png').image, + minScale: 0.5, + maxScale: 2.0, + backgroundDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, ), - body: PhotoView( - imageProvider: Image.asset('assets/img/raumplan.png').image, - minScale: 0.5, - maxScale: 2.0, - backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), - ), - ); + ), + ); } diff --git a/lib/view/pages/more/share/app_share_platform_view.dart b/lib/view/pages/more/share/app_share_platform_view.dart index b793d4e..fe3ed7f 100644 --- a/lib/view/pages/more/share/app_share_platform_view.dart +++ b/lib/view/pages/more/share/app_share_platform_view.dart @@ -14,7 +14,10 @@ class AppSharePlatformView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + Text( + title, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), const SizedBox(height: 30), Container( padding: const EdgeInsets.all(10), @@ -26,8 +29,8 @@ class AppSharePlatformView extends StatelessWidget { version: QrVersions.auto, size: 200, dataModuleStyle: QrDataModuleStyle( - color: foregroundColor, - dataModuleShape: QrDataModuleShape.square + color: foregroundColor, + dataModuleShape: QrDataModuleShape.square, ), eyeStyle: QrEyeStyle( color: foregroundColor, diff --git a/lib/view/pages/more/share/qr_share_view.dart b/lib/view/pages/more/share/qr_share_view.dart index ec66db1..d3482ca 100644 --- a/lib/view/pages/more/share/qr_share_view.dart +++ b/lib/view/pages/more/share/qr_share_view.dart @@ -25,23 +25,29 @@ class _QrShareViewState extends State { @override Widget build(BuildContext context) => DefaultTabController( - length: 2, - child: Scaffold( - appBar: AppBar( - title: const Text('Teile die App'), - bottom: const TabBar( - tabs: [ - Tab(icon: Icon(Icons.android_outlined), text: 'Android'), - Tab(icon: Icon(Icons.apple_outlined), text: 'iOS & iPadOS'), - ], - ), - ), - body: const TabBarView( - children: [ - AppSharePlatformView('Für Android', 'https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client'), - AppSharePlatformView('Für iOS & iPad', 'https://apps.apple.com/us/app/marianum-fulda/id6458789560'), + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('Teile die App'), + bottom: const TabBar( + tabs: [ + Tab(icon: Icon(Icons.android_outlined), text: 'Android'), + Tab(icon: Icon(Icons.apple_outlined), text: 'iOS & iPadOS'), ], ), ), - ); + body: const TabBarView( + children: [ + AppSharePlatformView( + 'Für Android', + 'https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client', + ), + AppSharePlatformView( + 'Für iOS & iPad', + 'https://apps.apple.com/us/app/marianum-fulda/id6458789560', + ), + ], + ), + ), + ); } diff --git a/lib/view/pages/more/share/select_share_type_dialog.dart b/lib/view/pages/more/share/select_share_type_dialog.dart index a1df4f4..61dc782 100644 --- a/lib/view/pages/more/share/select_share_type_dialog.dart +++ b/lib/view/pages/more/share/select_share_type_dialog.dart @@ -30,14 +30,17 @@ Future showSelectShareTypeSheet(BuildContext context) { trailing: const Icon(Icons.arrow_right), onTap: () { Navigator.of(sheetCtx).pop(); - SharePlus.instance.share(ShareParams( - sharePositionOrigin: SharePositionOrigin.get(sheetCtx), - subject: 'App Teilen', - text: 'Hol dir die für das Marianum maßgeschneiderte App:' - '\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client ' - '\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 ' - '\n\nViel Spaß!', - )); + SharePlus.instance.share( + ShareParams( + sharePositionOrigin: SharePositionOrigin.get(sheetCtx), + subject: 'App Teilen', + text: + 'Hol dir die für das Marianum maßgeschneiderte App:' + '\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client ' + '\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 ' + '\n\nViel Spaß!', + ), + ); }, ), ], diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart index e89ee2a..889eeca 100644 --- a/lib/view/pages/overhang.dart +++ b/lib/view/pages/overhang.dart @@ -1,4 +1,3 @@ - import 'dart:io'; import 'package:flutter/material.dart'; @@ -24,67 +23,81 @@ class _OverhangState extends State { appBar: AppBar( title: const Text('Mehr'), actions: [ - IconButton(onPressed: () => AppRoutes.openSettings(context), icon: const Icon(Icons.settings)), + IconButton( + onPressed: () => AppRoutes.openSettings(context), + icon: const Icon(Icons.settings), + ), ], ), body: _overhang(), ); Widget _overhang() => ListView( - children: [ - ...AppModule.getOverhangModules(context).map((e) => e.toListTile(context)), + children: [ + ...AppModule.getOverhangModules( + context, + ).map((e) => e.toListTile(context)), - const Divider(), + const Divider(), - ListTile( - leading: const Icon(Icons.share_outlined), - title: const Text('Teile die App'), - subtitle: const Text('Mit Freunden und deiner Klasse teilen'), + ListTile( + leading: const Icon(Icons.share_outlined), + title: const Text('Teile die App'), + subtitle: const Text('Mit Freunden und deiner Klasse teilen'), + trailing: const Icon(Icons.arrow_right), + onTap: () async { + final result = await showSelectShareTypeSheet(context); + if (!mounted || result != ShareTargetType.qr) return; + if (context.mounted) AppRoutes.openQrShare(context); + }, + ), + FutureBuilder( + future: InAppReview.instance.isAvailable(), + builder: (context, snapshot) { + if (!snapshot.hasData) return const SizedBox.shrink(); + + String? getPlatformStoreName() { + if (Platform.isAndroid) return 'Play store'; + if (Platform.isIOS) return 'App store'; + return null; + } + + return ListTile( + leading: const CenteredLeading(Icon(Icons.star_rate_outlined)), + title: const Text('App bewerten'), + subtitle: getPlatformStoreName().wrapNullable( + (data) => Text('Im $data'), + ), trailing: const Icon(Icons.arrow_right), - onTap: () async { - final result = await showSelectShareTypeSheet(context); - if (!mounted || result != ShareTargetType.qr) return; - if (context.mounted) AppRoutes.openQrShare(context); + onTap: () { + InAppReview.instance + .openStoreListing(appStoreId: '6458789560') + .then( + (value) { + if (!context.mounted) return; + InfoDialog.show(context, 'Vielen Dank!'); + }, + onError: (error) { + if (!context.mounted) return; + InfoDialog.show( + context, + error.toString(), + copyable: true, + title: 'Fehler', + ); + }, + ); }, - ), - FutureBuilder( - future: InAppReview.instance.isAvailable(), - builder: (context, snapshot) { - if(!snapshot.hasData) return const SizedBox.shrink(); - - String? getPlatformStoreName() { - if(Platform.isAndroid) return 'Play store'; - if(Platform.isIOS) return 'App store'; - return null; - } - - return ListTile( - leading: const CenteredLeading(Icon(Icons.star_rate_outlined)), - title: const Text('App bewerten'), - subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')), - trailing: const Icon(Icons.arrow_right), - onTap: () { - InAppReview.instance.openStoreListing(appStoreId: '6458789560').then( - (value) { - if (!context.mounted) return; - InfoDialog.show(context, 'Vielen Dank!'); - }, - onError: (error) { - if (!context.mounted) return; - InfoDialog.show(context, error.toString(), copyable: true, title: 'Fehler'); - }, - ); - }, - ); - }, - ), - ListTile( - leading: const CenteredLeading(Icon(Icons.feedback_outlined)), - title: const Text('Du hast eine Idee?'), - subtitle: const Text('Fehler und Verbessungsvorschläge'), - trailing: const Icon(Icons.arrow_right), - onTap: () => AppRoutes.openFeedback(context), - ), - ], - ); + ); + }, + ), + ListTile( + leading: const CenteredLeading(Icon(Icons.feedback_outlined)), + title: const Text('Du hast eine Idee?'), + subtitle: const Text('Fehler und Verbessungsvorschläge'), + trailing: const Icon(Icons.arrow_right), + onTap: () => AppRoutes.openFeedback(context), + ), + ], + ); } diff --git a/lib/view/pages/settings/data/default_settings.dart b/lib/view/pages/settings/data/default_settings.dart index d893b77..84d888b 100644 --- a/lib/view/pages/settings/data/default_settings.dart +++ b/lib/view/pages/settings/data/default_settings.dart @@ -17,53 +17,51 @@ import '../../files/data/sort_options.dart'; class DefaultSettings { static Settings get() => Settings( - appTheme: ThemeMode.system, - devToolsEnabled: false, - modulesSettings: ModulesSettings( - moduleOrder: [ - Modules.timetable, - Modules.talk, - Modules.files, - Modules.marianumMessage, - Modules.roomPlan, - Modules.gradeAveragesCalculator, - Modules.holidays, - Modules.marianumDates, - ], - hiddenModules: [], - autoFillBottomBar: true, - fixedBottomBarSlots: 3, - ), - timetableSettings: TimetableSettings( - connectDoubleLessons: true, - timetableNameMode: TimetableNameMode.name, - ), - talkSettings: TalkSettings( - sortFavoritesToTop: true, - sortUnreadToTop: false, - drafts: {}, - draftReplies: {}, - ), - fileSettings: FileSettings( - sortFoldersToTop: true, - ascending: true, - sortBy: SortOption.name - ), - holidaysSettings: HolidaysSettings( - dismissedDisclaimer: false, - showPastEvents: false, - ), - fileViewSettings: FileViewSettings( - alwaysOpenExternally: Platform.isIOS, - ), - notificationSettings: NotificationSettings( - askUsageDismissed: false, - enabled: false, - ), - devToolsSettings: DevToolsSettings( - checkerboardOffscreenLayers: false, - checkerboardRasterCacheImages: false, - showPerformanceOverlay: false, - ), - ); + appTheme: ThemeMode.system, + devToolsEnabled: false, + modulesSettings: ModulesSettings( + moduleOrder: [ + Modules.timetable, + Modules.talk, + Modules.files, + Modules.marianumMessage, + Modules.roomPlan, + Modules.gradeAveragesCalculator, + Modules.holidays, + Modules.marianumDates, + ], + hiddenModules: [], + autoFillBottomBar: true, + fixedBottomBarSlots: 3, + ), + timetableSettings: TimetableSettings( + connectDoubleLessons: true, + timetableNameMode: TimetableNameMode.name, + ), + talkSettings: TalkSettings( + sortFavoritesToTop: true, + sortUnreadToTop: false, + drafts: {}, + draftReplies: {}, + ), + fileSettings: FileSettings( + sortFoldersToTop: true, + ascending: true, + sortBy: SortOption.name, + ), + holidaysSettings: HolidaysSettings( + dismissedDisclaimer: false, + showPastEvents: false, + ), + fileViewSettings: FileViewSettings(alwaysOpenExternally: Platform.isIOS), + notificationSettings: NotificationSettings( + askUsageDismissed: false, + enabled: false, + ), + devToolsSettings: DevToolsSettings( + checkerboardOffscreenLayers: false, + checkerboardRasterCacheImages: false, + showPerformanceOverlay: false, + ), + ); } diff --git a/lib/view/pages/settings/modules_settings_page.dart b/lib/view/pages/settings/modules_settings_page.dart index c414d20..4a84b86 100644 --- a/lib/view/pages/settings/modules_settings_page.dart +++ b/lib/view/pages/settings/modules_settings_page.dart @@ -14,103 +14,144 @@ class ModuleSortBody extends StatelessWidget { const ModuleSortBody({super.key}); @override - Widget build(BuildContext context) => BlocBuilder(builder: (context, _) { - final settings = context.read(); - final modulesSettings = settings.val().modulesSettings; + Widget build( + BuildContext context, + ) => BlocBuilder( + builder: (context, _) { + final settings = context.read(); + final modulesSettings = settings.val().modulesSettings; - void changeVisibility(Modules module) { - var hidden = settings.val(write: true).modulesSettings.hiddenModules; - if (hidden.contains(module)) { - hidden.remove(module); - } else if (hidden.length < 3) { - hidden.add(module); + void changeVisibility(Modules module) { + var hidden = settings.val(write: true).modulesSettings.hiddenModules; + if (hidden.contains(module)) { + hidden.remove(module); + } else if (hidden.length < 3) { + hidden.add(module); + } } - } - return ReorderableListView( - header: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), - child: Text( - 'Halte und ziehe einen Eintrag, um ihn zu verschieben.\nEs können 3 Bereiche ausgeblendet werden.', - textAlign: TextAlign.center, - ), - ), - SwitchListTile( - title: const Text('Modulleiste automatisch füllen'), - subtitle: const Text('Auf größeren Bildschirmen werden mehr Module direkt angezeigt'), - value: modulesSettings.autoFillBottomBar, - onChanged: (value) => settings.val(write: true).modulesSettings.autoFillBottomBar = value, - ), - if (!modulesSettings.autoFillBottomBar) - ListTile( - title: const Text('Anzahl Slots in der Modulleiste'), - subtitle: Text('${modulesSettings.fixedBottomBarSlots} Module (zzgl. „Mehr")'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.remove_circle_outline), - onPressed: modulesSettings.fixedBottomBarSlots > AppModule.minBottomBarSlots - ? () => settings.val(write: true).modulesSettings.fixedBottomBarSlots -= 1 - : null, - ), - Text('${modulesSettings.fixedBottomBarSlots}'), - IconButton( - icon: const Icon(Icons.add_circle_outline), - onPressed: modulesSettings.fixedBottomBarSlots < AppModule.maxBottomBarSlots - ? () => settings.val(write: true).modulesSettings.fixedBottomBarSlots += 1 - : null, - ), - ], + return ReorderableListView( + header: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Text( + 'Halte und ziehe einen Eintrag, um ihn zu verschieben.\nEs können 3 Bereiche ausgeblendet werden.', + textAlign: TextAlign.center, ), ), - const Divider(), - ], - ), - children: AppModule.modules(context, showFiltered: true) - .map((key, value) => MapEntry(key, value.toListTile( - context, - key: Key(key.name), - isReorder: true, - onVisibleChange: () => changeVisibility(key), - isVisible: !settings.val().modulesSettings.hiddenModules.contains(key), - ))) - .values - .toList(), - onReorder: (oldIndex, newIndex) { - if (newIndex > oldIndex) newIndex -= 1; + SwitchListTile( + title: const Text('Modulleiste automatisch füllen'), + subtitle: const Text( + 'Auf größeren Bildschirmen werden mehr Module direkt angezeigt', + ), + value: modulesSettings.autoFillBottomBar, + onChanged: (value) => + settings.val(write: true).modulesSettings.autoFillBottomBar = + value, + ), + if (!modulesSettings.autoFillBottomBar) + ListTile( + title: const Text('Anzahl Slots in der Modulleiste'), + subtitle: Text( + '${modulesSettings.fixedBottomBarSlots} Module (zzgl. „Mehr")', + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove_circle_outline), + onPressed: + modulesSettings.fixedBottomBarSlots > + AppModule.minBottomBarSlots + ? () => + settings + .val(write: true) + .modulesSettings + .fixedBottomBarSlots -= + 1 + : null, + ), + Text('${modulesSettings.fixedBottomBarSlots}'), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: + modulesSettings.fixedBottomBarSlots < + AppModule.maxBottomBarSlots + ? () => + settings + .val(write: true) + .modulesSettings + .fixedBottomBarSlots += + 1 + : null, + ), + ], + ), + ), + const Divider(), + ], + ), + children: AppModule.modules(context, showFiltered: true) + .map( + (key, value) => MapEntry( + key, + value.toListTile( + context, + key: Key(key.name), + isReorder: true, + onVisibleChange: () => changeVisibility(key), + isVisible: !settings + .val() + .modulesSettings + .hiddenModules + .contains(key), + ), + ), + ) + .values + .toList(), + onReorder: (oldIndex, newIndex) { + if (newIndex > oldIndex) newIndex -= 1; - var order = settings.val().modulesSettings.moduleOrder.toList(); - final movedModule = order.removeAt(oldIndex); - order.insert(newIndex, movedModule); - settings.val(write: true).modulesSettings.moduleOrder = order; - }, - ); - }); + var order = settings.val().modulesSettings.moduleOrder.toList(); + final movedModule = order.removeAt(oldIndex); + order.insert(newIndex, movedModule); + settings.val(write: true).modulesSettings.moduleOrder = order; + }, + ); + }, + ); } class ModulesSettingsPage extends StatelessWidget { const ModulesSettingsPage({super.key}); @override - Widget build(BuildContext context) => BlocBuilder(builder: (context, _) { - final settings = context.read(); - final isModified = settings.val().modulesSettings.toJson().toString() != DefaultSettings.get().modulesSettings.toJson().toString(); - return Scaffold( - appBar: AppBar( - title: const Text('Module'), - actions: [ - IconButton( - tooltip: 'Auf Standard zurücksetzen', - onPressed: isModified ? () => settings.val(write: true).modulesSettings = DefaultSettings.get().modulesSettings : null, - icon: const Icon(Icons.undo_outlined), - ), - ], - ), - body: const ModuleSortBody(), - ); - }); + Widget build(BuildContext context) => + BlocBuilder( + builder: (context, _) { + final settings = context.read(); + final isModified = + settings.val().modulesSettings.toJson().toString() != + DefaultSettings.get().modulesSettings.toJson().toString(); + return Scaffold( + appBar: AppBar( + title: const Text('Module'), + actions: [ + IconButton( + tooltip: 'Auf Standard zurücksetzen', + onPressed: isModified + ? () => settings.val(write: true).modulesSettings = + DefaultSettings.get().modulesSettings + : null, + icon: const Icon(Icons.undo_outlined), + ), + ], + ), + body: const ModuleSortBody(), + ); + }, + ); } diff --git a/lib/view/pages/settings/sections/about_section.dart b/lib/view/pages/settings/sections/about_section.dart index 4de6616..eeda08b 100644 --- a/lib/view/pages/settings/sections/about_section.dart +++ b/lib/view/pages/settings/sections/about_section.dart @@ -37,14 +37,18 @@ class AboutSection extends StatelessWidget { leading: const CenteredLeading(Icon(Icons.code)), title: const Text('Quellcode MarianumMobile/Client'), subtitle: const Text('GNU GPL v3'), - onTap: () => ConfirmDialog.openBrowser(context, 'https://mhsl.eu/gitea/MarianumMobile/Client'), + onTap: () => ConfirmDialog.openBrowser( + context, + 'https://mhsl.eu/gitea/MarianumMobile/Client', + ), ), ListTile( leading: const Icon(Icons.developer_mode_outlined), title: const Text('Entwicklermodus'), trailing: Checkbox( value: settings.val().devToolsEnabled, - onChanged: (state) => _toggleDeveloperMode(context, settings, state), + onChanged: (state) => + _toggleDeveloperMode(context, settings, state), ), ), Visibility( @@ -62,8 +66,10 @@ class AboutSection extends StatelessWidget { context: context, applicationIcon: const Icon(Icons.apps), applicationName: 'MarianumMobile', - applicationVersion: '${appInfo.appName}\n\nPackage: ${appInfo.packageName}\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}', - applicationLegalese: 'Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n' + applicationVersion: + '${appInfo.appName}\n\nPackage: ${appInfo.packageName}\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}', + applicationLegalese: + 'Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n' 'Keinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!\n\n' "${kReleaseMode ? "Production" : "Development"} build\n" 'Marianum Fulda 2023-${Jiffy.now().year}\nElias Müller', @@ -71,49 +77,58 @@ class AboutSection extends StatelessWidget { } void _showPrivacyDialog(BuildContext context) => showDetailsBottomSheet( - context, - children: (sheetCtx) => [ - ListTile( - leading: const CenteredLeading(Icon(Icons.school_outlined)), - title: const Text('Infos zum Marianum Fulda'), - subtitle: const Text('Für Talk-Chats und Dateien'), - trailing: const Icon(Icons.arrow_right), - onTap: () => PrivacyInfo( - providerText: 'Marianum', - imprintUrl: 'https://www.marianum-fulda.de/impressum', - privacyUrl: 'https://www.marianum-fulda.de/datenschutz', - ).showPopup(sheetCtx), - ), - ListTile( - leading: const CenteredLeading(Icon(Icons.date_range_outlined)), - title: const Text('Infos zu Web-/ Untis'), - subtitle: const Text('Für den Stundenplan'), - trailing: const Icon(Icons.arrow_right), - onTap: () => PrivacyInfo( - providerText: 'Untis', - imprintUrl: 'https://www.untis.at/impressum', - privacyUrl: 'https://www.untis.at/datenschutz-wu-apps', - ).showPopup(sheetCtx), - ), - ListTile( - leading: const CenteredLeading(Icon(Icons.send_time_extension_outlined)), - title: const Text('Infos zu mhsl'), - subtitle: const Text('Für Countdowns, Marianum Message und mehr'), - trailing: const Icon(Icons.arrow_right), - onTap: () => PrivacyInfo( - providerText: 'mhsl', - imprintUrl: 'https://mhsl.eu/id.html', - privacyUrl: 'https://mhsl.eu/datenschutz.html', - ).showPopup(sheetCtx), - ), - ], - ); + context, + children: (sheetCtx) => [ + ListTile( + leading: const CenteredLeading(Icon(Icons.school_outlined)), + title: const Text('Infos zum Marianum Fulda'), + subtitle: const Text('Für Talk-Chats und Dateien'), + trailing: const Icon(Icons.arrow_right), + onTap: () => PrivacyInfo( + providerText: 'Marianum', + imprintUrl: 'https://www.marianum-fulda.de/impressum', + privacyUrl: 'https://www.marianum-fulda.de/datenschutz', + ).showPopup(sheetCtx), + ), + ListTile( + leading: const CenteredLeading(Icon(Icons.date_range_outlined)), + title: const Text('Infos zu Web-/ Untis'), + subtitle: const Text('Für den Stundenplan'), + trailing: const Icon(Icons.arrow_right), + onTap: () => PrivacyInfo( + providerText: 'Untis', + imprintUrl: 'https://www.untis.at/impressum', + privacyUrl: 'https://www.untis.at/datenschutz-wu-apps', + ).showPopup(sheetCtx), + ), + ListTile( + leading: const CenteredLeading( + Icon(Icons.send_time_extension_outlined), + ), + title: const Text('Infos zu mhsl'), + subtitle: const Text('Für Countdowns, Marianum Message und mehr'), + trailing: const Icon(Icons.arrow_right), + onTap: () => PrivacyInfo( + providerText: 'mhsl', + imprintUrl: 'https://mhsl.eu/id.html', + privacyUrl: 'https://mhsl.eu/datenschutz.html', + ).showPopup(sheetCtx), + ), + ], + ); - void _toggleDeveloperMode(BuildContext context, SettingsCubit settings, bool? state) { + void _toggleDeveloperMode( + BuildContext context, + SettingsCubit settings, + bool? state, + ) { void apply() { final enabled = state ?? false; settings.val(write: true).devToolsEnabled = enabled; - if (!enabled) settings.val(write: true).devToolsSettings = DefaultSettings.get().devToolsSettings; + if (!enabled) { + settings.val(write: true).devToolsSettings = + DefaultSettings.get().devToolsSettings; + } } if (!state!) { @@ -123,7 +138,8 @@ class AboutSection extends StatelessWidget { ConfirmDialog( title: 'Entwicklermodus', - content: 'Die Entwickleransicht bietet erweiterte Funktionen, die für den üblichen Gebrauch nicht benötigt werden.\n\n' + content: + 'Die Entwickleransicht bietet erweiterte Funktionen, die für den üblichen Gebrauch nicht benötigt werden.\n\n' 'Die Verwendung der Tools kann darüber hinaus bei falscher Verwendung zu Fehlern führen.\n\n' 'Aktivieren auf eigene Verantwortung.', confirmButton: 'Ja, ich verstehe das Risiko', diff --git a/lib/view/pages/settings/sections/account_section.dart b/lib/view/pages/settings/sections/account_section.dart index a6190cc..f7b37ea 100644 --- a/lib/view/pages/settings/sections/account_section.dart +++ b/lib/view/pages/settings/sections/account_section.dart @@ -12,11 +12,11 @@ class AccountSection extends StatelessWidget { @override Widget build(BuildContext context) => ListTile( - leading: const CenteredLeading(Icon(Icons.logout_outlined)), - title: const Text('Konto abmelden'), - subtitle: Text('Angemeldet als ${AccountData().getUsername()}'), - onTap: () => _showLogoutDialog(context), - ); + leading: const CenteredLeading(Icon(Icons.logout_outlined)), + title: const Text('Konto abmelden'), + subtitle: Text('Angemeldet als ${AccountData().getUsername()}'), + onTap: () => _showLogoutDialog(context), + ); Future _showLogoutDialog(BuildContext context) async { // Sequential logout flow: dialog wipes secure storage, dialog closes diff --git a/lib/view/pages/settings/sections/appearance_section.dart b/lib/view/pages/settings/sections/appearance_section.dart index 003441a..851c25f 100644 --- a/lib/view/pages/settings/sections/appearance_section.dart +++ b/lib/view/pages/settings/sections/appearance_section.dart @@ -17,17 +17,19 @@ class AppearanceSection extends StatelessWidget { value: settings.val().appTheme, icon: const Icon(Icons.arrow_drop_down), items: ThemeMode.values - .map((e) => DropdownMenuItem( - value: e, - enabled: e != settings.val().appTheme, - child: Row( - children: [ - Icon(AppTheme.getDisplayOptions(e).icon), - const SizedBox(width: 10), - Text(AppTheme.getDisplayOptions(e).displayName), - ], - ), - )) + .map( + (e) => DropdownMenuItem( + value: e, + enabled: e != settings.val().appTheme, + child: Row( + children: [ + Icon(AppTheme.getDisplayOptions(e).icon), + const SizedBox(width: 10), + Text(AppTheme.getDisplayOptions(e).displayName), + ], + ), + ), + ) .toList(), onChanged: (e) => settings.val(write: true).appTheme = e!, ), diff --git a/lib/view/pages/settings/sections/dev_tools_section.dart b/lib/view/pages/settings/sections/dev_tools_section.dart index 82d5019..f8978ab 100644 --- a/lib/view/pages/settings/sections/dev_tools_section.dart +++ b/lib/view/pages/settings/sections/dev_tools_section.dart @@ -1,4 +1,3 @@ - import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -24,117 +23,150 @@ class DevToolsSection extends StatefulWidget { class _DevToolsSectionState extends State { @override Widget build(BuildContext context) => Column( - children: [ - ListTile( - leading: const CenteredLeading(Icon(Icons.speed_outlined)), - title: const Text('Performance overlays'), - trailing: const Icon(Icons.arrow_right), - onTap: () { - showDetailsBottomSheet( - context, - children: (sheetCtx) => [ - BlocBuilder( - bloc: widget.settings, - builder: (_, _) { - final dev = widget.settings.val().devToolsSettings; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.auto_graph_outlined), - title: const Text('Performance graph'), - trailing: Checkbox( - value: dev.showPerformanceOverlay, - onChanged: (e) => widget.settings.val(write: true).devToolsSettings.showPerformanceOverlay = e!, - ), + children: [ + ListTile( + leading: const CenteredLeading(Icon(Icons.speed_outlined)), + title: const Text('Performance overlays'), + trailing: const Icon(Icons.arrow_right), + onTap: () { + showDetailsBottomSheet( + context, + children: (sheetCtx) => [ + BlocBuilder( + bloc: widget.settings, + builder: (_, _) { + final dev = widget.settings.val().devToolsSettings; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.auto_graph_outlined), + title: const Text('Performance graph'), + trailing: Checkbox( + value: dev.showPerformanceOverlay, + onChanged: (e) => + widget.settings + .val(write: true) + .devToolsSettings + .showPerformanceOverlay = + e!, ), - ListTile( - leading: const Icon(Icons.screen_search_desktop_outlined), - title: const Text('Indicate offscreen layers'), - trailing: Checkbox( - value: dev.checkerboardOffscreenLayers, - onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardOffscreenLayers = e!, - ), + ), + ListTile( + leading: const Icon( + Icons.screen_search_desktop_outlined, ), - ListTile( - leading: const Icon(Icons.imagesearch_roller_outlined), - title: const Text('Indicate raster cache images'), - trailing: Checkbox( - value: dev.checkerboardRasterCacheImages, - onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardRasterCacheImages = e!, - ), + title: const Text('Indicate offscreen layers'), + trailing: Checkbox( + value: dev.checkerboardOffscreenLayers, + onChanged: (e) => + widget.settings + .val(write: true) + .devToolsSettings + .checkerboardOffscreenLayers = + e!, ), - ], - ); - }, - ), - ], - ); - }, + ), + ListTile( + leading: const Icon(Icons.imagesearch_roller_outlined), + title: const Text('Indicate raster cache images'), + trailing: Checkbox( + value: dev.checkerboardRasterCacheImages, + onChanged: (e) => + widget.settings + .val(write: true) + .devToolsSettings + .checkerboardRasterCacheImages = + e!, + ), + ), + ], + ); + }, + ), + ], + ); + }, + ), + ListTile( + leading: const CenteredLeading(Icon(Icons.image_outlined)), + title: const Text('Thumb-storage'), + subtitle: Text( + 'etwa ${filesize(PaintingBinding.instance.imageCache.currentSizeBytes)}\nLange tippen um zu löschen', ), - ListTile( - leading: const CenteredLeading(Icon(Icons.image_outlined)), - title: const Text('Thumb-storage'), - subtitle: Text('etwa ${filesize(PaintingBinding.instance.imageCache.currentSizeBytes)}\nLange tippen um zu löschen'), - onLongPress: () { - ConfirmDialog( - title: 'Thumbs cache löschen', - content: 'Alle zwischengespeicherten Bilder werden gelöscht.', - confirmButton: 'Unwiederruflich löschen', - onConfirm: () => PaintingBinding.instance.imageCache.clear(), - ).asDialog(context); - }, + onLongPress: () { + ConfirmDialog( + title: 'Thumbs cache löschen', + content: 'Alle zwischengespeicherten Bilder werden gelöscht.', + confirmButton: 'Unwiederruflich löschen', + onConfirm: () => PaintingBinding.instance.imageCache.clear(), + ).asDialog(context); + }, + ), + ListTile( + leading: const CenteredLeading( + Icon(Icons.settings_applications_outlined), ), - ListTile( - leading: const CenteredLeading(Icon(Icons.settings_applications_outlined)), - title: const Text('Settings-storage JSON dump'), - subtitle: Text('etwa ${filesize(widget.settings.val().toJson().toString().length * 8)}\nLange tippen um zu löschen'), - onTap: () { - JsonViewer.asDialog(context, widget.settings.val().toJson()); - }, - onLongPress: () { - ConfirmDialog( - title: 'Einstellungen löschen', - content: 'Alle Einstellungen gehen verloren! Accountdaten sowie App-Daten sind nicht betroffen.', - confirmButton: 'Unwiederruflich Löschen', - onConfirm: () { - context.read().reset(); - }, - ).asDialog(context); - }, - trailing: const Icon(Icons.arrow_right), + title: const Text('Settings-storage JSON dump'), + subtitle: Text( + 'etwa ${filesize(widget.settings.val().toJson().toString().length * 8)}\nLange tippen um zu löschen', ), - ListTile( - leading: const CenteredLeading(Icon(Icons.data_object)), - title: const Text('Cache-storage JSON dump'), - subtitle: FutureBuilder( - future: const CacheView().totalSize(), - builder: (context, snapshot) => Text("etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen"), + onTap: () { + JsonViewer.asDialog(context, widget.settings.val().toJson()); + }, + onLongPress: () { + ConfirmDialog( + title: 'Einstellungen löschen', + content: + 'Alle Einstellungen gehen verloren! Accountdaten sowie App-Daten sind nicht betroffen.', + confirmButton: 'Unwiederruflich Löschen', + onConfirm: () { + context.read().reset(); + }, + ).asDialog(context); + }, + trailing: const Icon(Icons.arrow_right), + ), + ListTile( + leading: const CenteredLeading(Icon(Icons.data_object)), + title: const Text('Cache-storage JSON dump'), + subtitle: FutureBuilder( + future: const CacheView().totalSize(), + builder: (context, snapshot) => Text( + "etwa ${snapshot.hasError + ? "?" + : snapshot.hasData + ? filesize(snapshot.data) + : "..."}\nLange tippen um zu löschen", ), - onTap: () => AppRoutes.openCacheView(context), - onLongPress: () { - ConfirmDialog( - title: 'App-Cache löschen', - content: 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut', - confirmButton: 'Unwiederruflich löschen', - onConfirm: () => const CacheView().clear().then((value) => setState((){})), - ).asDialog(context); - }, - trailing: const Icon(Icons.arrow_right), ), - ListTile( - leading: const CenteredLeading(Icon(Icons.data_object)), - title: const Text('BLOC-storage state cache'), - subtitle: const Text('Lange tippen um zu löschen'), - onLongPress: () { - ConfirmDialog( - title: 'BLOC-Cache löschen', - content: 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut', - confirmButton: 'Unwiederruflich löschen', - onConfirm: () => HydratedBloc.storage.clear(), - ).asDialog(context); - }, - ), - ], - ); + onTap: () => AppRoutes.openCacheView(context), + onLongPress: () { + ConfirmDialog( + title: 'App-Cache löschen', + content: + 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut', + confirmButton: 'Unwiederruflich löschen', + onConfirm: () => + const CacheView().clear().then((value) => setState(() {})), + ).asDialog(context); + }, + trailing: const Icon(Icons.arrow_right), + ), + ListTile( + leading: const CenteredLeading(Icon(Icons.data_object)), + title: const Text('BLOC-storage state cache'), + subtitle: const Text('Lange tippen um zu löschen'), + onLongPress: () { + ConfirmDialog( + title: 'BLOC-Cache löschen', + content: + 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut', + confirmButton: 'Unwiederruflich löschen', + onConfirm: () => HydratedBloc.storage.clear(), + ).asDialog(context); + }, + ), + ], + ); } diff --git a/lib/view/pages/settings/sections/files_section.dart b/lib/view/pages/settings/sections/files_section.dart index 982a464..1c4a7d1 100644 --- a/lib/view/pages/settings/sections/files_section.dart +++ b/lib/view/pages/settings/sections/files_section.dart @@ -16,7 +16,8 @@ class FilesSection extends StatelessWidget { title: const Text('Ordner in Dateien nach oben sortieren'), trailing: Checkbox( value: settings.val().fileSettings.sortFoldersToTop, - onChanged: (e) => settings.val(write: true).fileSettings.sortFoldersToTop = e!, + onChanged: (e) => + settings.val(write: true).fileSettings.sortFoldersToTop = e!, ), ), ListTile( @@ -24,7 +25,12 @@ class FilesSection extends StatelessWidget { title: const Text('Dateien immer mit Systemdialog öffnen'), trailing: Checkbox( value: settings.val().fileViewSettings.alwaysOpenExternally, - onChanged: (e) => settings.val(write: true).fileViewSettings.alwaysOpenExternally = e!, + onChanged: (e) => + settings + .val(write: true) + .fileViewSettings + .alwaysOpenExternally = + e!, ), ), ], diff --git a/lib/view/pages/settings/sections/modules_section.dart b/lib/view/pages/settings/sections/modules_section.dart index b3fe499..94eb221 100644 --- a/lib/view/pages/settings/sections/modules_section.dart +++ b/lib/view/pages/settings/sections/modules_section.dart @@ -7,10 +7,10 @@ class ModulesSection extends StatelessWidget { @override Widget build(BuildContext context) => ListTile( - leading: const Icon(Icons.apps_outlined), - title: const Text('Module'), - subtitle: const Text('Reihenfolge, Sichtbarkeit und Modulleiste anpassen'), - trailing: const Icon(Icons.arrow_right), - onTap: () => AppRoutes.openModulesSettings(context), - ); + leading: const Icon(Icons.apps_outlined), + title: const Text('Module'), + subtitle: const Text('Reihenfolge, Sichtbarkeit und Modulleiste anpassen'), + trailing: const Icon(Icons.arrow_right), + onTap: () => AppRoutes.openModulesSettings(context), + ); } diff --git a/lib/view/pages/settings/sections/talk_section.dart b/lib/view/pages/settings/sections/talk_section.dart index 0b808fb..575f4cc 100644 --- a/lib/view/pages/settings/sections/talk_section.dart +++ b/lib/view/pages/settings/sections/talk_section.dart @@ -21,7 +21,8 @@ class TalkSection extends StatelessWidget { title: const Text('Favoriten im Talk nach oben sortieren'), trailing: Checkbox( value: talkSettings.sortFavoritesToTop, - onChanged: (e) => settings.val(write: true).talkSettings.sortFavoritesToTop = e!, + onChanged: (e) => + settings.val(write: true).talkSettings.sortFavoritesToTop = e!, ), ), ListTile( @@ -29,11 +30,14 @@ class TalkSection extends StatelessWidget { title: const Text('Ungelesene Chats nach oben sortieren'), trailing: Checkbox( value: talkSettings.sortUnreadToTop, - onChanged: (e) => settings.val(write: true).talkSettings.sortUnreadToTop = e!, + onChanged: (e) => + settings.val(write: true).talkSettings.sortUnreadToTop = e!, ), ), ListTile( - leading: const CenteredLeading(Icon(Icons.notifications_active_outlined)), + leading: const CenteredLeading( + Icon(Icons.notifications_active_outlined), + ), title: const Text('Push-Benachrichtigungen aktivieren'), subtitle: const Text('Lange tippen für mehr Informationen'), trailing: Checkbox( @@ -53,12 +57,12 @@ class TalkSection extends StatelessWidget { } void _showInfoDialog(BuildContext context) => InfoDialog.show( - context, - "Aufgrund technischer Limitationen müssen Push-Nachrichten über einen externen Server - hier 'mhsl.eu' (Author dieser App) - erfolgen.\n\n" - 'Wenn Push aktiviert wird, werden deine Zugangsdaten und ein Token verschlüsselt an den Betreiber gesendet und von ihm unverschlüsselt gespeichert.\n\n' - 'Der extene Server verwendet die Zugangsdaten um sich maschinell in Nextcloud Talk anzumelden und via Websockets auf neue Nachrichten zu warten.\n\n' - 'Wenn eine neue Nachricht eintrifft wird dein Telefon via FBC-Messaging (Google Firebase Push) vom externen Server benachrichtigt.\n\n' - 'Behalte im Hinterkopf, dass deine Zugangsdaten auf einem externen Server gespeichert werden und dies trotz bester Absichten ein Sicherheitsrisiko sein kann!', - title: 'Info über Push', - ); + context, + "Aufgrund technischer Limitationen müssen Push-Nachrichten über einen externen Server - hier 'mhsl.eu' (Author dieser App) - erfolgen.\n\n" + 'Wenn Push aktiviert wird, werden deine Zugangsdaten und ein Token verschlüsselt an den Betreiber gesendet und von ihm unverschlüsselt gespeichert.\n\n' + 'Der extene Server verwendet die Zugangsdaten um sich maschinell in Nextcloud Talk anzumelden und via Websockets auf neue Nachrichten zu warten.\n\n' + 'Wenn eine neue Nachricht eintrifft wird dein Telefon via FBC-Messaging (Google Firebase Push) vom externen Server benachrichtigt.\n\n' + 'Behalte im Hinterkopf, dass deine Zugangsdaten auf einem externen Server gespeichert werden und dies trotz bester Absichten ein Sicherheitsrisiko sein kann!', + title: 'Info über Push', + ); } diff --git a/lib/view/pages/settings/sections/timetable_section.dart b/lib/view/pages/settings/sections/timetable_section.dart index 4879c18..044cca0 100644 --- a/lib/view/pages/settings/sections/timetable_section.dart +++ b/lib/view/pages/settings/sections/timetable_section.dart @@ -20,20 +20,25 @@ class TimetableSection extends StatelessWidget { value: timetableSettings.timetableNameMode, icon: const Icon(Icons.arrow_drop_down), items: TimetableNameMode.values - .map((e) => DropdownMenuItem( - value: e, - enabled: e != timetableSettings.timetableNameMode, - child: Row( - children: [ - Icon(TimetableNameModes.getDisplayOptions(e).icon), - const SizedBox(width: 10), - Text(TimetableNameModes.getDisplayOptions(e).displayName), - ], - ), - )) + .map( + (e) => DropdownMenuItem( + value: e, + enabled: e != timetableSettings.timetableNameMode, + child: Row( + children: [ + Icon(TimetableNameModes.getDisplayOptions(e).icon), + const SizedBox(width: 10), + Text( + TimetableNameModes.getDisplayOptions(e).displayName, + ), + ], + ), + ), + ) .toList(), onChanged: (value) => - settings.val(write: true).timetableSettings.timetableNameMode = value!, + settings.val(write: true).timetableSettings.timetableNameMode = + value!, ), ), ListTile( @@ -42,7 +47,11 @@ class TimetableSection extends StatelessWidget { trailing: Checkbox( value: timetableSettings.connectDoubleLessons, onChanged: (e) => - settings.val(write: true).timetableSettings.connectDoubleLessons = e!, + settings + .val(write: true) + .timetableSettings + .connectDoubleLessons = + e!, ), ), ], diff --git a/lib/view/pages/settings/settings.dart b/lib/view/pages/settings/settings.dart index 040d6eb..6c6b970 100644 --- a/lib/view/pages/settings/settings.dart +++ b/lib/view/pages/settings/settings.dart @@ -13,23 +13,23 @@ class Settings extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar(title: const Text('Einstellungen')), - body: ListView( - children: const [ - AccountSection(), - Divider(), - AppearanceSection(), - Divider(), - ModulesSection(), - Divider(), - TimetableSection(), - Divider(), - TalkSection(), - Divider(), - FilesSection(), - Divider(), - AboutSection(), - ], - ), - ); + appBar: AppBar(title: const Text('Einstellungen')), + body: ListView( + children: const [ + AccountSection(), + Divider(), + AppearanceSection(), + Divider(), + ModulesSection(), + Divider(), + TimetableSection(), + Divider(), + TalkSection(), + Divider(), + FilesSection(), + Divider(), + AboutSection(), + ], + ), + ); } diff --git a/lib/view/pages/settings/widgets/privacy_info.dart b/lib/view/pages/settings/widgets/privacy_info.dart index ec96588..8e15c67 100644 --- a/lib/view/pages/settings/widgets/privacy_info.dart +++ b/lib/view/pages/settings/widgets/privacy_info.dart @@ -9,7 +9,11 @@ class PrivacyInfo { String privacyUrl; String imprintUrl; - PrivacyInfo({required this.providerText, required this.imprintUrl, required this.privacyUrl}); + PrivacyInfo({ + required this.providerText, + required this.imprintUrl, + required this.privacyUrl, + }); void showPopup(BuildContext context) { showDetailsBottomSheet( diff --git a/lib/view/pages/talk/chat_list.dart b/lib/view/pages/talk/chat_list.dart index f5bdc85..8939266 100644 --- a/lib/view/pages/talk/chat_list.dart +++ b/lib/view/pages/talk/chat_list.dart @@ -23,7 +23,8 @@ class ChatList extends StatelessWidget { const ChatList({super.key}); @override - Widget build(BuildContext context) => BlocModule>( + Widget build(BuildContext context) => + BlocModule>( create: (_) => ChatListBloc(), child: (context, bloc, _) => const _ChatListView(), ); @@ -83,16 +84,22 @@ class _ChatListViewState extends State<_ChatListView> { void _maybeAskForNotificationPermission() { final notificationSettings = _settings.val().notificationSettings; - if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return; + if (notificationSettings.enabled || + notificationSettings.askUsageDismissed) { + return; + } _settings.val(write: true).notificationSettings.askUsageDismissed = true; ConfirmDialog( icon: Icons.notifications_active_outlined, title: 'Benachrichtigungen aktivieren', - content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.', + content: + 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.', confirmButton: 'Weiter', onConfirm: () { - FirebaseMessaging.instance.requestPermission(provisional: false).then((value) { + FirebaseMessaging.instance.requestPermission(provisional: false).then(( + value, + ) { if (!mounted) return; switch (value.authorizationStatus) { case AuthorizationStatus.authorized: @@ -129,7 +136,10 @@ class _ChatListViewState extends State<_ChatListView> { onPressed: () { final rooms = bloc.state.data?.rooms; if (rooms == null) return; - showSearch(context: context, delegate: SearchChat(rooms.data.toList())); + showSearch( + context: context, + delegate: SearchChat(rooms.data.toList()), + ); }, ), ], @@ -138,11 +148,14 @@ class _ChatListViewState extends State<_ChatListView> { heroTag: 'createChat', backgroundColor: Theme.of(context).primaryColor, onPressed: () { - showSearch(context: context, delegate: JoinChat()).then((username) { + showSearch(context: context, delegate: JoinChat()).then(( + username, + ) { if (username == null || !context.mounted) return; ConfirmDialog( title: 'Chat starten', - content: "Möchtest du einen Chat mit Nutzer '$username' starten?", + content: + "Möchtest du einen Chat mit Nutzer '$username' starten?", confirmButton: 'Chat starten', onConfirmAsync: () => bloc.createDirectChat(username), ).asDialog(context); @@ -155,7 +168,10 @@ class _ChatListViewState extends State<_ChatListView> { final rooms = state.rooms; if (rooms == null) return const SizedBox.shrink(); - final talkSettings = context.watch().val().talkSettings; + final talkSettings = context + .watch() + .val() + .talkSettings; final sorted = rooms.sortBy( lastActivity: true, favoritesToTop: talkSettings.sortFavoritesToTop, @@ -172,7 +188,11 @@ class _ChatListViewState extends State<_ChatListView> { return ListView( padding: EdgeInsets.zero, children: sorted.map((room) { - final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token); + final hasDraft = _settings + .val() + .talkSettings + .drafts + .containsKey(room.token); return ChatTile(data: room, hasDraft: hasDraft); }).toList(), ); diff --git a/lib/view/pages/talk/chat_view.dart b/lib/view/pages/talk/chat_view.dart index 0b8c78c..2c81c48 100644 --- a/lib/view/pages/talk/chat_view.dart +++ b/lib/view/pages/talk/chat_view.dart @@ -20,7 +20,12 @@ class ChatView extends StatefulWidget { final String selfId; final UserAvatar avatar; - const ChatView({super.key, required this.room, required this.selfId, required this.avatar}); + const ChatView({ + super.key, + required this.room, + required this.selfId, + required this.avatar, + }); @override State createState() => _ChatViewState(); @@ -37,46 +42,58 @@ class _ChatViewState extends State { final messages = []; var lastDate = DateTime.now(); for (final element in response.sortByTimestamp()) { - final elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000); + final elementDate = DateTime.fromMillisecondsSinceEpoch( + element.timestamp * 1000, + ); if (element.systemMessage.contains('reaction')) continue; if (element.systemMessage.contains('poll_voted')) continue; - final commonRead = int.parse(response.headers?['x-chat-last-common-read'] ?? '0'); + final commonRead = int.parse( + response.headers?['x-chat-last-common-read'] ?? '0', + ); if (!elementDate.isSameDay(lastDate)) { lastDate = elementDate; - messages.add(ChatBubble( - context: context, - isSender: false, - bubbleData: GetChatResponseObject.getDateDummy(element.timestamp), - chatData: widget.room, - refetch: ({bool renew = false}) => _refresh(), - )); + messages.add( + ChatBubble( + context: context, + isSender: false, + bubbleData: GetChatResponseObject.getDateDummy(element.timestamp), + chatData: widget.room, + refetch: ({bool renew = false}) => _refresh(), + ), + ); } - messages.add(ChatBubble( - context: context, - isSender: element.actorId == widget.selfId && - element.messageType == GetRoomResponseObjectMessageType.comment, - bubbleData: element, - chatData: widget.room, - refetch: ({bool renew = false}) => _refresh(), - isRead: element.id <= commonRead, - selfId: widget.selfId, - )); + messages.add( + ChatBubble( + context: context, + isSender: + element.actorId == widget.selfId && + element.messageType == GetRoomResponseObjectMessageType.comment, + bubbleData: element, + chatData: widget.room, + refetch: ({bool renew = false}) => _refresh(), + isRead: element.id <= commonRead, + selfId: widget.selfId, + ), + ); } if (response.data.length >= 200) { - messages.insert(0, ChatBubble( - context: context, - isSender: false, - bubbleData: GetChatResponseObject.getTextDummy( - 'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. ' - 'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de', + messages.insert( + 0, + ChatBubble( + context: context, + isSender: false, + bubbleData: GetChatResponseObject.getTextDummy( + 'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. ' + 'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de', + ), + chatData: widget.room, + refetch: ({bool renew = false}) => _refresh(), ), - chatData: widget.room, - refetch: ({bool renew = false}) => _refresh(), - )); + ); } return messages; @@ -84,52 +101,62 @@ class _ChatViewState extends State { @override Widget build(BuildContext context) => Scaffold( - backgroundColor: const Color(0xffefeae2), - appBar: ClickableAppBar( - onTap: () => TalkNavigator.pushSplitView(context, ChatInfo(widget.room)), - appBar: AppBar( - title: Row( - children: [ - widget.avatar, - const SizedBox(width: 10), - Expanded( - child: Text(widget.room.displayName, overflow: TextOverflow.ellipsis, maxLines: 1), - ), - ], + backgroundColor: const Color(0xffefeae2), + appBar: ClickableAppBar( + onTap: () => TalkNavigator.pushSplitView(context, ChatInfo(widget.room)), + appBar: AppBar( + title: Row( + children: [ + widget.avatar, + const SizedBox(width: 10), + Expanded( + child: Text( + widget.room.displayName, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), ), - ), + ], ), - body: DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - image: const AssetImage('assets/background/chat.png'), - scale: 1.5, - opacity: 1, - repeat: ImageRepeat.repeat, - invertColors: AppTheme.isDarkMode(context), + ), + ), + body: DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: const AssetImage('assets/background/chat.png'), + scale: 1.5, + opacity: 1, + repeat: ImageRepeat.repeat, + invertColors: AppTheme.isDarkMode(context), + ), + ), + child: Column( + children: [ + Expanded( + child: LoadableStateConsumer( + isReady: (state) => + state.chatResponse != null && + state.currentToken == widget.room.token, + child: (state, _) => ListView( + reverse: true, + controller: _listController, + children: _buildMessages(state.chatResponse!).reversed.toList(), + ), ), ), - child: Column( - children: [ - Expanded( - child: LoadableStateConsumer( - isReady: (state) => - state.chatResponse != null && state.currentToken == widget.room.token, - child: (state, _) => ListView( - reverse: true, - controller: _listController, - children: _buildMessages(state.chatResponse!).reversed.toList(), + ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: TalkNavigator.isSecondaryVisible(context) + ? ChatTextfield(widget.room.token, selfId: widget.selfId) + : SafeArea( + child: ChatTextfield( + widget.room.token, + selfId: widget.selfId, + ), ), - ), - ), - ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: TalkNavigator.isSecondaryVisible(context) - ? ChatTextfield(widget.room.token, selfId: widget.selfId) - : SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId)), - ), - ], ), - ), - ); + ], + ), + ), + ); } diff --git a/lib/view/pages/talk/data/chat_bubble_styles.dart b/lib/view/pages/talk/data/chat_bubble_styles.dart index 6c7627a..c5c5f21 100644 --- a/lib/view/pages/talk/data/chat_bubble_styles.dart +++ b/lib/view/pages/talk/data/chat_bubble_styles.dart @@ -8,7 +8,12 @@ extension ColorExtensions on Color { final invertedR = 1.0 - r; final invertedG = 1.0 - g; final invertedB = 1.0 - b; - return Color.from(alpha: a, red: invertedR, green: invertedG, blue: invertedB); + return Color.from( + alpha: a, + red: invertedR, + green: invertedG, + blue: invertedB, + ); } Color withWhite(int whiteValue) { @@ -23,14 +28,18 @@ class ChatBubbleStyles { ChatBubbleStyles(this.context); BubbleStyle getSystemStyle() => BubbleStyle( - color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white, + color: AppTheme.isDarkMode(context) + ? const Color(0xff182229) + : Colors.white, 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; + var color = AppTheme.isDarkMode(context) + ? const Color(0xff202c33) + : Colors.white; return BubbleStyle( nip: BubbleNip.leftTop, color: seamless ? Colors.transparent : color, @@ -41,7 +50,9 @@ class ChatBubbleStyles { } BubbleStyle getSelfStyle(bool seamless) { - var color = AppTheme.isDarkMode(context) ? const Color(0xff005c4b) : const Color(0xffd3d3d3); + var color = AppTheme.isDarkMode(context) + ? const Color(0xff005c4b) + : const Color(0xffd3d3d3); return BubbleStyle( nip: BubbleNip.rightBottom, color: seamless ? Colors.transparent : color, diff --git a/lib/view/pages/talk/data/chat_message.dart b/lib/view/pages/talk/data/chat_message.dart index 66b5aa1..30ec493 100644 --- a/lib/view/pages/talk/data/chat_message.dart +++ b/lib/view/pages/talk/data/chat_message.dart @@ -1,4 +1,3 @@ - import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; @@ -19,20 +18,19 @@ class ChatMessage { bool get containsFile => file != null; ChatMessage({required this.originalMessage, this.originalData}) { - if(originalData?.containsKey('file') ?? false) { + if (originalData?.containsKey('file') ?? false) { file = originalData?['file']; } - content = RichObjectStringProcessor.parseToString(originalMessage, originalData); + content = RichObjectStringProcessor.parseToString( + originalMessage, + originalData, + ); } Widget getWidget() { + var contentWidget = Linkify(text: content, onOpen: UrlOpener.onOpen); - var contentWidget = Linkify( - text: content, - onOpen: UrlOpener.onOpen, - ); - - if(originalData?['object']?.type == RichObjectStringObjectType.talkPoll) { + if (originalData?['object']?.type == RichObjectStringObjectType.talkPoll) { return ListTile( leading: const Icon(Icons.poll_outlined), title: Text(originalData!['object']!.name), @@ -40,38 +38,49 @@ class ChatMessage { ); } - if(file == null) return contentWidget; + if (file == null) return contentWidget; return Padding( - padding: const EdgeInsets.only(top: 5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CachedNetworkImage( - errorWidget: (context, url, error) => Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.file_open_outlined, size: 35), - const SizedBox(width: 10), - 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) => const Padding(padding: EdgeInsets.all(15), child: SizedBox(width: 50, child: LinearProgressIndicator())), - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - errorListener: (value) {}, - httpHeaders: AccountData().authHeaders(), - imageUrl: 'https://${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=130&y=-1&a=1', + padding: const EdgeInsets.only(top: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + errorWidget: (context, url, error) => Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.file_open_outlined, size: 35), + const SizedBox(width: 10), + Flexible( + child: Text( + file!.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 10), + ], ), - if(originalMessage != '{file}') ...[ - SizedBox(height: 5), - contentWidget - ] + alignment: Alignment.center, + placeholder: (context, url) => const Padding( + padding: EdgeInsets.all(15), + child: SizedBox(width: 50, child: LinearProgressIndicator()), + ), + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + errorListener: (value) {}, + httpHeaders: AccountData().authHeaders(), + imageUrl: + 'https://${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=130&y=-1&a=1', + ), + if (originalMessage != '{file}') ...[ + SizedBox(height: 5), + contentWidget, ], - ) + ], + ), ); } } diff --git a/lib/view/pages/talk/details/chat_info.dart b/lib/view/pages/talk/details/chat_info.dart index 820ee1a..fad96f5 100644 --- a/lib/view/pages/talk/details/chat_info.dart +++ b/lib/view/pages/talk/details/chat_info.dart @@ -28,18 +28,17 @@ class _ChatInfoState extends State { setState(() { participants = data; }); - } + }, ); super.initState(); } @override Widget build(BuildContext context) { - var isGroup = widget.room.type != GetRoomResponseObjectConversationType.oneToOne; + var isGroup = + widget.room.type != GetRoomResponseObjectConversationType.oneToOne; return Scaffold( - appBar: AppBar( - title: Text(widget.room.displayName), - ), + appBar: AppBar(title: Text(widget.room.displayName)), body: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -52,23 +51,34 @@ class _ChatInfoState extends State { size: 80, ), onTap: () { - if(isGroup) return; - TalkNavigator.pushSplitView(context, LargeProfilePictureView(widget.room.name)); + if (isGroup) return; + TalkNavigator.pushSplitView( + context, + LargeProfilePictureView(widget.room.name), + ); }, ), const SizedBox(height: 30), - Text(widget.room.displayName, textAlign: TextAlign.center, style: const TextStyle(fontSize: 30)), - if(!isGroup) Text(widget.room.name), + Text( + widget.room.displayName, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 30), + ), + if (!isGroup) Text(widget.room.name), const SizedBox(height: 10), - if(isGroup) Text(widget.room.description, textAlign: TextAlign.center), + if (isGroup) + Text(widget.room.description, textAlign: TextAlign.center), const SizedBox(height: 30), - if(participants == null) const LoadingSpinner(), - if(participants != null) ...[ + if (participants == null) const LoadingSpinner(), + if (participants != null) ...[ ListTile( leading: const Icon(Icons.supervised_user_circle), title: Text('${participants!.data.length} Mitglieder'), trailing: const Icon(Icons.arrow_right), - onTap: () => TalkNavigator.pushSplitView(context, ParticipantsListView(participants!)), + onTap: () => TalkNavigator.pushSplitView( + context, + ParticipantsListView(participants!), + ), ), ], ], diff --git a/lib/view/pages/talk/details/message_reactions.dart b/lib/view/pages/talk/details/message_reactions.dart index 7f87faf..6a2a807 100644 --- a/lib/view/pages/talk/details/message_reactions.dart +++ b/lib/view/pages/talk/details/message_reactions.dart @@ -13,7 +13,11 @@ import '../../../../widget/user_avatar.dart'; class MessageReactions extends StatefulWidget { final String token; final int messageId; - const MessageReactions({super.key, required this.token, required this.messageId}); + const MessageReactions({ + super.key, + required this.token, + required this.messageId, + }); @override State createState() => _MessageReactionsState(); @@ -25,53 +29,67 @@ class _MessageReactionsState extends State { @override void initState() { super.initState(); - data = GetReactions(chatToken: widget.token, messageId: widget.messageId).run(); + data = GetReactions( + chatToken: widget.token, + messageId: widget.messageId, + ).run(); } @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Reaktionen'), - ), - body: FutureBuilder( - future: data, - builder: (context, snapshot) { - if(snapshot.connectionState == ConnectionState.waiting) return const LoadingSpinner(); - if(snapshot.data!.data.isEmpty) return const PlaceholderView(icon: Icons.search_off_outlined, text: 'Keine Reaktionen gefunden!'); - return ListView( - children: [ - ...snapshot.data!.data.entries.map((entry) => ExpansionTile( - textColor: Theme.of(context).colorScheme.onSurface, - collapsedTextColor: Theme.of(context).colorScheme.onSurface, - iconColor: Theme.of(context).colorScheme.onSurface, - collapsedIconColor: Theme.of(context).colorScheme.onSurface, + appBar: AppBar(title: const Text('Reaktionen')), + body: FutureBuilder( + future: data, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const LoadingSpinner(); + } + if (snapshot.data!.data.isEmpty) { + return const PlaceholderView( + icon: Icons.search_off_outlined, + text: 'Keine Reaktionen gefunden!', + ); + } + return ListView( + children: [ + ...snapshot.data!.data.entries.map( + (entry) => ExpansionTile( + textColor: Theme.of(context).colorScheme.onSurface, + collapsedTextColor: Theme.of(context).colorScheme.onSurface, + iconColor: Theme.of(context).colorScheme.onSurface, + collapsedIconColor: Theme.of(context).colorScheme.onSurface, - subtitle: const Text('Tippe für mehr'), - leading: CenteredLeading(Text(entry.key)), - title: Text('${entry.value.length} mal reagiert'), - children: entry.value.map((e) { - var isSelf = AccountData().getUsername() == e.actorId; - return ListTile( - leading: UserAvatar(id: e.actorId, isGroup: false), - title: Text(e.actorDisplayName), - subtitle: isSelf + subtitle: const Text('Tippe für mehr'), + leading: CenteredLeading(Text(entry.key)), + title: Text('${entry.value.length} mal reagiert'), + children: entry.value.map((e) { + var isSelf = AccountData().getUsername() == e.actorId; + return ListTile( + leading: UserAvatar(id: e.actorId, isGroup: false), + title: Text(e.actorDisplayName), + subtitle: isSelf ? const Text('Du') - : e.actorType == GetReactionsResponseObjectActorType.guests ? const Text('Gast') : null, - trailing: isSelf + : e.actorType == + GetReactionsResponseObjectActorType.guests + ? const Text('Gast') + : null, + trailing: isSelf ? null : Visibility( - visible: kReleaseMode, - child: IconButton( - onPressed: () => UnimplementedDialog.show(context), - icon: const Icon(Icons.textsms_outlined), + visible: kReleaseMode, + child: IconButton( + onPressed: () => + UnimplementedDialog.show(context), + icon: const Icon(Icons.textsms_outlined), + ), ), - ), - ); - }).toList(), - )) - ], - ); - }, - ), - ); + ); + }).toList(), + ), + ), + ], + ); + }, + ), + ); } diff --git a/lib/view/pages/talk/details/participants_list_view.dart b/lib/view/pages/talk/details/participants_list_view.dart index e2ec3d7..b3c4850 100644 --- a/lib/view/pages/talk/details/participants_list_view.dart +++ b/lib/view/pages/talk/details/participants_list_view.dart @@ -10,38 +10,46 @@ class ParticipantsListView extends StatelessWidget { @override Widget build(BuildContext context) { - String lastname(participant) => participant.displayName.toString().split(' ').last; - - final participants = participantsResponse.data - .sorted((a, b) { - final typeComparison = a.participantType.index.compareTo(b.participantType.index); - if (typeComparison != 0) return typeComparison; - return lastname(a).compareTo(lastname(b)); - }); - var groupedParticipants = participants.groupListsBy((participant) => participant.participantType); + String lastname(participant) => + participant.displayName.toString().split(' ').last; + + final participants = participantsResponse.data.sorted((a, b) { + final typeComparison = a.participantType.index.compareTo( + b.participantType.index, + ); + if (typeComparison != 0) return typeComparison; + return lastname(a).compareTo(lastname(b)); + }); + var groupedParticipants = participants.groupListsBy( + (participant) => participant.participantType, + ); return Scaffold( - appBar: AppBar( - title: const Text('Mitglieder'), - ), + appBar: AppBar(title: const Text('Mitglieder')), body: ListView( children: [ - ...groupedParticipants.entries.map((entry) => Column( - children: [ - ListTile( - title: Text(entry.key.prettyName), - titleTextStyle: Theme.of(context).textTheme.titleMedium - ), - ...entry.value.map((participant) => ListTile( - leading: UserAvatar(id: participant.actorId), - title: Text(participant.displayName), - subtitle: participant.statusMessage != null ? Text(participant.statusMessage!) : null, - )), - Divider(), - ], - )) + ...groupedParticipants.entries.map( + (entry) => Column( + children: [ + ListTile( + title: Text(entry.key.prettyName), + titleTextStyle: Theme.of(context).textTheme.titleMedium, + ), + ...entry.value.map( + (participant) => ListTile( + leading: UserAvatar(id: participant.actorId), + title: Text(participant.displayName), + subtitle: participant.statusMessage != null + ? Text(participant.statusMessage!) + : null, + ), + ), + Divider(), + ], + ), + ), ], - ) + ), ); } } diff --git a/lib/view/pages/talk/join_chat.dart b/lib/view/pages/talk/join_chat.dart index 56e99bb..51188f2 100644 --- a/lib/view/pages/talk/join_chat.dart +++ b/lib/view/pages/talk/join_chat.dart @@ -1,4 +1,3 @@ - import 'package:async/async.dart'; import 'package:flutter/material.dart'; @@ -14,10 +13,11 @@ class JoinChat extends SearchDelegate { @override List? buildActions(BuildContext context) => [ - if(future != null && query.isNotEmpty) FutureBuilder( + if (future != null && query.isNotEmpty) + FutureBuilder( future: future!.value, builder: (context, snapshot) { - if(snapshot.connectionState != ConnectionState.done) { + if (snapshot.connectionState != ConnectionState.done) { return const Padding( padding: EdgeInsets.all(10), child: Center(child: AppProgressIndicator.medium()), @@ -26,17 +26,18 @@ class JoinChat extends SearchDelegate { return const SizedBox.shrink(); }, ), - if(query.isNotEmpty) IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)), - ]; + if (query.isNotEmpty) + IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)), + ]; @override Widget? buildLeading(BuildContext context) => null; @override Widget buildResults(BuildContext context) { - if(future != null) future!.cancel(); + if (future != null) future!.cancel(); - if(query.isEmpty) { + if (query.isEmpty) { return const PlaceholderView( text: 'Suche nach benutzern', icon: Icons.person_search_outlined, @@ -47,13 +48,15 @@ class JoinChat extends SearchDelegate { return FutureBuilder( future: future!.value, builder: (context, snapshot) { - if(snapshot.hasData) { + if (snapshot.hasData) { return ListView.builder( itemCount: snapshot.data!.data.length, itemBuilder: (context, index) { var object = snapshot.data!.data[index]; var circleAvatar = CircleAvatar( - foregroundImage: Image.network('https://${EndpointData().nextcloud().full()}/avatar/${object.id}/128').image, + foregroundImage: Image.network( + 'https://${EndpointData().nextcloud().full()}/avatar/${object.id}/128', + ).image, backgroundColor: Theme.of(context).primaryColor, foregroundColor: Colors.white, child: const Icon(Icons.person), @@ -67,9 +70,9 @@ class JoinChat extends SearchDelegate { close(context, object.id); }, ); - } + }, ); - } else if(snapshot.hasError) { + } else if (snapshot.hasError) { return PlaceholderView( icon: Icons.search_off, text: errorToUserMessage(snapshot.error), @@ -83,5 +86,4 @@ class JoinChat extends SearchDelegate { @override Widget buildSuggestions(BuildContext context) => buildResults(context); - } diff --git a/lib/view/pages/talk/search_chat.dart b/lib/view/pages/talk/search_chat.dart index 0a58622..9f36bf7 100644 --- a/lib/view/pages/talk/search_chat.dart +++ b/lib/view/pages/talk/search_chat.dart @@ -10,17 +10,26 @@ class SearchChat extends SearchDelegate { @override List? buildActions(BuildContext context) => [ - if(query.isNotEmpty) IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)), - ]; + if (query.isNotEmpty) + IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)), + ]; @override Widget? buildLeading(BuildContext context) => null; @override Widget buildResults(BuildContext context) { - var items = chats.where( - (e) => e.displayName.toString().toLowerCase().contains(query.toLowerCase()) || e.name.toString().toLowerCase().contains(query.toLowerCase()) - ).toList()..sort((a, b) => b.lastActivity.compareTo(a.lastActivity)); + var items = + chats + .where( + (e) => + e.displayName.toString().toLowerCase().contains( + query.toLowerCase(), + ) || + e.name.toString().toLowerCase().contains(query.toLowerCase()), + ) + .toList() + ..sort((a, b) => b.lastActivity.compareTo(a.lastActivity)); return ListView.builder( itemCount: items.length, itemBuilder: (context, index) { diff --git a/lib/view/pages/talk/talk_navigator.dart b/lib/view/pages/talk/talk_navigator.dart index 18c33ab..a6a7e00 100644 --- a/lib/view/pages/talk/talk_navigator.dart +++ b/lib/view/pages/talk/talk_navigator.dart @@ -1,16 +1,23 @@ - import 'package:flutter/material.dart'; import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; class TalkNavigator { - static bool hasSplitViewState(BuildContext context) => context.findAncestorStateOfType() != null; - static bool isSecondaryVisible(BuildContext context) => hasSplitViewState(context) && SplitView.of(context).isSecondaryVisible; + static bool hasSplitViewState(BuildContext context) => + context.findAncestorStateOfType() != null; + static bool isSecondaryVisible(BuildContext context) => + hasSplitViewState(context) && SplitView.of(context).isSecondaryVisible; - static void pushSplitView(BuildContext context, Widget view, {bool overrideToSingleSubScreen = false}) { - if(isSecondaryVisible(context)) { + static void pushSplitView( + BuildContext context, + Widget view, { + bool overrideToSingleSubScreen = false, + }) { + if (isSecondaryVisible(context)) { var splitView = SplitView.of(context); - overrideToSingleSubScreen ? splitView.setSecondary(view) : splitView.push(view); + overrideToSingleSubScreen + ? splitView.setSecondary(view) + : splitView.push(view); } else { pushScreen(context, screen: view, withNavBar: false); } diff --git a/lib/view/pages/talk/widgets/answer_reference.dart b/lib/view/pages/talk/widgets/answer_reference.dart index 8171d14..6508ae5 100644 --- a/lib/view/pages/talk/widgets/answer_reference.dart +++ b/lib/view/pages/talk/widgets/answer_reference.dart @@ -8,7 +8,12 @@ class AnswerReference extends StatelessWidget { final BuildContext context; final GetChatResponseObject referenceMessage; final String? selfId; - const AnswerReference({required this.context, required this.referenceMessage, required this.selfId, super.key}); + const AnswerReference({ + required this.context, + required this.referenceMessage, + required this.selfId, + super.key, + }); @override Widget build(BuildContext context) { @@ -16,15 +21,25 @@ class AnswerReference extends StatelessWidget { return DecoratedBox( decoration: BoxDecoration( color: referenceMessage.actorId == selfId - ? style.getSelfStyle(false).color!.withGreen(200).withValues(alpha: 0.2) - : style.getRemoteStyle(false).color!.withWhite(200).withValues(alpha: 0.2), + ? style + .getSelfStyle(false) + .color! + .withGreen(200) + .withValues(alpha: 0.2) + : style + .getRemoteStyle(false) + .color! + .withWhite(200) + .withValues(alpha: 0.2), borderRadius: const BorderRadius.all(Radius.circular(5)), - border: Border(left: BorderSide( + border: Border( + left: BorderSide( color: referenceMessage.actorId == selfId ? style.getSelfStyle(false).color!.withGreen(200) : style.getRemoteStyle(false).color!.withWhite(200), - width: 5 - )), + width: 5, + ), + ), ), child: Padding( padding: const EdgeInsets.all(5).add(const EdgeInsets.only(left: 5)), @@ -43,7 +58,10 @@ class AnswerReference extends StatelessWidget { ), ), Text( - RichObjectStringProcessor.parseToString(referenceMessage.message, referenceMessage.messageParameters), + RichObjectStringProcessor.parseToString( + referenceMessage.message, + referenceMessage.messageParameters, + ), maxLines: 2, style: TextStyle( overflow: TextOverflow.ellipsis, diff --git a/lib/view/pages/talk/widgets/bubble.dart b/lib/view/pages/talk/widgets/bubble.dart index bd87a2a..30d1fac 100644 --- a/lib/view/pages/talk/widgets/bubble.dart +++ b/lib/view/pages/talk/widgets/bubble.dart @@ -3,12 +3,17 @@ import 'package:flutter/material.dart'; enum BubbleNip { leftTop, rightBottom, none } class BubbleEdges { - const BubbleEdges.only({this.top = 0, this.bottom = 0, this.left = 0, this.right = 0}); + const BubbleEdges.only({ + this.top = 0, + this.bottom = 0, + this.left = 0, + this.right = 0, + }); const BubbleEdges.all(double value) - : top = value, - bottom = value, - left = value, - right = value; + : top = value, + bottom = value, + left = value, + right = value; final double top; final double bottom; @@ -53,9 +58,19 @@ class Bubble extends StatelessWidget { final flat = Radius.zero; switch (style.nip) { case BubbleNip.leftTop: - return BorderRadius.only(topLeft: flat, topRight: r, bottomLeft: r, bottomRight: r); + return BorderRadius.only( + topLeft: flat, + topRight: r, + bottomLeft: r, + bottomRight: r, + ); case BubbleNip.rightBottom: - return BorderRadius.only(topLeft: r, topRight: r, bottomLeft: r, bottomRight: flat); + return BorderRadius.only( + topLeft: r, + topRight: r, + bottomLeft: r, + bottomRight: flat, + ); case BubbleNip.none: return BorderRadius.all(r); } @@ -72,10 +87,19 @@ class Bubble extends StatelessWidget { color: style.color, borderRadius: radius, border: style.borderWidth > 0 - ? Border.all(color: Theme.of(context).dividerColor, width: style.borderWidth) + ? Border.all( + color: Theme.of(context).dividerColor, + width: style.borderWidth, + ) : null, boxShadow: style.elevation > 0 - ? [BoxShadow(color: Colors.black26, blurRadius: style.elevation * 2, offset: Offset(0, style.elevation))] + ? [ + BoxShadow( + color: Colors.black26, + blurRadius: style.elevation * 2, + offset: Offset(0, style.elevation), + ), + ] : null, ), padding: style.padding.toEdgeInsets(), diff --git a/lib/view/pages/talk/widgets/chat_bubble.dart b/lib/view/pages/talk/widgets/chat_bubble.dart index 8958be9..7e50a9e 100644 --- a/lib/view/pages/talk/widgets/chat_bubble.dart +++ b/lib/view/pages/talk/widgets/chat_bubble.dart @@ -40,13 +40,15 @@ class ChatBubble extends StatefulWidget { required this.refetch, this.isRead = false, this.selfId, - super.key}); + super.key, + }); @override State createState() => _ChatBubbleState(); } -class _ChatBubbleState extends State with SingleTickerProviderStateMixin { +class _ChatBubbleState extends State + with SingleTickerProviderStateMixin { late ChatMessage message; DownloadJob? _job; @@ -109,7 +111,10 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM final file = message.file; final filePath = file?.path; if (file == null || filePath == null) return; - final job = await DownloadManager.instance.start(remotePath: filePath, name: file.name); + final job = await DownloadManager.instance.start( + remotePath: filePath, + name: file.name, + ); if (!mounted) return; if (_job == job) return; _detachJob(); @@ -129,19 +134,22 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM BubbleStyle _getStyle() { final styles = ChatBubbleStyles(context); - if (widget.bubbleData.messageType != GetRoomResponseObjectMessageType.comment) { + if (widget.bubbleData.messageType != + GetRoomResponseObjectMessageType.comment) { return styles.getSystemStyle(); } - return widget.isSender ? styles.getSelfStyle(false) : styles.getRemoteStyle(false); + return widget.isSender + ? styles.getSelfStyle(false) + : styles.getRemoteStyle(false); } void _showOptionsDialog() => showChatMessageOptionsDialog( - context, - chatData: widget.chatData, - bubbleData: widget.bubbleData, - isSender: widget.isSender, - onRefetch: widget.refetch, - ); + context, + chatData: widget.chatData, + bubbleData: widget.bubbleData, + isSender: widget.isSender, + onRefetch: widget.refetch, + ); void _onTap() { final obj = message.originalData?['object']; @@ -165,24 +173,40 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM @override Widget build(BuildContext context) { - message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters); - final showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment - && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne; - final showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system - && widget.bubbleData.messageType != GetRoomResponseObjectMessageType.deletedComment; + message = ChatMessage( + originalMessage: widget.bubbleData.message, + originalData: widget.bubbleData.messageParameters, + ); + final showActorDisplayName = + widget.bubbleData.messageType == + GetRoomResponseObjectMessageType.comment && + widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne; + final showBubbleTime = + widget.bubbleData.messageType != + GetRoomResponseObjectMessageType.system && + widget.bubbleData.messageType != + GetRoomResponseObjectMessageType.deletedComment; final parent = widget.bubbleData.parent; final actorText = Text( widget.bubbleData.actorDisplayName, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, - style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold), + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), ); final timeText = Text( - DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).formatHm(), + DateTime.fromMillisecondsSinceEpoch( + widget.bubbleData.timestamp * 1000, + ).formatHm(), textAlign: TextAlign.end, - style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize), + style: TextStyle( + color: widget.timeIconColor, + fontSize: widget.timeIconSize, + ), ); return Column( @@ -206,7 +230,9 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM final isAction = _position.dx.abs() > 50; setState(() => _position = Offset.zero); if (widget.bubbleData.isReplyable && isAction) { - context.read().setReferenceMessageId(widget.bubbleData.id); + context.read().setReferenceMessageId( + widget.bubbleData.id, + ); } }, onLongPress: _showOptionsDialog, @@ -281,67 +307,68 @@ class _BubbleContent extends StatelessWidget { @override Widget build(BuildContext context) => Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.9, - minWidth: showActorDisplayName - ? actorText.size.width - : timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.9, + minWidth: showActorDisplayName + ? actorText.size.width + : timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3, + ), + child: Stack( + children: [ + if (showActorDisplayName) Positioned(top: 0, left: 0, child: actorText), + Padding( + padding: EdgeInsets.only( + bottom: showBubbleTime ? 18 : 0, + top: showActorDisplayName ? 18 : 0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (parent != null && + bubbleData.messageType == + GetRoomResponseObjectMessageType.comment) ...[ + AnswerReference( + context: context, + referenceMessage: parent!, + selfId: selfId, + ), + const SizedBox(height: 5), + ], + messageWidget, + ], + ), ), - child: Stack( - children: [ - if (showActorDisplayName) - Positioned(top: 0, left: 0, child: actorText), - Padding( - padding: EdgeInsets.only( - bottom: showBubbleTime ? 18 : 0, - top: showActorDisplayName ? 18 : 0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (parent != null && bubbleData.messageType == GetRoomResponseObjectMessageType.comment) ...[ - AnswerReference( - context: context, - referenceMessage: parent!, - selfId: selfId, - ), - const SizedBox(height: 5), - ], - messageWidget, + if (showBubbleTime) + Positioned( + bottom: 0, + right: 0, + child: Row( + children: [ + timeText, + if (isSender) ...[ + SizedBox(width: spacing), + Icon( + isRead ? Icons.done_all_outlined : Icons.done_outlined, + size: timeIconSize, + color: timeIconColor, + ), ], - ), + ], ), - if (showBubbleTime) - Positioned( - bottom: 0, - right: 0, - child: Row( - children: [ - timeText, - if (isSender) ...[ - SizedBox(width: spacing), - Icon( - isRead ? Icons.done_all_outlined : Icons.done_outlined, - size: timeIconSize, - color: timeIconColor, - ), - ], - ], - ), - ), - if (downloadJob?.status.value is DownloadInProgress) - Positioned( - bottom: 0, - right: 0, - left: 0, - child: LinearProgressIndicator( - value: () { - final s = downloadJob!.status.value as DownloadInProgress; - return s.percent <= 0 ? null : s.percent / 100; - }(), - ), - ), - ], - ), - ); + ), + if (downloadJob?.status.value is DownloadInProgress) + Positioned( + bottom: 0, + right: 0, + left: 0, + child: LinearProgressIndicator( + value: () { + final s = downloadJob!.status.value as DownloadInProgress; + return s.percent <= 0 ? null : s.percent / 100; + }(), + ), + ), + ], + ), + ); } diff --git a/lib/view/pages/talk/widgets/chat_bubble_poll.dart b/lib/view/pages/talk/widgets/chat_bubble_poll.dart index 96c25ed..2f5b3a6 100644 --- a/lib/view/pages/talk/widgets/chat_bubble_poll.dart +++ b/lib/view/pages/talk/widgets/chat_bubble_poll.dart @@ -22,14 +22,14 @@ void showChatBubblePollDialog( future: pollState, builder: (_, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return const Column(mainAxisSize: MainAxisSize.min, children: [LoadingSpinner()]); + return const Column( + mainAxisSize: MainAxisSize.min, + children: [LoadingSpinner()], + ); } final pollData = snapshot.data!.data; return SingleChildScrollView( - child: PollOptionsList( - pollData: pollData, - chatToken: chatToken, - ), + child: PollOptionsList(pollData: pollData, chatToken: chatToken), ); }, ), diff --git a/lib/view/pages/talk/widgets/chat_bubble_reactions.dart b/lib/view/pages/talk/widgets/chat_bubble_reactions.dart index 9761474..02e80ac 100644 --- a/lib/view/pages/talk/widgets/chat_bubble_reactions.dart +++ b/lib/view/pages/talk/widgets/chat_bubble_reactions.dart @@ -37,14 +37,20 @@ class ChatBubbleReactions extends StatelessWidget { alignment: isSender ? WrapAlignment.end : WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, children: reactions.entries.map((e) { - final hasSelfReacted = bubbleData.reactionsSelf?.contains(e.key) ?? false; + final hasSelfReacted = + bubbleData.reactionsSelf?.contains(e.key) ?? false; return Container( margin: const EdgeInsets.only(right: 2.5, left: 2.5), child: ActionChip( label: Text('${e.key} ${e.value}'), - visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity), + visualDensity: const VisualDensity( + vertical: VisualDensity.minimumDensity, + horizontal: VisualDensity.minimumDensity, + ), padding: EdgeInsets.zero, - backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null, + backgroundColor: hasSelfReacted + ? Theme.of(context).primaryColor + : null, onPressed: () { runWithErrorDialog(context, () async { if (hasSelfReacted) { diff --git a/lib/view/pages/talk/widgets/chat_message_options_dialog.dart b/lib/view/pages/talk/widgets/chat_message_options_dialog.dart index 6816d71..227490b 100644 --- a/lib/view/pages/talk/widgets/chat_message_options_dialog.dart +++ b/lib/view/pages/talk/widgets/chat_message_options_dialog.dart @@ -29,11 +29,13 @@ void showChatMessageOptionsDialog( required void Function({bool renew}) onRefetch, }) { final parentContext = context; - final canReact = bubbleData.messageType == GetRoomResponseObjectMessageType.comment; - final canDelete = isSender && - DateTime.fromMillisecondsSinceEpoch(bubbleData.timestamp * 1000) - .add(const Duration(hours: 6)) - .isAfter(DateTime.now()); + final canReact = + bubbleData.messageType == GetRoomResponseObjectMessageType.comment; + final canDelete = + isSender && + DateTime.fromMillisecondsSinceEpoch( + bubbleData.timestamp * 1000, + ).add(const Duration(hours: 6)).isAfter(DateTime.now()); showDetailsBottomSheet( context, @@ -61,7 +63,11 @@ void showChatMessageOptionsDialog( onTap: () { Navigator.of(sheetCtx).pop(); if (!parentContext.mounted) return; - AppRoutes.openMessageReactions(parentContext, chatData.token, bubbleData.id); + AppRoutes.openMessageReactions( + parentContext, + chatData.token, + bubbleData.id, + ); }, ), if (bubbleData.message != '{file}') @@ -73,7 +79,9 @@ void showChatMessageOptionsDialog( Navigator.of(sheetCtx).pop(); }, ), - if (!kReleaseMode && !isSender && chatData.type != GetRoomResponseObjectConversationType.oneToOne) + if (!kReleaseMode && + !isSender && + chatData.type != GetRoomResponseObjectConversationType.oneToOne) ListTile( leading: const Icon(Icons.sms_outlined), title: Text("Private Nachricht an '${bubbleData.actorDisplayName}'"), @@ -136,54 +144,57 @@ class _ReactionsRowState extends State<_ReactionsRow> { @override Widget build(BuildContext context) => AnimatedBuilder( - animation: _controller, - builder: (context, _) { - final busy = _controller.busy; - final err = _controller.error; - return Column( - mainAxisSize: MainAxisSize.min, + animation: _controller, + builder: (context, _) { + final busy = _controller.busy; + final err = _controller.error; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + alignment: WrapAlignment.center, children: [ - Wrap( - alignment: WrapAlignment.center, - children: [ - ..._commonReactions.map( - (emoji) => TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: const Size(40, 40), - ), - onPressed: busy ? null : () => _react(emoji), - child: Text(emoji), - ), - ), - IconButton( - onPressed: busy ? null : () => _showEmojiPicker(context), - style: IconButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: const Size(40, 40), - ), - icon: busy - ? const AppProgressIndicator.small() - : const Icon(Icons.add_circle_outline_outlined), - ), - ], - ), - if (err != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - err, - textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12), + ..._commonReactions.map( + (emoji) => TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(40, 40), ), + onPressed: busy ? null : () => _react(emoji), + child: Text(emoji), ), - const Divider(), + ), + IconButton( + onPressed: busy ? null : () => _showEmojiPicker(context), + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(40, 40), + ), + icon: busy + ? const AppProgressIndicator.small() + : const Icon(Icons.add_circle_outline_outlined), + ), ], - ); - }, + ), + if (err != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Text( + err, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), + const Divider(), + ], ); + }, + ); void _showEmojiPicker(BuildContext rowContext) { showDialog( @@ -214,7 +225,9 @@ class _ReactionsRowState extends State<_ReactionsRow> { noRecents: const Text('Keine zuletzt verwendeten Emojis'), columns: 7, ), - bottomActionBarConfig: const emojis.BottomActionBarConfig(enabled: false), + bottomActionBarConfig: const emojis.BottomActionBarConfig( + enabled: false, + ), categoryViewConfig: emojis.CategoryViewConfig( backgroundColor: Theme.of(pickerCtx).hoverColor, iconColorSelected: Theme.of(pickerCtx).primaryColor, diff --git a/lib/view/pages/talk/widgets/chat_textfield.dart b/lib/view/pages/talk/widgets/chat_textfield.dart index 8cea791..f4067b8 100644 --- a/lib/view/pages/talk/widgets/chat_textfield.dart +++ b/lib/view/pages/talk/widgets/chat_textfield.dart @@ -39,13 +39,17 @@ class _ChatTextfieldState extends State { void share(String shareFolder, List filePaths) { for (final element in filePaths) { final fileName = element.split(Platform.pathSeparator).last; - FileSharingApi().share(FileSharingApiParams( - shareType: 10, - shareWith: widget.sendToToken, - path: '$shareFolder/$fileName', - )).then((_) { - if (mounted) context.read().refresh(); - }); + FileSharingApi() + .share( + FileSharingApiParams( + shareType: 10, + shareWith: widget.sendToToken, + path: '$shareFolder/$fileName', + ), + ) + .then((_) { + if (mounted) context.read().refresh(); + }); } } @@ -53,19 +57,25 @@ class _ChatTextfieldState extends State { if (paths == null) return; const shareFolder = 'MarianumMobile'; - unawaited(WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')))); + unawaited( + WebdavApi.webdav.then( + (webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')), + ), + ); if (!mounted) return; - unawaited(pushScreen( - context, - withNavBar: false, - screen: FilesUploadDialog( - filePaths: paths, - remotePath: shareFolder, - onUploadFinished: (uploaded) => share(shareFolder, uploaded), - uniqueNames: true, + unawaited( + pushScreen( + context, + withNavBar: false, + screen: FilesUploadDialog( + filePaths: paths, + remotePath: shareFolder, + onUploadFinished: (uploaded) => share(shareFolder, uploaded), + uniqueNames: true, + ), ), - )); + ); } void _setDraft(String text) { @@ -82,7 +92,9 @@ class _ChatTextfieldState extends State { if (messageId != null) { talkSettings.draftReplies[widget.sendToToken] = messageId; } else { - talkSettings.draftReplies.removeWhere((key, _) => key == widget.sendToToken); + talkSettings.draftReplies.removeWhere( + (key, _) => key == widget.sendToToken, + ); } } @@ -90,7 +102,10 @@ class _ChatTextfieldState extends State { void initState() { super.initState(); settings = context.read(); - final draftReply = settings.val().talkSettings.draftReplies[widget.sendToToken]; + final draftReply = settings + .val() + .talkSettings + .draftReplies[widget.sendToToken]; if (draftReply != null) { context.read().setReferenceMessageId(draftReply); } @@ -121,16 +136,19 @@ class _ChatTextfieldState extends State { @override Widget build(BuildContext context) { - _textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? ''; + _textBoxController.text = + settings.val().talkSettings.drafts[widget.sendToToken] ?? ''; final chatBloc = context.watch(); final chatState = chatBloc.state.data; Widget replyBanner = const SizedBox.shrink(); - if (chatState != null && chatState.referenceMessageId != null && chatState.chatResponse != null) { + if (chatState != null && + chatState.referenceMessageId != null && + chatState.chatResponse != null) { try { - final referenceMessage = chatState.chatResponse!.sortByTimestamp().firstWhere( - (e) => e.id == chatState.referenceMessageId, - ); + final referenceMessage = chatState.chatResponse! + .sortByTimestamp() + .firstWhere((e) => e.id == chatState.referenceMessageId); replyBanner = Row( children: [ Expanded( @@ -150,120 +168,150 @@ class _ChatTextfieldState extends State { ), ], ); - } catch (_) {/* reference no longer in current chat data */} + } catch (_) { + /* reference no longer in current chat data */ + } } - return Stack(children: [ - Align( - alignment: Alignment.bottomLeft, - child: Container( - padding: const EdgeInsets.only(left: 10, bottom: 3, top: 3, right: 10), - width: double.infinity, - child: Column( - children: [ - replyBanner, - if (_sendError != null) - Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text( - _sendError!, - style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12), - ), - ), - Row(children: [ - GestureDetector( - onTap: () { - showDetailsBottomSheet( - context, - children: (sheetCtx) => [ - ListTile( - leading: const Icon(Icons.file_open), - title: const Text('Aus Dateien auswählen'), - onTap: () { - FilePick.documentPick().then(mediaUpload); - Navigator.of(sheetCtx).pop(); - }, - ), - ListTile( - leading: const Icon(Icons.image), - title: const Text('Aus Galerie auswählen'), - onTap: () { - FilePick.multipleGalleryPick().then((value) { - if (value != null) mediaUpload(value.map((e) => e.path).toList()); - }); - Navigator.of(sheetCtx).pop(); - }, - ), - ListTile( - leading: const Icon(Icons.camera_alt_outlined), - title: const Text('Foto aufnehmen'), - onTap: () { - FilePick.cameraPick().then((image) { - if (image != null) mediaUpload([image.path]); - }); - Navigator.of(sheetCtx).pop(); - }, - ), - ], - ); - }, - child: Material( - elevation: 5, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(30), + return Stack( + children: [ + Align( + alignment: Alignment.bottomLeft, + child: Container( + padding: const EdgeInsets.only( + left: 10, + bottom: 3, + top: 3, + right: 10, + ), + width: double.infinity, + child: Column( + children: [ + replyBanner, + if (_sendError != null) + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + _sendError!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, ), - child: const Icon(Icons.attach_file_outlined, color: Colors.white, size: 20), ), ), - ), - const SizedBox(width: 15), - Expanded( - child: TextField( - autocorrect: true, - textCapitalization: TextCapitalization.sentences, - controller: _textBoxController, - maxLines: 7, - minLines: 1, - decoration: const InputDecoration( - hintText: 'Nachricht schreiben...', - border: InputBorder.none, + Row( + children: [ + GestureDetector( + onTap: () { + showDetailsBottomSheet( + context, + children: (sheetCtx) => [ + ListTile( + leading: const Icon(Icons.file_open), + title: const Text('Aus Dateien auswählen'), + onTap: () { + FilePick.documentPick().then(mediaUpload); + Navigator.of(sheetCtx).pop(); + }, + ), + ListTile( + leading: const Icon(Icons.image), + title: const Text('Aus Galerie auswählen'), + onTap: () { + FilePick.multipleGalleryPick().then((value) { + if (value != null) { + mediaUpload( + value.map((e) => e.path).toList(), + ); + } + }); + Navigator.of(sheetCtx).pop(); + }, + ), + ListTile( + leading: const Icon(Icons.camera_alt_outlined), + title: const Text('Foto aufnehmen'), + onTap: () { + FilePick.cameraPick().then((image) { + if (image != null) mediaUpload([image.path]); + }); + Navigator.of(sheetCtx).pop(); + }, + ), + ], + ); + }, + child: Material( + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + child: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(30), + ), + child: const Icon( + Icons.attach_file_outlined, + color: Colors.white, + size: 20, + ), + ), + ), ), - onChanged: (text) { - if (text.trim().toLowerCase() == 'marbot marbot marbot') { - const newText = 'Roboter sind cool und so, aber Marbots sind besser!'; - _textBoxController.text = newText; - text = newText; - } - _setDraft(text); - }, - onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context), - ), + const SizedBox(width: 15), + Expanded( + child: TextField( + autocorrect: true, + textCapitalization: TextCapitalization.sentences, + controller: _textBoxController, + maxLines: 7, + minLines: 1, + decoration: const InputDecoration( + hintText: 'Nachricht schreiben...', + border: InputBorder.none, + ), + onChanged: (text) { + if (text.trim().toLowerCase() == + 'marbot marbot marbot') { + const newText = + 'Roboter sind cool und so, aber Marbots sind besser!'; + _textBoxController.text = newText; + text = newText; + } + _setDraft(text); + }, + onTapOutside: (_) => + FocusBehaviour.textFieldTapOutside(context), + ), + ), + const SizedBox(width: 15), + ValueListenableBuilder( + valueListenable: _textBoxController, + builder: (context, value, _) => AsyncFab( + mini: true, + heroTag: 'chatSend_${widget.sendToToken}', + icon: Icons.send, + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + controller: _sendController, + onPressed: value.text.trim().isEmpty + ? null + : () => _sendMessage(chatBloc), + onError: (message) => + setState(() => _sendError = message), + onSuccess: () => setState(() => _sendError = null), + ), + ), + ], ), - const SizedBox(width: 15), - ValueListenableBuilder( - valueListenable: _textBoxController, - builder: (context, value, _) => AsyncFab( - mini: true, - heroTag: 'chatSend_${widget.sendToToken}', - icon: Icons.send, - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Colors.white, - controller: _sendController, - onPressed: value.text.trim().isEmpty ? null : () => _sendMessage(chatBloc), - onError: (message) => setState(() => _sendError = message), - onSuccess: () => setState(() => _sendError = null), - ), - ), - ]), - ], + ], + ), ), ), - ), - ]); + ], + ); } } diff --git a/lib/view/pages/talk/widgets/chat_tile.dart b/lib/view/pages/talk/widgets/chat_tile.dart index f6a5cab..ddb0127 100644 --- a/lib/view/pages/talk/widgets/chat_tile.dart +++ b/lib/view/pages/talk/widgets/chat_tile.dart @@ -25,7 +25,12 @@ class ChatTile extends StatefulWidget { final bool disableContextActions; final bool hasDraft; - const ChatTile({super.key, required this.data, this.disableContextActions = false, this.hasDraft = false}); + const ChatTile({ + super.key, + required this.data, + this.disableContextActions = false, + this.hasDraft = false, + }); @override State createState() => _ChatTileState(); @@ -39,7 +44,11 @@ class _ChatTileState extends State { super.initState(); AccountData().waitForPopulation().then((_) { if (!mounted) return; - setState(() => selfUsername = AccountData().isPopulated() ? AccountData().getUsername() : null); + setState( + () => selfUsername = AccountData().isPopulated() + ? AccountData().getUsername() + : null, + ); }); } @@ -49,7 +58,9 @@ class _ChatTileState extends State { await SetReadMarker( widget.data.token, true, - setReadMarkerParams: SetReadMarkerParams(lastReadMessage: widget.data.lastMessage.id), + setReadMarkerParams: SetReadMarkerParams( + lastReadMessage: widget.data.lastMessage.id, + ), ).run(); if (!mounted) return; _refreshList(); @@ -58,12 +69,18 @@ class _ChatTileState extends State { @override Widget build(BuildContext context) { final chatBloc = context.watch(); - final isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne; - final circleAvatar = UserAvatar(id: isGroup ? widget.data.token : widget.data.name, isGroup: isGroup); + final isGroup = + widget.data.type != GetRoomResponseObjectConversationType.oneToOne; + final circleAvatar = UserAvatar( + id: isGroup ? widget.data.token : widget.data.name, + isGroup: isGroup, + ); return ListTile( style: ListTileStyle.list, - tileColor: chatBloc.state.data?.currentToken == widget.data.token && TalkNavigator.isSecondaryVisible(context) + tileColor: + chatBloc.state.data?.currentToken == widget.data.token && + TalkNavigator.isSecondaryVisible(context) ? Theme.of(context).primaryColor.withAlpha(100) : null, leading: Stack( @@ -80,16 +97,25 @@ class _ChatTileState extends State { color: Theme.of(context).primaryColor.withAlpha(200), borderRadius: BorderRadius.circular(90.0), ), - child: const Icon(Icons.star, color: Colors.amberAccent, size: 15), + child: const Icon( + Icons.star, + color: Colors.amberAccent, + size: 15, + ), ), ), - ) + ), ], ), title: Row( mainAxisSize: MainAxisSize.min, children: [ - Flexible(child: Text(widget.data.displayName, overflow: TextOverflow.ellipsis)), + Flexible( + child: Text( + widget.data.displayName, + overflow: TextOverflow.ellipsis, + ), + ), if (widget.hasDraft) ...[ const SizedBox(width: 5), const Icon(Icons.edit_outlined, size: 15), @@ -119,8 +145,16 @@ class _ChatTileState extends State { onTap: () { if (selfUsername == null) return; unawaited(_setCurrentAsRead()); - final view = ChatView(room: widget.data, selfId: selfUsername!, avatar: circleAvatar); - TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true); + final view = ChatView( + room: widget.data, + selfId: selfUsername!, + avatar: circleAvatar, + ); + TalkNavigator.pushSplitView( + context, + view, + overrideToSingleSubScreen: true, + ); context.read().setToken(widget.data.token); }, onLongPress: () { @@ -168,7 +202,8 @@ class _ChatTileState extends State { Navigator.of(sheetCtx).pop(); ConfirmDialog( title: 'Chat verlassen', - content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.', + content: + 'Du benötigst ggf. eine Einladung um erneut beizutreten.', confirmButton: 'Verlassen', onConfirmAsync: () async { await LeaveRoom(widget.data.token).run(); diff --git a/lib/view/pages/talk/widgets/poll_options_list.dart b/lib/view/pages/talk/widgets/poll_options_list.dart index a02a320..637b153 100644 --- a/lib/view/pages/talk/widgets/poll_options_list.dart +++ b/lib/view/pages/talk/widgets/poll_options_list.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; @@ -8,7 +7,11 @@ import '../../../../utils/url_opener.dart'; class PollOptionsList extends StatefulWidget { final GetPollStateResponseObject pollData; final String chatToken; - const PollOptionsList({super.key, required this.pollData, required this.chatToken}); + const PollOptionsList({ + super.key, + required this.pollData, + required this.chatToken, + }); @override State createState() => _PollOptionsListState(); @@ -23,44 +26,48 @@ class _PollOptionsListState extends State { var votedSelf = widget.pollData.votedSelf.contains(optionId); var portionsVisible = widget.pollData.votes is Map; var votes = portionsVisible - ? (widget.pollData.votes['option-$optionId'] as num?) ?? 0 - : 0; + ? (widget.pollData.votes['option-$optionId'] as num?) ?? 0 + : 0; var numVoters = widget.pollData.numVoters ?? 0; final portion = numVoters == 0 ? 0.0 : (votes / numVoters); return ListTile( isThreeLine: portionsVisible, dense: true, - title: Text( - option, - style: Theme.of(context).textTheme.bodyLarge, - ), + title: Text(option, style: Theme.of(context).textTheme.bodyLarge), leading: Icon( votedSelf ? Icons.check_circle_outlined : Icons.circle_outlined, color: votedSelf ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.6) - : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + : Theme.of( + context, + ).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), ), - subtitle: portionsVisible ? Row( - children: [ - Expanded( - child: LinearProgressIndicator(value: portion.clamp(0.0, 1.0)), - ), - Container( - margin: const EdgeInsets.only(left: 10), - child: Text('${(portion * 100).round()}%'), - ), - ], - ) : null, + subtitle: portionsVisible + ? Row( + children: [ + Expanded( + child: LinearProgressIndicator( + value: portion.clamp(0.0, 1.0), + ), + ), + Container( + margin: const EdgeInsets.only(left: 10), + child: Text('${(portion * 100).round()}%'), + ), + ], + ) + : null, ); }), ListTile( title: Linkify( - text: 'Wenn du abstimmen möchtest, verwende die Webversion unter https://cloud.marianum-fulda.de/call/${widget.chatToken}', + text: + 'Wenn du abstimmen möchtest, verwende die Webversion unter https://cloud.marianum-fulda.de/call/${widget.chatToken}', onOpen: UrlOpener.onOpen, style: Theme.of(context).textTheme.bodySmall, ), - ) + ), ], ); } diff --git a/lib/view/pages/talk/widgets/split_view_placeholder.dart b/lib/view/pages/talk/widgets/split_view_placeholder.dart index 590a1ec..d5b12e5 100644 --- a/lib/view/pages/talk/widgets/split_view_placeholder.dart +++ b/lib/view/pages/talk/widgets/split_view_placeholder.dart @@ -7,21 +7,25 @@ class SplitViewPlaceholder extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar(), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MediaQuery( - data: MediaQuery.of(context).copyWith( - invertColors: !AppTheme.isDarkMode(context), - ), - child: Image.asset('assets/logo/icon.png', height: 200), - ), - const SizedBox(height: 30), - const Text('Marianum Fulda\nTalk', textAlign: TextAlign.center, style: TextStyle(fontSize: 30)), - ], - ), - ) - ); + appBar: AppBar(), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MediaQuery( + data: MediaQuery.of( + context, + ).copyWith(invertColors: !AppTheme.isDarkMode(context)), + child: Image.asset('assets/logo/icon.png', height: 200), + ), + const SizedBox(height: 30), + const Text( + 'Marianum Fulda\nTalk', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 30), + ), + ], + ), + ), + ); } diff --git a/lib/view/pages/timetable/custom_events/custom_event_colors.dart b/lib/view/pages/timetable/custom_events/custom_event_colors.dart index 04f623a..3a74984 100644 --- a/lib/view/pages/timetable/custom_events/custom_event_colors.dart +++ b/lib/view/pages/timetable/custom_events/custom_event_colors.dart @@ -5,7 +5,8 @@ import '../../../../theming/dark_app_theme.dart'; enum CustomTimetableColors { orange, red, green, blue } class TimetableColors { - static const CustomTimetableColors defaultColor = CustomTimetableColors.orange; + static const CustomTimetableColors defaultColor = + CustomTimetableColors.orange; static ColorModeDisplay getDisplayOptions(CustomTimetableColors color) { switch (color) { @@ -14,17 +15,24 @@ class TimetableColors { case CustomTimetableColors.blue: return ColorModeDisplay(color: Colors.blue, displayName: 'Blau'); case CustomTimetableColors.orange: - return ColorModeDisplay(color: Colors.orange.shade800, displayName: 'Orange'); + return ColorModeDisplay( + color: Colors.orange.shade800, + displayName: 'Orange', + ); case CustomTimetableColors.red: - return ColorModeDisplay(color: DarkAppTheme.marianumRed, displayName: 'Rot'); + return ColorModeDisplay( + color: DarkAppTheme.marianumRed, + displayName: 'Rot', + ); } } - static Color getColorFromString(String color) => - getDisplayOptions(CustomTimetableColors.values.firstWhere( - (e) => e.name == color, - orElse: () => defaultColor, - )).color; + static Color getColorFromString(String color) => getDisplayOptions( + CustomTimetableColors.values.firstWhere( + (e) => e.name == color, + orElse: () => defaultColor, + ), + ).color; } class ColorModeDisplay { diff --git a/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart b/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart index db1d06b..8dd64f2 100644 --- a/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart +++ b/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart @@ -42,7 +42,8 @@ class _CustomEventEditDialogState extends State { static const TimeOfDay _defaultEnd = TimeOfDay(hour: 9, minute: 30); static const int _minDurationMinutes = 15; - late DateTime _date = widget.existingEvent?.startDate ?? widget.initialStart ?? DateTime.now(); + late DateTime _date = + widget.existingEvent?.startDate ?? widget.initialStart ?? DateTime.now(); late TimeOfDay _startTime; late TimeOfDay _endTime; late bool _isAllDay; @@ -85,13 +86,18 @@ class _CustomEventEditDialogState extends State { _endTime = clamped.$2; } - static (TimeOfDay, TimeOfDay) _clampToVisibleWindow(TimeOfDay rawStart, TimeOfDay rawEnd) { + static (TimeOfDay, TimeOfDay) _clampToVisibleWindow( + TimeOfDay rawStart, + TimeOfDay rawEnd, + ) { int toMin(TimeOfDay t) => t.hour * 60 + t.minute; TimeOfDay fromMin(int m) => TimeOfDay(hour: m ~/ 60, minute: m % 60); final windowStart = toMin(_windowStart); final windowEnd = toMin(_windowEnd); - var start = toMin(rawStart).clamp(windowStart, windowEnd - _minDurationMinutes); + var start = toMin( + rawStart, + ).clamp(windowStart, windowEnd - _minDurationMinutes); var end = toMin(rawEnd); if (end < start + _minDurationMinutes) end = start + _minDurationMinutes; if (end > windowEnd) { @@ -165,10 +171,7 @@ class _CustomEventEditDialogState extends State { context: context, start: _startTime, end: _endTime, - disabledTime: TimeRange( - startTime: _windowEnd, - endTime: _windowStart, - ), + disabledTime: TimeRange(startTime: _windowEnd, endTime: _windowStart), disabledColor: Colors.grey, paintingStyle: PaintingStyle.fill, interval: const Duration(minutes: 5), @@ -188,103 +191,118 @@ class _CustomEventEditDialogState extends State { @override Widget build(BuildContext context) => AlertDialog( - insetPadding: const EdgeInsets.all(20), - contentPadding: const EdgeInsets.all(10), - title: Text(_isEditing ? 'Termin bearbeiten' : 'Termin hinzufügen'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: TextField( - controller: _name, - autofocus: true, - decoration: const InputDecoration(labelText: 'Terminname', border: OutlineInputBorder()), - onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context), - ), + insetPadding: const EdgeInsets.all(20), + contentPadding: const EdgeInsets.all(10), + title: Text(_isEditing ? 'Termin bearbeiten' : 'Termin hinzufügen'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: TextField( + controller: _name, + autofocus: true, + decoration: const InputDecoration( + labelText: 'Terminname', + border: OutlineInputBorder(), ), - ListTile( - title: TextField( - controller: _description, - maxLines: 2, - minLines: 2, - decoration: const InputDecoration(labelText: 'Beschreibung', border: OutlineInputBorder()), - onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context), - ), - ), - const Divider(), - ListTile( - leading: const Icon(Icons.date_range_outlined), - title: Text(Jiffy.parseFromDateTime(_date).yMMMd), - subtitle: const Text('Datum'), - onTap: _pickDate, - ), - SwitchListTile( - secondary: const Icon(Icons.today_outlined), - title: const Text('Ganztägig'), - value: _isAllDay, - onChanged: (v) => setState(() => _isAllDay = v), - ), - if (!_isAllDay) - ListTile( - leading: const Icon(Icons.access_time_outlined), - title: Text('${_startTime.format(context)} - ${_endTime.format(context)}'), - subtitle: const Text('Zeitraum'), - onTap: _pickTimeRange, - ), - const Divider(), - ListTile( - leading: const Icon(Icons.color_lens_outlined), - title: const Text('Farbgebung'), - trailing: DropdownButton( - value: _color, - icon: const Icon(Icons.arrow_drop_down), - items: CustomTimetableColors.values - .map((e) => DropdownMenuItem( - value: e, - enabled: e != _color, - child: Row( - children: [ - Icon(Icons.circle, color: TimetableColors.getDisplayOptions(e).color), - const SizedBox(width: 10), - Text(TimetableColors.getDisplayOptions(e).displayName), - ], - ), - )) - .toList(), - onChanged: (e) => setState(() => _color = e!), - ), - ), - const Divider(), - RRuleGenerator( - config: RRuleGeneratorConfig( - selectDayStyle: RRuleSelectDayStyle( - dayStyle: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.secondary, - ), - dayTextStyle: const TextStyle(color: Colors.black), - selectedDayStyle: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).primaryColor, - ), - ), - ), - initialRRule: _rrule, - locale: RRuleLocale.de_DE, - onChange: (newValue) { - log('Rule: $newValue'); - setState(() => _rrule = newValue); - }, - ), - ], + onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context), + ), ), - ), - actions: [ - AsyncDialogAction( - confirmLabel: _isEditing ? 'Speichern' : 'Erstellen', - onConfirm: _save, + ListTile( + title: TextField( + controller: _description, + maxLines: 2, + minLines: 2, + decoration: const InputDecoration( + labelText: 'Beschreibung', + border: OutlineInputBorder(), + ), + onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context), + ), + ), + const Divider(), + ListTile( + leading: const Icon(Icons.date_range_outlined), + title: Text(Jiffy.parseFromDateTime(_date).yMMMd), + subtitle: const Text('Datum'), + onTap: _pickDate, + ), + SwitchListTile( + secondary: const Icon(Icons.today_outlined), + title: const Text('Ganztägig'), + value: _isAllDay, + onChanged: (v) => setState(() => _isAllDay = v), + ), + if (!_isAllDay) + ListTile( + leading: const Icon(Icons.access_time_outlined), + title: Text( + '${_startTime.format(context)} - ${_endTime.format(context)}', + ), + subtitle: const Text('Zeitraum'), + onTap: _pickTimeRange, + ), + const Divider(), + ListTile( + leading: const Icon(Icons.color_lens_outlined), + title: const Text('Farbgebung'), + trailing: DropdownButton( + value: _color, + icon: const Icon(Icons.arrow_drop_down), + items: CustomTimetableColors.values + .map( + (e) => DropdownMenuItem( + value: e, + enabled: e != _color, + child: Row( + children: [ + Icon( + Icons.circle, + color: TimetableColors.getDisplayOptions(e).color, + ), + const SizedBox(width: 10), + Text( + TimetableColors.getDisplayOptions(e).displayName, + ), + ], + ), + ), + ) + .toList(), + onChanged: (e) => setState(() => _color = e!), + ), + ), + const Divider(), + RRuleGenerator( + config: RRuleGeneratorConfig( + selectDayStyle: RRuleSelectDayStyle( + dayStyle: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary, + ), + dayTextStyle: const TextStyle(color: Colors.black), + selectedDayStyle: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).primaryColor, + ), + ), + ), + initialRRule: _rrule, + locale: RRuleLocale.de_DE, + onChange: (newValue) { + log('Rule: $newValue'); + setState(() => _rrule = newValue); + }, ), ], - ); + ), + ), + actions: [ + AsyncDialogAction( + confirmLabel: _isEditing ? 'Speichern' : 'Erstellen', + onConfirm: _save, + ), + ], + ); } diff --git a/lib/view/pages/timetable/custom_events/custom_events_view.dart b/lib/view/pages/timetable/custom_events/custom_events_view.dart index 83d0b24..24263f7 100644 --- a/lib/view/pages/timetable/custom_events/custom_events_view.dart +++ b/lib/view/pages/timetable/custom_events/custom_events_view.dart @@ -22,57 +22,69 @@ class CustomEventsView extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Eigene Termine'), - actions: [ - IconButton( - icon: const Icon(Icons.add), + appBar: AppBar( + title: const Text('Eigene Termine'), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () => _openCreateDialog(context), + ), + ], + ), + body: LoadableStateConsumer( + child: (state, _) { + final events = state.customEvents?.events ?? const []; + + if (events.isEmpty) { + return PlaceholderView( + icon: Icons.calendar_today_outlined, + text: 'Keine Einträge vorhanden', + button: TextButton( onPressed: () => _openCreateDialog(context), + child: const Text('Termin erstellen'), ), - ], - ), - body: LoadableStateConsumer( - child: (state, _) { - final events = state.customEvents?.events ?? const []; + ); + } - if (events.isEmpty) { - return PlaceholderView( - icon: Icons.calendar_today_outlined, - text: 'Keine Einträge vorhanden', - button: TextButton( - onPressed: () => _openCreateDialog(context), - child: const Text('Termin erstellen'), + return ListView( + children: events + .map( + (e) => ListTile( + title: Text(e.title), + subtitle: Text( + '${e.rrule.isNotEmpty ? "wiederholend, " : ""}' + 'beginnend ${e.startDate.formatRelative()}', + ), + leading: CenteredLeading( + Icon( + e.rrule.isEmpty + ? Icons.event_outlined + : Icons.event_repeat_outlined, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => showDialog( + context: context, + builder: (_) => + CustomEventEditDialog(existingEvent: e), + ), + ), + IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () => + showDeleteCustomEventDialog(context, e), + ), + ], + ), ), - ); - } - - return ListView( - children: events.map((e) => ListTile( - title: Text(e.title), - subtitle: Text( - '${e.rrule.isNotEmpty ? "wiederholend, " : ""}' - 'beginnend ${e.startDate.formatRelative()}', - ), - leading: CenteredLeading(Icon(e.rrule.isEmpty ? Icons.event_outlined : Icons.event_repeat_outlined)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit_outlined), - onPressed: () => showDialog( - context: context, - builder: (_) => CustomEventEditDialog(existingEvent: e), - ), - ), - IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () => showDeleteCustomEventDialog(context, e), - ), - ], - ), - )).toList(), - ); - }, - ), - ); + ) + .toList(), + ); + }, + ), + ); } diff --git a/lib/view/pages/timetable/data/arbitrary_appointment.dart b/lib/view/pages/timetable/data/arbitrary_appointment.dart index 6d2320a..1f8dd82 100644 --- a/lib/view/pages/timetable/data/arbitrary_appointment.dart +++ b/lib/view/pages/timetable/data/arbitrary_appointment.dart @@ -8,9 +8,9 @@ sealed class ArbitraryAppointment { required T Function(GetTimetableResponseObject lesson) webuntis, required T Function(CustomTimetableEvent event) custom, }) => switch (this) { - WebuntisAppointment(:final lesson) => webuntis(lesson), - CustomAppointment(:final event) => custom(event), - }; + WebuntisAppointment(:final lesson) => webuntis(lesson), + CustomAppointment(:final event) => custom(event), + }; } class WebuntisAppointment extends ArbitraryAppointment { diff --git a/lib/view/pages/timetable/data/calendar_logic.dart b/lib/view/pages/timetable/data/calendar_logic.dart index e16faa9..aeead8b 100644 --- a/lib/view/pages/timetable/data/calendar_logic.dart +++ b/lib/view/pages/timetable/data/calendar_logic.dart @@ -43,24 +43,28 @@ List expandRegionsForDay(List regions, DateTime day) { final result = []; final dayStart = DateTime(day.year, day.month, day.day); for (final region in regions) { - final isRecurringDaily = region.recurrenceRule != null && + final isRecurringDaily = + region.recurrenceRule != null && region.recurrenceRule!.toUpperCase().contains('FREQ=DAILY'); if (isRecurringDaily) { - final start = dayStart.add(Duration( - hours: region.startTime.hour, - minutes: region.startTime.minute, - )); - final end = dayStart.add(Duration( - hours: region.endTime.hour, - minutes: region.endTime.minute, - )); + final start = dayStart.add( + Duration( + hours: region.startTime.hour, + minutes: region.startTime.minute, + ), + ); + final end = dayStart.add( + Duration(hours: region.endTime.hour, minutes: region.endTime.minute), + ); result.add(BoundRegion(region: region, start: start, end: end)); } else if (region.startTime.isSameDay(day)) { - result.add(BoundRegion( - region: region, - start: region.startTime, - end: region.endTime, - )); + result.add( + BoundRegion( + region: region, + start: region.startTime, + end: region.endTime, + ), + ); } } return result; @@ -73,8 +77,10 @@ List expandRegionsForDay(List regions, DateTime day) { /// [kCalendarStartHour] or end after [kCalendarEndHour]). The outside bucket /// is rendered as chips above the grid. ({List> inside, List> outside}) - partitionAppointmentsForWeek( - List appointments, DateTime weekStart) { +partitionAppointmentsForWeek( + List appointments, + DateTime weekStart, +) { final inside = List>.generate(5, (_) => []); final outside = List>.generate(5, (_) => []); final weekEnd = weekStart.add(const Duration(days: 5)); @@ -104,12 +110,19 @@ List expandRegionsForDay(List regions, DateTime day) { if (!occUtc.isBefore(weekEndUtc)) break; if (occUtc.isBefore(weekStartUtc)) continue; final occLocal = occUtc.toLocal(); - final idx = DateTime(occLocal.year, occLocal.month, occLocal.day) - .difference(weekStart) - .inDays; + final idx = DateTime( + occLocal.year, + occLocal.month, + occLocal.day, + ).difference(weekStart).inDays; if (idx < 0 || idx >= 5) continue; - final newStart = DateTime(occLocal.year, occLocal.month, occLocal.day, - a.startTime.hour, a.startTime.minute); + final newStart = DateTime( + occLocal.year, + occLocal.month, + occLocal.day, + a.startTime.hour, + a.startTime.minute, + ); place( idx, Appointment( @@ -150,8 +163,7 @@ class PeriodLayout { double _h(LessonPeriod p) => p.isBreak ? breakHeight : lessonHeight; - double get totalHeight => - periods.fold(0, (sum, p) => sum + _h(p)); + double get totalHeight => periods.fold(0, (sum, p) => sum + _h(p)); double topOf(LessonPeriod period) { var y = 0.0; @@ -241,7 +253,13 @@ class LaidOutOverflow extends LaidOutCell { final DateTime startTime; @override final DateTime endTime; - LaidOutOverflow(this.appointments, this.lane, this.laneCount, this.startTime, this.endTime); + LaidOutOverflow( + this.appointments, + this.lane, + this.laneCount, + this.startTime, + this.endTime, + ); } /// Horizontal ordering rank for parallel appointments. Lower = further left. @@ -269,17 +287,21 @@ int _appointmentPriority(Appointment a) { /// is free at its `startTime`. When no lane is free, open a new one. /// 3. A cluster ends as soon as every active lane's end is at or before the /// next appointment's start. -List assignLanes(List appts, {required int maxLanes}) { +List assignLanes( + List appts, { + required int maxLanes, +}) { assert(maxLanes >= 2, 'maxLanes must reserve at least one slot for overflow'); if (appts.isEmpty) return const []; - final sorted = [...appts]..sort((a, b) { - final c = a.startTime.compareTo(b.startTime); - if (c != 0) return c; - final p = _appointmentPriority(a).compareTo(_appointmentPriority(b)); - if (p != 0) return p; - return b.endTime.compareTo(a.endTime); - }); + final sorted = [...appts] + ..sort((a, b) { + final c = a.startTime.compareTo(b.startTime); + if (c != 0) return c; + final p = _appointmentPriority(a).compareTo(_appointmentPriority(b)); + if (p != 0) return p; + return b.endTime.compareTo(a.endTime); + }); // Phase 1: greedy lane assignment, grouped by cluster. final clusters = >[]; @@ -288,7 +310,8 @@ List assignLanes(List appts, {required int maxLanes}) for (final apt in sorted) { final allFree = - laneEnds.isNotEmpty && laneEnds.every((end) => !end.isAfter(apt.startTime)); + laneEnds.isNotEmpty && + laneEnds.every((end) => !end.isAfter(apt.startTime)); if (allFree) { clusters.add(current); current = <({Appointment apt, int lane})>[]; @@ -316,8 +339,10 @@ List assignLanes(List appts, {required int maxLanes}) // Phase 2: emit cells per cluster, collapsing if too wide. final result = []; for (final cluster in clusters) { - final laneCount = - cluster.fold(0, (m, e) => e.lane + 1 > m ? e.lane + 1 : m); + final laneCount = cluster.fold( + 0, + (m, e) => e.lane + 1 > m ? e.lane + 1 : m, + ); if (laneCount <= maxLanes) { for (final entry in cluster) { @@ -348,8 +373,9 @@ List assignLanes(List appts, {required int maxLanes}) if (a.startTime.isBefore(earliest)) earliest = a.startTime; if (a.endTime.isAfter(latest)) latest = a.endTime; } - result.add(LaidOutOverflow( - overflow, maxLanes - 1, maxLanes, earliest, latest)); + result.add( + LaidOutOverflow(overflow, maxLanes - 1, maxLanes, earliest, latest), + ); } } return result; diff --git a/lib/view/pages/timetable/data/lesson_period_schedule.dart b/lib/view/pages/timetable/data/lesson_period_schedule.dart index 01b3f47..dfd9b5a 100644 --- a/lib/view/pages/timetable/data/lesson_period_schedule.dart +++ b/lib/view/pages/timetable/data/lesson_period_schedule.dart @@ -17,8 +17,8 @@ class LessonPeriod { }); Duration get duration => Duration( - minutes: (end.hour * 60 + end.minute) - (start.hour * 60 + start.minute), - ); + minutes: (end.hour * 60 + end.minute) - (start.hour * 60 + start.minute), + ); int get _startMinutes => start.hour * 60 + start.minute; } @@ -31,39 +31,94 @@ class LessonPeriodSchedule { static LessonPeriodSchedule? fromApi(GetTimegridUnitsResponse response) { final canonical = response.result.firstWhere( (d) => d.day == 1, - orElse: () => response.result.isNotEmpty ? response.result.first : GetTimegridUnitsResponseDay(0, []), + orElse: () => response.result.isNotEmpty + ? response.result.first + : GetTimegridUnitsResponseDay(0, []), ); if (canonical.timeUnits.isEmpty) return null; - final periods = canonical.timeUnits - .map((u) => LessonPeriod( - name: u.name, - start: _fromHHMM(u.startTime), - end: _fromHHMM(u.endTime), - )) - .toList() - ..sort((a, b) => a._startMinutes.compareTo(b._startMinutes)); + final periods = + canonical.timeUnits + .map( + (u) => LessonPeriod( + name: u.name, + start: _fromHHMM(u.startTime), + end: _fromHHMM(u.endTime), + ), + ) + .toList() + ..sort((a, b) => a._startMinutes.compareTo(b._startMinutes)); return LessonPeriodSchedule(periods); } static LessonPeriodSchedule fallback() => const LessonPeriodSchedule([ - LessonPeriod(name: '0', start: TimeOfDay(hour: 7, minute: 10), end: TimeOfDay(hour: 7, minute: 53)), - LessonPeriod(name: '1', start: TimeOfDay(hour: 7, minute: 55), end: TimeOfDay(hour: 8, minute: 40)), - LessonPeriod(name: '2', start: TimeOfDay(hour: 8, minute: 40), end: TimeOfDay(hour: 9, minute: 25)), - LessonPeriod(name: '3', start: TimeOfDay(hour: 9, minute: 30), end: TimeOfDay(hour: 10, minute: 15)), - LessonPeriod(name: '4', start: TimeOfDay(hour: 10, minute: 35), end: TimeOfDay(hour: 11, minute: 20)), - LessonPeriod(name: '5', start: TimeOfDay(hour: 11, minute: 25), end: TimeOfDay(hour: 12, minute: 10)), - LessonPeriod(name: '6', start: TimeOfDay(hour: 12, minute: 15), end: TimeOfDay(hour: 13, minute: 0)), - LessonPeriod(name: '7', start: TimeOfDay(hour: 13, minute: 5), end: TimeOfDay(hour: 13, minute: 50)), - LessonPeriod(name: '8', start: TimeOfDay(hour: 14, minute: 5), end: TimeOfDay(hour: 14, minute: 50)), - LessonPeriod(name: '9', start: TimeOfDay(hour: 14, minute: 50), end: TimeOfDay(hour: 15, minute: 35)), - LessonPeriod(name: '10', start: TimeOfDay(hour: 15, minute: 40), end: TimeOfDay(hour: 16, minute: 25)), - LessonPeriod(name: '11', start: TimeOfDay(hour: 16, minute: 25), end: TimeOfDay(hour: 17, minute: 10)), - ]); + LessonPeriod( + name: '0', + start: TimeOfDay(hour: 7, minute: 10), + end: TimeOfDay(hour: 7, minute: 53), + ), + LessonPeriod( + name: '1', + start: TimeOfDay(hour: 7, minute: 55), + end: TimeOfDay(hour: 8, minute: 40), + ), + LessonPeriod( + name: '2', + start: TimeOfDay(hour: 8, minute: 40), + end: TimeOfDay(hour: 9, minute: 25), + ), + LessonPeriod( + name: '3', + start: TimeOfDay(hour: 9, minute: 30), + end: TimeOfDay(hour: 10, minute: 15), + ), + LessonPeriod( + name: '4', + start: TimeOfDay(hour: 10, minute: 35), + end: TimeOfDay(hour: 11, minute: 20), + ), + LessonPeriod( + name: '5', + start: TimeOfDay(hour: 11, minute: 25), + end: TimeOfDay(hour: 12, minute: 10), + ), + LessonPeriod( + name: '6', + start: TimeOfDay(hour: 12, minute: 15), + end: TimeOfDay(hour: 13, minute: 0), + ), + LessonPeriod( + name: '7', + start: TimeOfDay(hour: 13, minute: 5), + end: TimeOfDay(hour: 13, minute: 50), + ), + LessonPeriod( + name: '8', + start: TimeOfDay(hour: 14, minute: 5), + end: TimeOfDay(hour: 14, minute: 50), + ), + LessonPeriod( + name: '9', + start: TimeOfDay(hour: 14, minute: 50), + end: TimeOfDay(hour: 15, minute: 35), + ), + LessonPeriod( + name: '10', + start: TimeOfDay(hour: 15, minute: 40), + end: TimeOfDay(hour: 16, minute: 25), + ), + LessonPeriod( + name: '11', + start: TimeOfDay(hour: 16, minute: 25), + end: TimeOfDay(hour: 17, minute: 10), + ), + ]); static LessonPeriodSchedule fromState(TimetableState state) { - final fromApi = state.timegrid != null ? LessonPeriodSchedule.fromApi(state.timegrid!) : null; + final fromApi = state.timegrid != null + ? LessonPeriodSchedule.fromApi(state.timegrid!) + : null; return (fromApi ?? fallback()).withSyntheticBreaks(); } @@ -74,21 +129,22 @@ class LessonPeriodSchedule { result.add(current); if (i + 1 >= periods.length) continue; final next = periods[i + 1]; - final gapMinutes = next._startMinutes - (current.end.hour * 60 + current.end.minute); + final gapMinutes = + next._startMinutes - (current.end.hour * 60 + current.end.minute); if (gapMinutes >= 10) { - result.add(LessonPeriod( - name: 'Pause', - start: current.end, - end: next.start, - isBreak: true, - )); + result.add( + LessonPeriod( + name: 'Pause', + start: current.end, + end: next.start, + isBreak: true, + ), + ); } } return LessonPeriodSchedule(result); } - static TimeOfDay _fromHHMM(int hhmm) => TimeOfDay( - hour: hhmm ~/ 100, - minute: hhmm % 100, - ); + static TimeOfDay _fromHHMM(int hhmm) => + TimeOfDay(hour: hhmm ~/ 100, minute: hhmm % 100); } diff --git a/lib/view/pages/timetable/data/lesson_status.dart b/lib/view/pages/timetable/data/lesson_status.dart index 90e24d0..6f63937 100644 --- a/lib/view/pages/timetable/data/lesson_status.dart +++ b/lib/view/pages/timetable/data/lesson_status.dart @@ -20,10 +20,17 @@ class LessonStatusClassifier { }) { if (lesson.code == 'cancelled') return LessonStatus.cancelled; if (isEvent) return LessonStatus.event; - if (lesson.code == 'irregular' || (lesson.te.isNotEmpty && lesson.te.first.id == 0)) return LessonStatus.irregular; - if (lesson.te.any((t) => t.orgname != null)) return LessonStatus.teacherChanged; + if (lesson.code == 'irregular' || + (lesson.te.isNotEmpty && lesson.te.first.id == 0)) { + return LessonStatus.irregular; + } + if (lesson.te.any((t) => t.orgname != null)) { + return LessonStatus.teacherChanged; + } if (endTime.isBefore(now)) return LessonStatus.past; - if (startTime.isBefore(now) && endTime.isAfter(now)) return LessonStatus.ongoing; + if (startTime.isBefore(now) && endTime.isAfter(now)) { + return LessonStatus.ongoing; + } return LessonStatus.regular; } } diff --git a/lib/view/pages/timetable/data/timetable_appointment_factory.dart b/lib/view/pages/timetable/data/timetable_appointment_factory.dart index cdcb733..cbce1a1 100644 --- a/lib/view/pages/timetable/data/timetable_appointment_factory.dart +++ b/lib/view/pages/timetable/data/timetable_appointment_factory.dart @@ -31,7 +31,9 @@ class TimetableAppointmentFactory { }); List build() { - final source = settings.connectDoubleLessons ? _mergeAdjacentLessons(lessons) : lessons; + final source = settings.connectDoubleLessons + ? _mergeAdjacentLessons(lessons) + : lessons; return [ ...source.map(_lessonToAppointment), ...customEvents.map(_customEventToAppointment), @@ -42,7 +44,9 @@ class TimetableAppointmentFactory { try { final startTime = WebuntisTime.parse(lesson.date, lesson.startTime); final endTime = WebuntisTime.parse(lesson.date, lesson.endTime); - final subject = subjects.result.firstWhereOrNull((s) => s.id == lesson.su.firstOrNull?.id); + final subject = subjects.result.firstWhereOrNull( + (s) => s.id == lesson.su.firstOrNull?.id, + ); final status = LessonStatusClassifier.classify( lesson, startTime, @@ -81,16 +85,26 @@ class TimetableAppointmentFactory { id: CustomAppointment(event), startTime: event.startDate, endTime: allDay - ? DateTime(event.startDate.year, event.startDate.month, event.startDate.day, 23, 59) + ? DateTime( + event.startDate.year, + event.startDate.month, + event.startDate.day, + 23, + 59, + ) : event.endDate, isAllDay: allDay, // Preserve user-entered newlines in descriptions; the tile soft-wraps to // fill the available height. For lessons we still collapse whitespace // so room/teacher stay on one line each. - location: event.description.trim().isEmpty ? null : event.description.trim(), + location: event.description.trim().isEmpty + ? null + : event.description.trim(), subject: _collapseWhitespace(event.title) ?? event.title, recurrenceRule: event.rrule, - color: TimetableColors.getColorFromString(event.color ?? TimetableColors.defaultColor.name), + color: TimetableColors.getColorFromString( + event.color ?? TimetableColors.defaultColor.name, + ), startTimeZone: '', endTimeZone: '', ); @@ -114,7 +128,10 @@ class TimetableAppointmentFactory { e.second == 0; } - String _subjectName(GetTimetableResponseObject lesson, GetSubjectsResponseObject? subject) { + String _subjectName( + GetTimetableResponseObject lesson, + GetSubjectsResponseObject? subject, + ) { if (subject == null) return 'Event'; final name = switch (settings.timetableNameMode) { TimetableNameMode.name => subject.name, @@ -125,10 +142,15 @@ class TimetableAppointmentFactory { } String _locationLabel(GetTimetableResponseObject lesson) { - final roomName = _collapseWhitespace( - rooms.result.firstWhereOrNull((r) => r.id == lesson.ro.firstOrNull?.id)?.name) ?? + final roomName = + _collapseWhitespace( + rooms.result + .firstWhereOrNull((r) => r.id == lesson.ro.firstOrNull?.id) + ?.name, + ) ?? 'Unbekannt'; - final teacherName = _collapseWhitespace(lesson.te.firstOrNull?.longname) ?? 'Unbekannt'; + final teacherName = + _collapseWhitespace(lesson.te.firstOrNull?.longname) ?? 'Unbekannt'; return '$roomName\n$teacherName'; } @@ -161,8 +183,13 @@ class TimetableAppointmentFactory { }) { if (input.isEmpty) return const []; - final sorted = [...input]..sort((a, b) => - WebuntisTime.parse(a.date, a.startTime).compareTo(WebuntisTime.parse(b.date, b.startTime))); + final sorted = [...input] + ..sort( + (a, b) => WebuntisTime.parse( + a.date, + a.startTime, + ).compareTo(WebuntisTime.parse(b.date, b.startTime)), + ); final merged = []; for (final current in sorted) { @@ -180,10 +207,16 @@ class TimetableAppointmentFactory { static GetTimetableResponseObject _copyLesson(GetTimetableResponseObject l) => GetTimetableResponseObject.fromJson(l.toJson()); - static bool _canMerge(GetTimetableResponseObject a, GetTimetableResponseObject b, Duration maxGap) { + static bool _canMerge( + GetTimetableResponseObject a, + GetTimetableResponseObject b, + Duration maxGap, + ) { final aSubject = a.su.firstOrNull?.id; final bSubject = b.su.firstOrNull?.id; - if (aSubject == null || bSubject == null || aSubject != bSubject) return false; + if (aSubject == null || bSubject == null || aSubject != bSubject) { + return false; + } if (a.ro.firstOrNull?.id != b.ro.firstOrNull?.id) return false; if (a.te.firstOrNull?.id != b.te.firstOrNull?.id) return false; if (a.code != b.code) return false; @@ -193,7 +226,10 @@ class TimetableAppointmentFactory { // overlap in time would silently collapse into one — and because the // merge sets `previous.endTime = current.endTime`, an overlapping merge // can even truncate the earlier lesson. - final gap = WebuntisTime.parse(b.date, b.startTime).difference(WebuntisTime.parse(a.date, a.endTime)); + final gap = WebuntisTime.parse( + b.date, + b.startTime, + ).difference(WebuntisTime.parse(a.date, a.endTime)); return !gap.isNegative && gap <= maxGap; } } diff --git a/lib/view/pages/timetable/data/timetable_name_mode.dart b/lib/view/pages/timetable/data/timetable_name_mode.dart index 7e534a9..39e3e24 100644 --- a/lib/view/pages/timetable/data/timetable_name_mode.dart +++ b/lib/view/pages/timetable/data/timetable_name_mode.dart @@ -8,11 +8,20 @@ class TimetableNameModes { static DropdownDisplay getDisplayOptions(TimetableNameMode mode) { switch (mode) { case TimetableNameMode.name: - return DropdownDisplay(icon: Icons.device_unknown_outlined, displayName: 'Name'); + return DropdownDisplay( + icon: Icons.device_unknown_outlined, + displayName: 'Name', + ); case TimetableNameMode.longName: - return DropdownDisplay(icon: Icons.perm_device_info_outlined, displayName: 'Langname'); + return DropdownDisplay( + icon: Icons.perm_device_info_outlined, + displayName: 'Langname', + ); case TimetableNameMode.alternateName: - return DropdownDisplay(icon: Icons.on_device_training_outlined, displayName: 'Kurzform'); + return DropdownDisplay( + icon: Icons.on_device_training_outlined, + displayName: 'Kurzform', + ); } } } diff --git a/lib/view/pages/timetable/data/webuntis_time.dart b/lib/view/pages/timetable/data/webuntis_time.dart index bd9b6fa..da9ff04 100644 --- a/lib/view/pages/timetable/data/webuntis_time.dart +++ b/lib/view/pages/timetable/data/webuntis_time.dart @@ -5,7 +5,9 @@ class WebuntisTime { static DateTime parse(int date, int time) { final timeString = time.toString().padLeft(4, '0'); - return DateTime.parse('$date ${timeString.substring(0, 2)}:${timeString.substring(2, 4)}'); + return DateTime.parse( + '$date ${timeString.substring(0, 2)}:${timeString.substring(2, 4)}', + ); } static int formatDate(DateTime date) => int.parse(_dateFormat.format(date)); diff --git a/lib/view/pages/timetable/details/appointment_details_dispatcher.dart b/lib/view/pages/timetable/details/appointment_details_dispatcher.dart index 4a6ae76..f1ce427 100644 --- a/lib/view/pages/timetable/details/appointment_details_dispatcher.dart +++ b/lib/view/pages/timetable/details/appointment_details_dispatcher.dart @@ -7,12 +7,17 @@ import 'custom_event_sheet.dart'; import 'webuntis_lesson_sheet.dart'; class AppointmentDetailsDispatcher { - static void show(BuildContext context, TimetableBloc bloc, Appointment appointment) { + static void show( + BuildContext context, + TimetableBloc bloc, + Appointment appointment, + ) { final id = appointment.id; if (id is! ArbitraryAppointment) return; id.when( - webuntis: (lesson) => WebuntisLessonSheet.show(context, bloc, appointment, lesson), + webuntis: (lesson) => + WebuntisLessonSheet.show(context, bloc, appointment, lesson), custom: (event) => CustomEventSheet.show(context, event), ); } diff --git a/lib/view/pages/timetable/details/custom_event_sheet.dart b/lib/view/pages/timetable/details/custom_event_sheet.dart index 1615a9e..dc7b6d5 100644 --- a/lib/view/pages/timetable/details/custom_event_sheet.dart +++ b/lib/view/pages/timetable/details/custom_event_sheet.dart @@ -17,7 +17,10 @@ class CustomEventSheet { context, header: ListTile( leading: const Icon(Icons.event_outlined, size: 32), - title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold)), + title: Text( + event.title, + style: const TextStyle(fontWeight: FontWeight.bold), + ), subtitle: Text(timeRange), ), children: (sheetCtx) => [ @@ -31,7 +34,8 @@ class CustomEventSheet { Navigator.of(sheetCtx).pop(); showDialog( context: context, - builder: (_) => CustomEventEditDialog(existingEvent: event), + builder: (_) => + CustomEventEditDialog(existingEvent: event), ); }, label: const Text('Bearbeiten'), @@ -39,7 +43,9 @@ class CustomEventSheet { ), TextButton.icon( onPressed: () { - showDeleteCustomEventDialog(context, event).future.then((_) { + showDeleteCustomEventDialog(context, event).future.then(( + _, + ) { if (!sheetCtx.mounted) return; Navigator.of(sheetCtx).pop(); }); @@ -54,18 +60,28 @@ class CustomEventSheet { const Divider(height: 1), ListTile( leading: const Icon(Icons.info_outline), - title: Text(event.description.isEmpty ? 'Keine Beschreibung' : event.description), + title: Text( + event.description.isEmpty + ? 'Keine Beschreibung' + : event.description, + ), ), ListTile( leading: const CenteredLeading(Icon(Icons.repeat_outlined)), - title: Text('Serie: ${event.rrule.isNotEmpty ? "Wiederholend" : "Einmalig"}'), + title: Text( + 'Serie: ${event.rrule.isNotEmpty ? "Wiederholend" : "Einmalig"}', + ), subtitle: FutureBuilder( future: RruleL10nEn.create(), builder: (_, snapshot) { - if (event.rrule.isEmpty) return const Text('Keine weiteren Vorkommnisse'); + if (event.rrule.isEmpty) { + return const Text('Keine weiteren Vorkommnisse'); + } if (snapshot.data == null) return const Text('...'); final rrule = RecurrenceRule.fromString(event.rrule); - if (!rrule.canFullyConvertToText) return const Text('Keine genauere Angabe möglich.'); + if (!rrule.canFullyConvertToText) { + return const Text('Keine genauere Angabe möglich.'); + } return Text(rrule.toText(l10n: snapshot.data!)); }, ), diff --git a/lib/view/pages/timetable/details/delete_custom_event.dart b/lib/view/pages/timetable/details/delete_custom_event.dart index 1ada219..33d186c 100644 --- a/lib/view/pages/timetable/details/delete_custom_event.dart +++ b/lib/view/pages/timetable/details/delete_custom_event.dart @@ -7,12 +7,16 @@ import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart' import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../../../../widget/confirm_dialog.dart'; -Completer showDeleteCustomEventDialog(BuildContext context, CustomTimetableEvent event) { +Completer showDeleteCustomEventDialog( + BuildContext context, + CustomTimetableEvent event, +) { final completer = Completer(); final bloc = context.read(); ConfirmDialog( title: 'Termin löschen', - content: 'Der ${event.rrule.isEmpty ? "Termin" : "Serientermin"} wird unwiederruflich gelöscht.', + content: + 'Der ${event.rrule.isEmpty ? "Termin" : "Serientermin"} wird unwiederruflich gelöscht.', confirmButton: 'Löschen', onConfirmAsync: () async { await bloc.removeCustomEvent(event.id); diff --git a/lib/view/pages/timetable/details/webuntis_lesson_sheet.dart b/lib/view/pages/timetable/details/webuntis_lesson_sheet.dart index afd3230..a5cd101 100644 --- a/lib/view/pages/timetable/details/webuntis_lesson_sheet.dart +++ b/lib/view/pages/timetable/details/webuntis_lesson_sheet.dart @@ -14,13 +14,30 @@ import '../../../../widget/details_bottom_sheet.dart'; import '../../../../widget/unimplemented_dialog.dart'; class WebuntisLessonSheet { - static void show(BuildContext context, TimetableBloc bloc, Appointment appointment, GetTimetableResponseObject lesson) { + static void show( + BuildContext context, + TimetableBloc bloc, + Appointment appointment, + GetTimetableResponseObject lesson, + ) { final state = bloc.state.data; if (state == null) return; - final headerSubject = LessonResolver.resolveSubject(state, lesson.su.firstOrNull?.id); - final headerTitle = firstNonEmpty([headerSubject.alternateName, headerSubject.name, headerSubject.longName, '?']); - final headerLongName = headerSubject.longName.isNotEmpty && headerSubject.longName != headerTitle ? headerSubject.longName : ''; + final headerSubject = LessonResolver.resolveSubject( + state, + lesson.su.firstOrNull?.id, + ); + final headerTitle = firstNonEmpty([ + headerSubject.alternateName, + headerSubject.name, + headerSubject.longName, + '?', + ]); + final headerLongName = + headerSubject.longName.isNotEmpty && + headerSubject.longName != headerTitle + ? headerSubject.longName + : ''; final timeRange = appointment.startTime.timeRangeTo(appointment.endTime); @@ -32,9 +49,9 @@ class WebuntisLessonSheet { '${LessonFormatter.codePrefix(lesson.code)}$headerTitle', style: const TextStyle(fontWeight: FontWeight.bold), ), - subtitle: Text(headerLongName.isNotEmpty - ? '$timeRange\n$headerLongName' - : timeRange), + subtitle: Text( + headerLongName.isNotEmpty ? '$timeRange\n$headerLongName' : timeRange, + ), isThreeLine: headerLongName.isNotEmpty, ), children: (_) => [ @@ -66,10 +83,12 @@ class WebuntisLessonSheet { icon: Icons.people, label: lesson.kl.length == 1 ? 'Klasse' : 'Klassen', entries: lesson.kl - .map((k) => LessonFormatter.formatLine( - k.name.isNotEmpty ? k.name : '?', - longname: k.longname, - )) + .map( + (k) => LessonFormatter.formatLine( + k.name.isNotEmpty ? k.name : '?', + longname: k.longname, + ), + ) .toList(), ), ..._optionalTextTiles(lesson), @@ -78,7 +97,11 @@ class WebuntisLessonSheet { ); } - static Widget _roomTile(BuildContext context, TimetableState state, GetTimetableResponseObject lesson) { + static Widget _roomTile( + BuildContext context, + TimetableState state, + GetTimetableResponseObject lesson, + ) { final trailing = IconButton( icon: const Icon(Icons.house_outlined), onPressed: () => AppRoutes.openRoomplan(context), @@ -112,7 +135,10 @@ class WebuntisLessonSheet { ); } - static Widget _teacherTile(BuildContext context, GetTimetableResponseObject lesson) { + static Widget _teacherTile( + BuildContext context, + GetTimetableResponseObject lesson, + ) { final trailing = Visibility( visible: !kReleaseMode, child: IconButton( diff --git a/lib/view/pages/timetable/timetable.dart b/lib/view/pages/timetable/timetable.dart index ffced37..6b7c8f3 100644 --- a/lib/view/pages/timetable/timetable.dart +++ b/lib/view/pages/timetable/timetable.dart @@ -27,7 +27,8 @@ class Timetable extends StatefulWidget { } class _TimetableState extends State { - final GlobalKey _calendarKey = GlobalKey(); + final GlobalKey _calendarKey = + GlobalKey(); List? _cachedAppointments; int? _lastDataVersion; @@ -53,7 +54,10 @@ class _TimetableState extends State { } List _appointments(TimetableState state) { - final timetableSettings = context.watch().val().timetableSettings; + final timetableSettings = context + .watch() + .val() + .timetableSettings; if (_cachedAppointments != null && _lastDataVersion == state.dataVersion && identical(_lastTimetableSettings, timetableSettings)) { @@ -81,7 +85,11 @@ class _TimetableState extends State { bool _isOnInitialWeek(TimetableState state) { final target = _initialDisplayDate(); final targetMonday = target.subtract(Duration(days: target.weekday - 1)); - final mondayOnly = DateTime(targetMonday.year, targetMonday.month, targetMonday.day); + final mondayOnly = DateTime( + targetMonday.year, + targetMonday.month, + targetMonday.day, + ); return state.startDate == mondayOnly; } @@ -105,7 +113,10 @@ class _TimetableState extends State { itemBuilder: (_) => const [ PopupMenuItem( value: _CalendarAction.addEvent, - child: ListTile(title: Text('Kalendereintrag hinzufügen'), leading: Icon(Icons.add)), + child: ListTile( + title: Text('Kalendereintrag hinzufügen'), + leading: Icon(Icons.add), + ), ), PopupMenuItem( value: _CalendarAction.viewEvents, @@ -142,9 +153,14 @@ class _TimetableState extends State { appointments: appointments, timeRegions: regions, initialDate: _initialDisplayDate(), - minDate: DateTime.now().subtract(const Duration(days: 14)).nextWeekday(DateTime.sunday), - maxDate: DateTime.now().add(const Duration(days: 7)).nextWeekday(DateTime.saturday), - onAppointmentTap: (apt) => AppointmentDetailsDispatcher.show(context, bloc, apt), + minDate: DateTime.now() + .subtract(const Duration(days: 14)) + .nextWeekday(DateTime.sunday), + maxDate: DateTime.now() + .add(const Duration(days: 7)) + .nextWeekday(DateTime.saturday), + onAppointmentTap: (apt) => + AppointmentDetailsDispatcher.show(context, bloc, apt), onWeekChanged: (start, end) => bloc.changeWeek(start, end), isCrossedOut: _isCrossedOut, onCreateEvent: _onCreateEventAt, @@ -154,7 +170,8 @@ class _TimetableState extends State { void _onCreateEventAt(DateTime start, DateTime end) { showDialog( context: context, - builder: (_) => CustomEventEditDialog(initialStart: start, initialEnd: end), + builder: (_) => + CustomEventEditDialog(initialStart: start, initialEnd: end), barrierDismissible: false, ); } diff --git a/lib/view/pages/timetable/widgets/appointment_tile.dart b/lib/view/pages/timetable/widgets/appointment_tile.dart index 940b1d0..61d08c0 100644 --- a/lib/view/pages/timetable/widgets/appointment_tile.dart +++ b/lib/view/pages/timetable/widgets/appointment_tile.dart @@ -11,7 +11,11 @@ class AppointmentTile extends StatelessWidget { final Appointment appointment; final bool crossedOut; - const AppointmentTile({super.key, required this.appointment, this.crossedOut = false}); + const AppointmentTile({ + super.key, + required this.appointment, + this.crossedOut = false, + }); @override Widget build(BuildContext context) { @@ -56,11 +60,15 @@ class AppointmentTile extends StatelessWidget { ), ), ] else ...[ - for (final line in description - .split('\n') - .where((p) => p.isNotEmpty) - .take(2)) - _ScaledLine(text: line, fontSize: kAppointmentBodyFontSize), + for (final line + in description + .split('\n') + .where((p) => p.isNotEmpty) + .take(2)) + _ScaledLine( + text: line, + fontSize: kAppointmentBodyFontSize, + ), ], ], ), @@ -72,7 +80,10 @@ class AppointmentTile extends StatelessWidget { borderRadius: _radius, child: DecoratedBox( decoration: BoxDecoration( - border: Border.all(width: 2, color: Colors.red.withAlpha(200)), + border: Border.all( + width: 2, + color: Colors.red.withAlpha(200), + ), borderRadius: _radius, ), child: CustomPaint(painter: CrossPainter()), @@ -114,7 +125,10 @@ class _AdaptiveTitle extends StatelessWidget { builder: (context, constraints) { // Probe at the minimum size: if even that overflows, we have to ellipsize. final probe = TextPainter( - text: TextSpan(text: text, style: baseStyle.copyWith(fontSize: minFontSize)), + text: TextSpan( + text: text, + style: baseStyle.copyWith(fontSize: minFontSize), + ), textDirection: TextDirection.ltr, maxLines: 1, textScaler: textScaler, @@ -131,12 +145,7 @@ class _AdaptiveTitle extends StatelessWidget { return FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, - child: Text( - text, - style: baseStyle, - maxLines: 1, - softWrap: false, - ), + child: Text(text, style: baseStyle, maxLines: 1, softWrap: false), ); }, ); @@ -187,24 +196,17 @@ class _ScaledLine extends StatelessWidget { final String text; final double fontSize; - const _ScaledLine({ - required this.text, - required this.fontSize, - }); + const _ScaledLine({required this.text, required this.fontSize}); @override Widget build(BuildContext context) => FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - text, - style: TextStyle( - color: Colors.white, - fontSize: fontSize, - height: 1.1, - ), - maxLines: 1, - softWrap: false, - ), - ); + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + text, + style: TextStyle(color: Colors.white, fontSize: fontSize, height: 1.1), + maxLines: 1, + softWrap: false, + ), + ); } diff --git a/lib/view/pages/timetable/widgets/calendar/day_header.dart b/lib/view/pages/timetable/widgets/calendar/day_header.dart index 3bd683d..5f1ca7f 100644 --- a/lib/view/pages/timetable/widgets/calendar/day_header.dart +++ b/lib/view/pages/timetable/widgets/calendar/day_header.dart @@ -14,17 +14,17 @@ class _DayHeaderStrip extends StatelessWidget { @override Widget build(BuildContext context) => Row( - children: [ - SizedBox(width: rulerWidth), - for (var d = 0; d < 5; d++) - Expanded( - child: _DayHeaderCell( - date: weekStart.add(Duration(days: d)), - today: today, - ), - ), - ], - ); + children: [ + SizedBox(width: rulerWidth), + for (var d = 0; d < 5; d++) + Expanded( + child: _DayHeaderCell( + date: weekStart.add(Duration(days: d)), + today: today, + ), + ), + ], + ); } class _DayHeaderCell extends StatelessWidget { @@ -37,7 +37,10 @@ class _DayHeaderCell extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final isToday = date.isSameDay(today); - final dayName = DateFormat('EE', Localizations.localeOf(context).toString()).format(date).toUpperCase(); + final dayName = DateFormat( + 'EE', + Localizations.localeOf(context).toString(), + ).format(date).toUpperCase(); final accent = theme.colorScheme.primary; final onAccent = theme.colorScheme.onPrimary; diff --git a/lib/view/pages/timetable/widgets/calendar/outside_chips.dart b/lib/view/pages/timetable/widgets/calendar/outside_chips.dart index b861f78..360c780 100644 --- a/lib/view/pages/timetable/widgets/calendar/outside_chips.dart +++ b/lib/view/pages/timetable/widgets/calendar/outside_chips.dart @@ -18,20 +18,30 @@ class _OutsideHoursStrip extends StatelessWidget { @override Widget build(BuildContext context) { - final outside = partitionAppointmentsForWeek(appointments, weekStart).outside; + final outside = partitionAppointmentsForWeek( + appointments, + weekStart, + ).outside; if (outside.every((day) => day.isEmpty)) return const SizedBox.shrink(); final theme = Theme.of(context); final maxChipsPerDay = outside - .map((day) => day.length > kOutsideChipsMaxVisible ? kOutsideChipsMaxVisible : day.length) + .map( + (day) => day.length > kOutsideChipsMaxVisible + ? kOutsideChipsMaxVisible + : day.length, + ) .fold(0, (m, c) => c > m ? c : m); - final stripHeight = kOutsideStripVerticalPadding * 2 + + final stripHeight = + kOutsideStripVerticalPadding * 2 + maxChipsPerDay * kOutsideChipHeight + (maxChipsPerDay > 1 ? (maxChipsPerDay - 1) * kOutsideChipSpacing : 0); return Container( color: theme.colorScheme.surfaceContainerLowest, - padding: const EdgeInsets.symmetric(vertical: kOutsideStripVerticalPadding), + padding: const EdgeInsets.symmetric( + vertical: kOutsideStripVerticalPadding, + ), child: SizedBox( height: stripHeight - kOutsideStripVerticalPadding * 2, child: Row( @@ -72,27 +82,29 @@ class _OutsideDayColumn extends StatelessWidget { for (var i = 0; i < hidden.length; i++) { if (i > 0) tiles.add(const Divider(height: 1)); final apt = hidden[i]; - tiles.add(ListTile( - leading: Container( - width: 12, - height: 12, - decoration: BoxDecoration( - color: apt.color, - borderRadius: BorderRadius.circular(3), + tiles.add( + ListTile( + leading: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: apt.color, + borderRadius: BorderRadius.circular(3), + ), ), + title: Text( + apt.subject, + style: isCrossedOut(apt) + ? const TextStyle(decoration: TextDecoration.lineThrough) + : null, + ), + subtitle: Text(_subtitleFor(apt)), + onTap: () { + Navigator.of(sheetCtx).pop(); + onAppointmentTap(apt); + }, ), - title: Text( - apt.subject, - style: isCrossedOut(apt) - ? const TextStyle(decoration: TextDecoration.lineThrough) - : null, - ), - subtitle: Text(_subtitleFor(apt)), - onTap: () { - Navigator.of(sheetCtx).pop(); - onAppointmentTap(apt); - }, - )); + ); } return tiles; }, diff --git a/lib/view/pages/timetable/widgets/calendar/week_grid.dart b/lib/view/pages/timetable/widgets/calendar/week_grid.dart index 4cb7762..132298f 100644 --- a/lib/view/pages/timetable/widgets/calendar/week_grid.dart +++ b/lib/view/pages/timetable/widgets/calendar/week_grid.dart @@ -34,11 +34,7 @@ class _WeekGrid extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _PeriodRuler( - schedule: schedule, - layout: layout, - width: rulerWidth, - ), + _PeriodRuler(schedule: schedule, layout: layout, width: rulerWidth), for (var d = 0; d < 5; d++) Expanded( child: _DayColumn( @@ -112,7 +108,11 @@ class _PeriodLabel extends StatelessWidget { ), ), alignment: Alignment.center, - child: Icon(Icons.coffee_outlined, size: 12, color: secondaryTextColor.withAlpha(180)), + child: Icon( + Icons.coffee_outlined, + size: 12, + color: secondaryTextColor.withAlpha(180), + ), ); } @@ -207,27 +207,49 @@ class _DayColumn extends StatelessWidget { required this.onCreateEvent, }); - bool _overlapsExistingAppointment(DateTime start, DateTime end, List dayAppts) { + bool _overlapsExistingAppointment( + DateTime start, + DateTime end, + List dayAppts, + ) { for (final a in dayAppts) { if (a.endTime.isAfter(start) && a.startTime.isBefore(end)) return true; } return false; } - void _handleLongPress(LongPressStartDetails details, List dayAppts) { + void _handleLongPress( + LongPressStartDetails details, + List dayAppts, + ) { if (onCreateEvent == null) return; final period = layout.periodAtY(details.localPosition.dy); if (period == null) return; - final start = DateTime(date.year, date.month, date.day, period.start.hour, period.start.minute); - final end = DateTime(date.year, date.month, date.day, period.end.hour, period.end.minute); + final start = DateTime( + date.year, + date.month, + date.day, + period.start.hour, + period.start.minute, + ); + final end = DateTime( + date.year, + date.month, + date.day, + period.end.hour, + period.end.minute, + ); if (_overlapsExistingAppointment(start, end, dayAppts)) return; HapticFeedback.mediumImpact(); onCreateEvent!(start, end); } - void _showOverflowSheet(BuildContext context, List appointments) { + void _showOverflowSheet( + BuildContext context, + List appointments, + ) { final sorted = [...appointments] ..sort((a, b) => a.startTime.compareTo(b.startTime)); showDetailsBottomSheet( @@ -237,27 +259,29 @@ class _DayColumn extends StatelessWidget { for (var i = 0; i < sorted.length; i++) { if (i > 0) tiles.add(const Divider(height: 1)); final apt = sorted[i]; - tiles.add(ListTile( - leading: Container( - width: 12, - height: 12, - decoration: BoxDecoration( - color: apt.color, - borderRadius: BorderRadius.circular(3), + tiles.add( + ListTile( + leading: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: apt.color, + borderRadius: BorderRadius.circular(3), + ), ), + title: Text( + apt.subject, + style: isCrossedOut(apt) + ? const TextStyle(decoration: TextDecoration.lineThrough) + : null, + ), + subtitle: Text(_overflowSubtitle(apt)), + onTap: () { + Navigator.of(sheetContext).pop(); + onAppointmentTap(apt); + }, ), - title: Text( - apt.subject, - style: isCrossedOut(apt) - ? const TextStyle(decoration: TextDecoration.lineThrough) - : null, - ), - subtitle: Text(_overflowSubtitle(apt)), - onTap: () { - Navigator.of(sheetContext).pop(); - onAppointmentTap(apt); - }, - )); + ); } return tiles; }, @@ -288,46 +312,53 @@ class _DayColumn extends StatelessWidget { behavior: HitTestBehavior.translucent, onLongPressStart: (details) => _handleLongPress(details, dayAppointments), child: DecoratedBox( - decoration: BoxDecoration( - color: isToday ? theme.colorScheme.primary.withAlpha(14) : null, - border: Border(left: BorderSide(color: theme.dividerColor.withAlpha(90), width: 0.5)), - ), - child: LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth; - return Stack( - clipBehavior: Clip.none, - children: [ - for (final period in schedule.periods) - Positioned( - top: layout.topOf(period), - left: 0, - right: 0, - child: Container( - height: 0.5, - color: theme.dividerColor.withAlpha(60), + decoration: BoxDecoration( + color: isToday ? theme.colorScheme.primary.withAlpha(14) : null, + border: Border( + left: BorderSide( + color: theme.dividerColor.withAlpha(90), + width: 0.5, + ), + ), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + return Stack( + clipBehavior: Clip.none, + children: [ + for (final period in schedule.periods) + Positioned( + top: layout.topOf(period), + left: 0, + right: 0, + child: Container( + height: 0.5, + color: theme.dividerColor.withAlpha(60), + ), ), - ), - for (final region in dayRegions) - Positioned( - top: layout.yOfDateTime(region.start), - height: (layout.yOfDateTime(region.end) - - layout.yOfDateTime(region.start)) - .clamp(0, double.infinity), - left: 0, - right: 0, - child: TimeRegionTile(region: region.region), - ), - for (final cell in laidOut) - Positioned( - top: layout.yOfDateTime(cell.startTime), - height: (layout.yOfDateTime(cell.endTime) - - layout.yOfDateTime(cell.startTime)) - .clamp(0, double.infinity), - left: cell.lane * width / cell.laneCount, - width: width / cell.laneCount, - child: switch (cell) { - LaidOutAppointment(:final appointment) => GestureDetector( + for (final region in dayRegions) + Positioned( + top: layout.yOfDateTime(region.start), + height: + (layout.yOfDateTime(region.end) - + layout.yOfDateTime(region.start)) + .clamp(0, double.infinity), + left: 0, + right: 0, + child: TimeRegionTile(region: region.region), + ), + for (final cell in laidOut) + Positioned( + top: layout.yOfDateTime(cell.startTime), + height: + (layout.yOfDateTime(cell.endTime) - + layout.yOfDateTime(cell.startTime)) + .clamp(0, double.infinity), + left: cell.lane * width / cell.laneCount, + width: width / cell.laneCount, + child: switch (cell) { + LaidOutAppointment(:final appointment) => GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => onAppointmentTap(appointment), child: AppointmentTile( @@ -335,25 +366,27 @@ class _DayColumn extends StatelessWidget { crossedOut: isCrossedOut(appointment), ), ), - LaidOutOverflow(:final appointments) => GestureDetector( + LaidOutOverflow(:final appointments) => GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => - _showOverflowSheet(context, appointments), + onTap: () => _showOverflowSheet(context, appointments), child: _OverflowTile(count: appointments.length), ), - }, - ), - if (isToday) - ValueListenableBuilder( - valueListenable: nowNotifier, - builder: (_, now, child) => - _CurrentTimeMarker(now: now, layout: layout, theme: theme), - ), - ], - ); - }, + }, + ), + if (isToday) + ValueListenableBuilder( + valueListenable: nowNotifier, + builder: (_, now, child) => _CurrentTimeMarker( + now: now, + layout: layout, + theme: theme, + ), + ), + ], + ); + }, + ), ), - ), ); } } @@ -376,8 +409,7 @@ class _CurrentTimeMarker extends StatelessWidget { final tMin = now.hour * 60 + now.minute; final firstStart = periods.first.start.hour * 60 + periods.first.start.minute; - final lastEnd = - periods.last.end.hour * 60 + periods.last.end.minute; + final lastEnd = periods.last.end.hour * 60 + periods.last.end.minute; if (tMin < firstStart || tMin > lastEnd) return const SizedBox.shrink(); final y = layout.yOfDateTime(now); @@ -392,10 +424,7 @@ class _CurrentTimeMarker extends StatelessWidget { child: Stack( clipBehavior: Clip.none, children: [ - Container( - height: 2, - color: theme.colorScheme.primary, - ), + Container(height: 2, color: theme.colorScheme.primary), Positioned( top: -3, left: -4, @@ -456,7 +485,10 @@ class _OverflowTile extends StatelessWidget { child: FittedBox( fit: BoxFit.scaleDown, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/view/pages/timetable/widgets/custom_workweek_calendar.dart b/lib/view/pages/timetable/widgets/custom_workweek_calendar.dart index 642618f..b8f0a5c 100644 --- a/lib/view/pages/timetable/widgets/custom_workweek_calendar.dart +++ b/lib/view/pages/timetable/widgets/custom_workweek_calendar.dart @@ -72,7 +72,8 @@ class CustomWorkWeekCalendarState extends State { _firstMonday = _mondayOf(widget.minDate); final lastMonday = _mondayOf(widget.maxDate); _totalWeeks = lastMonday.difference(_firstMonday).inDays ~/ 7 + 1; - _currentWeekIndex = _mondayOf(widget.initialDate).difference(_firstMonday).inDays ~/ 7; + _currentWeekIndex = + _mondayOf(widget.initialDate).difference(_firstMonday).inDays ~/ 7; _pageController = PageController(initialPage: _currentWeekIndex); _nowNotifier = ValueNotifier(DateTime.now()); @@ -113,7 +114,9 @@ class CustomWorkWeekCalendarState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final visibleWeekStart = _firstMonday.add(Duration(days: _currentWeekIndex * 7)); + final visibleWeekStart = _firstMonday.add( + Duration(days: _currentWeekIndex * 7), + ); return Column( children: [ @@ -168,13 +171,13 @@ class CustomWorkWeekCalendarState extends State { child: LayoutBuilder( builder: (context, constraints) { final periods = widget.schedule.periods; - final lessonCount = - periods.where((p) => !p.isBreak).length; + final lessonCount = periods.where((p) => !p.isBreak).length; final breakCount = periods.length - lessonCount; final available = constraints.maxHeight - breakCount * kBreakBlockHeight; - final fitLessonH = - lessonCount > 0 ? available / lessonCount : kLessonBlockMinHeight; + final fitLessonH = lessonCount > 0 + ? available / lessonCount + : kLessonBlockMinHeight; final lessonH = fitLessonH < kLessonBlockMinHeight ? kLessonBlockMinHeight : fitLessonH; @@ -194,11 +197,18 @@ class CustomWorkWeekCalendarState extends State { itemCount: _totalWeeks, onPageChanged: (index) { setState(() => _currentWeekIndex = index); - final weekStart = _firstMonday.add(Duration(days: index * 7)); - widget.onWeekChanged(weekStart, weekStart.add(const Duration(days: 4))); + final weekStart = _firstMonday.add( + Duration(days: index * 7), + ); + widget.onWeekChanged( + weekStart, + weekStart.add(const Duration(days: 4)), + ); }, itemBuilder: (_, weekIndex) { - final weekStart = _firstMonday.add(Duration(days: weekIndex * 7)); + final weekStart = _firstMonday.add( + Duration(days: weekIndex * 7), + ); return _WeekGrid( weekStart: weekStart, schedule: widget.schedule, diff --git a/lib/view/pages/timetable/widgets/special_regions_builder.dart b/lib/view/pages/timetable/widgets/special_regions_builder.dart index bffdfb7..02a06be 100644 --- a/lib/view/pages/timetable/widgets/special_regions_builder.dart +++ b/lib/view/pages/timetable/widgets/special_regions_builder.dart @@ -22,45 +22,61 @@ class SpecialRegionsBuilder { }); List build() { - final lastMonday = DateTime.now().subtract(const Duration(days: 14)).nextWeekday(DateTime.monday); + final lastMonday = DateTime.now() + .subtract(const Duration(days: 14)) + .nextWeekday(DateTime.monday); final holidayRegions = _buildHolidayRegions().toList(); - bool isInHoliday(DateTime time) => holidayRegions.any((region) => region.startTime.isSameDay(time)); + bool isInHoliday(DateTime time) => + holidayRegions.any((region) => region.startTime.isSameDay(time)); - final breakRegions = schedule.periods.where((p) => p.isBreak).map((p) { - final start = lastMonday.copyWith(hour: p.start.hour, minute: p.start.minute); - return _breakRegion(start, p.duration); - }).where((region) => !isInHoliday(region.startTime)); + final breakRegions = schedule.periods + .where((p) => p.isBreak) + .map((p) { + final start = lastMonday.copyWith( + hour: p.start.hour, + minute: p.start.minute, + ); + return _breakRegion(start, p.duration); + }) + .where((region) => !isInHoliday(region.startTime)); - return [ - ...holidayRegions, - ...breakRegions, - ]; + return [...holidayRegions, ...breakRegions]; } - Iterable _buildHolidayRegions() => holidays.result.expand((holiday) { - final startDay = WebuntisTime.parse(holiday.startDate, 0); - final dayCount = WebuntisTime.parse(holiday.endDate, 0).difference(startDay).inDays; - final days = List.generate(dayCount, (i) => startDay.add(Duration(days: i))); - final gridStartHour = kCalendarStartHour.floor(); - final gridStartMinute = ((kCalendarStartHour - gridStartHour) * 60).round(); - final gridEndHour = kCalendarEndHour.floor(); - final gridEndMinute = ((kCalendarEndHour - gridEndHour) * 60).round(); - return days.map((day) => TimeRegion( - startTime: day.copyWith(hour: gridStartHour, minute: gridStartMinute), - endTime: day.copyWith(hour: gridEndHour, minute: gridEndMinute), - text: '$kTimeRegionHolidayPrefix${holiday.name}', - color: disabledColor.withAlpha(50), - iconData: Icons.holiday_village_outlined, - )); - }); + Iterable _buildHolidayRegions() => holidays.result.expand(( + holiday, + ) { + final startDay = WebuntisTime.parse(holiday.startDate, 0); + final dayCount = WebuntisTime.parse( + holiday.endDate, + 0, + ).difference(startDay).inDays; + final days = List.generate( + dayCount, + (i) => startDay.add(Duration(days: i)), + ); + final gridStartHour = kCalendarStartHour.floor(); + final gridStartMinute = ((kCalendarStartHour - gridStartHour) * 60).round(); + final gridEndHour = kCalendarEndHour.floor(); + final gridEndMinute = ((kCalendarEndHour - gridEndHour) * 60).round(); + return days.map( + (day) => TimeRegion( + startTime: day.copyWith(hour: gridStartHour, minute: gridStartMinute), + endTime: day.copyWith(hour: gridEndHour, minute: gridEndMinute), + text: '$kTimeRegionHolidayPrefix${holiday.name}', + color: disabledColor.withAlpha(50), + iconData: Icons.holiday_village_outlined, + ), + ); + }); TimeRegion _breakRegion(DateTime start, Duration duration) => TimeRegion( - startTime: start, - endTime: start.add(duration), - recurrenceRule: 'FREQ=DAILY;INTERVAL=1', - text: kTimeRegionCenterIcon, - color: colorScheme.primary.withAlpha(50), - iconData: Icons.restaurant, - ); + startTime: start, + endTime: start.add(duration), + recurrenceRule: 'FREQ=DAILY;INTERVAL=1', + text: kTimeRegionCenterIcon, + color: colorScheme.primary.withAlpha(50), + iconData: Icons.restaurant, + ); } diff --git a/lib/view/pages/timetable/widgets/time_region_tile.dart b/lib/view/pages/timetable/widgets/time_region_tile.dart index 9fa946c..292e170 100644 --- a/lib/view/pages/timetable/widgets/time_region_tile.dart +++ b/lib/view/pages/timetable/widgets/time_region_tile.dart @@ -18,7 +18,11 @@ class TimeRegionTile extends StatelessWidget { return Container( color: color, alignment: Alignment.center, - child: Icon(region.iconData, size: 17, color: Theme.of(context).colorScheme.primary), + child: Icon( + region.iconData, + size: 17, + color: Theme.of(context).colorScheme.primary, + ), ); } diff --git a/lib/widget/about/about.dart b/lib/widget/about/about.dart index 17003f2..0d1d30e 100644 --- a/lib/widget/about/about.dart +++ b/lib/widget/about/about.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; class About extends StatelessWidget { @@ -6,13 +5,11 @@ class About extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Über diese App'), - ), - body: const Card( - elevation: 1, - borderOnForeground: true, - child: Text('Marianum Fulda'), - ), - ); + appBar: AppBar(title: const Text('Über diese App')), + body: const Card( + elevation: 1, + borderOnForeground: true, + child: Text('Marianum Fulda'), + ), + ); } diff --git a/lib/widget/animated_time.dart b/lib/widget/animated_time.dart index 2c00b18..1ba4ccd 100644 --- a/lib/widget/animated_time.dart +++ b/lib/widget/animated_time.dart @@ -29,30 +29,43 @@ class _AnimatedTimeState extends State { @override Widget build(BuildContext context) => Row( - children: [ - const Text('Noch '), - buildWidget(current.inDays), - const Text(' Tage, '), - buildWidget(current.inHours > 24 ? current.inHours - current.inDays * 24 : current.inHours), - const Text(':'), - buildWidget(current.inMinutes > 60 ? current.inMinutes - current.inHours * 60 : current.inMinutes), - const Text(':'), - buildWidget(current.inSeconds > 60 ? current.inSeconds - current.inMinutes * 60 : current.inSeconds), - ], - ); + children: [ + const Text('Noch '), + buildWidget(current.inDays), + const Text(' Tage, '), + buildWidget( + current.inHours > 24 + ? current.inHours - current.inDays * 24 + : current.inHours, + ), + const Text(':'), + buildWidget( + current.inMinutes > 60 + ? current.inMinutes - current.inHours * 60 + : current.inMinutes, + ), + const Text(':'), + buildWidget( + current.inSeconds > 60 + ? current.inSeconds - current.inMinutes * 60 + : current.inSeconds, + ), + ], + ); Widget buildWidget(int value) => AnimatedSwitcher( - duration: const Duration(milliseconds: 100), - transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), - child: Text( - '$value', - key: ValueKey(value), - style: TextStyle( - fontSize: 15, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ); + duration: const Duration(milliseconds: 100), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: Text( + '$value', + key: ValueKey(value), + style: TextStyle( + fontSize: 15, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ); @override void dispose() { diff --git a/lib/widget/app_progress_indicator.dart b/lib/widget/app_progress_indicator.dart index 643096f..c383687 100644 --- a/lib/widget/app_progress_indicator.dart +++ b/lib/widget/app_progress_indicator.dart @@ -12,13 +12,13 @@ class AppProgressIndicator extends StatelessWidget { }); const AppProgressIndicator.small({Color? color}) - : this._(size: 16, strokeWidth: 2, color: color); + : this._(size: 16, strokeWidth: 2, color: color); const AppProgressIndicator.medium({Color? color}) - : this._(size: 24, strokeWidth: 2.5, color: color); + : this._(size: 24, strokeWidth: 2.5, color: color); const AppProgressIndicator.large({Color? color}) - : this._(size: 40, strokeWidth: 3, color: color); + : this._(size: 40, strokeWidth: 3, color: color); @override Widget build(BuildContext context) { diff --git a/lib/widget/async_actions/async_action_button.dart b/lib/widget/async_actions/async_action_button.dart index 29367b2..51e4373 100644 --- a/lib/widget/async_actions/async_action_button.dart +++ b/lib/widget/async_actions/async_action_button.dart @@ -26,33 +26,33 @@ class AsyncActionButton extends StatelessWidget { @override Widget build(BuildContext context) => _AsyncMixin( - onPressed: onPressed, - controller: controller, - errorBuilder: errorBuilder, - onError: onError, - onSuccess: onSuccess, - builder: (context, busy, handler) { - final spinner = AppProgressIndicator.small( - color: Theme.of(context).colorScheme.onPrimary, - ); - final content = busy - ? Row( - mainAxisSize: MainAxisSize.min, - children: [spinner, const SizedBox(width: 8), child], - ) - : (icon != null - ? Row( - mainAxisSize: MainAxisSize.min, - children: [Icon(icon), const SizedBox(width: 8), child], - ) - : child); - final button = ElevatedButton( - onPressed: handler, - style: style, - child: content, - ); - if (!showInlineError) return button; - return _InlineErrorWrapper(controller: controller, child: button); - }, + onPressed: onPressed, + controller: controller, + errorBuilder: errorBuilder, + onError: onError, + onSuccess: onSuccess, + builder: (context, busy, handler) { + final spinner = AppProgressIndicator.small( + color: Theme.of(context).colorScheme.onPrimary, ); + final content = busy + ? Row( + mainAxisSize: MainAxisSize.min, + children: [spinner, const SizedBox(width: 8), child], + ) + : (icon != null + ? Row( + mainAxisSize: MainAxisSize.min, + children: [Icon(icon), const SizedBox(width: 8), child], + ) + : child); + final button = ElevatedButton( + onPressed: handler, + style: style, + child: content, + ); + if (!showInlineError) return button; + return _InlineErrorWrapper(controller: controller, child: button); + }, + ); } diff --git a/lib/widget/async_actions/async_action_controller.dart b/lib/widget/async_actions/async_action_controller.dart index 8b63cbe..2046c22 100644 --- a/lib/widget/async_actions/async_action_controller.dart +++ b/lib/widget/async_actions/async_action_controller.dart @@ -16,9 +16,13 @@ Future runWithErrorDialog( return true; } catch (e) { if (!context.mounted) return false; - final message = errorBuilder != null ? errorBuilder(e) : errorToUserMessage(e); + final message = errorBuilder != null + ? errorBuilder(e) + : errorToUserMessage(e); final details = errorToTechnicalDetails(e); - final body = details != null && details != message ? '$message\n\n$details' : message; + final body = details != null && details != message + ? '$message\n\n$details' + : message; InfoDialog.show(context, body, copyable: true, title: 'Fehler'); return false; } diff --git a/lib/widget/async_actions/async_dialog_action.dart b/lib/widget/async_actions/async_dialog_action.dart index 24309c2..e96d59b 100644 --- a/lib/widget/async_actions/async_dialog_action.dart +++ b/lib/widget/async_actions/async_dialog_action.dart @@ -31,60 +31,65 @@ class _AsyncDialogActionState extends State { @override Widget build(BuildContext context) => AnimatedBuilder( - animation: _controller, - builder: (context, _) { - final err = _controller.error; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (err != null) - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text( - err, - textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 13), - ), + animation: _controller, + builder: (context, _) { + final err = _controller.error; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (err != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + err, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 13, ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (widget.cancelLabel != null) - TextButton( - onPressed: _controller.busy ? null : () => Navigator.of(context).pop(), - child: Text(widget.cancelLabel!), - ), - TextButton( - style: widget.confirmStyle, - onPressed: _controller.busy - ? null - : () async { - final ok = await _controller.run( - widget.onConfirm, - errorBuilder: widget.errorBuilder, - ); - if (ok && context.mounted) { - Navigator.of(context).pop(true); - } - }, - child: _controller.busy - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - AppProgressIndicator.small( - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text(widget.confirmLabel), - ], - ) - : Text(widget.confirmLabel), - ), - ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (widget.cancelLabel != null) + TextButton( + onPressed: _controller.busy + ? null + : () => Navigator.of(context).pop(), + child: Text(widget.cancelLabel!), + ), + TextButton( + style: widget.confirmStyle, + onPressed: _controller.busy + ? null + : () async { + final ok = await _controller.run( + widget.onConfirm, + errorBuilder: widget.errorBuilder, + ); + if (ok && context.mounted) { + Navigator.of(context).pop(true); + } + }, + child: _controller.busy + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + AppProgressIndicator.small( + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text(widget.confirmLabel), + ], + ) + : Text(widget.confirmLabel), ), ], - ); - }, + ), + ], ); + }, + ); } diff --git a/lib/widget/async_actions/async_fab.dart b/lib/widget/async_actions/async_fab.dart index 04eff65..e041d83 100644 --- a/lib/widget/async_actions/async_fab.dart +++ b/lib/widget/async_actions/async_fab.dart @@ -28,21 +28,21 @@ class AsyncFab extends StatelessWidget { @override Widget build(BuildContext context) => _AsyncMixin( - onPressed: onPressed, - controller: controller, - errorBuilder: errorBuilder, - onError: onError, - onSuccess: onSuccess, - builder: (context, busy, handler) { - final fg = foregroundColor ?? Theme.of(context).colorScheme.onPrimary; - return FloatingActionButton( - heroTag: heroTag, - backgroundColor: backgroundColor, - foregroundColor: fg, - mini: mini, - onPressed: handler, - child: busy ? AppProgressIndicator.small(color: fg) : Icon(icon), - ); - }, + onPressed: onPressed, + controller: controller, + errorBuilder: errorBuilder, + onError: onError, + onSuccess: onSuccess, + builder: (context, busy, handler) { + final fg = foregroundColor ?? Theme.of(context).colorScheme.onPrimary; + return FloatingActionButton( + heroTag: heroTag, + backgroundColor: backgroundColor, + foregroundColor: fg, + mini: mini, + onPressed: handler, + child: busy ? AppProgressIndicator.small(color: fg) : Icon(icon), ); + }, + ); } diff --git a/lib/widget/async_actions/async_icon_button.dart b/lib/widget/async_actions/async_icon_button.dart index de9cea4..46b5cbf 100644 --- a/lib/widget/async_actions/async_icon_button.dart +++ b/lib/widget/async_actions/async_icon_button.dart @@ -24,23 +24,23 @@ class AsyncIconButton extends StatelessWidget { @override Widget build(BuildContext context) => _AsyncMixin( - onPressed: onPressed, - controller: controller, - errorBuilder: errorBuilder, - onError: onError, - onSuccess: onSuccess, - builder: (context, busy, handler) { - if (busy) { - return Padding( - padding: const EdgeInsets.all(12), - child: AppProgressIndicator.small(color: color), - ); - } - return IconButton( - icon: Icon(icon, color: color), - tooltip: tooltip, - onPressed: handler, - ); - }, + onPressed: onPressed, + controller: controller, + errorBuilder: errorBuilder, + onError: onError, + onSuccess: onSuccess, + builder: (context, busy, handler) { + if (busy) { + return Padding( + padding: const EdgeInsets.all(12), + child: AppProgressIndicator.small(color: color), + ); + } + return IconButton( + icon: Icon(icon, color: color), + tooltip: tooltip, + onPressed: handler, ); + }, + ); } diff --git a/lib/widget/async_actions/async_list_tile.dart b/lib/widget/async_actions/async_list_tile.dart index 526ee25..5422679 100644 --- a/lib/widget/async_actions/async_list_tile.dart +++ b/lib/widget/async_actions/async_list_tile.dart @@ -36,7 +36,10 @@ class _AsyncListTileState extends State { } Future _handleTap() async { - final ok = await _controller.run(widget.onPressed, errorBuilder: widget.errorBuilder); + final ok = await _controller.run( + widget.onPressed, + errorBuilder: widget.errorBuilder, + ); if (!mounted) return; if (ok) { widget.onSuccess?.call(); @@ -48,38 +51,41 @@ class _AsyncListTileState extends State { @override Widget build(BuildContext context) => AnimatedBuilder( - animation: _controller, - builder: (context, _) { - final busy = _controller.busy; - final err = _controller.error; - final leading = busy - ? const SizedBox( - width: 24, - height: 24, - child: AppProgressIndicator.small(), - ) - : widget.leading; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: leading, - title: widget.title, - subtitle: widget.subtitle, - enabled: widget.enabled && !busy, - onTap: busy ? null : _handleTap, - ), - if (err != null) - Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), - child: Text( - err, - style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 13), - ), + animation: _controller, + builder: (context, _) { + final busy = _controller.busy; + final err = _controller.error; + final leading = busy + ? const SizedBox( + width: 24, + height: 24, + child: AppProgressIndicator.small(), + ) + : widget.leading; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListTile( + leading: leading, + title: widget.title, + subtitle: widget.subtitle, + enabled: widget.enabled && !busy, + onTap: busy ? null : _handleTap, + ), + if (err != null) + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: Text( + err, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 13, ), - ], - ); - }, + ), + ), + ], ); + }, + ); } diff --git a/lib/widget/async_actions/async_mixin.dart b/lib/widget/async_actions/async_mixin.dart index 72ff984..85d2dc3 100644 --- a/lib/widget/async_actions/async_mixin.dart +++ b/lib/widget/async_actions/async_mixin.dart @@ -6,7 +6,8 @@ class _AsyncMixin extends StatefulWidget { final AsyncErrorBuilder? errorBuilder; final void Function(String message)? onError; final VoidCallback? onSuccess; - final Widget Function(BuildContext context, bool busy, VoidCallback? handler) builder; + final Widget Function(BuildContext context, bool busy, VoidCallback? handler) + builder; const _AsyncMixin({ required this.onPressed, @@ -59,7 +60,10 @@ class _AsyncMixinState extends State<_AsyncMixin> { Future _trigger() async { final action = widget.onPressed; if (action == null) return; - final success = await _controller.run(action, errorBuilder: widget.errorBuilder); + final success = await _controller.run( + action, + errorBuilder: widget.errorBuilder, + ); if (!mounted) return; if (success) { widget.onSuccess?.call(); @@ -71,7 +75,11 @@ class _AsyncMixinState extends State<_AsyncMixin> { @override Widget build(BuildContext context) { final handler = widget.onPressed == null ? null : _trigger; - return widget.builder(context, _controller.busy, _controller.busy ? null : handler); + return widget.builder( + context, + _controller.busy, + _controller.busy ? null : handler, + ); } } @@ -98,7 +106,10 @@ class _InlineErrorWrapper extends StatelessWidget { Text( err, textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 13), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 13, + ), ), ], ], diff --git a/lib/widget/async_actions/async_text_button.dart b/lib/widget/async_actions/async_text_button.dart index cf662f6..725ac79 100644 --- a/lib/widget/async_actions/async_text_button.dart +++ b/lib/widget/async_actions/async_text_button.dart @@ -22,27 +22,27 @@ class AsyncTextButton extends StatelessWidget { @override Widget build(BuildContext context) => _AsyncMixin( - onPressed: onPressed, - controller: controller, - errorBuilder: errorBuilder, - onError: onError, - onSuccess: onSuccess, - builder: (context, busy, handler) { - final content = busy - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - AppProgressIndicator.small( - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - child, - ], - ) - : child; - final button = TextButton(onPressed: handler, child: content); - if (!showInlineError) return button; - return _InlineErrorWrapper(controller: controller, child: button); - }, - ); + onPressed: onPressed, + controller: controller, + errorBuilder: errorBuilder, + onError: onError, + onSuccess: onSuccess, + builder: (context, busy, handler) { + final content = busy + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + AppProgressIndicator.small( + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + child, + ], + ) + : child; + final button = TextButton(onPressed: handler, child: content); + if (!showInlineError) return button; + return _InlineErrorWrapper(controller: controller, child: button); + }, + ); } diff --git a/lib/widget/breaker/breaker.dart b/lib/widget/breaker/breaker.dart index 006844e..b969096 100644 --- a/lib/widget/breaker/breaker.dart +++ b/lib/widget/breaker/breaker.dart @@ -18,7 +18,8 @@ class Breaker extends StatelessWidget { if (blocked != null) { return PlaceholderView( icon: Icons.app_blocking_outlined, - text: 'Die App / Dieser Bereich ist zurzeit nicht verfügbar!\n\n' + text: + 'Die App / Dieser Bereich ist zurzeit nicht verfügbar!\n\n' '${blocked.isEmpty ? "Es wurde vom Server kein Grund übermittelt.\nAktualisiere die App und versuche es später erneut" : blocked}', ); } diff --git a/lib/widget/centered_leading.dart b/lib/widget/centered_leading.dart index 2993e3f..be8bc15 100644 --- a/lib/widget/centered_leading.dart +++ b/lib/widget/centered_leading.dart @@ -6,8 +6,8 @@ class CenteredLeading extends StatelessWidget { @override Widget build(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [child], - ); + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [child], + ); } diff --git a/lib/widget/clickable_app_bar.dart b/lib/widget/clickable_app_bar.dart index a928113..c74a692 100644 --- a/lib/widget/clickable_app_bar.dart +++ b/lib/widget/clickable_app_bar.dart @@ -6,7 +6,8 @@ class ClickableAppBar extends StatelessWidget implements PreferredSizeWidget { const ClickableAppBar({required this.onTap, required this.appBar, super.key}); @override - Widget build(BuildContext context) => GestureDetector(onTap: onTap, child: appBar); + Widget build(BuildContext context) => + GestureDetector(onTap: onTap, child: appBar); @override Size get preferredSize => appBar.preferredSize; diff --git a/lib/widget/confirm_dialog.dart b/lib/widget/confirm_dialog.dart index 5bb6d1f..acdcf65 100644 --- a/lib/widget/confirm_dialog.dart +++ b/lib/widget/confirm_dialog.dart @@ -23,8 +23,10 @@ class ConfirmDialog extends StatelessWidget { this.onConfirm, this.onConfirmAsync, this.errorBuilder, - }) : assert(onConfirm != null || onConfirmAsync != null, - 'ConfirmDialog requires either onConfirm or onConfirmAsync'); + }) : assert( + onConfirm != null || onConfirmAsync != null, + 'ConfirmDialog requires either onConfirm or onConfirmAsync', + ); void asDialog(BuildContext context) { showDialog(context: context, builder: build); @@ -32,32 +34,32 @@ class ConfirmDialog extends StatelessWidget { @override Widget build(BuildContext context) => AlertDialog( - icon: icon != null ? Icon(icon) : null, - title: Text(title), - content: Text(content), - actions: onConfirmAsync != null - ? [ - AsyncDialogAction( - confirmLabel: confirmButton, - cancelLabel: cancelButton, - onConfirm: onConfirmAsync!, - errorBuilder: errorBuilder, - ), - ] - : [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(cancelButton), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - onConfirm!(); - }, - child: Text(confirmButton), - ), - ], - ); + icon: icon != null ? Icon(icon) : null, + title: Text(title), + content: Text(content), + actions: onConfirmAsync != null + ? [ + AsyncDialogAction( + confirmLabel: confirmButton, + cancelLabel: cancelButton, + onConfirm: onConfirmAsync!, + errorBuilder: errorBuilder, + ), + ] + : [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(cancelButton), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + onConfirm!(); + }, + child: Text(confirmButton), + ), + ], + ); static void openBrowser(BuildContext context, String url) { showDialog( @@ -66,7 +68,8 @@ class ConfirmDialog extends StatelessWidget { title: 'Link öffnen', content: 'Möchtest du den folgenden Link öffnen?\n$url', confirmButton: 'Öffnen', - onConfirm: () => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication), + onConfirm: () => + launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication), ), ); } diff --git a/lib/widget/debug/cache_view.dart b/lib/widget/debug/cache_view.dart index beb46ce..abfa8c9 100644 --- a/lib/widget/debug/cache_view.dart +++ b/lib/widget/debug/cache_view.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'dart:convert'; import 'package:filesize/filesize.dart'; @@ -21,9 +20,15 @@ class CacheView extends StatefulWidget { } Future totalSize() async { - final data = await Localstore.instance.collection(RequestCache.collection).get(); + final data = await Localstore.instance + .collection(RequestCache.collection) + .get(); if (data == null || data.isEmpty) return 0; - return data.values.fold(0, (sum, value) => sum + jsonEncode(value).length) * 8; + return data.values.fold( + 0, + (sum, value) => sum + jsonEncode(value).length, + ) * + 8; } } @@ -39,41 +44,45 @@ class _CacheViewState extends State { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Cache storage'), - ), - body: FutureBuilder( - future: files, - builder: (context, snapshot) { - if(snapshot.hasData) { - return ListView.builder( - itemCount: snapshot.data!.length, - itemBuilder: (context, index) { - final key = snapshot.data!.keys.elementAt(index); - final element = snapshot.data![key] as Map; - final filename = key.split('/').last; + appBar: AppBar(title: const Text('Cache storage')), + body: FutureBuilder( + future: files, + builder: (context, snapshot) { + if (snapshot.hasData) { + return ListView.builder( + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + final key = snapshot.data!.keys.elementAt(index); + final element = snapshot.data![key] as Map; + final filename = key.split('/').last; - return ListTile( - leading: const Icon(Icons.text_snippet_outlined), - title: Text(filename), - subtitle: Text('${filesize(jsonEncode(element).length * 8)}, ${Jiffy.parseFromMillisecondsSinceEpoch(element['lastupdate'] as int).fromNow()}'), - trailing: const Icon(Icons.arrow_right), - onTap: () => JsonViewer.asDialog(context, jsonDecode(element['json'] as String) as Map), - ); - }, - ); - } else if(snapshot.connectionState != ConnectionState.done) { - return const Center( - child: CircularProgressIndicator() - ); - } else { - return const Center( - child: PlaceholderView(icon: Icons.hourglass_empty, text: 'Keine Daten'), - ); - } - }, - ), - ); + return ListTile( + leading: const Icon(Icons.text_snippet_outlined), + title: Text(filename), + subtitle: Text( + '${filesize(jsonEncode(element).length * 8)}, ${Jiffy.parseFromMillisecondsSinceEpoch(element['lastupdate'] as int).fromNow()}', + ), + trailing: const Icon(Icons.arrow_right), + onTap: () => JsonViewer.asDialog( + context, + jsonDecode(element['json'] as String) as Map, + ), + ); + }, + ); + } else if (snapshot.connectionState != ConnectionState.done) { + return const Center(child: CircularProgressIndicator()); + } else { + return const Center( + child: PlaceholderView( + icon: Icons.hourglass_empty, + text: 'Keine Daten', + ), + ); + } + }, + ), + ); } extension FutureExtension on Future { diff --git a/lib/widget/debug/debug_tile.dart b/lib/widget/debug/debug_tile.dart index 50bf937..f167d1c 100644 --- a/lib/widget/debug/debug_tile.dart +++ b/lib/widget/debug/debug_tile.dart @@ -12,23 +12,29 @@ class DebugTile { DebugTile(this.context, {this.onlyInDebug = false}); bool devConditionFulfilled() => - context.read().val().devToolsEnabled && (onlyInDebug ? kDebugMode : true); + context.read().val().devToolsEnabled && + (onlyInDebug ? kDebugMode : true); - Widget jsonData(Map data, {bool ignoreConfig = false}) => callback( + Widget jsonData(Map data, {bool ignoreConfig = false}) => + callback( title: 'JSON daten anzeigen', onTab: () => JsonViewer.asDialog(context, data), ); - Widget callback({String title = 'Debugaktion', required void Function() onTab}) => child( - ListTile( - leading: const CenteredLeading(Icon(Icons.developer_mode_outlined)), - title: Text(title), - subtitle: const Text('Entwicklermodus aktiviert'), - onTap: onTab, - ), - ); + Widget callback({ + String title = 'Debugaktion', + required void Function() onTab, + }) => child( + ListTile( + leading: const CenteredLeading(Icon(Icons.developer_mode_outlined)), + title: Text(title), + subtitle: const Text('Entwicklermodus aktiviert'), + onTap: onTab, + ), + ); - Widget child(Widget child) => Visibility(visible: devConditionFulfilled(), child: child); + Widget child(Widget child) => + Visibility(visible: devConditionFulfilled(), child: child); void run(void Function() callback) { if (!devConditionFulfilled()) return; diff --git a/lib/widget/debug/json_viewer.dart b/lib/widget/debug/json_viewer.dart index 461a15e..57e13a0 100644 --- a/lib/widget/debug/json_viewer.dart +++ b/lib/widget/debug/json_viewer.dart @@ -12,35 +12,50 @@ class JsonViewer extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: Text(title), - ), - body: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Text(format(data)), - ), - ); + appBar: AppBar(title: Text(title)), + body: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Text(format(data)), + ), + ); static final _encoder = const JsonEncoder.withIndent(' '); - static String format(Map jsonInput) => _encoder.convert(jsonInput); + static String format(Map jsonInput) => + _encoder.convert(jsonInput); static void asDialog(BuildContext context, Map dataMap) { - showDialog(context: context, builder: (dialogCtx) => AlertDialog( + showDialog( + context: context, + builder: (dialogCtx) => AlertDialog( scrollable: true, - title: const Row(children: [Icon(Icons.bug_report_outlined), Text('Rohdaten')]), + title: const Row( + children: [Icon(Icons.bug_report_outlined), Text('Rohdaten')], + ), content: Text(JsonViewer.format(dataMap)), actions: [ TextButton( - onPressed: () => copyToClipboard(dialogCtx, JsonViewer.format(dataMap), successMessage: 'Formatiertes JSON kopiert'), + onPressed: () => copyToClipboard( + dialogCtx, + JsonViewer.format(dataMap), + successMessage: 'Formatiertes JSON kopiert', + ), child: const Text('Kopieren'), ), TextButton( - onPressed: () => copyToClipboard(dialogCtx, dataMap.toString(), successMessage: 'Inline JSON kopiert'), + onPressed: () => copyToClipboard( + dialogCtx, + dataMap.toString(), + successMessage: 'Inline JSON kopiert', + ), child: const Text('Inline Kopieren'), ), - TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Schließen')) + TextButton( + onPressed: () => Navigator.of(dialogCtx).pop(), + child: const Text('Schließen'), + ), ], - )); + ), + ); } } diff --git a/lib/widget/details_bottom_sheet.dart b/lib/widget/details_bottom_sheet.dart index 0618f40..db6aff2 100644 --- a/lib/widget/details_bottom_sheet.dart +++ b/lib/widget/details_bottom_sheet.dart @@ -21,10 +21,7 @@ void showDetailsBottomSheet( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (header != null) ...[ - header, - const Divider(height: 1), - ], + if (header != null) ...[header, const Divider(height: 1)], ...children(sheetContext), ], ), diff --git a/lib/widget/file_pick.dart b/lib/widget/file_pick.dart index 0e678f3..130880e 100644 --- a/lib/widget/file_pick.dart +++ b/lib/widget/file_pick.dart @@ -9,7 +9,8 @@ class FilePick { return pickedImages.isNotEmpty ? pickedImages : null; } - static Future cameraPick() => _picker.pickImage(source: ImageSource.camera); + static Future cameraPick() => + _picker.pickImage(source: ImageSource.camera); static Future?> documentPick() async { final result = await FilePicker.pickFiles(allowMultiple: true); diff --git a/lib/widget/file_viewer.dart b/lib/widget/file_viewer.dart index 80c5664..6440bad 100644 --- a/lib/widget/file_viewer.dart +++ b/lib/widget/file_viewer.dart @@ -25,11 +25,7 @@ class FileViewer extends StatefulWidget { State createState() => _FileViewerState(); } -enum FileViewingActions { - openExternal, - share, - save -} +enum FileViewingActions { openExternal, share, save } /// Workaround for a Syncfusion PDF viewer race: SfPdfViewer's internal /// LayoutBuilder calls `localToGlobal` during build, which asserts when an @@ -88,7 +84,9 @@ class _FileViewerState extends State { @override void initState() { - openExternal = settings.val().fileViewSettings.alwaysOpenExternally || widget.openExternal; + openExternal = + settings.val().fileViewSettings.alwaysOpenExternally || + widget.openExternal; super.initState(); } @@ -101,96 +99,113 @@ class _FileViewerState extends State { @override Widget build(BuildContext context) { AppBar appbar({List actions = const []}) => AppBar( - title: Text(widget.path.split('/').last), - actions: [ - ...actions, - PopupMenuButton( - onSelected: (value) async { - switch(value) { - case FileViewingActions.openExternal: - AppRoutes.openFileViewer(context, widget.path, openExternal: true); - break; - case FileViewingActions.share: - unawaited(SharePlus.instance.share( + title: Text(widget.path.split('/').last), + actions: [ + ...actions, + PopupMenuButton( + onSelected: (value) async { + switch (value) { + case FileViewingActions.openExternal: + AppRoutes.openFileViewer( + context, + widget.path, + openExternal: true, + ); + break; + case FileViewingActions.share: + unawaited( + SharePlus.instance.share( ShareParams( files: [XFile(widget.path)], sharePositionOrigin: SharePositionOrigin.get(context), ), - )); - break; - case FileViewingActions.save: - try { - final bytes = await File(widget.path).readAsBytes(); - final saved = await FilePicker.saveFile( - fileName: widget.path.split('/').last, - bytes: bytes, - ); - if (!context.mounted) return; - if (saved != null) InfoDialog.show(context, 'Datei gespeichert.'); - } on Object catch (e) { - if (!context.mounted) return; - InfoDialog.show(context, 'Speichern fehlgeschlagen: $e', copyable: true, title: 'Fehler'); + ), + ); + break; + case FileViewingActions.save: + try { + final bytes = await File(widget.path).readAsBytes(); + final saved = await FilePicker.saveFile( + fileName: widget.path.split('/').last, + bytes: bytes, + ); + if (!context.mounted) return; + if (saved != null) { + InfoDialog.show(context, 'Datei gespeichert.'); } - break; - } - }, - itemBuilder: (context) => >[ - const PopupMenuItem( - value: FileViewingActions.openExternal, - child: ListTile( - leading: Icon(Icons.open_in_new), - title: Text('Extern öffnen'), - dense: true, - ), + } on Object catch (e) { + if (!context.mounted) return; + InfoDialog.show( + context, + 'Speichern fehlgeschlagen: $e', + copyable: true, + title: 'Fehler', + ); + } + break; + } + }, + itemBuilder: (context) => >[ + const PopupMenuItem( + value: FileViewingActions.openExternal, + child: ListTile( + leading: Icon(Icons.open_in_new), + title: Text('Extern öffnen'), + dense: true, ), - const PopupMenuItem( - value: FileViewingActions.share, - child: ListTile( - leading: Icon(Icons.share_outlined), - title: Text('Teilen'), - dense: true, - ), + ), + const PopupMenuItem( + value: FileViewingActions.share, + child: ListTile( + leading: Icon(Icons.share_outlined), + title: Text('Teilen'), + dense: true, ), - const PopupMenuItem( - value: FileViewingActions.save, - child: ListTile( - leading: Icon(Icons.save_alt_outlined), - title: Text('Speichern'), - dense: true, - ), + ), + const PopupMenuItem( + value: FileViewingActions.save, + child: ListTile( + leading: Icon(Icons.save_alt_outlined), + title: Text('Speichern'), + dense: true, ), - ], - ), - ], - ); + ), + ], + ), + ], + ); - switch(openExternal ? '' : widget.path.split('.').last.toLowerCase()) { + switch (openExternal ? '' : widget.path.split('.').last.toLowerCase()) { case 'png': case 'jpg': case 'jpeg': case 'webp': case 'gif': return Scaffold( - appBar: appbar( - actions: [ - IconButton(onPressed: () { - setState(() { - photoViewController.rotation += pi/2; - }); - }, icon: const Icon(Icons.rotate_right)), - ] + appBar: appbar( + actions: [ + IconButton( + onPressed: () { + setState(() { + photoViewController.rotation += pi / 2; + }); + }, + icon: const Icon(Icons.rotate_right), + ), + ], + ), + backgroundColor: Colors.white, + body: PhotoView( + controller: photoViewController, + maxScale: 3.0, + minScale: 0.1, + imageProvider: Image.file(File(widget.path)).image, + backgroundDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, ), - backgroundColor: Colors.white, - body: PhotoView( - controller: photoViewController, - maxScale: 3.0, - minScale: 0.1, - imageProvider: Image.file(File(widget.path)).image, - backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), - ) + ), ); - case 'pdf': return Scaffold( appBar: appbar(), diff --git a/lib/widget/large_profile_picture_view.dart b/lib/widget/large_profile_picture_view.dart index c8abe23..6751bdd 100644 --- a/lib/widget/large_profile_picture_view.dart +++ b/lib/widget/large_profile_picture_view.dart @@ -9,12 +9,14 @@ class LargeProfilePictureView extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Profilbild'), + appBar: AppBar(title: const Text('Profilbild')), + body: PhotoView( + imageProvider: Image.network( + 'https://${EndpointData().nextcloud().full()}/avatar/$username/1024', + ).image, + backgroundDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, ), - body: PhotoView( - imageProvider: Image.network('https://${EndpointData().nextcloud().full()}/avatar/$username/1024').image, - backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), - ), - ); + ), + ); } diff --git a/lib/widget/list_view_util.dart b/lib/widget/list_view_util.dart index 468ae95..efa8ee8 100644 --- a/lib/widget/list_view_util.dart +++ b/lib/widget/list_view_util.dart @@ -1,9 +1,10 @@ - import 'package:flutter/material.dart'; class ListViewUtil { - static ListView fromList(List? items, Widget Function(T item) map) => ListView.builder( - itemCount: items?.length ?? 0, - itemBuilder: (context, index) => items != null ? map(items[index]) : null, - ); + static ListView fromList(List? items, Widget Function(T item) map) => + ListView.builder( + itemCount: items?.length ?? 0, + itemBuilder: (context, index) => + items != null ? map(items[index]) : null, + ); } diff --git a/lib/widget/loading_spinner.dart b/lib/widget/loading_spinner.dart index 4cf9ab0..054214c 100644 --- a/lib/widget/loading_spinner.dart +++ b/lib/widget/loading_spinner.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:flutter/material.dart'; @@ -16,7 +15,7 @@ class _LoadingSpinnerState extends State { @override void initState() { - timer = Timer(const Duration(seconds: 30), () { + timer = Timer(const Duration(seconds: 30), () { setState(() { textVisible = true; }); @@ -27,25 +26,25 @@ class _LoadingSpinnerState extends State { @override Widget build(BuildContext context) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Visibility( - visible: !textVisible, - replacement: const Icon(Icons.sentiment_dissatisfied_outlined), - child: const CircularProgressIndicator(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Visibility( + visible: !textVisible, + replacement: const Icon(Icons.sentiment_dissatisfied_outlined), + child: const CircularProgressIndicator(), + ), + const SizedBox(height: 30), + Visibility( + visible: textVisible, + child: const Text( + textAlign: TextAlign.center, + 'Irgendetwas funktioniert nicht!\nBist du mit dem Internet verbunden?\n\nVersuche die App neuzustarten', ), - const SizedBox(height: 30), - Visibility( - visible: textVisible, - child: const Text( - textAlign: TextAlign.center, - 'Irgendetwas funktioniert nicht!\nBist du mit dem Internet verbunden?\n\nVersuche die App neuzustarten' - ), - ), - ], - ), - ); + ), + ], + ), + ); @override void dispose() { diff --git a/lib/widget/placeholder_view.dart b/lib/widget/placeholder_view.dart index ce14d8d..27e114a 100644 --- a/lib/widget/placeholder_view.dart +++ b/lib/widget/placeholder_view.dart @@ -4,7 +4,12 @@ class PlaceholderView extends StatelessWidget { final IconData icon; final String text; final Widget? button; - const PlaceholderView({super.key, required this.icon, required this.text, this.button}); + const PlaceholderView({ + super.key, + required this.icon, + required this.text, + this.button, + }); @override Widget build(BuildContext context) => Scaffold( @@ -19,7 +24,7 @@ class PlaceholderView extends StatelessWidget { ), Text( text, - style: const TextStyle(fontSize: 20,), + style: const TextStyle(fontSize: 20), textAlign: TextAlign.center, ), const SizedBox(height: 30), diff --git a/lib/widget/share_position_origin.dart b/lib/widget/share_position_origin.dart index f110beb..2086f09 100644 --- a/lib/widget/share_position_origin.dart +++ b/lib/widget/share_position_origin.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; class SharePositionOrigin { - static Rect get(BuildContext context) => Rect.fromLTWH(0, 0, MediaQuery.of(context).size.width, MediaQuery.of(context).size.height / 2); + static Rect get(BuildContext context) => Rect.fromLTWH( + 0, + 0, + MediaQuery.of(context).size.width, + MediaQuery.of(context).size.height / 2, + ); } diff --git a/lib/widget/string_extensions.dart b/lib/widget/string_extensions.dart index 21c3901..d802e9a 100644 --- a/lib/widget/string_extensions.dart +++ b/lib/widget/string_extensions.dart @@ -1,3 +1,4 @@ extension StringExtensions on String { - String capitalize() => '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; + String capitalize() => + '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; } diff --git a/lib/widget/unimplemented_dialog.dart b/lib/widget/unimplemented_dialog.dart index c02472d..dfae0eb 100644 --- a/lib/widget/unimplemented_dialog.dart +++ b/lib/widget/unimplemented_dialog.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; class UnimplementedDialog { static void show(BuildContext context) { - showDialog(context: context, builder: (context) => const AlertDialog(content: Text('Not implemented yet'))); + showDialog( + context: context, + builder: (context) => + const AlertDialog(content: Text('Not implemented yet')), + ); } } diff --git a/lib/widget/user_avatar.dart b/lib/widget/user_avatar.dart index bcabac6..7e8cf4d 100644 --- a/lib/widget/user_avatar.dart +++ b/lib/widget/user_avatar.dart @@ -12,7 +12,12 @@ class UserAvatar extends StatefulWidget { final String id; final bool isGroup; final int size; - const UserAvatar({required this.id, this.isGroup = false, this.size = 20, super.key}); + const UserAvatar({ + required this.id, + this.isGroup = false, + this.size = 20, + super.key, + }); @override State createState() => _UserAvatarState(); @@ -79,10 +84,12 @@ class _UserAvatarState extends State { } static bool _looksLikeSvg(Uint8List bytes) { - final head = utf8.decode( - bytes.sublist(0, bytes.length < 256 ? bytes.length : 256), - allowMalformed: true, - ).trimLeft(); + final head = utf8 + .decode( + bytes.sublist(0, bytes.length < 256 ? bytes.length : 256), + allowMalformed: true, + ) + .trimLeft(); return head.startsWith(' - RichObjectString(type, 'id-$name', name, null, null); +RichObjectString _r( + String name, { + RichObjectStringObjectType type = RichObjectStringObjectType.user, +}) => RichObjectString(type, 'id-$name', name, null, null); void main() { group('RichObjectStringProcessor.parseToString', () { @@ -40,20 +42,18 @@ void main() { test('replaces every occurrence of the same placeholder', () { expect( - RichObjectStringProcessor.parseToString( - '{actor} {actor} {actor}', - {'actor': _r('A')}, - ), + RichObjectStringProcessor.parseToString('{actor} {actor} {actor}', { + 'actor': _r('A'), + }), 'A A A', ); }); test('placeholders with no matching key remain unchanged', () { expect( - RichObjectStringProcessor.parseToString( - '{actor} sah {file}', - {'actor': _r('Elias')}, - ), + RichObjectStringProcessor.parseToString('{actor} sah {file}', { + 'actor': _r('Elias'), + }), 'Elias sah {file}', ); }); @@ -67,8 +67,9 @@ void main() { test('messages without placeholders are returned verbatim', () { expect( - RichObjectStringProcessor.parseToString('reine Textnachricht', - {'actor': _r('A')}), + RichObjectStringProcessor.parseToString('reine Textnachricht', { + 'actor': _r('A'), + }), 'reine Textnachricht', ); }); diff --git a/test/api/webuntis/lesson_resolver_test.dart b/test/api/webuntis/lesson_resolver_test.dart index 4c900c7..01a675b 100644 --- a/test/api/webuntis/lesson_resolver_test.dart +++ b/test/api/webuntis/lesson_resolver_test.dart @@ -7,13 +7,12 @@ import 'package:marianum_mobile/state/app/modules/timetable/bloc/timetable_state TimetableState _state({ Set subjects = const {}, Set rooms = const {}, -}) => - TimetableState( - subjects: subjects.isEmpty ? null : GetSubjectsResponse(subjects), - rooms: rooms.isEmpty ? null : GetRoomsResponse(rooms), - startDate: DateTime(2026, 1, 1), - endDate: DateTime(2026, 12, 31), - ); +}) => TimetableState( + subjects: subjects.isEmpty ? null : GetSubjectsResponse(subjects), + rooms: rooms.isEmpty ? null : GetRoomsResponse(rooms), + startDate: DateTime(2026, 1, 1), + endDate: DateTime(2026, 12, 31), +); void main() { group('LessonResolver.resolveSubject', () { @@ -47,7 +46,13 @@ void main() { group('LessonResolver.resolveRoom', () { test('returns the matching room when the id is found', () { - final room = GetRoomsResponseObject(3, 'A1', 'Aula 1', true, 'Hauptgebäude'); + final room = GetRoomsResponseObject( + 3, + 'A1', + 'Aula 1', + true, + 'Hauptgebäude', + ); final state = _state(rooms: {room}); final result = LessonResolver.resolveRoom(state, 3); @@ -66,10 +71,14 @@ void main() { group('LessonFormatter', () { test('iconForCode picks the right icon per status', () { - expect(LessonFormatter.iconForCode('cancelled').codePoint, - isNot(LessonFormatter.iconForCode('irregular').codePoint)); - expect(LessonFormatter.iconForCode(null).codePoint, - isNot(LessonFormatter.iconForCode('cancelled').codePoint)); + expect( + LessonFormatter.iconForCode('cancelled').codePoint, + isNot(LessonFormatter.iconForCode('irregular').codePoint), + ); + expect( + LessonFormatter.iconForCode(null).codePoint, + isNot(LessonFormatter.iconForCode('cancelled').codePoint), + ); }); test('statusLabel maps known codes to German labels', () { @@ -88,7 +97,11 @@ void main() { test('formatLine renders name + (longname) + · extra in that order', () { expect( - LessonFormatter.formatLine('Mathe', longname: 'Mathematik', extra: 'Hauptgebäude'), + LessonFormatter.formatLine( + 'Mathe', + longname: 'Mathematik', + extra: 'Hauptgebäude', + ), 'Mathe (Mathematik) · Hauptgebäude', ); }); diff --git a/test/utils/debouncer_test.dart b/test/utils/debouncer_test.dart index 6084188..494c908 100644 --- a/test/utils/debouncer_test.dart +++ b/test/utils/debouncer_test.dart @@ -5,24 +5,34 @@ import 'package:marianum_mobile/utils/debouncer.dart'; void main() { // Each test is wrapped in fakeAsync so timers fire deterministically. group('Debouncer.debounce', () { - test('runs the action once after the delay elapses without further calls', () { - fakeAsync((async) { - var calls = 0; - Debouncer.debounce('tag', const Duration(milliseconds: 100), () => calls++); + test( + 'runs the action once after the delay elapses without further calls', + () { + fakeAsync((async) { + var calls = 0; + Debouncer.debounce( + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); - async.elapse(const Duration(milliseconds: 99)); - expect(calls, 0); + async.elapse(const Duration(milliseconds: 99)); + expect(calls, 0); - async.elapse(const Duration(milliseconds: 1)); - expect(calls, 1); - }); - }); + async.elapse(const Duration(milliseconds: 1)); + expect(calls, 1); + }); + }, + ); test('subsequent calls within the delay reset the timer (coalesce)', () { fakeAsync((async) { var calls = 0; void schedule() => Debouncer.debounce( - 'tag', const Duration(milliseconds: 100), () => calls++); + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); schedule(); async.elapse(const Duration(milliseconds: 80)); @@ -41,8 +51,16 @@ void main() { fakeAsync((async) { var aCalls = 0; var bCalls = 0; - Debouncer.debounce('a', const Duration(milliseconds: 100), () => aCalls++); - Debouncer.debounce('b', const Duration(milliseconds: 100), () => bCalls++); + Debouncer.debounce( + 'a', + const Duration(milliseconds: 100), + () => aCalls++, + ); + Debouncer.debounce( + 'b', + const Duration(milliseconds: 100), + () => bCalls++, + ); async.elapse(const Duration(milliseconds: 100)); expect(aCalls, 1); @@ -52,28 +70,67 @@ void main() { }); group('Debouncer.throttle', () { - test('first call runs immediately, subsequent calls within window are dropped', () { - fakeAsync((async) { - var calls = 0; - Debouncer.throttle('tag', const Duration(milliseconds: 100), () => calls++); - expect(calls, 1, reason: 'throttle fires the first call synchronously'); + test( + 'first call runs immediately, subsequent calls within window are dropped', + () { + fakeAsync((async) { + var calls = 0; + Debouncer.throttle( + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); + expect( + calls, + 1, + reason: 'throttle fires the first call synchronously', + ); - Debouncer.throttle('tag', const Duration(milliseconds: 100), () => calls++); - Debouncer.throttle('tag', const Duration(milliseconds: 100), () => calls++); - expect(calls, 1, reason: 'subsequent calls within the gate are ignored'); + Debouncer.throttle( + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); + Debouncer.throttle( + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); + expect( + calls, + 1, + reason: 'subsequent calls within the gate are ignored', + ); - async.elapse(const Duration(milliseconds: 100)); - Debouncer.throttle('tag', const Duration(milliseconds: 100), () => calls++); - expect(calls, 2, reason: 'after the window elapses, throttle fires again'); - }); - }); + async.elapse(const Duration(milliseconds: 100)); + Debouncer.throttle( + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); + expect( + calls, + 2, + reason: 'after the window elapses, throttle fires again', + ); + }); + }, + ); test('different tags throttle independently', () { fakeAsync((async) { var aCalls = 0; var bCalls = 0; - Debouncer.throttle('a', const Duration(milliseconds: 100), () => aCalls++); - Debouncer.throttle('b', const Duration(milliseconds: 100), () => bCalls++); + Debouncer.throttle( + 'a', + const Duration(milliseconds: 100), + () => aCalls++, + ); + Debouncer.throttle( + 'b', + const Duration(milliseconds: 100), + () => bCalls++, + ); expect(aCalls, 1); expect(bCalls, 1); @@ -86,7 +143,11 @@ void main() { test('cancels a pending debounce so the action never runs', () { fakeAsync((async) { var calls = 0; - Debouncer.debounce('tag', const Duration(milliseconds: 100), () => calls++); + Debouncer.debounce( + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); Debouncer.cancel('tag'); async.elapse(const Duration(milliseconds: 200)); @@ -94,19 +155,33 @@ void main() { }); }); - test('cancels an active throttle gate so the next call fires immediately', () { - fakeAsync((async) { - var calls = 0; - Debouncer.throttle('tag', const Duration(milliseconds: 100), () => calls++); - expect(calls, 1); + test( + 'cancels an active throttle gate so the next call fires immediately', + () { + fakeAsync((async) { + var calls = 0; + Debouncer.throttle( + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); + expect(calls, 1); - Debouncer.cancel('tag'); - Debouncer.throttle('tag', const Duration(milliseconds: 100), () => calls++); - expect(calls, 2, - reason: 'cancel removed the gate so the next throttle fires again'); + Debouncer.cancel('tag'); + Debouncer.throttle( + 'tag', + const Duration(milliseconds: 100), + () => calls++, + ); + expect( + calls, + 2, + reason: 'cancel removed the gate so the next throttle fires again', + ); - async.elapse(const Duration(milliseconds: 100)); - }); - }); + async.elapse(const Duration(milliseconds: 100)); + }); + }, + ); }); } diff --git a/test/view/files/sort_options_test.dart b/test/view/files/sort_options_test.dart index d7ddbe6..0f80d52 100644 --- a/test/view/files/sort_options_test.dart +++ b/test/view/files/sort_options_test.dart @@ -8,14 +8,13 @@ CacheableFile _file({ bool isDirectory = false, int? size, DateTime? modifiedAt, -}) => - CacheableFile( - path: '/$name', - isDirectory: isDirectory, - name: name, - size: size, - modifiedAt: modifiedAt, - ); +}) => CacheableFile( + path: '/$name', + isDirectory: isDirectory, + name: name, + size: size, + modifiedAt: modifiedAt, +); void main() { group('SortOptions.options', () { @@ -55,8 +54,14 @@ void main() { test('size comparator orders by file size when both known', () { final cmp = SortOptions.getOption(SortOption.size).compare; - expect(cmp(_file(name: 'a', size: 100), _file(name: 'b', size: 200)), lessThan(0)); - expect(cmp(_file(name: 'a', size: 200), _file(name: 'b', size: 100)), greaterThan(0)); + expect( + cmp(_file(name: 'a', size: 100), _file(name: 'b', size: 200)), + lessThan(0), + ); + expect( + cmp(_file(name: 'a', size: 200), _file(name: 'b', size: 100)), + greaterThan(0), + ); }); test('options map contains all enum values exactly once', () { @@ -67,8 +72,16 @@ void main() { group('ListFilesResponse.sortBy', () { final folderA = _file(name: 'A', isDirectory: true); final folderB = _file(name: 'B', isDirectory: true); - final fileA = _file(name: 'aaa', size: 100, modifiedAt: DateTime(2026, 1, 1)); - final fileB = _file(name: 'bbb', size: 50, modifiedAt: DateTime(2026, 5, 1)); + final fileA = _file( + name: 'aaa', + size: 100, + modifiedAt: DateTime(2026, 1, 1), + ); + final fileB = _file( + name: 'bbb', + size: 50, + modifiedAt: DateTime(2026, 5, 1), + ); // Note: sortBy uses a string-buffer sort + compareTo descending. The actual // list ordering reflects what users see in the file list. @@ -81,7 +94,10 @@ void main() { test('foldersToTop=false intermixes folders and files', () { final response = ListFilesResponse({fileA, fileB, folderA, folderB}); - final sorted = response.sortBy(sortOption: SortOption.name, foldersToTop: false); + final sorted = response.sortBy( + sortOption: SortOption.name, + foldersToTop: false, + ); final folderPositions = []; for (var i = 0; i < sorted.length; i++) { if (sorted[i].isDirectory) folderPositions.add(i); @@ -94,9 +110,15 @@ void main() { test('reversed flips the order within each section', () { final response = ListFilesResponse({fileA, fileB}); - final asc = response.sortBy(sortOption: SortOption.name, foldersToTop: false); + final asc = response.sortBy( + sortOption: SortOption.name, + foldersToTop: false, + ); final desc = response.sortBy( - sortOption: SortOption.name, foldersToTop: false, reversed: true); + sortOption: SortOption.name, + foldersToTop: false, + reversed: true, + ); expect(desc, asc.reversed.toList()); }); diff --git a/test/view/marianum_dates/event_formatter_test.dart b/test/view/marianum_dates/event_formatter_test.dart index 3dabafc..7d14a1b 100644 --- a/test/view/marianum_dates/event_formatter_test.dart +++ b/test/view/marianum_dates/event_formatter_test.dart @@ -7,15 +7,14 @@ MarianumDate _event({ required DateTime start, required DateTime end, bool isAllDay = false, -}) => - MarianumDate( - uid: 't', - title: 't', - description: null, - start: start, - end: end, - isAllDay: isAllDay, - ); +}) => MarianumDate( + uid: 't', + title: 't', + description: null, + start: start, + end: end, + isAllDay: isAllDay, +); void main() { setUpAll(() async { @@ -66,23 +65,32 @@ void main() { expect(EventFormatter.longRange(e), '08.05.2026 · Ganztägig'); }); - test('all-day multi-day shows inclusive end (one day before exclusive end)', () { - final e = _event( - start: DateTime(2026, 5, 8), - end: DateTime(2026, 5, 11), // exclusive → display "until 10.05." - isAllDay: true, - ); - expect(EventFormatter.longRange(e), '08.05.2026 – 10.05.2026 · Ganztägig'); - }); + test( + 'all-day multi-day shows inclusive end (one day before exclusive end)', + () { + final e = _event( + start: DateTime(2026, 5, 8), + end: DateTime(2026, 5, 11), // exclusive → display "until 10.05." + isAllDay: true, + ); + expect( + EventFormatter.longRange(e), + '08.05.2026 – 10.05.2026 · Ganztägig', + ); + }, + ); - test('all-day event whose end equals start (degenerate) renders as single day', () { - final e = _event( - start: DateTime(2026, 5, 8), - end: DateTime(2026, 5, 8), - isAllDay: true, - ); - expect(EventFormatter.longRange(e), '08.05.2026 · Ganztägig'); - }); + test( + 'all-day event whose end equals start (degenerate) renders as single day', + () { + final e = _event( + start: DateTime(2026, 5, 8), + end: DateTime(2026, 5, 8), + isAllDay: true, + ); + expect(EventFormatter.longRange(e), '08.05.2026 · Ganztägig'); + }, + ); test('zero-length same-day timed event shows single time', () { final at = DateTime(2026, 5, 8, 9, 30); @@ -103,7 +111,10 @@ void main() { start: DateTime(2026, 5, 8, 9), end: DateTime(2026, 5, 9, 11), ); - expect(EventFormatter.longRange(e), '08.05.2026 09:00 – 09.05.2026 11:00'); + expect( + EventFormatter.longRange(e), + '08.05.2026 09:00 – 09.05.2026 11:00', + ); }); }); } diff --git a/test/view/timetable/calendar_logic_test.dart b/test/view/timetable/calendar_logic_test.dart index fcd0a63..5e06910 100644 --- a/test/view/timetable/calendar_logic_test.dart +++ b/test/view/timetable/calendar_logic_test.dart @@ -17,18 +17,18 @@ Appointment _appt({ bool isAllDay = false, Object? id, String? rrule, -}) => - Appointment( - id: id, - startTime: start, - endTime: end, - subject: subject, - color: Colors.blue, - isAllDay: isAllDay, - recurrenceRule: rrule, - ); +}) => Appointment( + id: id, + startTime: start, + endTime: end, + subject: subject, + color: Colors.blue, + isAllDay: isAllDay, + recurrenceRule: rrule, +); -GetTimetableResponseObject _lesson({String? code}) => GetTimetableResponseObject( +GetTimetableResponseObject _lesson({String? code}) => + GetTimetableResponseObject( id: 0, date: 0, startTime: 0, @@ -41,21 +41,25 @@ GetTimetableResponseObject _lesson({String? code}) => GetTimetableResponseObject ); CustomTimetableEvent _customEvent() => CustomTimetableEvent( - id: 'x', - title: '', - description: '', - startDate: DateTime(2026), - endDate: DateTime(2026), - color: null, - rrule: '', - createdAt: DateTime(2026), - updatedAt: DateTime(2026), - ); + id: 'x', + title: '', + description: '', + startDate: DateTime(2026), + endDate: DateTime(2026), + color: null, + rrule: '', + createdAt: DateTime(2026), + updatedAt: DateTime(2026), +); void main() { group('isAllDayLike', () { test('explicit isAllDay flag wins', () { - final a = _appt(start: _at(2026, 5, 8, 9), end: _at(2026, 5, 8, 10), isAllDay: true); + final a = _appt( + start: _at(2026, 5, 8, 9), + end: _at(2026, 5, 8, 10), + isAllDay: true, + ); expect(isAllDayLike(a), isTrue); }); @@ -69,11 +73,20 @@ void main() { expect(isAllDayLike(a), isTrue); }); - test('Duration.inHours truncation does not let a 9h 30min event escape', () { - final a = _appt(start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 17, 30)); - expect(isAllDayLike(a), isTrue, - reason: 'inHours would say 9; we compare in minutes (570 ≥ 480)'); - }); + test( + 'Duration.inHours truncation does not let a 9h 30min event escape', + () { + final a = _appt( + start: _at(2026, 5, 8, 8), + end: _at(2026, 5, 8, 17, 30), + ); + expect( + isAllDayLike(a), + isTrue, + reason: 'inHours would say 9; we compare in minutes (570 ≥ 480)', + ); + }, + ); }); group('isOutsideSchoolHours', () { @@ -85,7 +98,11 @@ void main() { }); test('all-day-like events are always outside', () { - final a = _appt(start: _at(2026, 5, 8, 9), end: _at(2026, 5, 8, 10), isAllDay: true); + final a = _appt( + start: _at(2026, 5, 8, 9), + end: _at(2026, 5, 8, 10), + isAllDay: true, + ); expect(isOutsideSchoolHours(a), isTrue); }); @@ -120,7 +137,9 @@ void main() { test('single non-recurring lesson lands in the right day bucket', () { final wednesday9 = _appt( - start: _at(2026, 5, 6, 9), end: _at(2026, 5, 6, 10)); + start: _at(2026, 5, 6, 9), + end: _at(2026, 5, 6, 10), + ); final result = partitionAppointmentsForWeek([wednesday9], monday); expect(result.inside[0], isEmpty); expect(result.inside[1], isEmpty); @@ -131,9 +150,10 @@ void main() { test('all-day events go to the outside bucket on their day', () { final tuesdayAllDay = _appt( - start: _at(2026, 5, 5), - end: _at(2026, 5, 6), - isAllDay: true); + start: _at(2026, 5, 5), + end: _at(2026, 5, 6), + isAllDay: true, + ); final result = partitionAppointmentsForWeek([tuesdayAllDay], monday); expect(result.inside.expand((e) => e), isEmpty); expect(result.outside[1], hasLength(1)); @@ -141,9 +161,13 @@ void main() { test('events outside the visible week are dropped', () { final lastWeek = _appt( - start: _at(2026, 4, 27, 9), end: _at(2026, 4, 27, 10)); + start: _at(2026, 4, 27, 9), + end: _at(2026, 4, 27, 10), + ); final nextWeek = _appt( - start: _at(2026, 5, 11, 9), end: _at(2026, 5, 11, 10)); + start: _at(2026, 5, 11, 9), + end: _at(2026, 5, 11, 10), + ); final result = partitionAppointmentsForWeek([lastWeek, nextWeek], monday); expect(result.inside.expand((e) => e), isEmpty); expect(result.outside.expand((e) => e), isEmpty); @@ -151,7 +175,9 @@ void main() { test('weekend events (Sat/Sun) are dropped, only Mon–Fri counted', () { final saturday = _appt( - start: _at(2026, 5, 9, 9), end: _at(2026, 5, 9, 10)); + start: _at(2026, 5, 9, 9), + end: _at(2026, 5, 9, 10), + ); final result = partitionAppointmentsForWeek([saturday], monday); expect(result.inside.expand((e) => e), isEmpty); }); @@ -160,20 +186,25 @@ void main() { // Anchor on the Monday before our visible week, repeating weekly. // The visible week's Monday should produce one occurrence. final anchor = _appt( - start: _at(2026, 4, 27, 9), - end: _at(2026, 4, 27, 10), - rrule: 'RRULE:FREQ=WEEKLY;BYDAY=MO'); + start: _at(2026, 4, 27, 9), + end: _at(2026, 4, 27, 10), + rrule: 'RRULE:FREQ=WEEKLY;BYDAY=MO', + ); final result = partitionAppointmentsForWeek([anchor], monday); - expect(result.inside[0], hasLength(1), - reason: 'Monday of the visible week should get one expansion'); + expect( + result.inside[0], + hasLength(1), + reason: 'Monday of the visible week should get one expansion', + ); expect(result.inside[0].first.startTime, _at(2026, 5, 4, 9)); }); test('malformed RRULE falls back to placing the anchor', () { final broken = _appt( - start: _at(2026, 5, 6, 9), - end: _at(2026, 5, 6, 10), - rrule: 'this is not a valid rrule'); + start: _at(2026, 5, 6, 9), + end: _at(2026, 5, 6, 10), + rrule: 'this is not a valid rrule', + ); final result = partitionAppointmentsForWeek([broken], monday); expect(result.inside[2], hasLength(1)); }); @@ -181,16 +212,21 @@ void main() { group('PeriodLayout', () { final p1 = const LessonPeriod( - name: '1', start: TimeOfDay(hour: 8, minute: 0), end: TimeOfDay(hour: 9, minute: 0)); + name: '1', + start: TimeOfDay(hour: 8, minute: 0), + end: TimeOfDay(hour: 9, minute: 0), + ); final brk = const LessonPeriod( - name: 'Pause', - start: TimeOfDay(hour: 9, minute: 0), - end: TimeOfDay(hour: 9, minute: 15), - isBreak: true); + name: 'Pause', + start: TimeOfDay(hour: 9, minute: 0), + end: TimeOfDay(hour: 9, minute: 15), + isBreak: true, + ); final p2 = const LessonPeriod( - name: '2', - start: TimeOfDay(hour: 9, minute: 15), - end: TimeOfDay(hour: 10, minute: 15)); + name: '2', + start: TimeOfDay(hour: 9, minute: 15), + end: TimeOfDay(hour: 10, minute: 15), + ); final layout = PeriodLayout( periods: [p1, brk, p2], @@ -249,7 +285,11 @@ void main() { expect(result, hasLength(2)); for (final cell in result) { expect(cell.lane, 0); - expect(cell.laneCount, 1, reason: 'separate clusters → laneCount=1 each'); + expect( + cell.laneCount, + 1, + reason: 'separate clusters → laneCount=1 each', + ); } }); @@ -262,85 +302,121 @@ void main() { expect(result.every((c) => c.laneCount == 2), isTrue); }); - test('three overlapping appointments with maxLanes=2 collapse the third into overflow', () { - final a = _appt(start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 11)); - final b = _appt(start: _at(2026, 5, 8, 9), end: _at(2026, 5, 8, 11)); - final c = _appt(start: _at(2026, 5, 8, 10), end: _at(2026, 5, 8, 11)); - final result = assignLanes([a, b, c], maxLanes: 2); + test( + 'three overlapping appointments with maxLanes=2 collapse the third into overflow', + () { + final a = _appt(start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 11)); + final b = _appt(start: _at(2026, 5, 8, 9), end: _at(2026, 5, 8, 11)); + final c = _appt(start: _at(2026, 5, 8, 10), end: _at(2026, 5, 8, 11)); + final result = assignLanes([a, b, c], maxLanes: 2); - final visible = result.whereType().toList(); - final overflow = result.whereType().toList(); - expect(visible, hasLength(1), reason: 'maxLanes-1 = 1 visible appointment'); - expect(overflow, hasLength(1)); - expect(overflow.first.appointments, hasLength(2)); - expect(overflow.first.lane, 1); - expect(overflow.first.laneCount, 2); - }); + final visible = result.whereType().toList(); + final overflow = result.whereType().toList(); + expect( + visible, + hasLength(1), + reason: 'maxLanes-1 = 1 visible appointment', + ); + expect(overflow, hasLength(1)); + expect(overflow.first.appointments, hasLength(2)); + expect(overflow.first.lane, 1); + expect(overflow.first.laneCount, 2); + }, + ); test('CustomAppointment beats a regular lesson on lane priority', () { final custom = _appt( id: CustomAppointment(_customEvent()), - start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 10), + start: _at(2026, 5, 8, 8), + end: _at(2026, 5, 8, 10), ); final regular = _appt( id: WebuntisAppointment(_lesson()), - start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 10), + start: _at(2026, 5, 8, 8), + end: _at(2026, 5, 8, 10), ); - final result = assignLanes([regular, custom], maxLanes: 2) - .whereType() - .toList(); + final result = assignLanes([ + regular, + custom, + ], maxLanes: 2).whereType().toList(); // Same startTime → priority decides: custom (0) goes left of regular (2). - final customCell = result.firstWhere((c) => c.appointment.id is CustomAppointment); - final regularCell = result.firstWhere((c) => c.appointment.id is WebuntisAppointment); + final customCell = result.firstWhere( + (c) => c.appointment.id is CustomAppointment, + ); + final regularCell = result.firstWhere( + (c) => c.appointment.id is WebuntisAppointment, + ); expect(customCell.lane, lessThan(regularCell.lane)); }); test('cancelled lesson lands left of a non-cancelled one on tie', () { final cancelled = _appt( id: WebuntisAppointment(_lesson(code: 'cancelled')), - start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 10), + start: _at(2026, 5, 8, 8), + end: _at(2026, 5, 8, 10), ); final regular = _appt( id: WebuntisAppointment(_lesson()), - start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 10), + start: _at(2026, 5, 8, 8), + end: _at(2026, 5, 8, 10), ); - final result = assignLanes([regular, cancelled], maxLanes: 2) - .whereType() - .toList(); + final result = assignLanes([ + regular, + cancelled, + ], maxLanes: 2).whereType().toList(); String? codeOf(LaidOutAppointment c) { final id = c.appointment.id; return id is WebuntisAppointment ? id.lesson.code : null; } + final cancelledCell = result.firstWhere((c) => codeOf(c) == 'cancelled'); final regularCell = result.firstWhere((c) => codeOf(c) == null); expect(cancelledCell.lane, lessThan(regularCell.lane)); }); - test('overflow time-range spans earliest start to latest end of collapsed appointments', () { - // 4 overlapping appointments, maxLanes = 2 → 1 visible + overflow of 3. - final a = _appt(start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 12)); - final b = _appt(start: _at(2026, 5, 8, 9), end: _at(2026, 5, 8, 10)); - final c = _appt(start: _at(2026, 5, 8, 9, 30), end: _at(2026, 5, 8, 14)); - final d = _appt(start: _at(2026, 5, 8, 10), end: _at(2026, 5, 8, 11)); + test( + 'overflow time-range spans earliest start to latest end of collapsed appointments', + () { + // 4 overlapping appointments, maxLanes = 2 → 1 visible + overflow of 3. + final a = _appt(start: _at(2026, 5, 8, 8), end: _at(2026, 5, 8, 12)); + final b = _appt(start: _at(2026, 5, 8, 9), end: _at(2026, 5, 8, 10)); + final c = _appt( + start: _at(2026, 5, 8, 9, 30), + end: _at(2026, 5, 8, 14), + ); + final d = _appt(start: _at(2026, 5, 8, 10), end: _at(2026, 5, 8, 11)); - final overflow = assignLanes([a, b, c, d], maxLanes: 2) - .whereType() - .single; - expect(overflow.appointments, hasLength(3)); - expect(overflow.startTime, _at(2026, 5, 8, 9), - reason: 'earliest non-visible start time'); - expect(overflow.endTime, _at(2026, 5, 8, 14), - reason: 'latest non-visible end time'); - }); + final overflow = assignLanes([ + a, + b, + c, + d, + ], maxLanes: 2).whereType().single; + expect(overflow.appointments, hasLength(3)); + expect( + overflow.startTime, + _at(2026, 5, 8, 9), + reason: 'earliest non-visible start time', + ); + expect( + overflow.endTime, + _at(2026, 5, 8, 14), + reason: 'latest non-visible end time', + ); + }, + ); test('empty input returns an empty list', () { expect(assignLanes(const [], maxLanes: 2), isEmpty); }); test('asserts maxLanes >= 2', () { - expect(() => assignLanes(const [], maxLanes: 1), throwsA(isA())); + expect( + () => assignLanes(const [], maxLanes: 1), + throwsA(isA()), + ); }); }); } diff --git a/test/widget/async_action_controller_test.dart b/test/widget/async_action_controller_test.dart index 8bf8d18..a91489b 100644 --- a/test/widget/async_action_controller_test.dart +++ b/test/widget/async_action_controller_test.dart @@ -14,27 +14,33 @@ void main() { seenBusyInsideCallback = controller.busy; }); - expect(seenBusyInsideCallback, isTrue, - reason: 'busy must be true while the callback is running'); + expect( + seenBusyInsideCallback, + isTrue, + reason: 'busy must be true while the callback is running', + ); expect(ok, isTrue); expect(controller.busy, isFalse); expect(controller.error, isNull); }); - test('captures mapped error message on failure and returns false', () async { - final controller = AsyncActionController(); - addTearDown(controller.dispose); + test( + 'captures mapped error message on failure and returns false', + () async { + final controller = AsyncActionController(); + addTearDown(controller.dispose); - final ok = await controller.run( - () async => throw Exception('boom'), - errorBuilder: (e) => 'custom: $e', - ); + final ok = await controller.run( + () async => throw Exception('boom'), + errorBuilder: (e) => 'custom: $e', + ); - expect(ok, isFalse); - expect(controller.busy, isFalse); - expect(controller.error, contains('custom:')); - expect(controller.error, contains('boom')); - }); + expect(ok, isFalse); + expect(controller.busy, isFalse); + expect(controller.error, contains('custom:')); + expect(controller.error, contains('boom')); + }, + ); test('rejects re-entry while busy', () async { final controller = AsyncActionController(); @@ -51,8 +57,12 @@ void main() { expect(controller.busy, isTrue); final reentrant = await controller.run(() async {}); - expect(reentrant, isFalse, - reason: 'second run while busy must be rejected without invoking callback'); + expect( + reentrant, + isFalse, + reason: + 'second run while busy must be rejected without invoking callback', + ); firstCanFinish.complete(); expect(await firstFuture, isTrue);