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
This commit is contained in:
parent
371d4f06d4
commit
b8edf595ca
17 changed files with 654 additions and 473 deletions
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -4737,5 +4737,6 @@
|
|||
}
|
||||
},
|
||||
"commandHint_googly": "Enviar unos ojos saltones",
|
||||
"@commandHint_googly": {}
|
||||
"@commandHint_googly": {},
|
||||
"reportContentIssue": "Problema de contenido"
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ class ExistingActivityMetaData {
|
|||
factory ExistingActivityMetaData.fromJson(Map<String, dynamic> 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<String, dynamic>))
|
||||
.toList(),
|
||||
activityType: ActivityTypeEnum.values.firstWhere(
|
||||
|
|
@ -124,18 +125,47 @@ class ExistingActivityMetaData {
|
|||
Map<String, dynamic> 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<String, dynamic> json) {
|
||||
return ActivityQualityFeedback(
|
||||
feedbackText: json['feedback_text'] as String,
|
||||
badActivity: PracticeActivityModel.fromJson(
|
||||
json['bad_activity'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<TokenWithXP> tokensWithXP;
|
||||
|
||||
|
|
@ -151,6 +181,7 @@ class MessageActivityRequest {
|
|||
required this.tokensWithXP,
|
||||
required this.messageId,
|
||||
required this.existingActivities,
|
||||
required this.activityQualityFeedback,
|
||||
});
|
||||
|
||||
factory MessageActivityRequest.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -167,6 +198,11 @@ class MessageActivityRequest {
|
|||
(e) => ExistingActivityMetaData.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
activityQualityFeedback: json['activity_quality_feedback'] != null
|
||||
? ActivityQualityFeedback.fromJson(
|
||||
json['activity_quality_feedback'] as Map<String, dynamic>,
|
||||
)
|
||||
: 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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,7 +270,8 @@ class PracticeActivityModel {
|
|||
|
||||
factory PracticeActivityModel.fromJson(Map<String, dynamic> 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<String, dynamic>))
|
||||
.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<String, dynamic> 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(),
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _isLoading
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
: localAudioEvent != null || audioFile != null
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
int activitiesLeftToComplete = neededActivities;
|
||||
|
||||
PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -301,13 +303,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
? 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<MessageSelectionOverlay>
|
|||
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<MessageSelectionOverlay>
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MessageToolbar> {
|
||||
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<MessageToolbar> {
|
|||
|
||||
@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,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
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),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
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<ToolbarButtons> {
|
||||
PangeaMessageEvent get pangeaMessageEvent =>
|
||||
widget.messageToolbarController.widget.pangeaMessageEvent;
|
||||
|
||||
List<MessageMode> 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,
|
||||
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.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),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
122
lib/pangea/widgets/chat/message_toolbar_buttons.dart
Normal file
122
lib/pangea/widgets/chat/message_toolbar_buttons.dart
Normal file
|
|
@ -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<ToolbarButtons> {
|
||||
PangeaMessageEvent get pangeaMessageEvent =>
|
||||
widget.overlayController.pangeaMessageEvent;
|
||||
|
||||
List<MessageMode> 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/pangea/widgets/chat/message_toolbar_selection_area.dart
Normal file
48
lib/pangea/widgets/chat/message_toolbar_selection_area.dart
Normal file
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -151,6 +151,7 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _fetchingTranslation
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
: Column(
|
||||
|
|
@ -170,6 +171,7 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
body: InlineInstructions.l1Translation.body(context),
|
||||
onClose: closeHint,
|
||||
),
|
||||
// if (widget.selection != null)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class GamifiedTextWidget extends StatelessWidget {
|
|||
constraints: const BoxConstraints(
|
||||
minHeight: 80,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
userMessage,
|
||||
style: const TextStyle(
|
||||
|
|
|
|||
|
|
@ -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<PracticeActivityCard> {
|
|||
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<PracticeActivityEvent> get practiceActivities =>
|
||||
|
|
@ -108,7 +110,9 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
return incompleteActivities.firstOrNull;
|
||||
}
|
||||
|
||||
Future<PracticeActivityEvent?> _fetchNewActivity() async {
|
||||
Future<PracticeActivityEvent?> _fetchNewActivity([
|
||||
ActivityQualityFeedback? activityFeedback,
|
||||
]) async {
|
||||
try {
|
||||
debugPrint('Fetching new activity');
|
||||
|
||||
|
|
@ -143,6 +147,7 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
existingActivities: practiceActivities
|
||||
.map((activity) => activity.activityRequestMetaData)
|
||||
.toList(),
|
||||
activityQualityFeedback: activityFeedback,
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
|
@ -192,62 +197,138 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
/// 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<dynamic> result = await Future.wait([
|
||||
_savorTheJoy(),
|
||||
_fetchNewActivity(),
|
||||
]);
|
||||
|
||||
_setPracticeActivity(result.last as PracticeActivityEvent?);
|
||||
} catch (e, s) {
|
||||
_setPracticeActivity(null);
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to get new activity',
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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,
|
||||
_fetchNewActivity(
|
||||
ActivityQualityFeedback(
|
||||
feedbackText: feedback,
|
||||
badActivity: currentActivity!.practiceActivity,
|
||||
),
|
||||
context,
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
).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();
|
||||
});
|
||||
|
||||
// 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<dynamic> 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();
|
||||
// }
|
||||
// clear the current activity and record
|
||||
currentActivity = null;
|
||||
currentCompletionRecord = null;
|
||||
}
|
||||
|
||||
RepresentationEvent? get representation =>
|
||||
|
|
@ -300,120 +381,52 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
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
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.5), // Darkening effect
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 350,
|
||||
minWidth: 350,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Main content
|
||||
const Positioned(
|
||||
child: PointsGainedAnimation(),
|
||||
),
|
||||
// Circular progress indicator in the center
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: activityWidget,
|
||||
),
|
||||
// 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<TokenWithXP>? _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<List<TokenWithXP>> 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<List<TokenWithXP>> _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<void> updateTokensWithConstructs(
|
||||
List<OneConstructUse> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TokenWithXP>? _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<List<TokenWithXP>> 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<List<TokenWithXP>> _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<void> updateTokensWithConstructs(
|
||||
List<OneConstructUse> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
pubspec.lock
36
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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue