From b8edf595ca110116f091fe42f81d75f4aa778110 Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:19:31 -0400 Subject: [PATCH] Toolbar practice (#707) * remove print statement * ending animation, savoring joy, properly adding xp in session * forgot to switch env again... * increment version number * about to move toolbar buttons up to level of overlay controller * added ability to give feedback and get new activity --- assets/l10n/intl_en.arb | 7 +- assets/l10n/intl_es.arb | 3 +- lib/pages/chat/events/message_content.dart | 2 +- .../message_activity_request.dart | 41 ++- .../practice_activity_model.dart | 12 +- .../widgets/chat/message_audio_card.dart | 1 + .../chat/message_selection_overlay.dart | 40 ++- lib/pangea/widgets/chat/message_toolbar.dart | 253 ++----------- .../widgets/chat/message_toolbar_buttons.dart | 122 +++++++ .../chat/message_toolbar_selection_area.dart | 48 +++ .../chat/message_translation_card.dart | 2 + lib/pangea/widgets/igc/pangea_rich_text.dart | 2 +- lib/pangea/widgets/igc/word_data_card.dart | 117 +++--- .../no_more_practice_card.dart | 1 + .../practice_activity_card.dart | 337 +++++++++--------- .../target_tokens_controller.dart | 99 +++++ pubspec.lock | 36 +- 17 files changed, 652 insertions(+), 471 deletions(-) create mode 100644 lib/pangea/widgets/chat/message_toolbar_buttons.dart create mode 100644 lib/pangea/widgets/chat/message_toolbar_selection_area.dart create mode 100644 lib/pangea/widgets/practice_activity/target_tokens_controller.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 818c7e184..de1b2f8e7 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4121,7 +4121,7 @@ "suggestToSpaceDesc": "Suggested sub spaces will appear in their main space's chat list", "practice": "Practice", "noLanguagesSet": "No languages set", - "noActivitiesFound": "You're all practiced for now! Come back later for more.", + "noActivitiesFound": "That's enough on this for now! Come back later for more.", "hintTitle": "Hint:", "speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores", "previous": "Previous", @@ -4229,5 +4229,8 @@ "grammar": "Grammar", "contactHasBeenInvitedToTheChat": "Contact has been invited to the chat", "inviteChat": "📨 Invite chat", - "chatName": "Chat name" + "chatName": "Chat name", + "reportContentIssueTitle": "Report content issue", + "feedback": "Your feedback (optional)", + "reportContentIssueDescription": "Sorry! AI can make personalized experiences but also may have issues. Please provide any feedback you have and we'll generate a new activity." } \ No newline at end of file diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index d371276b6..632ff11ee 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4737,5 +4737,6 @@ } }, "commandHint_googly": "Enviar unos ojos saltones", - "@commandHint_googly": {} + "@commandHint_googly": {}, + "reportContentIssue": "Problema de contenido" } \ No newline at end of file diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index eca0cfd5f..2cddb6f67 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -4,7 +4,7 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar_selection_area.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message_text.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; diff --git a/lib/pangea/models/practice_activities.dart/message_activity_request.dart b/lib/pangea/models/practice_activities.dart/message_activity_request.dart index 9dabc879d..b8d44f41b 100644 --- a/lib/pangea/models/practice_activities.dart/message_activity_request.dart +++ b/lib/pangea/models/practice_activities.dart/message_activity_request.dart @@ -110,7 +110,8 @@ class ExistingActivityMetaData { factory ExistingActivityMetaData.fromJson(Map json) { return ExistingActivityMetaData( activityEventId: json['activity_event_id'] as String, - tgtConstructs: (json['tgt_constructs'] as List) + tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs']) + as List) .map((e) => ConstructIdentifier.fromJson(e as Map)) .toList(), activityType: ActivityTypeEnum.values.firstWhere( @@ -124,18 +125,47 @@ class ExistingActivityMetaData { Map toJson() { return { 'activity_event_id': activityEventId, - 'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(), + 'target_constructs': tgtConstructs.map((e) => e.toJson()).toList(), 'activity_type': activityType.string, }; } } +// includes feedback text and the bad activity model +class ActivityQualityFeedback { + final String feedbackText; + final PracticeActivityModel badActivity; + + ActivityQualityFeedback({ + required this.feedbackText, + required this.badActivity, + }); + + factory ActivityQualityFeedback.fromJson(Map json) { + return ActivityQualityFeedback( + feedbackText: json['feedback_text'] as String, + badActivity: PracticeActivityModel.fromJson( + json['bad_activity'] as Map, + ), + ); + } + + Map toJson() { + return { + 'feedback_text': feedbackText, + 'bad_activity': badActivity.toJson(), + }; + } +} + class MessageActivityRequest { final String userL1; final String userL2; final String messageText; + final ActivityQualityFeedback? activityQualityFeedback; + /// tokens with their associated constructs and xp final List tokensWithXP; @@ -151,6 +181,7 @@ class MessageActivityRequest { required this.tokensWithXP, required this.messageId, required this.existingActivities, + required this.activityQualityFeedback, }); factory MessageActivityRequest.fromJson(Map json) { @@ -167,6 +198,11 @@ class MessageActivityRequest { (e) => ExistingActivityMetaData.fromJson(e as Map), ) .toList(), + activityQualityFeedback: json['activity_quality_feedback'] != null + ? ActivityQualityFeedback.fromJson( + json['activity_quality_feedback'] as Map, + ) + : null, ); } @@ -178,6 +214,7 @@ class MessageActivityRequest { 'tokens_with_xp': tokensWithXP.map((e) => e.toJson()).toList(), 'message_id': messageId, 'existing_activities': existingActivities.map((e) => e.toJson()).toList(), + 'activity_quality_feedback': activityQualityFeedback?.toJson(), }; } diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index 4e93cb279..7c02a7aae 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -270,7 +270,8 @@ class PracticeActivityModel { factory PracticeActivityModel.fromJson(Map json) { return PracticeActivityModel( - tgtConstructs: (json['tgt_constructs'] as List) + tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs']) + as List) .map((e) => ConstructIdentifier.fromJson(e as Map)) .toList(), langCode: json['lang_code'] as String, @@ -278,7 +279,9 @@ class PracticeActivityModel { activityType: json['activity_type'] == "multipleChoice" ? ActivityTypeEnum.multipleChoice : ActivityTypeEnum.values.firstWhere( - (e) => e.string == json['activity_type'], + (e) => + e.string == json['activity_type'] as String || + e.string.split('.').last == json['activity_type'] as String, ), multipleChoice: json['multiple_choice'] != null ? MultipleChoice.fromJson( @@ -301,12 +304,13 @@ class PracticeActivityModel { RelevantSpanDisplayDetails? get relevantSpanDisplayDetails => multipleChoice?.spanDisplayDetails; + Map toJson() { return { - 'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(), + 'target_constructs': tgtConstructs.map((e) => e.toJson()).toList(), 'lang_code': langCode, 'msg_id': msgId, - 'activity_type': activityType.toString().split('.').last, + 'activity_type': activityType.string, 'multiple_choice': multipleChoice?.toJson(), 'listening': listening?.toJson(), 'speaking': speaking?.toJson(), diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 9a8fa0f34..02b637ee1 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -76,6 +76,7 @@ class MessageAudioCardState extends State { @override Widget build(BuildContext context) { return Container( + padding: const EdgeInsets.all(8), child: _isLoading ? const ToolbarContentLoadingIndicator() : localAudioEvent != null || audioFile != null diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index d3db75e6e..34ea42ffc 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -57,6 +57,8 @@ class MessageOverlayController extends State int activitiesLeftToComplete = neededActivities; + PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent; + @override void initState() { super.initState(); @@ -301,13 +303,8 @@ class MessageOverlayController extends State ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.only( - left: widget._pangeaMessageEvent.ownMessage - ? 0 - : Avatar.defaultSize + 16, - right: widget._pangeaMessageEvent.ownMessage ? 8 : 0, - ), + MessagePadding( + pangeaMessageEvent: pangeaMessageEvent, child: MessageToolbar( pangeaMessageEvent: widget._pangeaMessageEvent, overLayController: this, @@ -330,6 +327,13 @@ class MessageOverlayController extends State nextEvent: widget._nextEvent, previousEvent: widget._prevEvent, ), + // TODO for @ggurdin - move reactions and toolbar here + // MessageReactions(widget._event, widget.chatController.timeline!), + // const SizedBox(height: 6), + // MessagePadding( + // pangeaMessageEvent: pangeaMessageEvent, + // child: ToolbarButtons(overlayController: this, width: 250), + // ), ], ), ), @@ -396,3 +400,25 @@ class MessageOverlayController extends State ); } } + +class MessagePadding extends StatelessWidget { + const MessagePadding({ + super.key, + required this.child, + required this.pangeaMessageEvent, + }); + + final Widget child; + final PangeaMessageEvent pangeaMessageEvent; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + left: pangeaMessageEvent.ownMessage ? 0 : Avatar.defaultSize + 16, + right: pangeaMessageEvent.ownMessage ? 8 : 0, + ), + child: child, + ); + } +} diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 23db604fd..f204aec47 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -1,16 +1,14 @@ import 'dart:developer'; -import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart'; import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; @@ -18,7 +16,6 @@ import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_ca import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; class MessageToolbar extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; @@ -35,33 +32,9 @@ class MessageToolbar extends StatefulWidget { } class MessageToolbarState extends State { - bool updatingMode = false; - @override void initState() { super.initState(); - - // why can't this just be initstate or the build mode? - // WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - // //determine the starting mode - // // if (widget.pangeaMessageEvent.isAudioMessage) { - // // updateMode(MessageMode.speechToText); - // // return; - // // } - - // // if (widget.initialMode != null) { - // // updateMode(widget.initialMode!); - // // } else { - // // MatrixState.pangeaController.userController.profile.userSettings - // // .autoPlayMessages - // // ? updateMode(MessageMode.textToSpeech) - // // : updateMode(MessageMode.translation); - // // } - // // }); - - // // just set mode based on messageSelectionOverlay mode which is now handling the state - // updateMode(widget.overLayController.toolbarMode); - // }); } Widget get toolbarContent { @@ -141,208 +114,50 @@ class MessageToolbarState extends State { @override Widget build(BuildContext context) { - debugPrint("building toolbar"); return Material( key: MatrixState.pAnyState .layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar') .key, type: MaterialType.transparency, - child: Container( - constraints: const BoxConstraints( - maxHeight: AppConfig.toolbarMaxHeight, - maxWidth: 275, - minWidth: 275, - ), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border.all( - width: 2, - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: const BorderRadius.all( - Radius.circular(25), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: toolbarContent, - ), - ), - ), - ToolbarButtons(messageToolbarController: this, width: 250), - ], - ), - ), - ); - } -} - -class ToolbarSelectionArea extends StatelessWidget { - final ChatController controller; - final PangeaMessageEvent? pangeaMessageEvent; - final bool isOverlay; - final Widget child; - final Event? nextEvent; - final Event? prevEvent; - - const ToolbarSelectionArea({ - required this.controller, - this.pangeaMessageEvent, - this.isOverlay = false, - required this.child, - this.nextEvent, - this.prevEvent, - super.key, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (pangeaMessageEvent != null && !isOverlay) { - controller.showToolbar( - pangeaMessageEvent!, - nextEvent: nextEvent, - prevEvent: prevEvent, - ); - } - }, - onLongPress: () { - if (pangeaMessageEvent != null && !isOverlay) { - controller.showToolbar( - pangeaMessageEvent!, - nextEvent: nextEvent, - prevEvent: prevEvent, - ); - } - }, - child: child, - ); - } -} - -class ToolbarButtons extends StatefulWidget { - final MessageToolbarState messageToolbarController; - final double width; - - const ToolbarButtons({ - required this.messageToolbarController, - required this.width, - super.key, - }); - - @override - ToolbarButtonsState createState() => ToolbarButtonsState(); -} - -class ToolbarButtonsState extends State { - PangeaMessageEvent get pangeaMessageEvent => - widget.messageToolbarController.widget.pangeaMessageEvent; - - List get modes => MessageMode.values - .where((mode) => mode.isValidMode(pangeaMessageEvent.event)) - .toList(); - - static const double iconWidth = 36.0; - - MessageOverlayController get overlayController => - widget.messageToolbarController.widget.overLayController; - - // @ggurdin - maybe this can be stateless now? - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - final double barWidth = widget.width - iconWidth; - - if (widget - .messageToolbarController.widget.pangeaMessageEvent.isAudioMessage) { - return const SizedBox(); - } - - return SizedBox( - width: widget.width, - child: Stack( - alignment: Alignment.center, + child: Column( children: [ - Stack( - children: [ - Container( - width: widget.width, - height: 12, - decoration: BoxDecoration( - color: MessageModeExtension.barAndLockedButtonColor(context), - ), - margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2), + Container( + constraints: const BoxConstraints( + maxHeight: AppConfig.toolbarMaxHeight, + maxWidth: 350, + minWidth: 350, + ), + padding: const EdgeInsets.all(0), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + width: 2, + color: Theme.of(context).colorScheme.primary.withOpacity(0.5), ), - AnimatedContainer( - duration: FluffyThemes.animationDuration, - height: 12, - width: overlayController.isPracticeComplete - ? barWidth - : min( - barWidth, - (barWidth / 3) * - pangeaMessageEvent.numberOfActivitiesCompleted, - ), - color: AppConfig.success, - margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2), + borderRadius: const BorderRadius.all( + Radius.circular(AppConfig.borderRadius), ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: modes - .mapIndexed( - (index, mode) => Tooltip( - message: mode.tooltip(context), - child: IconButton( - iconSize: 20, - icon: Icon(mode.icon), - color: mode == - widget.messageToolbarController.widget - .overLayController.toolbarMode - ? Colors.white - : null, - isSelected: mode == - widget.messageToolbarController.widget - .overLayController.toolbarMode, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - mode.iconButtonColor( - context, - index, - widget.messageToolbarController.widget - .overLayController.toolbarMode, - pangeaMessageEvent.numberOfActivitiesCompleted, - widget.messageToolbarController.widget - .overLayController.isPracticeComplete, - ), - ), - ), - onPressed: mode.isUnlocked( - index, - pangeaMessageEvent.numberOfActivitiesCompleted, - overlayController.isPracticeComplete, - ) - ? () => widget - .messageToolbarController.widget.overLayController - .updateToolbarMode(mode) - : null, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: toolbarContent, ), ), - ) - .toList(), + ), + ], + ), + ), + const SizedBox(height: 6), + ToolbarButtons( + overlayController: widget.overLayController, + width: 250, ), + const SizedBox(height: 6), ], ), ); diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart new file mode 100644 index 000000000..564538a75 --- /dev/null +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -0,0 +1,122 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:flutter/material.dart'; + +class ToolbarButtons extends StatefulWidget { + final MessageOverlayController overlayController; + final double width; + + const ToolbarButtons({ + required this.overlayController, + required this.width, + super.key, + }); + + @override + ToolbarButtonsState createState() => ToolbarButtonsState(); +} + +class ToolbarButtonsState extends State { + PangeaMessageEvent get pangeaMessageEvent => + widget.overlayController.pangeaMessageEvent; + + List get modes => MessageMode.values + .where((mode) => mode.isValidMode(pangeaMessageEvent.event)) + .toList(); + + static const double iconWidth = 36.0; + + MessageOverlayController get overlayController => widget.overlayController; + + // @ggurdin - maybe this can be stateless now? + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final double barWidth = widget.width - iconWidth; + + if (widget.overlayController.pangeaMessageEvent.isAudioMessage) { + return const SizedBox(); + } + + return SizedBox( + width: widget.width, + child: Stack( + alignment: Alignment.center, + children: [ + Stack( + children: [ + Container( + width: widget.width, + height: 12, + decoration: BoxDecoration( + color: MessageModeExtension.barAndLockedButtonColor(context), + ), + margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2), + ), + AnimatedContainer( + duration: FluffyThemes.animationDuration, + height: 12, + width: overlayController.isPracticeComplete + ? barWidth + : min( + barWidth, + (barWidth / 3) * + pangeaMessageEvent.numberOfActivitiesCompleted, + ), + color: AppConfig.success, + margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: modes + .mapIndexed( + (index, mode) => Tooltip( + message: mode.tooltip(context), + child: IconButton( + iconSize: 20, + icon: Icon(mode.icon), + color: mode == widget.overlayController.toolbarMode + ? Colors.white + : null, + isSelected: mode == widget.overlayController.toolbarMode, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + mode.iconButtonColor( + context, + index, + widget.overlayController.toolbarMode, + pangeaMessageEvent.numberOfActivitiesCompleted, + widget.overlayController.isPracticeComplete, + ), + ), + ), + onPressed: mode.isUnlocked( + index, + pangeaMessageEvent.numberOfActivitiesCompleted, + overlayController.isPracticeComplete, + ) + ? () => + widget.overlayController.updateToolbarMode(mode) + : null, + ), + ), + ) + .toList(), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_toolbar_selection_area.dart b/lib/pangea/widgets/chat/message_toolbar_selection_area.dart new file mode 100644 index 000000000..6ddc1026b --- /dev/null +++ b/lib/pangea/widgets/chat/message_toolbar_selection_area.dart @@ -0,0 +1,48 @@ +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class ToolbarSelectionArea extends StatelessWidget { + final ChatController controller; + final PangeaMessageEvent? pangeaMessageEvent; + final bool isOverlay; + final Widget child; + final Event? nextEvent; + final Event? prevEvent; + + const ToolbarSelectionArea({ + required this.controller, + this.pangeaMessageEvent, + this.isOverlay = false, + required this.child, + this.nextEvent, + this.prevEvent, + super.key, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (pangeaMessageEvent != null && !isOverlay) { + controller.showToolbar( + pangeaMessageEvent!, + nextEvent: nextEvent, + prevEvent: prevEvent, + ); + } + }, + onLongPress: () { + if (pangeaMessageEvent != null && !isOverlay) { + controller.showToolbar( + pangeaMessageEvent!, + nextEvent: nextEvent, + prevEvent: prevEvent, + ); + } + }, + child: child, + ); + } +} diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 891a5493f..081163558 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -151,6 +151,7 @@ class MessageTranslationCardState extends State { } return Container( + padding: const EdgeInsets.all(8), child: _fetchingTranslation ? const ToolbarContentLoadingIndicator() : Column( @@ -170,6 +171,7 @@ class MessageTranslationCardState extends State { body: InlineInstructions.l1Translation.body(context), onClose: closeHint, ), + // if (widget.selection != null) ], ), ); diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index e0b0e95ac..9da9b45b6 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -8,7 +8,7 @@ import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar_selection_area.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index aeb41d08b..54ff49544 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -174,56 +174,59 @@ class WordDataCardView extends StatelessWidget { final ScrollController scrollController = ScrollController(); - return Scrollbar( - thumbVisibility: true, - controller: scrollController, - child: SingleChildScrollView( + return Container( + padding: const EdgeInsets.all(8), + child: Scrollbar( + thumbVisibility: true, controller: scrollController, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (controller.widget.choiceFeedback != null) - Text( - controller.widget.choiceFeedback!, - style: BotStyle.text(context), - ), - const SizedBox(height: 5.0), - if (controller.wordData != null && - controller.wordNetError == null && - controller.activeL1 != null && - controller.activeL2 != null) - WordNetInfo( - wordData: controller.wordData!, - activeL1: controller.activeL1!, - activeL2: controller.activeL2!, - ), - if (controller.isLoadingWordNet) const PCircular(), - const SizedBox(height: 5.0), - // if (controller.widget.hasInfo && - // !controller.isLoadingContextualDefinition && - // controller.contextualDefinitionRes == null) - // Material( - // type: MaterialType.transparency, - // child: ListTile( - // leading: const BotFace( - // width: 40, expression: BotExpression.surprised), - // title: Text(L10n.of(context)!.askPangeaBot), - // onTap: controller.handleGetDefinitionButtonPress, - // ), - // ), - if (controller.isLoadingContextualDefinition) const PCircular(), - if (controller.contextualDefinitionRes != null) - Text( - controller.contextualDefinitionRes!.text, - style: BotStyle.text(context), - ), - if (controller.definitionError != null) - Text( - L10n.of(context)!.sorryNoResults, - style: BotStyle.text(context), - ), - ], + child: SingleChildScrollView( + controller: scrollController, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (controller.widget.choiceFeedback != null) + Text( + controller.widget.choiceFeedback!, + style: BotStyle.text(context), + ), + const SizedBox(height: 5.0), + if (controller.wordData != null && + controller.wordNetError == null && + controller.activeL1 != null && + controller.activeL2 != null) + WordNetInfo( + wordData: controller.wordData!, + activeL1: controller.activeL1!, + activeL2: controller.activeL2!, + ), + if (controller.isLoadingWordNet) const PCircular(), + const SizedBox(height: 5.0), + // if (controller.widget.hasInfo && + // !controller.isLoadingContextualDefinition && + // controller.contextualDefinitionRes == null) + // Material( + // type: MaterialType.transparency, + // child: ListTile( + // leading: const BotFace( + // width: 40, expression: BotExpression.surprised), + // title: Text(L10n.of(context)!.askPangeaBot), + // onTap: controller.handleGetDefinitionButtonPress, + // ), + // ), + if (controller.isLoadingContextualDefinition) const PCircular(), + if (controller.contextualDefinitionRes != null) + Text( + controller.contextualDefinitionRes!.text, + style: BotStyle.text(context), + ), + if (controller.definitionError != null) + Text( + L10n.of(context)!.sorryNoResults, + style: BotStyle.text(context), + ), + ], + ), ), ), ); @@ -405,11 +408,17 @@ class SelectToDefine extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8), - child: Text( - L10n.of(context)!.selectToDefine, - style: BotStyle.text(context), + return Center( + child: Container( + height: 80, + width: 200, + padding: const EdgeInsets.all(8), + child: Center( + child: Text( + L10n.of(context)!.selectToDefine, + style: BotStyle.text(context), + ), + ), ), ); } diff --git a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart index cdcacea35..46df44ce5 100644 --- a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart +++ b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart @@ -81,6 +81,7 @@ class GamifiedTextWidget extends StatelessWidget { constraints: const BoxConstraints( minHeight: 80, ), + padding: const EdgeInsets.all(8), child: Text( userMessage, style: const TextStyle( diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 0e8d4051a..067288157 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -6,7 +6,6 @@ import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; -import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; @@ -16,6 +15,7 @@ import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -44,6 +44,8 @@ class MessagePracticeActivityCardState extends State { PracticeActivityRecordModel? currentCompletionRecord; bool fetchingActivity = false; + // tracks the target tokens for the current message + // in a separate controller to manage the state TargetTokensController targetTokensController = TargetTokensController(); List get practiceActivities => @@ -108,7 +110,9 @@ class MessagePracticeActivityCardState extends State { return incompleteActivities.firstOrNull; } - Future _fetchNewActivity() async { + Future _fetchNewActivity([ + ActivityQualityFeedback? activityFeedback, + ]) async { try { debugPrint('Fetching new activity'); @@ -143,6 +147,7 @@ class MessagePracticeActivityCardState extends State { existingActivities: practiceActivities .map((activity) => activity.activityRequestMetaData) .toList(), + activityQualityFeedback: activityFeedback, ), widget.pangeaMessageEvent, ); @@ -192,62 +197,138 @@ class MessagePracticeActivityCardState extends State { /// Fetches a new activity if there are any left to complete. /// Exits the practice flow if there are no more activities. void onActivityFinish() async { - // try { - if (currentCompletionRecord == null || currentActivity == null) { + try { + if (currentCompletionRecord == null || currentActivity == null) { + debugger(when: kDebugMode); + return; + } + + // update the target tokens with the new construct uses + // NOTE - multiple choice activity is handling adding these to analytics + await targetTokensController.updateTokensWithConstructs( + currentCompletionRecord!.usesForAllResponses( + currentActivity!.practiceActivity, + metadata, + ), + context, + widget.pangeaMessageEvent, + ); + + // save the record without awaiting to avoid blocking the UI + // send a copy of the activity record to make sure its not overwritten by + // the new activity + MatrixState.pangeaController.activityRecordController + .send(currentCompletionRecord!, currentActivity!) + .catchError( + (e, s) => ErrorHandler.logError( + e: e, + s: s, + m: 'Failed to save record', + data: { + 'record': currentCompletionRecord?.toJson(), + 'activity': currentActivity?.practiceActivity.toJson(), + }, + ), + ); + + widget.overlayController.onActivityFinish(); + + final Iterable result = await Future.wait([ + _savorTheJoy(), + _fetchNewActivity(), + ]); + + _setPracticeActivity(result.last as PracticeActivityEvent?); + } catch (e, s) { + _setPracticeActivity(null); debugger(when: kDebugMode); - return; + ErrorHandler.logError( + e: e, + s: s, + m: 'Failed to get new activity', + data: { + 'activity': currentActivity, + 'record': currentCompletionRecord, + }, + ); } + } - // update the target tokens with the new construct uses - // NOTE - multiple choice activity is handling adding these to analytics - await targetTokensController.updateTokensWithConstructs( - currentCompletionRecord!.usesForAllResponses( - currentActivity!.practiceActivity, - metadata, - ), - context, - widget.pangeaMessageEvent, - ); - - // save the record without awaiting to avoid blocking the UI - // send a copy of the activity record to make sure its not overwritten by - // the new activity - MatrixState.pangeaController.activityRecordController - .send(currentCompletionRecord!, currentActivity!) - .catchError( - (e, s) => ErrorHandler.logError( - e: e, - s: s, - m: 'Failed to save record', - data: { - 'record': currentCompletionRecord?.toJson(), - 'activity': currentActivity?.practiceActivity.toJson(), - }, + void onFlagClick(BuildContext context) { + final TextEditingController feedbackController = TextEditingController(); + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(L10n.of(context)!.reportContentIssueTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(L10n.of(context)!.reportContentIssueDescription), + const SizedBox(height: 10), + TextField( + controller: feedbackController, + decoration: InputDecoration( + labelText: L10n.of(context)!.feedback, + border: const OutlineInputBorder(), + ), + maxLines: 3, + ), + ], ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + child: Text(L10n.of(context)!.cancel), + ), + ElevatedButton( + onPressed: () { + // Call the additional callback function + submitFeedback(feedbackController.text); + Navigator.of(context).pop(); // Close the dialog + }, + child: Text(L10n.of(context)!.submit), + ), + ], ); + }, + ); + } + + /// clear the current activity, record, and selection + /// fetch a new activity, including the offending activity in the request + void submitFeedback(String feedback) { + if (currentActivity == null) { + debugger(when: kDebugMode); + return; + } - widget.overlayController.onActivityFinish(); - - final Iterable result = await Future.wait([ - _savorTheJoy(), - _fetchNewActivity(), - ]); - - _setPracticeActivity(result.last as PracticeActivityEvent?); - - // } catch (e, s) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // e: e, - // s: s, - // m: 'Failed to get new activity', - // data: { - // 'activity': currentActivity, - // 'record': currentCompletionRecord, - // }, - // ); - // widget.overlayController.exitPracticeFlow(); - // } + _fetchNewActivity( + ActivityQualityFeedback( + feedbackText: feedback, + badActivity: currentActivity!.practiceActivity, + ), + ).then((activity) { + _setPracticeActivity(activity); + }).catchError((onError) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: onError, + m: 'Failed to get new activity', + data: { + 'activity': currentActivity, + 'record': currentCompletionRecord, + }, + ); + widget.overlayController.exitPracticeFlow(); + }); + + // clear the current activity and record + currentActivity = null; + currentCompletionRecord = null; } RepresentationEvent? get representation => @@ -300,120 +381,52 @@ class MessagePracticeActivityCardState extends State { return GamifiedTextWidget(userMessage: userMessage!); } - return Stack( - alignment: Alignment.center, - children: [ - // Main content - const Positioned( - child: PointsGainedAnimation(), - ), - Column( - children: [ - activityWidget, - // navigationButtons, - ], - ), - // Conditionally show the darkening and progress indicator based on the loading state - if (!savoringTheJoy && fetchingActivity) ...[ - // Semi-transparent overlay + return Container( + constraints: const BoxConstraints( + maxWidth: 350, + minWidth: 350, + ), + child: Stack( + alignment: Alignment.center, + children: [ + // Main content + const Positioned( + child: PointsGainedAnimation(), + ), Container( - color: Colors.black.withOpacity(0.5), // Darkening effect + padding: const EdgeInsets.all(8), + child: activityWidget, ), - // Circular progress indicator in the center - const Center( - child: CircularProgressIndicator(), + // Conditionally show the darkening and progress indicator based on the loading state + if (!savoringTheJoy && fetchingActivity) ...[ + // Semi-transparent overlay + Container( + color: Colors.black.withOpacity(0.5), // Darkening effect + ), + // Circular progress indicator in the center + const Center( + child: CircularProgressIndicator(), + ), + ], + // Flag button in the top right corner + Positioned( + top: 0, + right: 0, + child: Opacity( + opacity: 0.8, // Slight opacity + child: Tooltip( + message: L10n.of(context)!.reportContentIssueTitle, + child: IconButton( + icon: const Icon(Icons.flag), + iconSize: 16, + onPressed: () => + currentActivity == null ? null : onFlagClick(context), + ), + ), + ), ), ], - ], - ); - } -} - -/// Seperated out the target tokens from the practice activity card -/// in order to control the state of the target tokens -class TargetTokensController { - List? _targetTokens; - - TargetTokensController(); - - /// From the tokens in the message, do a preliminary filtering of which to target - /// Then get the construct uses for those tokens - Future> targetTokens( - BuildContext context, - PangeaMessageEvent pangeaMessageEvent, - ) async { - if (_targetTokens != null) { - return _targetTokens!; - } - - _targetTokens = await _initialize(context, pangeaMessageEvent); - - await updateTokensWithConstructs( - MatrixState.pangeaController.analytics.analyticsStream.value ?? [], - context, - pangeaMessageEvent, - ); - - return _targetTokens!; - } - - Future> _initialize( - BuildContext context, - PangeaMessageEvent pangeaMessageEvent, - ) async { - if (!context.mounted) { - ErrorHandler.logError( - m: 'getTargetTokens called when not mounted', - s: StackTrace.current, - ); - return _targetTokens = []; - } - - final tokens = await pangeaMessageEvent - .representationByLanguage(pangeaMessageEvent.messageDisplayLangCode) - ?.tokensGlobal(context); - - if (tokens == null || tokens.isEmpty) { - debugger(when: kDebugMode); - return _targetTokens = []; - } - - _targetTokens = []; - for (int i = 0; i < tokens.length; i++) { - //don't bother with tokens that we don't save to vocab - if (!tokens[i].lemma.saveVocab) { - continue; - } - - _targetTokens!.add(tokens[i].emptyTokenWithXP); - } - - return _targetTokens!; - } - - Future updateTokensWithConstructs( - List constructUses, - context, - pangeaMessageEvent, - ) async { - final ConstructListModel constructList = ConstructListModel( - uses: constructUses, - type: null, + ), ); - - _targetTokens ??= await _initialize(context, pangeaMessageEvent); - - for (final token in _targetTokens!) { - for (final construct in token.constructs) { - final constructUseModel = constructList.getConstructUses( - construct.id.lemma, - construct.id.type, - ); - if (constructUseModel != null) { - construct.xp += constructUseModel.points; - construct.lastUsed = constructUseModel.lastUsed; - } - } - } } } diff --git a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart new file mode 100644 index 000000000..f22e097e4 --- /dev/null +++ b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart @@ -0,0 +1,99 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Seperated out the target tokens from the practice activity card +/// in order to control the state of the target tokens +class TargetTokensController { + List? _targetTokens; + + TargetTokensController(); + + /// From the tokens in the message, do a preliminary filtering of which to target + /// Then get the construct uses for those tokens + Future> targetTokens( + BuildContext context, + PangeaMessageEvent pangeaMessageEvent, + ) async { + if (_targetTokens != null) { + return _targetTokens!; + } + + _targetTokens = await _initialize(context, pangeaMessageEvent); + + await updateTokensWithConstructs( + MatrixState.pangeaController.analytics.analyticsStream.value ?? [], + context, + pangeaMessageEvent, + ); + + return _targetTokens!; + } + + Future> _initialize( + BuildContext context, + PangeaMessageEvent pangeaMessageEvent, + ) async { + if (!context.mounted) { + ErrorHandler.logError( + m: 'getTargetTokens called when not mounted', + s: StackTrace.current, + ); + return _targetTokens = []; + } + + final tokens = await pangeaMessageEvent + .representationByLanguage(pangeaMessageEvent.messageDisplayLangCode) + ?.tokensGlobal(context); + + if (tokens == null || tokens.isEmpty) { + debugger(when: kDebugMode); + return _targetTokens = []; + } + + _targetTokens = []; + for (int i = 0; i < tokens.length; i++) { + //don't bother with tokens that we don't save to vocab + if (!tokens[i].lemma.saveVocab) { + continue; + } + + _targetTokens!.add(tokens[i].emptyTokenWithXP); + } + + return _targetTokens!; + } + + Future updateTokensWithConstructs( + List constructUses, + context, + pangeaMessageEvent, + ) async { + final ConstructListModel constructList = ConstructListModel( + uses: constructUses, + type: null, + ); + + _targetTokens ??= await _initialize(context, pangeaMessageEvent); + + for (final token in _targetTokens!) { + for (final construct in token.constructs) { + final constructUseModel = constructList.getConstructUses( + construct.id.lemma, + construct.id.type, + ); + if (constructUseModel != null) { + construct.xp += constructUseModel.points; + construct.lastUsed = constructUseModel.lastUsed; + } + } + } + } +} diff --git a/pubspec.lock b/pubspec.lock index ed344d6a6..bb56964d1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1305,18 +1305,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -1417,10 +1417,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" material_symbols_icons: dependency: "direct main" description: @@ -1442,10 +1442,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mgrs_dart: dependency: transitive description: @@ -1682,10 +1682,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" platform_detect: dependency: transitive description: @@ -2303,26 +2303,26 @@ packages: dependency: transitive description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.4" timezone: dependency: transitive description: @@ -2615,10 +2615,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" wakelock_plus: dependency: "direct main" description: