diff --git a/flutter/Makefile b/flutter/Makefile index de96c3d..da51fdf 100644 --- a/flutter/Makefile +++ b/flutter/Makefile @@ -4,6 +4,8 @@ run: dart run build_runner build flutter run +test: + dart analyze gen: dart run build_runner build diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml index 64222f2..8eac293 100644 --- a/flutter/analysis_options.yaml +++ b/flutter/analysis_options.yaml @@ -1,29 +1,117 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +include: + - package:lints/recommended.yaml + - package:flutter_lints/flutter.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. + + rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - prefer_relative_imports: true, + always_use_package_imports: true, + avoid_empty_else: true, + avoid_returning_null_for_future: true, + avoid_type_to_string: true, + avoid_types_as_parameter_names: true, + avoid_web_libraries_in_flutter: true, + collection_methods_unrelated_type: true, + discarded_futures: true, + empty_statements: true, + hash_and_equals: true, + implicit_reopen: true, + invalid_case_patterns: true, + invariant_booleans: true, + no_duplicate_case_values: true, + no_logic_in_create_state: true, + no_self_assignments: true, + no_wildcard_variable_uses: true, + prefer_void_to_null: true, + unnecessary_statements: true, + valid_regexps: true, + always_declare_return_types: true, + always_put_control_body_on_new_line: true, + always_specify_types: true, + annotate_overrides: true, + annotate_redeclares: true, + avoid_annotating_with_dynamic: true, + avoid_function_literals_in_foreach_calls: true, + avoid_init_to_null: true, + avoid_null_checks_in_equality_operators: true, + avoid_renaming_method_parameters: true, + avoid_return_types_on_setters: true, + avoid_returning_null: true, + avoid_returning_null_for_void: true, + avoid_returning_this: true, + avoid_shadowing_type_parameters: true, + avoid_single_cascade_in_expression_statements: true, + avoid_unnecessary_containers: true, + avoid_unused_constructor_parameters: true, + avoid_void_async: true, + await_only_futures: true, + camel_case_extensions: true, + camel_case_types: true, + cast_nullable_to_non_nullable: true, + constant_identifier_names: true, + empty_catches: true, + eol_at_end_of_file: true, + exhaustive_cases: true, + file_names: true, + no_literal_bool_comparisons: true, + null_check_on_nullable_type_parameter: true, + null_closures: true, + overridden_fields: true, + prefer_adjacent_string_concatenation: true, + prefer_collection_literals: true, + prefer_conditional_assignment: true, + prefer_const_constructors: true, + prefer_const_constructors_in_immutables: true, + prefer_const_declarations: true, + prefer_const_literals_to_create_immutables: true, + prefer_contains: true, + prefer_final_fields: true, + prefer_for_elements_to_map_fromIterable: true, + prefer_function_declarations_over_variables: true, + prefer_generic_function_type_aliases: true, + prefer_if_null_operators: true, + prefer_initializing_formals: true, + prefer_inlined_adds: true, + prefer_interpolation_to_compose_strings: true, + prefer_is_empty: true, + prefer_is_not_empty: true, + prefer_is_not_operator: true, + prefer_iterable_whereType: true, + prefer_null_aware_operators: true, + prefer_spread_collections: true, + prefer_typing_uninitialized_variables: true, + provide_deprecation_message: true, + recursive_getters: true, + sized_box_for_whitespace: true, + type_init_formals: true, + type_literal_in_constant_pattern: true, + unnecessary_const: true, + unnecessary_constructor_name: true, + unnecessary_getters_setters: true, + unnecessary_late: true, + unnecessary_new: true, + unnecessary_null_aware_assignments: true, + unnecessary_null_in_if_null_operators: true, + unnecessary_nullable_for_final_variable_declarations: true, + unnecessary_overrides: true, + unnecessary_string_escapes: true, + unnecessary_string_interpolations: true, + unnecessary_this: true, + unnecessary_to_list_in_spreads: true, + use_full_hex_values_for_flutter_colors: true, + use_function_type_syntax_for_parameters: true, + use_rethrow_when_possible: true, + use_string_in_part_of_directives: true, + use_super_parameters: true, + void_checks: true, + depend_on_referenced_packages: true, + package_names: true, + secure_pubspec_urls: true, -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true \ No newline at end of file diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index 47ea5da..d02174e 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -74,7 +74,7 @@ class APIClient { if (responseStatusCode != 200) { try { - final apierr = APIError.fromJson(jsonDecode(responseBody)); + final apierr = APIError.fromJson(jsonDecode(responseBody) as Map); RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr); showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); @@ -90,7 +90,7 @@ class APIClient { final data = jsonDecode(responseBody); if (fn != null) { - final result = fn(data); + final result = fn(data as Map); RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); return result; } else { @@ -137,7 +137,7 @@ class APIClient { method: 'GET', relURL: 'users/${auth.userId}/channels', query: {'selector': sel.apiKey}, - fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels']), + fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels'] as List), auth: auth, ); } diff --git a/flutter/lib/components/layout/app_bar.dart b/flutter/lib/components/layout/app_bar.dart index b690ae6..4c3218f 100644 --- a/flutter/lib/components/layout/app_bar.dart +++ b/flutter/lib/components/layout/app_bar.dart @@ -39,7 +39,7 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget { icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow), tooltip: 'Debug', onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => DebugMainPage())); + Navigator.push(context, MaterialPageRoute(builder: (context) => DebugMainPage())); }, ), if (!showDebug) SizedBox.square(dimension: 40), diff --git a/flutter/lib/models/api_error.dart b/flutter/lib/models/api_error.dart index 374d712..adb6fb2 100644 --- a/flutter/lib/models/api_error.dart +++ b/flutter/lib/models/api_error.dart @@ -13,10 +13,10 @@ class APIError { factory APIError.fromJson(Map json) { return APIError( - success: json['success'], - error: json['error'], - errhighlight: json['errhighlight'], - message: json['message'], + success: json['success'] as String, + error: json['error'] as String, + errhighlight: json['errhighlight'] as String, + message: json['message'] as String, ); } } diff --git a/flutter/lib/models/channel.dart b/flutter/lib/models/channel.dart index e92dad3..337e19d 100644 --- a/flutter/lib/models/channel.dart +++ b/flutter/lib/models/channel.dart @@ -25,15 +25,15 @@ class Channel { factory Channel.fromJson(Map json) { return Channel( - channelID: json['channel_id'], - ownerUserID: json['owner_user_id'], - internalName: json['internal_name'], - displayName: json['display_name'], - descriptionName: json['description_name'], - subscribeKey: json['subscribe_key'], - timestampCreated: json['timestamp_created'], - timestampLastSent: json['timestamp_lastsent'], - messagesSent: json['messages_sent'], + channelID: json['channel_id'] as String, + ownerUserID: json['owner_user_id'] as String, + internalName: json['internal_name'] as String, + displayName: json['display_name'] as String, + descriptionName: json['description_name'] as String?, + subscribeKey: json['subscribe_key'] as String?, + timestampCreated: json['timestamp_created'] as String, + timestampLastSent: json['timestamp_lastsent'] as String?, + messagesSent: json['messages_sent'] as int, ); } } @@ -56,20 +56,20 @@ class ChannelWithSubscription extends Channel { factory ChannelWithSubscription.fromJson(Map json) { return ChannelWithSubscription( - channelID: json['channel_id'], - ownerUserID: json['owner_user_id'], - internalName: json['internal_name'], - displayName: json['display_name'], - descriptionName: json['description_name'], - subscribeKey: json['subscribe_key'], - timestampCreated: json['timestamp_created'], - timestampLastSent: json['timestamp_lastsent'], - messagesSent: json['messages_sent'], - subscription: Subscription.fromJson(json['subscription']), + channelID: json['channel_id'] as String, + ownerUserID: json['owner_user_id'] as String, + internalName: json['internal_name'] as String, + displayName: json['display_name'] as String, + descriptionName: json['description_name'] as String?, + subscribeKey: json['subscribe_key'] as String?, + timestampCreated: json['timestamp_created'] as String, + timestampLastSent: json['timestamp_lastsent'] as String?, + messagesSent: json['messages_sent'] as int, + subscription: Subscription.fromJson(json['subscription'] as Map), ); } static List fromJsonArray(List jsonArr) { - return jsonArr.map((e) => ChannelWithSubscription.fromJson(e)).toList(); + return jsonArr.map((e) => ChannelWithSubscription.fromJson(e as Map)).toList(); } } diff --git a/flutter/lib/models/message.dart b/flutter/lib/models/message.dart index faa65fd..cee7fca 100644 --- a/flutter/lib/models/message.dart +++ b/flutter/lib/models/message.dart @@ -31,26 +31,26 @@ class Message { factory Message.fromJson(Map json) { return Message( - messageID: json['message_id'], - senderUserID: json['sender_user_id'], - channelInternalName: json['channel_internal_name'], - channelID: json['channel_id'], - senderName: json['sender_name'], - senderIP: json['sender_ip'], - timestamp: json['timestamp'], - title: json['title'], - content: json['content'], - priority: json['priority'], - userMessageID: json['usr_message_id'], - usedKeyID: json['used_key_id'], - trimmed: json['trimmed'], + messageID: json['message_id'] as String, + senderUserID: json['sender_user_id'] as String, + channelInternalName: json['channel_internal_name'] as String, + channelID: json['channel_id'] as String, + senderName: json['sender_name'] as String, + senderIP: json['sender_ip'] as String, + timestamp: json['timestamp'] as String, + title: json['title'] as String, + content: json['content'] as String, + priority: json['priority'] as int, + userMessageID: json['usr_message_id'] as String, + usedKeyID: json['used_key_id'] as String, + trimmed: json['trimmed'] as bool, ); } - static fromPaginatedJsonArray(Map data, String keyMessages, String keyToken) { + static (String, List) fromPaginatedJsonArray(Map data, String keyMessages, String keyToken) { final npt = data[keyToken] as String; - final messages = (data[keyMessages] as List).map((e) => Message.fromJson(e)).toList(); + final messages = (data[keyMessages] as List).map((e) => Message.fromJson(e as Map)).toList(); return (npt, messages); } diff --git a/flutter/lib/models/subscription.dart b/flutter/lib/models/subscription.dart index b8432f4..f927565 100644 --- a/flutter/lib/models/subscription.dart +++ b/flutter/lib/models/subscription.dart @@ -19,13 +19,13 @@ class Subscription { factory Subscription.fromJson(Map json) { return Subscription( - subscriptionID: json['subscription_id'], - subscriberUserID: json['subscriber_user_id'], - channelOwnerUserID: json['channel_owner_user_id'], - channelID: json['channel_id'], - channelInternalName: json['channel_internal_name'], - timestampCreated: json['timestamp_created'], - confirmed: json['confirmed'], + subscriptionID: json['subscription_id'] as String, + subscriberUserID: json['subscriber_user_id'] as String, + channelOwnerUserID: json['channel_owner_user_id'] as String, + channelID: json['channel_id'] as String, + channelInternalName: json['channel_internal_name'] as String, + timestampCreated: json['timestamp_created'] as String, + confirmed: json['confirmed'] as bool, ); } } diff --git a/flutter/lib/models/user.dart b/flutter/lib/models/user.dart index 28cc99a..c8f4eb8 100644 --- a/flutter/lib/models/user.dart +++ b/flutter/lib/models/user.dart @@ -41,24 +41,24 @@ class User { factory User.fromJson(Map json) { return User( - userID: json['user_id'], - username: json['username'], - timestampCreated: json['timestamp_created'], - timestampLastRead: json['timestamp_lastread'], - timestampLastSent: json['timestamp_lastsent'], - messagesSent: json['messages_sent'], - quotaUsed: json['quota_used'], - quotaRemaining: json['quota_remaining'], - quotaPerDay: json['quota_max'], - isPro: json['is_pro'], - defaultChannel: json['default_channel'], - maxBodySize: json['max_body_size'], - maxTitleLength: json['max_title_length'], - defaultPriority: json['default_priority'], - maxChannelNameLength: json['max_channel_name_length'], - maxChannelDescriptionLength: json['max_channel_description_length'], - maxSenderNameLength: json['max_sender_name_length'], - maxUserMessageIDLength: json['max_user_message_id_length'], + userID: json['user_id'] as String, + username: json['username'] as String?, + timestampCreated: json['timestamp_created'] as String, + timestampLastRead: json['timestamp_lastread'] as String?, + timestampLastSent: json['timestamp_lastsent'] as String?, + messagesSent: json['messages_sent'] as int, + quotaUsed: json['quota_used'] as int, + quotaRemaining: json['quota_remaining'] as int, + quotaPerDay: json['quota_max'] as int, + isPro: json['is_pro'] as bool, + defaultChannel: json['default_channel'] as String, + maxBodySize: json['max_body_size'] as int, + maxTitleLength: json['max_title_length'] as int, + defaultPriority: json['default_priority'] as int, + maxChannelNameLength: json['max_channel_name_length'] as int, + maxChannelDescriptionLength: json['max_channel_description_length'] as int, + maxSenderNameLength: json['max_sender_name_length'] as int, + maxUserMessageIDLength: json['max_user_message_id_length'] as int, ); } } diff --git a/flutter/lib/pages/debug/debug_requests.dart b/flutter/lib/pages/debug/debug_requests.dart index 65f0754..27331e2 100644 --- a/flutter/lib/pages/debug/debug_requests.dart +++ b/flutter/lib/pages/debug/debug_requests.dart @@ -25,62 +25,9 @@ class _DebugRequestsPageState extends State { itemBuilder: (context, listIndex) { final req = requestsBox.getAt(requestsBox.length - listIndex - 1)!; if (req.type == 'SUCCESS') { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0), - child: GestureDetector( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugRequestViewPage(request: req))), - child: ListTile( - title: Row( - children: [ - SizedBox( - width: 120, - child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), - ), - Expanded( - child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), - ), - SizedBox(width: 2), - Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)), - ], - ), - subtitle: Text(req.type), - ), - ), - ); + return buildItemSuccess(context, req); } else { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0), - child: GestureDetector( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugRequestViewPage(request: req))), - child: ListTile( - tileColor: Theme.of(context).colorScheme.errorContainer, - textColor: Theme.of(context).colorScheme.onErrorContainer, - title: Row( - children: [ - SizedBox( - width: 120, - child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), - ), - Expanded( - child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), - ), - SizedBox(width: 2), - Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(req.type), - Text( - req.error, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - )), - ), - ); + return buildItemError(context, req); } }, ); @@ -88,4 +35,65 @@ class _DebugRequestsPageState extends State { ), ); } + + Padding buildItemError(BuildContext context, SCNRequest req) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0), + child: GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugRequestViewPage(request: req))), + child: ListTile( + tileColor: Theme.of(context).colorScheme.errorContainer, + textColor: Theme.of(context).colorScheme.onErrorContainer, + title: Row( + children: [ + SizedBox( + width: 120, + child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), + ), + Expanded( + child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), + ), + SizedBox(width: 2), + Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(req.type), + Text( + req.error, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + )), + ), + ); + } + + Padding buildItemSuccess(BuildContext context, SCNRequest req) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0), + child: GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugRequestViewPage(request: req))), + child: ListTile( + title: Row( + children: [ + SizedBox( + width: 120, + child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), + ), + Expanded( + child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), + ), + SizedBox(width: 2), + Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)), + ], + ), + subtitle: Text(req.type), + ), + ), + ); + } } diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index 38b0702..818f022 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -48,7 +48,7 @@ class _MessageListPageState extends State { try { if (_channels == null) { final channels = await APIClient.getChannelList(acc.auth!, ChannelSelector.allAny); - _channels = Map.fromIterable(channels, key: (e) => e.channelID); + _channels = {for (var v in channels) v.channelID: v}; } final (npt, newItems) = await APIClient.getMessageList(acc.auth!, thisPageToken, _pageSize); @@ -76,7 +76,7 @@ class _MessageListPageState extends State { message: item, allChannels: _channels ?? {}, onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => MessageViewPage(message: item))); + Navigator.push(context, MaterialPageRoute(builder: (context) => MessageViewPage(message: item))); }, ), ), diff --git a/flutter/lib/pages/message_list/message_list_item.dart b/flutter/lib/pages/message_list/message_list_item.dart index b2742be..a76d3ac 100644 --- a/flutter/lib/pages/message_list/message_list_item.dart +++ b/flutter/lib/pages/message_list/message_list_item.dart @@ -145,7 +145,7 @@ class MessageListItem extends StatelessWidget { ); } - processContent(String? v) { + String processContent(String? v) { if (v == null) { return ''; } @@ -158,7 +158,7 @@ class MessageListItem extends StatelessWidget { return lines.sublist(0, min(_lineCount, lines.length)).join("\n").trim(); } - processTitle(String? v) { + String processTitle(String? v) { if (v == null) { return ''; } @@ -174,7 +174,7 @@ class MessageListItem extends StatelessWidget { return allChannels[message.channelID]?.displayName ?? message.channelInternalName; } - showChannel(Message message) { + bool showChannel(Message message) { return message.channelInternalName != 'main'; } } diff --git a/flutter/lib/pages/send/root.dart b/flutter/lib/pages/send/root.dart index c4ba722..1f2002f 100644 --- a/flutter/lib/pages/send/root.dart +++ b/flutter/lib/pages/send/root.dart @@ -79,7 +79,7 @@ class _SendRootPageState extends State { //... } - _buildQRCode(BuildContext context, UserAccount acc) { + Widget _buildQRCode(BuildContext context, UserAccount acc) { if (acc.auth == null) { return const Placeholder(); } diff --git a/flutter/lib/state/globals.dart b/flutter/lib/state/globals.dart index 70287aa..8a3ffb8 100644 --- a/flutter/lib/state/globals.dart +++ b/flutter/lib/state/globals.dart @@ -18,7 +18,7 @@ class Globals { String platform = ''; String hostname = ''; - init() async { + Future init() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); this.appName = packageInfo.appName; diff --git a/flutter/lib/state/user_account.dart b/flutter/lib/state/user_account.dart index d2fb8b8..bc5a3cc 100644 --- a/flutter/lib/state/user_account.dart +++ b/flutter/lib/state/user_account.dart @@ -38,7 +38,7 @@ class UserAccount extends ChangeNotifier { notifyListeners(); } - load() async { + void load() async { final prefs = await SharedPreferences.getInstance(); final uid = prefs.getString('auth.userid'); @@ -51,7 +51,7 @@ class UserAccount extends ChangeNotifier { } } - save() async { + Future save() async { final prefs = await SharedPreferences.getInstance(); if (_auth == null) { await prefs.remove('auth.userid'); @@ -62,9 +62,9 @@ class UserAccount extends ChangeNotifier { } } - loadUser(bool force) async { + Future loadUser(bool force) async { if (!force && _user != null) { - return _user; + return _user!; } if (_auth == null) {