Reading assistance (#2175)
* still in draft * feat(reading_assistance): whole message activity oriented * chore: fix .env file path * feat: animate selected toolbar into middle of screen * chore: initial work for message bubble size animation * refactor(reading_assistance): hooking up the choice interactions and polishing UI * chore: animate in content and buttons * formatting * position reading content relative to selected token * working on limiting choices * chore: fix positioning of toolbar animation * chore: simplify positioning logic * chore: animate in button height * getting there * rough draft with restricted activity number is complete --------- Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
parent
f8feab5eea
commit
379e4a8db9
105 changed files with 5009 additions and 2906 deletions
|
|
@ -4507,7 +4507,7 @@
|
|||
},
|
||||
"botModeValidation": "Please select a chat mode",
|
||||
"clickBestOption": "Choose the best options to translate your message!",
|
||||
"completeActivitiesToUnlock": "Complete word activities to unlock audio and translation!",
|
||||
"completeActivitiesToUnlock": "Complete one of the activities (emoji, meaning, listening OR grammar) to unlock the translation!",
|
||||
"botSettingsSubtitle": "Invite bot to moderate chat activity",
|
||||
"invitePeople": "Invite users",
|
||||
"noCapacityLimit": "No capacity limit",
|
||||
|
|
@ -4590,7 +4590,7 @@
|
|||
"pleaseEnterEmail": "Please enter a valid email address.",
|
||||
"pleaseSelectALanguage": "Please select a language",
|
||||
"myBaseLanguage": "My base language",
|
||||
"clickWordsInstructions": "Click on a word or the buttons below to learn more",
|
||||
"clickWordsInstructions": "🕵️ Click any word for details. 🧐",
|
||||
"chooseBestDefinition": "What does this word mean?",
|
||||
"meaningSectionHeader": "Meaning:",
|
||||
"formSectionHeader": "Forms used in chats:",
|
||||
|
|
@ -4671,7 +4671,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lemmaMeaningInstructionsBody": "Above is the meaning of the lemma. Double-click to edit.",
|
||||
"chooseLemmaMeaningInstructionsBody": "Match the meanings below with the underlined words in the message.",
|
||||
"doubleClickToEdit": "Double-click to edit.",
|
||||
"removeFeature": "Remove {feature}",
|
||||
"@removeFeature": {
|
||||
|
|
@ -4711,7 +4711,6 @@
|
|||
"video": "Video",
|
||||
"nan": "Not applicable",
|
||||
"activityPlannerOverviewInstructionsBody": "Choose a topic, mode, learning objective and generate an activity for the chat!",
|
||||
"completeActivitiesToUnlock": "Complete the highlighted word activities to unlock",
|
||||
"myBookmarkedActivities": "My Bookmarked Activities",
|
||||
"noBookmarkedActivities": "No bookmarked activities",
|
||||
"activityTitle": "Activity Title",
|
||||
|
|
@ -4793,7 +4792,7 @@
|
|||
"downloadGboard": "Download Gboard",
|
||||
"autocorrectNotAvailable": "Unfortunately your platform is not currently supported for this feature. Stay tuned for further development!",
|
||||
"pleaseUpdateApp": "Please update the app to continue.",
|
||||
"chooseEmojiInstructionsBody": "Pick an emoji for the word! There's no wrong answer and you can switch anytime! 😀",
|
||||
"chooseEmojiInstructionsBody": "Match emojis with the words they best represent. Don't worry! No points off for disagreeing. 😅",
|
||||
"pickAnEmojiFor": "Pick an emoji for ${lemma}",
|
||||
"@pickAnEmojiFor": {
|
||||
"type": "String",
|
||||
|
|
@ -4808,11 +4807,14 @@
|
|||
"knockSpaceSuccess": "You have requested to join this space! An admin will respond to your request when they receive it 😀",
|
||||
"joinByCode": "Join by code",
|
||||
"createASpace": "Create a space",
|
||||
"chooseWordAudioInstructionsBody": "Listen to the full message then match the word audios to the right blanks!",
|
||||
"chooseMorphsInstructionsBody": "Match the grammar tags with the words in the message. Click and hold an option for a hint!",
|
||||
"inviteAndLaunch": "Launch and invite",
|
||||
"createOwnChat": "Create your own chat",
|
||||
"pleaseEnterInt": "Please enter a number",
|
||||
"home": "Home",
|
||||
"join": "Join",
|
||||
"readingAssistanceOverviewBody": "Click the buttons below for mini-games on visualizing vocab, practice listening, meaning, and grammar concepts. Click any word for details.",
|
||||
"learnByTexting": "Learn by texting",
|
||||
"levelSummaryTrigger": "View summary",
|
||||
"levelSummaryPopupTitle": "Level {level} Summary",
|
||||
|
|
|
|||
|
|
@ -5144,7 +5144,6 @@
|
|||
"other": "Otros",
|
||||
"botModeValidation": "Seleccione un modo de chat",
|
||||
"clickBestOption": "Elija las mejores opciones para traducir su mensaje",
|
||||
"completeActivitiesToUnlock": "¡Completa actividades de palabras para desbloquear audio y traducción!",
|
||||
"botSettingsSubtitle": "Invitar a un bot a moderar la actividad del chat",
|
||||
"invitePeople": "Invitar a usuarios",
|
||||
"noCapacityLimit": "Sin límite de capacidad",
|
||||
|
|
@ -5334,7 +5333,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lemmaMeaningInstructionsBody": "Arriba está el significado del lema. Haz doble clic para editar.",
|
||||
"doubleClickToEdit": "Haz doble clic para editar.",
|
||||
"removeFeature": "Eliminar {feature}",
|
||||
"@removeFeature": {
|
||||
|
|
@ -5374,7 +5372,6 @@
|
|||
"video": "Video",
|
||||
"nan": "No aplicable",
|
||||
"activityPlannerOverviewInstructionsBody": "¡Elige un tema, modo, objetivo de aprendizaje y genera una actividad para el chat!",
|
||||
"completeActivitiesToUnlock": "Completa las actividades de palabras resaltadas para desbloquear",
|
||||
"myBookmarkedActivities": "Mis Actividades Marcadas",
|
||||
"noBookmarkedActivities": "No hay actividades marcadas",
|
||||
"activityTitle": "Título de la Actividad",
|
||||
|
|
@ -5453,4 +5450,4 @@
|
|||
},
|
||||
"downloadGboard": "Descargar Gboard",
|
||||
"autocorrectNotAvailable": "Desafortunadamente, tu plataforma no es compatible actualmente con esta función. ¡Mantente atento a futuros desarrollos!"
|
||||
}
|
||||
}
|
||||
|
|
@ -3304,7 +3304,6 @@
|
|||
},
|
||||
"botModeValidation": "Vui lòng chọn một chế độ trò chuyện",
|
||||
"clickBestOption": "Chọn phương án tốt nhất để dịch tin nhắn của bạn!",
|
||||
"completeActivitiesToUnlock": "Hoàn thành các hoạt động từ vựng được đánh dấu để mở khóa",
|
||||
"botSettingsSubtitle": "Mời bot kiểm duyệt hoặc khởi tạo hoạt động trò chuyện",
|
||||
"invitePeople": "Mời người dùng",
|
||||
"invitePeopleChatSubtitle": "Mời người dùng hoặc quản trị viên đến cuộc trò chuyện này",
|
||||
|
|
@ -3527,7 +3526,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lemmaMeaningInstructionsBody": "Ở trên là nghĩa của mục từ. Nhấp đúp để chỉnh sửa.",
|
||||
"doubleClickToEdit": "Nhấp đúp để chỉnh sửa.",
|
||||
"removeFeature": "Xóa {feature}",
|
||||
"@removeFeature": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
abstract class AppConfig {
|
||||
// #Pangea
|
||||
|
|
@ -23,16 +21,14 @@ abstract class AppConfig {
|
|||
static const double messageFontSize = 16.0;
|
||||
static const bool allowOtherHomeservers = true;
|
||||
static const bool enableRegistration = true;
|
||||
static const double toolbarMaxHeight = 175.0;
|
||||
static const double toolbarMinHeight = 140.0;
|
||||
static const double toolbarMaxHeight = 250.0;
|
||||
static const double toolbarMinHeight = 200.0;
|
||||
static const double toolbarMinWidth = 350.0;
|
||||
static const double toolbarButtonsColumnWidth = 50.0;
|
||||
static const double toolbarButtonAndProgressColumnHeight = 200.0;
|
||||
static const double defaultHeaderHeight = 56.0;
|
||||
static const double readingAssistanceInputBarHeight = 150;
|
||||
static const double readingAssistanceInputBarHeight = 170;
|
||||
static const double toolbarSpacing = 8.0;
|
||||
static const double toolbarIconSize = 24.0;
|
||||
static const double toolbarButtonsColumnHeight = 240;
|
||||
|
||||
static TextStyle messageTextStyle(
|
||||
Event? event,
|
||||
Color textColor,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlighter/flutter_highlighter.dart';
|
||||
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
|
@ -8,13 +14,6 @@ import 'package:html/dom.dart' as dom;
|
|||
import 'package:html/parser.dart' as parser;
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import '../../../utils/url_launcher.dart';
|
||||
|
||||
class HtmlMessage extends StatelessWidget {
|
||||
|
|
@ -270,6 +269,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
) ??
|
||||
false;
|
||||
|
||||
// @ggurdin: probably changing this, not sure when it shows up
|
||||
final didMeaningActivity = token?.didActivitySuccessfully(
|
||||
ActivityTypeEnum.wordMeaning,
|
||||
) ??
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_rich_text.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_selection_area.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../../utils/platform_infos.dart';
|
||||
import '../../../utils/url_launcher.dart';
|
||||
|
|
@ -42,6 +42,7 @@ class MessageContent extends StatelessWidget {
|
|||
final ChatController controller;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
final bool isTransitionAnimation;
|
||||
// Pangea#
|
||||
final Timeline timeline;
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ class MessageContent extends StatelessWidget {
|
|||
required this.controller,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
this.isTransitionAnimation = false,
|
||||
// Pangea#
|
||||
required this.linkColor,
|
||||
required this.borderRadius,
|
||||
|
|
@ -377,6 +379,18 @@ class MessageContent extends StatelessWidget {
|
|||
style: messageTextStyle,
|
||||
onClick: onClick,
|
||||
isSelected: overlayController != null ? isSelected : null,
|
||||
messageMode: overlayController?.toolbarMode,
|
||||
isHighlighted: (PangeaToken token) =>
|
||||
overlayController!.toolbarMode.associatedActivityType !=
|
||||
null &&
|
||||
overlayController?.messageAnalyticsEntry?.hasActivity(
|
||||
overlayController!
|
||||
.toolbarMode.associatedActivityType!,
|
||||
token,
|
||||
) ==
|
||||
true,
|
||||
overlayController: overlayController,
|
||||
isTransitionAnimation: isTransitionAnimation,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ class AnalyticsPopupWrapperState extends State<AnalyticsPopupWrapper> {
|
|||
ConstructIdentifier? localConstructZoom;
|
||||
ConstructTypeEnum localView = ConstructTypeEnum.vocab;
|
||||
|
||||
// @ggurdin
|
||||
//TODO: make language-specific
|
||||
MorphFeaturesAndTags morphs = defaultMorphMapping;
|
||||
List<MorphFeature> features = defaultMorphMapping.displayFeatures;
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class AnalyticsDetailsViewContent extends StatelessWidget {
|
|||
construct: construct,
|
||||
category: LearningSkillsEnum.hearing,
|
||||
tooltip: L10n.of(context).listeningExercisesTooltip,
|
||||
icon: Symbols.hearing,
|
||||
icon: Icons.volume_up,
|
||||
),
|
||||
// Reading exercise section
|
||||
LemmaUsageDots(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
|
|
@ -12,9 +8,11 @@ import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
|||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/user/client_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MorphAnalyticsListView extends StatelessWidget {
|
||||
final void Function(ConstructIdentifier) onConstructZoom;
|
||||
|
|
@ -75,20 +73,8 @@ class MorphFeatureBox extends StatelessWidget {
|
|||
required this.onConstructZoom,
|
||||
});
|
||||
|
||||
String _categoryCopy(
|
||||
String category,
|
||||
BuildContext context,
|
||||
) {
|
||||
if (category.toLowerCase() == "other") {
|
||||
return L10n.of(context).other;
|
||||
}
|
||||
|
||||
return ConstructTypeEnum.morph.getDisplayCopy(
|
||||
category,
|
||||
context,
|
||||
) ??
|
||||
category;
|
||||
}
|
||||
MorphFeaturesEnum get feature =>
|
||||
MorphFeaturesEnumExtension.fromString(morphFeature);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -98,7 +84,7 @@ class MorphFeatureBox extends StatelessWidget {
|
|||
padding: const EdgeInsets.all(16.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppConfig.primaryColorLight
|
||||
|
|
@ -116,10 +102,10 @@ class MorphFeatureBox extends StatelessWidget {
|
|||
SizedBox(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: MorphIcon(morphFeature: morphFeature, morphTag: null),
|
||||
child: MorphIcon(morphFeature: feature, morphTag: null),
|
||||
),
|
||||
Text(
|
||||
_categoryCopy(morphFeature, context),
|
||||
feature.getDisplayCopy(context),
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
|
@ -193,13 +179,16 @@ class MorphTagChip extends StatelessWidget {
|
|||
this.onTap,
|
||||
});
|
||||
|
||||
MorphFeaturesEnum get feature =>
|
||||
MorphFeaturesEnumExtension.fromString(morphFeature);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final unlocked = constructAnalytics.points > 10;
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
onTap: onTap,
|
||||
child: Opacity(
|
||||
opacity: unlocked ? 1.0 : 0.3,
|
||||
|
|
@ -231,7 +220,7 @@ class MorphTagChip extends StatelessWidget {
|
|||
height: 28.0,
|
||||
child: unlocked || Matrix.of(context).client.isSupportAccount
|
||||
? MorphIcon(
|
||||
morphFeature: morphFeature,
|
||||
morphFeature: feature,
|
||||
morphTag: morphTag,
|
||||
)
|
||||
: const Icon(
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_feature_display.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_tag_display.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class MorphDetailsView extends StatelessWidget {
|
||||
final ConstructIdentifier constructId;
|
||||
|
|
@ -37,70 +35,91 @@ class MorphDetailsView extends StatelessWidget {
|
|||
: _construct.lemmaCategory.darkColor(context);
|
||||
|
||||
return AnalyticsDetailsViewContent(
|
||||
title:
|
||||
MorphFeatureDisplay(morphFeature: _morphFeature, morphTag: _morphTag),
|
||||
subtitle:
|
||||
MorphTagDisplay(morphFeature: _morphFeature, textColor: textColor),
|
||||
subtitle: MorphFeatureDisplay(morphFeature: _morphFeature),
|
||||
title: MorphTagDisplay(
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(_morphFeature),
|
||||
morphTag: _morphTag,
|
||||
textColor: textColor,
|
||||
),
|
||||
headerContent: Padding(
|
||||
padding: const EdgeInsets.all(25.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: FutureBuilder(
|
||||
future: _getDefinition(context),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
AsyncSnapshot<String?> snapshot,
|
||||
) {
|
||||
if (snapshot.hasData) {
|
||||
return MorphMeaningWidget(
|
||||
feature: _morphFeature,
|
||||
tag: _morphTag,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
leading: TextSpan(
|
||||
text: L10n.of(context).meaningSectionHeader,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Wrap(
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).meaningSectionHeader,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).meaningNotFound,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Wrap(
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).meaningSectionHeader,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
const TextLoadingShimmer(width: 100),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MorphMeaningWidget(
|
||||
feature: _morphFeature,
|
||||
tag: _morphTag,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
// leading: TextSpan(
|
||||
// text: L10n.of(context).meaningSectionHeader,
|
||||
// style: const TextStyle(
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// headerContent: Padding(
|
||||
// padding: const EdgeInsets.all(25.0),
|
||||
// child: Align(
|
||||
// alignment: Alignment.topLeft,
|
||||
// child: FutureBuilder(
|
||||
// future: _getDefinition(context),
|
||||
// builder: (
|
||||
// BuildContext context,
|
||||
// AsyncSnapshot<String?> snapshot,
|
||||
// ) {
|
||||
// if (snapshot.hasData) {
|
||||
// return MorphMeaningWidget(
|
||||
// feature: _morphFeature,
|
||||
// tag: _morphTag,
|
||||
// style: Theme.of(context).textTheme.bodyLarge,
|
||||
// leading: TextSpan(
|
||||
// text: L10n.of(context).meaningSectionHeader,
|
||||
// style: const TextStyle(
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// } else if (snapshot.hasError) {
|
||||
// return Wrap(
|
||||
// children: [
|
||||
// Text(
|
||||
// L10n.of(context).meaningSectionHeader,
|
||||
// style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// width: 10,
|
||||
// ),
|
||||
// Text(
|
||||
// L10n.of(context).meaningNotFound,
|
||||
// style: Theme.of(context).textTheme.bodyLarge,
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// } else {
|
||||
// return Wrap(
|
||||
// children: [
|
||||
// Text(
|
||||
// L10n.of(context).meaningSectionHeader,
|
||||
// style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// width: 10,
|
||||
// ),
|
||||
// const TextLoadingShimmer(width: 100),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
xpIcon: ConstructXpWidget(id: constructId),
|
||||
constructId: constructId,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
/// Displays information about selected lemma, and its usage
|
||||
class VocabDetailsView extends StatelessWidget {
|
||||
|
|
@ -27,41 +23,6 @@ class VocabDetailsView extends StatelessWidget {
|
|||
|
||||
ConstructUses get _construct => constructId.constructUses;
|
||||
|
||||
String? get _emoji => PangeaToken(
|
||||
text: PangeaTokenText(
|
||||
offset: 0,
|
||||
content: _construct.lemma,
|
||||
length: _construct.lemma.length,
|
||||
),
|
||||
lemma: Lemma(
|
||||
text: _construct.lemma,
|
||||
saveVocab: false,
|
||||
form: _construct.lemma,
|
||||
),
|
||||
pos: _construct.category,
|
||||
morph: {},
|
||||
).getEmoji();
|
||||
|
||||
/// Get string representing forms of the given lemma that have been used
|
||||
String? get _formString {
|
||||
// Get possible forms of lemma
|
||||
final constructs = MatrixState
|
||||
.pangeaController.getAnalytics.constructListModel
|
||||
.getConstructUsesByLemma(_construct.lemma);
|
||||
|
||||
final forms = constructs
|
||||
.map((e) => e.uses)
|
||||
.expand((element) => element)
|
||||
.where((use) => use.useType.pointValue > 0)
|
||||
.map((e) => e.form?.toLowerCase())
|
||||
.toSet()
|
||||
.whereType<String>()
|
||||
.toList();
|
||||
|
||||
if (forms.isEmpty) return null;
|
||||
return forms.join(", ");
|
||||
}
|
||||
|
||||
/// Get the language code for the current lemma
|
||||
String? get _userL2 =>
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
|
@ -75,101 +36,102 @@ class VocabDetailsView extends StatelessWidget {
|
|||
return AnalyticsDetailsViewContent(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 42,
|
||||
child: _emoji == null
|
||||
? Tooltip(
|
||||
message: L10n.of(context).noEmojiSelectedTooltip,
|
||||
child: Icon(
|
||||
Icons.add_reaction_outlined,
|
||||
size: 24,
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
),
|
||||
)
|
||||
: Text(_emoji!),
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
Text(
|
||||
_construct.lemma,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
WordAudioButton(
|
||||
text: _construct.lemma,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
_construct.lemma,
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: L10n.of(context).grammarCopyPOS,
|
||||
child: Icon(
|
||||
Symbols.toys_and_games,
|
||||
size: 23,
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
),
|
||||
MorphIcon(
|
||||
morphFeature: MorphFeaturesEnum.Pos,
|
||||
morphTag: _construct.category,
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
getGrammarCopy(
|
||||
category: "pos",
|
||||
lemma: _construct.category,
|
||||
context: context,
|
||||
) ??
|
||||
_construct.category,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
MorphFeaturesEnum.Pos.getDisplayCopy(context),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
headerContent: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: _userL2 == null
|
||||
? Text(L10n.of(context).meaningNotFound)
|
||||
: LemmaMeaningWidget(
|
||||
constructUse: _construct,
|
||||
langCode: _userL2!,
|
||||
controller: null,
|
||||
token: null,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
leading: TextSpan(
|
||||
text: L10n.of(context).meaningSectionHeader,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
LemmaEmojiRow(
|
||||
cId: constructId,
|
||||
onTap: () => {},
|
||||
removeCallback: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: _userL2 == null
|
||||
? Text(L10n.of(context).meaningNotFound)
|
||||
: LemmaMeaningWidget(
|
||||
constructUse: _construct,
|
||||
langCode: _userL2!,
|
||||
controller: null,
|
||||
token: null,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
leading: TextSpan(
|
||||
text: L10n.of(context).meaningSectionHeader,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).formSectionHeader,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
" ${_formString ?? L10n.of(context).formsNotFound}",
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
runAlignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).formSectionHeader,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
...MatrixState
|
||||
.pangeaController.getAnalytics.constructListModel
|
||||
.getConstructUsesByLemma(_construct.lemma)
|
||||
.map((e) => e.uses)
|
||||
.expand((element) => element)
|
||||
.map((e) => e.form?.toLowerCase())
|
||||
.toSet()
|
||||
.whereType<String>()
|
||||
.map(
|
||||
(form) => WordTextWithAudioButton(
|
||||
text: form,
|
||||
textSize: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.fontSize ??
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VocabAnalyticsListTile extends StatefulWidget {
|
||||
const VocabAnalyticsListTile({
|
||||
|
|
@ -53,10 +52,10 @@ class VocabAnalyticsListTileState extends State<VocabAnalyticsListTile> {
|
|||
height: (maxWidth - padding * 2) * 0.6,
|
||||
child: Opacity(
|
||||
opacity:
|
||||
widget.constructUse.id.userSetEmoji == null ? 0.2 : 1,
|
||||
child: widget.constructUse.id.userSetEmoji != null
|
||||
widget.constructUse.id.userSetEmoji.isEmpty ? 0.2 : 1,
|
||||
child: widget.constructUse.id.userSetEmoji.isNotEmpty
|
||||
? Text(
|
||||
widget.constructUse.id.userSetEmoji!,
|
||||
widget.constructUse.id.userSetEmoji.first,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
|
||||
|
||||
enum ConstructTypeEnum {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_downloads/analytics_summary_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum ConstructUseTypeEnum {
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a correct use
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
|
||||
|
|
@ -23,6 +16,11 @@ import 'package:fluffychat/pangea/constructs/construct_repo.dart';
|
|||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// A minimized version of AnalyticsController that get the logged in user's analytics
|
||||
class GetAnalyticsController extends BaseController {
|
||||
|
|
@ -42,10 +40,6 @@ class GetAnalyticsController extends BaseController {
|
|||
|
||||
GetAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
|
||||
perMessage = MessageAnalyticsController(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
LanguageModel? get _l2 => _pangeaController.languageController.userL2;
|
||||
|
|
|
|||
|
|
@ -1,265 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
|
||||
/// Picks which tokens to do activities on and what types of activities to do
|
||||
/// Caches result so that we don't have to recompute it
|
||||
/// Most importantly, we can't do this in the state of a message widget because the state is disposed of and recreated
|
||||
/// If we decided that the first token should have a hidden word listening, we need to remember that
|
||||
/// Otherwise, the user might leave the chat, return, and see a different word hidden
|
||||
|
||||
class TargetTokensAndActivityType {
|
||||
final List<PangeaToken> tokens;
|
||||
final ActivityTypeEnum activityType;
|
||||
|
||||
TargetTokensAndActivityType({
|
||||
required this.tokens,
|
||||
required this.activityType,
|
||||
});
|
||||
|
||||
bool matchesActivity(PracticeActivityModel activity) {
|
||||
// check if the existing activity has the same type as the target
|
||||
if (activity.activityType != activityType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This is kind of complicated
|
||||
// if it's causing problems,
|
||||
// maybe we just verify that the target span of the activity is the same as the target span of the target?
|
||||
final List<ConstructIdentifier> relevantConstructs = tokens
|
||||
.map((t) => t.constructs)
|
||||
.expand((e) => e)
|
||||
.map((c) => c.id)
|
||||
.where(activityType.constructFilter)
|
||||
.toList();
|
||||
|
||||
final List<ConstructIdentifier>? otherRelevantConstructs = activity
|
||||
.targetTokens
|
||||
?.map((t) => t.constructs)
|
||||
.expand((e) => e)
|
||||
.map((c) => c.id)
|
||||
.where(activityType.constructFilter)
|
||||
.toList();
|
||||
|
||||
return listEquals(otherRelevantConstructs, relevantConstructs);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TargetTokensAndActivityType &&
|
||||
listEquals(other.tokens, tokens) &&
|
||||
other.activityType == activityType;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => tokens.hashCode ^ activityType.hashCode;
|
||||
}
|
||||
|
||||
class MessageAnalyticsEntry {
|
||||
final DateTime createdAt = DateTime.now();
|
||||
|
||||
late final List<PangeaToken> _tokens;
|
||||
|
||||
final List<TargetTokensAndActivityType> _activityQueue = [];
|
||||
|
||||
final int _maxQueueLength = 3;
|
||||
|
||||
MessageAnalyticsEntry({
|
||||
required List<PangeaToken> tokens,
|
||||
required bool includeHiddenWordActivities,
|
||||
required PangeaMessageEvent pangeaMessageEvent,
|
||||
}) {
|
||||
_tokens = tokens;
|
||||
setActivityQueue();
|
||||
}
|
||||
|
||||
void _pushQueue(TargetTokensAndActivityType entry) {
|
||||
if (nextActivity?.activityType == ActivityTypeEnum.hiddenWordListening) {
|
||||
if (entry.activityType == ActivityTypeEnum.hiddenWordListening) {
|
||||
_activityQueue[0] = entry;
|
||||
} else {
|
||||
_activityQueue.insert(1, entry);
|
||||
}
|
||||
} else {
|
||||
_activityQueue.insert(0, entry);
|
||||
}
|
||||
|
||||
if (_activityQueue.length > _maxQueueLength) {
|
||||
_activityQueue.removeRange(
|
||||
_maxQueueLength,
|
||||
_activityQueue.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _popQueue() {
|
||||
if (_activityQueue.isNotEmpty) _activityQueue.removeAt(0);
|
||||
}
|
||||
|
||||
void _filterQueue(ActivityTypeEnum activityType) {
|
||||
_activityQueue.removeWhere((a) => a.activityType == activityType);
|
||||
}
|
||||
|
||||
void _clearQueue() {
|
||||
_activityQueue.clear();
|
||||
}
|
||||
|
||||
TargetTokensAndActivityType? get nextActivity =>
|
||||
_activityQueue.isNotEmpty ? _activityQueue.first : null;
|
||||
|
||||
bool get hasHiddenWordActivity =>
|
||||
nextActivity?.activityType.hiddenType ?? false;
|
||||
|
||||
bool get hasMessageMeaningActivity => _activityQueue
|
||||
.any((a) => a.activityType == ActivityTypeEnum.messageMeaning);
|
||||
|
||||
int get numActivities => _activityQueue.length;
|
||||
|
||||
// /// If there are more than 4 tokens that can be heard, we don't want to do word focus listening
|
||||
// /// Otherwise, we don't have enough distractors
|
||||
// bool get canDoWordFocusListening =>
|
||||
// _tokens.where((t) => t.canBeHeard).length > 4;
|
||||
|
||||
/// On initialization, we pick which tokens to do activities on and what types of activities to do
|
||||
void setActivityQueue() {
|
||||
final List<TargetTokensAndActivityType> queue = [];
|
||||
|
||||
// if applicable, add a hidden word activity to the front of the queue
|
||||
final hiddenWordActivity = getHiddenWordActivity(queue.length);
|
||||
if (hiddenWordActivity != null) {
|
||||
_pushQueue(hiddenWordActivity);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a message meaning activity to the front of the queue
|
||||
/// And limits to _maxQueueLength activities
|
||||
void addMessageMeaningActivity() {
|
||||
final entry = TargetTokensAndActivityType(
|
||||
tokens: _tokens,
|
||||
activityType: ActivityTypeEnum.messageMeaning,
|
||||
);
|
||||
_pushQueue(entry);
|
||||
}
|
||||
|
||||
/// Returns a hidden word activity if there is a sequence of tokens that have hiddenWordListening in their eligibleActivityTypes
|
||||
TargetTokensAndActivityType? getHiddenWordActivity(int numOtherActivities) {
|
||||
return null;
|
||||
|
||||
// don't do hidden word listening on own messages
|
||||
// if (!_includeHiddenWordActivities) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// // we will only do hidden word listening 30% of the time
|
||||
// // if there are no other activities to do, we will always do hidden word listening
|
||||
// if (Random().nextDouble() < 0.7) {
|
||||
// // if (numOtherActivities >= _maxQueueLength && Random().nextDouble() < 0.5) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// // We will find the longest sequence of tokens that have hiddenWordListening in their eligibleActivityTypes
|
||||
// final List<List<PangeaToken>> sequences = [];
|
||||
// List<PangeaToken> currentSequence = [];
|
||||
// for (final token in _tokens) {
|
||||
// if (_pangeaMessageEvent.shouldDoActivity(
|
||||
// token: token,
|
||||
// a: ActivityTypeEnum.hiddenWordListening,
|
||||
// feature: null,
|
||||
// tag: null,
|
||||
// )) {
|
||||
// currentSequence.add(token);
|
||||
// } else {
|
||||
// if (currentSequence.isNotEmpty) {
|
||||
// sequences.add(currentSequence);
|
||||
// currentSequence = [];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (sequences.isEmpty) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// final longestSequence = sequences.reduce(
|
||||
// (a, b) => a.length > b.length ? a : b,
|
||||
// );
|
||||
|
||||
// // Truncate the sequence to a maximum of 2 words
|
||||
// final truncatedSequence = longestSequence.take(2).toList();
|
||||
|
||||
// return TargetTokensAndActivityType(
|
||||
// tokens: truncatedSequence,
|
||||
// activityType: ActivityTypeEnum.hiddenWordListening,
|
||||
// );
|
||||
}
|
||||
|
||||
void onActivityComplete() => _popQueue();
|
||||
|
||||
void exitPracticeFlow() => _clearQueue();
|
||||
|
||||
void revealAllTokens() => _filterQueue(ActivityTypeEnum.hiddenWordListening);
|
||||
|
||||
bool isTokenInHiddenWordActivity(PangeaToken token) => _activityQueue.any(
|
||||
(activity) =>
|
||||
activity.tokens.contains(token) && activity.activityType.hiddenType,
|
||||
);
|
||||
}
|
||||
|
||||
/// computes TokenWithXP for given a pangeaMessageEvent and caches the result, according to the full text of the message
|
||||
/// listens for analytics updates and updates the cache accordingly
|
||||
class MessageAnalyticsController {
|
||||
final GetAnalyticsController getAnalytics;
|
||||
final Map<String, MessageAnalyticsEntry> _cache = {};
|
||||
|
||||
MessageAnalyticsController(this.getAnalytics);
|
||||
|
||||
void dispose() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
// if over 50, remove oldest 5 entries by createdAt
|
||||
void clean() {
|
||||
if (_cache.length > 50) {
|
||||
final sortedEntries = _cache.entries.toList()
|
||||
..sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
|
||||
for (var i = 0; i < 5; i++) {
|
||||
_cache.remove(sortedEntries[i].key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _key(List<PangeaToken> tokens) => PangeaToken.reconstructText(tokens);
|
||||
|
||||
MessageAnalyticsEntry? get(
|
||||
List<PangeaToken> tokens,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) {
|
||||
final String key = _key(tokens);
|
||||
|
||||
if (_cache.containsKey(key)) {
|
||||
return _cache[key];
|
||||
}
|
||||
|
||||
final bool includeHiddenWordActivities = !pangeaMessageEvent.ownMessage &&
|
||||
pangeaMessageEvent.messageDisplayRepresentation?.tokens != null &&
|
||||
pangeaMessageEvent.messageDisplayLangIsL2 &&
|
||||
!pangeaMessageEvent.event.isRichMessage;
|
||||
|
||||
_cache[key] = MessageAnalyticsEntry(
|
||||
tokens: tokens,
|
||||
includeHiddenWordActivities: includeHiddenWordActivities,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
);
|
||||
|
||||
clean();
|
||||
|
||||
return _cache[key];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../../utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
|
||||
class ChatListItemSubtitle extends StatelessWidget {
|
||||
|
|
@ -88,7 +88,7 @@ class ChatListItemSubtitle extends StatelessWidget {
|
|||
final tokens = messageEventAndTokens.tokens;
|
||||
|
||||
final analyticsEntry = tokens != null
|
||||
? MatrixState.pangeaController.getAnalytics.perMessage.get(
|
||||
? MessageAnalyticsController.get(
|
||||
tokens,
|
||||
pangeaMessageEvent,
|
||||
)
|
||||
|
|
@ -96,7 +96,7 @@ class ChatListItemSubtitle extends StatelessWidget {
|
|||
|
||||
return MessageTextWidget(
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
style: style,
|
||||
existingStyle: style,
|
||||
messageAnalyticsEntry: analyticsEntry,
|
||||
isSelected: null,
|
||||
onClick: null,
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class ModelKey {
|
|||
static const String latestBuildNumber = "latest_build_number";
|
||||
static const String mandatoryUpdate = "mandatory_update";
|
||||
static const String emoji = "emoji";
|
||||
static const String emojiList = "emoji_list";
|
||||
|
||||
static const String analytics = "analytics";
|
||||
static const String level = "level";
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ class PangeaAnyState {
|
|||
}
|
||||
|
||||
void disposeByWidgetKey(String transformTargetId) {
|
||||
final index =
|
||||
entries.indexWhere((element) => element.key == transformTargetId);
|
||||
if (index != -1) {
|
||||
entries[index].entry.remove();
|
||||
entries.removeAt(index);
|
||||
}
|
||||
_layerLinkAndKeys.remove(transformTargetId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
class PangeaWarningError implements Exception {
|
||||
|
|
@ -22,18 +21,18 @@ class ErrorHandler {
|
|||
ErrorHandler();
|
||||
|
||||
static Future<void> initialize() async {
|
||||
await SentryFlutter.init(
|
||||
(options) {
|
||||
options.dsn = Environment.sentryDsn;
|
||||
options.tracesSampleRate = 0.1;
|
||||
options.debug = kDebugMode;
|
||||
options.environment = kDebugMode
|
||||
? "debug"
|
||||
: Environment.isStaging
|
||||
? "staging"
|
||||
: "productionC";
|
||||
},
|
||||
);
|
||||
// await SentryFlutter.init(
|
||||
// (options) {
|
||||
// options.dsn = Environment.sentryDsn;
|
||||
// options.tracesSampleRate = 0.1;
|
||||
// options.debug = kDebugMode;
|
||||
// options.environment = kDebugMode
|
||||
// ? "debug"
|
||||
// : Environment.isStaging
|
||||
// ? "staging"
|
||||
// : "productionC";
|
||||
// },
|
||||
// );
|
||||
|
||||
// Error handling
|
||||
FlutterError.onError = (FlutterErrorDetails details) async {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ class OverlayUtil {
|
|||
bool closePrevOverlay = true,
|
||||
String? overlayKey,
|
||||
bool isScrollable = true,
|
||||
bool addBorder = true,
|
||||
}) {
|
||||
try {
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
|
|
@ -119,17 +120,21 @@ class OverlayUtil {
|
|||
final Offset transformTargetOffset =
|
||||
(targetRenderBox).localToGlobal(Offset.zero);
|
||||
final Size transformTargetSize = targetRenderBox.size;
|
||||
final horizontalMidpoint =
|
||||
transformTargetOffset.dx + (transformTargetSize.width / 2);
|
||||
|
||||
final columnWidth = FluffyThemes.isColumnMode(context)
|
||||
? FluffyThemes.columnWidth + FluffyThemes.navRailWidth
|
||||
: 0;
|
||||
|
||||
final horizontalMidpoint = (transformTargetOffset.dx - columnWidth) +
|
||||
(transformTargetSize.width / 2);
|
||||
|
||||
final verticalMidpoint =
|
||||
transformTargetOffset.dy + (transformTargetSize.height / 2);
|
||||
debugPrint("vertical midpoint $verticalMidpoint");
|
||||
|
||||
final halfMaxWidth = maxWidth / 2;
|
||||
final hasLeftOverflow = (horizontalMidpoint - halfMaxWidth) < 0;
|
||||
final hasRightOverflow = (horizontalMidpoint + halfMaxWidth) >
|
||||
MediaQuery.of(context).size.width;
|
||||
(MediaQuery.of(context).size.width - columnWidth);
|
||||
hasTopOverflow = (verticalMidpoint - maxHeight) < 0;
|
||||
|
||||
double xOffset = 0;
|
||||
|
|
@ -138,24 +143,26 @@ class OverlayUtil {
|
|||
if (hasLeftOverflow) {
|
||||
xOffset = (horizontalMidpoint - halfMaxWidth - 10) * -1;
|
||||
} else if (hasRightOverflow) {
|
||||
xOffset = MediaQuery.of(context).size.width -
|
||||
xOffset = (MediaQuery.of(context).size.width - columnWidth) -
|
||||
(horizontalMidpoint + halfMaxWidth + 10);
|
||||
}
|
||||
offset = Offset(xOffset, 0);
|
||||
}
|
||||
|
||||
final Widget child = Material(
|
||||
borderOnForeground: false,
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: OverlayContainer(
|
||||
cardToShow: cardToShow,
|
||||
borderColor: borderColor,
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: maxWidth,
|
||||
isScrollable: isScrollable,
|
||||
),
|
||||
);
|
||||
final Widget child = addBorder
|
||||
? Material(
|
||||
borderOnForeground: false,
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: OverlayContainer(
|
||||
cardToShow: cardToShow,
|
||||
borderColor: borderColor,
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: maxWidth,
|
||||
isScrollable: isScrollable,
|
||||
),
|
||||
)
|
||||
: cardToShow;
|
||||
|
||||
showOverlay(
|
||||
context: context,
|
||||
|
|
|
|||
21
lib/pangea/constructs/construct_form.dart
Normal file
21
lib/pangea/constructs/construct_form.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
|
||||
class ConstructForm {
|
||||
String form;
|
||||
ConstructIdentifier cId;
|
||||
|
||||
ConstructForm(
|
||||
this.form,
|
||||
this.cId,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ConstructForm && other.form == form && other.cId == cId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => form.hashCode ^ cId.hashCode;
|
||||
}
|
||||
|
|
@ -1,21 +1,27 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix_api_lite/utils/try_get_map_extension.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/emojis/emoji_stack.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class ConstructIdentifier {
|
||||
final String lemma;
|
||||
|
|
@ -111,28 +117,49 @@ class ConstructIdentifier {
|
|||
uses: [],
|
||||
);
|
||||
|
||||
String? get userSetEmoji {
|
||||
if (type == ConstructTypeEnum.morph) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception("Morphs should not have userSetEmoji"),
|
||||
data: toJson(),
|
||||
);
|
||||
return null;
|
||||
List<String> get userSetEmoji => userLemmaInfo?.emojis ?? [];
|
||||
|
||||
String? get userSetMeaning => userLemmaInfo?.meaning;
|
||||
|
||||
UserSetLemmaInfo? get userLemmaInfo {
|
||||
switch (type) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
final dynamic lemmaInfoContent = MatrixState
|
||||
.pangeaController.matrixState.client
|
||||
.analyticsRoomLocal()
|
||||
?.getState(PangeaEventTypes.userSetLemmaInfo, string)
|
||||
?.content;
|
||||
if (lemmaInfoContent != null && lemmaInfoContent is Map) {
|
||||
try {
|
||||
return UserSetLemmaInfo.fromJson(
|
||||
lemmaInfoContent as Map<String, dynamic>,
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
data: {
|
||||
"construct": string,
|
||||
"content": lemmaInfoContent,
|
||||
},
|
||||
s: s,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
case ConstructTypeEnum.morph:
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception("Morphs should not have userSetEmoji"),
|
||||
data: toJson(),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (type == ConstructTypeEnum.vocab) {
|
||||
return MatrixState.pangeaController.matrixState.client
|
||||
.analyticsRoomLocal()
|
||||
?.getState(PangeaEventTypes.userChosenEmoji, string)
|
||||
?.content
|
||||
.tryGet<String>(ModelKey.emoji);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [setEmoji] sets the emoji for the lemma
|
||||
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
|
||||
Future<void> setEmoji(String emoji) async {
|
||||
Future<void> setUserLemmaInfo(UserSetLemmaInfo newLemmaInfo) async {
|
||||
final analyticsRoom =
|
||||
MatrixState.pangeaController.matrixState.client.analyticsRoomLocal();
|
||||
if (analyticsRoom == null) return;
|
||||
|
|
@ -140,40 +167,151 @@ class ConstructIdentifier {
|
|||
final client = MatrixState.pangeaController.matrixState.client;
|
||||
final syncFuture = client.onRoomState.stream.firstWhere((event) {
|
||||
return event.roomId == analyticsRoom.id &&
|
||||
event.state.type == PangeaEventTypes.userChosenEmoji;
|
||||
event.state.type == PangeaEventTypes.userSetLemmaInfo;
|
||||
});
|
||||
|
||||
client.setRoomStateWithKey(
|
||||
analyticsRoom.id,
|
||||
PangeaEventTypes.userChosenEmoji,
|
||||
PangeaEventTypes.userSetLemmaInfo,
|
||||
string,
|
||||
{ModelKey.emoji: emoji},
|
||||
UserSetLemmaInfo(
|
||||
emojis: newLemmaInfo.emojis ?? userLemmaInfo?.emojis,
|
||||
meaning: newLemmaInfo.meaning ?? userLemmaInfo?.meaning,
|
||||
).toJson(),
|
||||
);
|
||||
await syncFuture;
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
data: {
|
||||
"construct": string,
|
||||
"emoji": emoji,
|
||||
},
|
||||
data: newLemmaInfo.toJson(),
|
||||
s: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// [getEmojiChoices] gets the emoji choices for the lemma
|
||||
// assumes that the language of the lemma is the same as the user's current l2
|
||||
Future<List<String>> getEmojiChoices() => LemmaInfoRepo.get(
|
||||
/// [lemmmaLang] if not set, assumed to be userL2
|
||||
Future<LemmaInfoResponse> getLemmaInfo([
|
||||
String? lemmaLang,
|
||||
String? userl1,
|
||||
]) =>
|
||||
LemmaInfoRepo.get(
|
||||
LemmaInfoRequest(
|
||||
lemma: lemma,
|
||||
partOfSpeech: category,
|
||||
lemmaLang: MatrixState
|
||||
.pangeaController.languageController.userL2?.langCode ??
|
||||
LanguageKeys.unknownLanguage,
|
||||
userL1: MatrixState
|
||||
.pangeaController.languageController.userL1?.langCode ??
|
||||
lemmaLang: lemmaLang ??
|
||||
MatrixState
|
||||
.pangeaController.languageController.userL2?.langCodeShort ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
userL1: userl1 ??
|
||||
MatrixState
|
||||
.pangeaController.languageController.userL1?.langCodeShort ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
lemma: lemma,
|
||||
),
|
||||
).then((onValue) => onValue.emoji);
|
||||
);
|
||||
|
||||
bool get isContentWord =>
|
||||
PartOfSpeechEnumExtensions.fromString(category)?.isContentWord ?? false;
|
||||
|
||||
/// [form] should be passed if available and is required for morphId
|
||||
bool isActivityProbablyLevelAppropriate(ActivityTypeEnum a, String? form) {
|
||||
switch (a) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
final double contentModifier = isContentWord ? 0.5 : 1;
|
||||
if (daysSinceLastEligibleUseForMeaning <
|
||||
3 * constructUses.points * contentModifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
case ActivityTypeEnum.emoji:
|
||||
return userSetEmoji.length < maxEmojisPerLemma;
|
||||
case ActivityTypeEnum.morphId:
|
||||
if (form == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception(
|
||||
"form is null in isActivityProbablyLevelAppropriate for morphId",
|
||||
),
|
||||
data: {
|
||||
"activity": a,
|
||||
"construct": toJson(),
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final uses = constructUses.uses
|
||||
.where((u) => u.form == form)
|
||||
.map((u) => u.timeStamp)
|
||||
.toList();
|
||||
|
||||
if (uses.isEmpty) return true;
|
||||
|
||||
final lastUsed = uses.reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
|
||||
return DateTime.now().difference(lastUsed).inDays >
|
||||
1 * constructUses.points;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
final pos = PartOfSpeechEnumExtensions.fromString(lemma) ??
|
||||
PartOfSpeechEnumExtensions.fromString(category);
|
||||
|
||||
if (pos == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return false;
|
||||
}
|
||||
|
||||
return pos.canBeHeard;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception(
|
||||
"Activity type $a not handled in ConstructIdentifier.isActivityProbablyLevelAppropriate",
|
||||
),
|
||||
data: {
|
||||
"activity": a,
|
||||
"construct": toJson(),
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// days since last eligible use for meaning
|
||||
/// this is the number of days since the last time the user used this word
|
||||
/// in a way that would engage with the meaning of the word
|
||||
/// importantly, this excludes emoji activities
|
||||
/// we want users to be able to do an emoji activity as a ramp up to
|
||||
/// a word meaning activity
|
||||
int get daysSinceLastEligibleUseForMeaning {
|
||||
final times = constructUses.uses
|
||||
.where(
|
||||
(u) =>
|
||||
u.useType.sentByUser ||
|
||||
ActivityTypeEnum.wordMeaning.associatedUseTypes
|
||||
.contains(u.useType) ||
|
||||
ActivityTypeEnum.messageMeaning.associatedUseTypes
|
||||
.contains(u.useType),
|
||||
)
|
||||
.map((u) => u.timeStamp)
|
||||
.toList();
|
||||
|
||||
if (times.isEmpty) return 1000;
|
||||
|
||||
// return the most recent timestamp
|
||||
final last = times.reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
|
||||
return DateTime.now().difference(last).inDays;
|
||||
}
|
||||
|
||||
Widget get visual {
|
||||
switch (type) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
return EmojiStack(emoji: userSetEmoji);
|
||||
case ConstructTypeEnum.morph:
|
||||
return MorphIcon(
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(category),
|
||||
morphTag: lemma,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
lib/pangea/emojis/emoji_stack.dart
Normal file
36
lib/pangea/emojis/emoji_stack.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmojiStack extends StatelessWidget {
|
||||
const EmojiStack({
|
||||
super.key,
|
||||
required List<String> emoji,
|
||||
this.style,
|
||||
}) : _emoji = emoji;
|
||||
|
||||
final List<String> _emoji;
|
||||
final TextStyle? style;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// return Text(
|
||||
// _emoji.first,
|
||||
// style: style,
|
||||
// );
|
||||
return Text(
|
||||
_emoji.join(''),
|
||||
style: style,
|
||||
);
|
||||
// return Stack(
|
||||
// children: [
|
||||
// for (final emoji in _emoji)
|
||||
// Positioned(
|
||||
// left: _emoji.indexOf(emoji) * style!.fontSize! * 0.5,
|
||||
// child: Text(
|
||||
// emoji,
|
||||
// style: style,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ class PangeaEventTypes {
|
|||
// static const studentAnalyticsSummary = "pangea.usranalytics";
|
||||
static const summaryAnalytics = "pangea.summaryAnalytics";
|
||||
static const construct = "pangea.construct";
|
||||
static const userSetLemmaInfo = "p.user_lemma_info";
|
||||
static const constructSummary = "pangea.construct_summary";
|
||||
static const userChosenEmoji = "p.emoji";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_repo.dart';
|
||||
|
|
@ -17,13 +12,17 @@ import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/audio_encoding_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../choreographer/enums/use_type.dart';
|
||||
import '../../common/utils/error_handler.dart';
|
||||
|
|
|
|||
|
|
@ -2,13 +2,7 @@
|
|||
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/event_wrappers/pangea_choreo_event.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart';
|
||||
|
|
@ -20,7 +14,14 @@ import 'package:fluffychat/pangea/events/models/representation_content_model.dar
|
|||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class RepresentationEvent {
|
||||
Event? _event;
|
||||
|
|
@ -283,4 +284,42 @@ class RepresentationEvent {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<PangeaToken> get tokensToSave =>
|
||||
tokens?.where((token) => token.lemma.saveVocab).toList() ?? [];
|
||||
|
||||
// List<ConstructIdentifier> get allTokenMorphsToConstructIdentifiers => tokens?.map((t) => t.morphConstructIds).toList() ??
|
||||
// [];
|
||||
|
||||
/// get allTokenMorphsToConstructIdentifiers
|
||||
Set<MorphFeaturesEnum> get morphFeatureSetToPractice =>
|
||||
MorphFeaturesEnum.values.where((feature) {
|
||||
// pos is always included
|
||||
if (feature == MorphFeaturesEnum.Pos) {
|
||||
return true;
|
||||
}
|
||||
return tokens?.any((token) => token.morph.containsKey(feature.name)) ??
|
||||
false;
|
||||
}).toSet();
|
||||
|
||||
Set<PartOfSpeechEnum> posSetToPractice(ActivityTypeEnum a) =>
|
||||
PartOfSpeechEnum.values.where((pos) {
|
||||
// some pos are not eligible for practice at all
|
||||
if (!pos.eligibleForPractice(a)) {
|
||||
return false;
|
||||
}
|
||||
return tokens?.any(
|
||||
(token) => token.pos.toLowerCase() == pos.name.toLowerCase(),
|
||||
) ??
|
||||
false;
|
||||
}).toSet();
|
||||
|
||||
List<String> tagsByFeature(MorphFeaturesEnum feature) {
|
||||
return tokens
|
||||
?.where((t) => t.morph.containsKey(feature.name))
|
||||
.map((t) => t.morph[feature.name])
|
||||
.cast<String>()
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
extension PangeaEvent on Event {
|
||||
V getPangeaContent<V>() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
|
|
@ -13,10 +10,18 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../common/constants/model_keys.dart';
|
||||
import '../../lemmas/lemma.dart';
|
||||
|
||||
|
|
@ -153,44 +158,16 @@ class PangeaToken {
|
|||
/// alias for the end of the token ie offset + length
|
||||
int get end => text.offset + text.length;
|
||||
|
||||
bool get isContentWord =>
|
||||
["NOUN", "VERB", "ADJ", "ADV"].contains(pos) && lemma.saveVocab;
|
||||
bool get isContentWord => vocabConstructID.isContentWord;
|
||||
|
||||
String get analyticsDebugPrint =>
|
||||
"content: ${text.content} isContentWord: $isContentWord total_xp:$xp vocab_construct_xp: ${vocabConstruct.points} daysSincelastUseInWordMeaning ${daysSinceLastUseByType(ActivityTypeEnum.wordMeaning)}";
|
||||
"content: ${text.content} isContentWord: $isContentWord vocab_construct_xp: ${vocabConstruct.points} daysSincelastUseInWordMeaning ${daysSinceLastUseByType(ActivityTypeEnum.wordMeaning, null)}";
|
||||
|
||||
bool get canBeDefined =>
|
||||
[
|
||||
"ADJ",
|
||||
"ADP",
|
||||
"ADV",
|
||||
"AUX",
|
||||
"CCONJ",
|
||||
"DET",
|
||||
"INTJ",
|
||||
"NOUN",
|
||||
"NUM",
|
||||
"PRON",
|
||||
"SCONJ",
|
||||
"VERB",
|
||||
].contains(pos) &&
|
||||
lemma.saveVocab;
|
||||
PartOfSpeechEnumExtensions.fromString(pos)?.canBeDefined ?? false;
|
||||
|
||||
bool get canBeHeard =>
|
||||
[
|
||||
"ADJ",
|
||||
"ADV",
|
||||
"AUX",
|
||||
"DET",
|
||||
"INTJ",
|
||||
"NOUN",
|
||||
"NUM",
|
||||
"PRON",
|
||||
"PROPN",
|
||||
"SCONJ",
|
||||
"VERB",
|
||||
].contains(pos) &&
|
||||
lemma.saveVocab;
|
||||
PartOfSpeechEnumExtensions.fromString(pos)?.canBeHeard ?? false;
|
||||
|
||||
/// Given a [type] and [metadata], returns a [OneConstructUse] for this lemma
|
||||
OneConstructUse toVocabUse(
|
||||
|
|
@ -316,9 +293,10 @@ class PangeaToken {
|
|||
debugger(when: kDebugMode);
|
||||
return false;
|
||||
}
|
||||
return morphConstruct(morphFeature, morphTag)
|
||||
.uses
|
||||
.any((u) => u.useType == a.correctUse && u.form == text.content);
|
||||
return morphConstruct(morphFeature)?.uses.any(
|
||||
(u) => u.useType == a.correctUse && u.form == text.content,
|
||||
) ??
|
||||
false;
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -332,49 +310,45 @@ class PangeaToken {
|
|||
bool _isActivityProbablyLevelAppropriate(
|
||||
ActivityTypeEnum a, [
|
||||
String? morphFeature,
|
||||
String? morphTag,
|
||||
]) {
|
||||
switch (a) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
final double contentModifier = isContentWord ? 0.5 : 1;
|
||||
|
||||
// TODO: turn this back on, see how much it fires, and discuss
|
||||
// debugPrint(
|
||||
// "days since last eligible use for meaning: $_daysSinceLastEligibleUseForMeaning for ${text.content}",
|
||||
// );
|
||||
|
||||
if (_daysSinceLastEligibleUseForMeaning <
|
||||
3 * vocabConstruct.points * contentModifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return vocabConstructID.isActivityProbablyLevelAppropriate(
|
||||
a,
|
||||
text.content,
|
||||
);
|
||||
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return !didActivitySuccessfully(a) || daysSinceLastUseByType(a) > 30;
|
||||
return !didActivitySuccessfully(a) ||
|
||||
daysSinceLastUseByType(a, null) > 30;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return daysSinceLastUseByType(a) > 7;
|
||||
return daysSinceLastUseByType(a, null) > 7;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return false;
|
||||
// disabling lemma activities for now
|
||||
// It has 2 purposes:• learning value• triangulating our determination of the lemma with
|
||||
// AI plus user verification.However, displaying the lemma during the meaning activity helps
|
||||
// It has 2 purposes:
|
||||
// • learning value
|
||||
// • triangulating our determination of the lemma with AI plus user verification.
|
||||
// However, displaying the lemma during the meaning activity helps
|
||||
// disambiguate what the meaning activity is about. This is probably more valuable than the
|
||||
// lemma activity itself. The piping for the lemma activity will stay there if we want to turn
|
||||
//it back on, maybe in select instances.
|
||||
// it back on, maybe in select instances.
|
||||
// return _didActivitySuccessfully(ActivityTypeEnum.wordMeaning) &&
|
||||
// daysSinceLastUseByType(a) > 7;
|
||||
case ActivityTypeEnum.emoji:
|
||||
return vocabConstructID.isActivityProbablyLevelAppropriate(
|
||||
a,
|
||||
text.content,
|
||||
);
|
||||
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
return true;
|
||||
case ActivityTypeEnum.morphId:
|
||||
if (morphFeature == null || morphTag == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return false;
|
||||
}
|
||||
// return _daysSinceLastUsedMorphForm(morphFeature, morphTag) >
|
||||
// 2 * morphConstruct(morphFeature, morphTag).points;
|
||||
return _daysSinceLastUsedMorphForm(morphFeature, morphTag) > 1;
|
||||
return morphFeature != null
|
||||
? morphIdByFeature(morphFeature)
|
||||
?.isActivityProbablyLevelAppropriate(a, text.content) ??
|
||||
false
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -401,17 +375,13 @@ class PangeaToken {
|
|||
required String? tag,
|
||||
}) {
|
||||
return isActivityBasicallyEligible(a, feature, tag) &&
|
||||
_isActivityProbablyLevelAppropriate(a, feature, tag);
|
||||
_isActivityProbablyLevelAppropriate(a, feature);
|
||||
}
|
||||
|
||||
ConstructUses get vocabConstruct =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel
|
||||
.getConstructUses(
|
||||
ConstructIdentifier(
|
||||
lemma: lemma.text,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
category: pos,
|
||||
),
|
||||
vocabConstructID,
|
||||
) ??
|
||||
ConstructUses(
|
||||
lemma: lemma.text,
|
||||
|
|
@ -420,63 +390,32 @@ class PangeaToken {
|
|||
uses: [],
|
||||
);
|
||||
|
||||
ConstructUses morphConstruct(String morphFeature, String morphTag) =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel
|
||||
.getConstructUses(
|
||||
ConstructIdentifier(
|
||||
lemma: morphTag,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: morphFeature,
|
||||
),
|
||||
) ??
|
||||
ConstructUses(
|
||||
lemma: morphTag,
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
category: morphFeature,
|
||||
uses: [],
|
||||
);
|
||||
ConstructUses? morphConstruct(String morphFeature) =>
|
||||
morphIdByFeature(morphFeature)?.constructUses;
|
||||
|
||||
int get xp {
|
||||
return constructs.fold<int>(
|
||||
0,
|
||||
(previousValue, element) => previousValue + element.points,
|
||||
ConstructIdentifier? morphIdByFeature(String feature) {
|
||||
final tag = getMorphTag(feature);
|
||||
if (tag == null) return null;
|
||||
return ConstructIdentifier(
|
||||
lemma: tag,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: feature,
|
||||
);
|
||||
}
|
||||
|
||||
/// days since last eligible use for meaning
|
||||
/// this is the number of days since the last time the user used this word
|
||||
/// in a way that would engage with the meaning of the word
|
||||
/// importantly, this excludes emoji activities
|
||||
/// we want users to be able to do an emoji activity as a ramp up to
|
||||
/// a word meaning activity
|
||||
int get _daysSinceLastEligibleUseForMeaning {
|
||||
final times = vocabConstruct.uses
|
||||
.where(
|
||||
(u) =>
|
||||
u.useType.sentByUser ||
|
||||
ActivityTypeEnum.wordMeaning.associatedUseTypes
|
||||
.contains(u.useType) ||
|
||||
ActivityTypeEnum.messageMeaning.associatedUseTypes
|
||||
.contains(u.useType),
|
||||
)
|
||||
.map((u) => u.timeStamp)
|
||||
.toList();
|
||||
/// lastUsed by activity type, construct and form
|
||||
DateTime? _lastUsedByActivityType(ActivityTypeEnum a, String? feature) {
|
||||
if (a == ActivityTypeEnum.morphId && feature == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return null;
|
||||
}
|
||||
final ConstructIdentifier? cId = a == ActivityTypeEnum.morphId
|
||||
? morphIdByFeature(feature!)
|
||||
: vocabConstructID;
|
||||
|
||||
if (times.isEmpty) return 1000;
|
||||
if (cId == null) return null;
|
||||
|
||||
// return the most recent timestamp
|
||||
final last = times.reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
|
||||
return DateTime.now().difference(last).inDays;
|
||||
}
|
||||
|
||||
/// lastUsed by activity type
|
||||
DateTime? _lastUsedByActivityType(ActivityTypeEnum a) {
|
||||
final List<ConstructUses> filteredConstructs =
|
||||
constructs.where((c) => a.constructFilter(c.id)).toList();
|
||||
|
||||
final correctUseTimestamps = filteredConstructs
|
||||
.expand((c) => c.uses)
|
||||
final correctUseTimestamps = cId.constructUses.uses
|
||||
.where((u) => u.form == text.content)
|
||||
.map((u) => u.timeStamp)
|
||||
.toList();
|
||||
|
|
@ -488,26 +427,13 @@ class PangeaToken {
|
|||
}
|
||||
|
||||
/// daysSinceLastUse by activity type
|
||||
int daysSinceLastUseByType(ActivityTypeEnum a) {
|
||||
final lastUsed = _lastUsedByActivityType(a);
|
||||
/// returns 1000 if there is no last use
|
||||
int daysSinceLastUseByType(ActivityTypeEnum a, String? feature) {
|
||||
final lastUsed = _lastUsedByActivityType(a, feature);
|
||||
if (lastUsed == null) return 1000;
|
||||
return DateTime.now().difference(lastUsed).inDays;
|
||||
}
|
||||
|
||||
int _daysSinceLastUsedMorphForm(String morphFeature, String morphTag) {
|
||||
final uses = morphConstruct(morphFeature, morphTag)
|
||||
.uses
|
||||
.where((u) => u.form == text.content)
|
||||
.map((u) => u.timeStamp)
|
||||
.toList();
|
||||
|
||||
if (uses.isEmpty) return 1000;
|
||||
|
||||
final lastUsed = uses.reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
|
||||
return DateTime.now().difference(lastUsed).inDays;
|
||||
}
|
||||
|
||||
List<ConstructIdentifier> get _constructIDs {
|
||||
final List<ConstructIdentifier> ids = [];
|
||||
ids.add(
|
||||
|
|
@ -538,7 +464,18 @@ class PangeaToken {
|
|||
.cast<ConstructUses>()
|
||||
.toList();
|
||||
|
||||
Future<List<String>> getEmojiChoices() => vocabConstructID.getEmojiChoices();
|
||||
Future<List<String>> getEmojiChoices() => LemmaInfoRepo.get(
|
||||
LemmaInfoRequest(
|
||||
lemma: lemma.text,
|
||||
partOfSpeech: pos,
|
||||
lemmaLang: MatrixState
|
||||
.pangeaController.languageController.userL2?.langCode ??
|
||||
LanguageKeys.unknownLanguage,
|
||||
userL1: MatrixState
|
||||
.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
),
|
||||
).then((onValue) => onValue.emoji);
|
||||
|
||||
ConstructIdentifier get vocabConstructID => ConstructIdentifier(
|
||||
lemma: lemma.text,
|
||||
|
|
@ -546,11 +483,14 @@ class PangeaToken {
|
|||
category: pos,
|
||||
);
|
||||
|
||||
Future<void> setEmoji(String emoji) => vocabConstructID.setEmoji(emoji);
|
||||
/// [setEmoji] sets the emoji for the lemma
|
||||
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
|
||||
Future<void> setEmoji(List<String> emojis) =>
|
||||
vocabConstructID.setUserLemmaInfo(UserSetLemmaInfo(emojis: emojis));
|
||||
|
||||
/// [getEmoji] gets the emoji for the lemma
|
||||
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
|
||||
String? getEmoji() => vocabConstructID.userSetEmoji;
|
||||
List<String> getEmoji() => vocabConstructID.userSetEmoji;
|
||||
|
||||
String get xpEmoji => vocabConstruct.xpEmoji;
|
||||
|
||||
|
|
@ -582,9 +522,9 @@ class PangeaToken {
|
|||
|
||||
/// initial default input mode for a token
|
||||
MessageMode get modeForToken {
|
||||
if (getEmoji() == null) {
|
||||
return MessageMode.wordEmoji;
|
||||
}
|
||||
// if (getEmoji() == null) {
|
||||
// return MessageMode.wordEmoji;
|
||||
// }
|
||||
|
||||
if (shouldDoActivity(
|
||||
a: ActivityTypeEnum.wordMeaning,
|
||||
|
|
@ -643,4 +583,62 @@ class PangeaToken {
|
|||
});
|
||||
return morphEntries;
|
||||
}
|
||||
|
||||
bool shouldDoActivityByMessageMode(MessageMode mode) {
|
||||
// debugPrint("should do activity for ${text.content} in $mode");
|
||||
return mode.associatedActivityType != null
|
||||
? shouldDoActivity(
|
||||
a: mode.associatedActivityType!,
|
||||
feature: null,
|
||||
tag: null,
|
||||
)
|
||||
: false;
|
||||
}
|
||||
|
||||
List<ConstructIdentifier> get allConstructIds => _constructIDs;
|
||||
|
||||
List<ConstructIdentifier> get morphConstructIds => morph.entries
|
||||
.map(
|
||||
(e) => ConstructIdentifier(
|
||||
lemma: e.key,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: e.value,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
List<ConstructIdentifier> get morphsBasicallyEligibleForPracticeByPriority =>
|
||||
MorphFeaturesEnumExtension.eligibleForPractice.where((f) {
|
||||
return f == MorphFeaturesEnum.Pos || getMorphTag(f.name) != null;
|
||||
}).map((f) {
|
||||
if (f == MorphFeaturesEnum.Pos) {
|
||||
return ConstructIdentifier(
|
||||
lemma: pos,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: f.name,
|
||||
);
|
||||
}
|
||||
return ConstructIdentifier(
|
||||
lemma: getMorphTag(f.name)!,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: f.name,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
bool hasMorph(ConstructIdentifier cId) {
|
||||
if (cId.category == "pos") {
|
||||
return morph["pos"].toString().toLowerCase() == cId.lemma.toLowerCase();
|
||||
}
|
||||
return morph.entries.any(
|
||||
(e) =>
|
||||
e.key.toLowerCase() == cId.lemma.toLowerCase() &&
|
||||
e.value.toString().toLowerCase() == cId.category.toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
/// [0,infinity) - a lower number means higher priority
|
||||
int activityPriorityScore(ActivityTypeEnum a, String? morphFeature) {
|
||||
return daysSinceLastUseByType(a, morphFeature) *
|
||||
(vocabConstructID.isContentWord ? 1 : 2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,4 +42,6 @@ class PangeaTokenText {
|
|||
|
||||
@override
|
||||
int get hashCode => offset.hashCode ^ content.hashCode ^ length.hashCode;
|
||||
|
||||
String get uniqueKey => "$content-$offset-$length";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,45 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TokenPosition {
|
||||
/// Start index of the full substring in the message
|
||||
final int start;
|
||||
|
||||
/// End index of the full substring in the message
|
||||
final int end;
|
||||
|
||||
/// Start index of the token in the message
|
||||
final int tokenStart;
|
||||
|
||||
/// End index of the token in the message
|
||||
final int tokenEnd;
|
||||
|
||||
final bool selected;
|
||||
final bool hideContent;
|
||||
final PangeaToken? token;
|
||||
final bool isHighlighted;
|
||||
|
||||
const TokenPosition({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.tokenStart,
|
||||
required this.tokenEnd,
|
||||
required this.hideContent,
|
||||
required this.selected,
|
||||
required this.isHighlighted,
|
||||
this.token,
|
||||
});
|
||||
}
|
||||
|
||||
class MessageTextUtil {
|
||||
static List<TokenPosition>? getTokenPositions(
|
||||
PangeaMessageEvent pangeaMessageEvent, {
|
||||
MessageAnalyticsEntry? messageAnalyticsEntry,
|
||||
bool Function(PangeaToken)? isSelected,
|
||||
bool Function(PangeaToken)? isHighlighted,
|
||||
}) {
|
||||
try {
|
||||
if (pangeaMessageEvent.messageDisplayRepresentation?.tokens == null) {
|
||||
|
|
@ -49,6 +78,7 @@ class MessageTextUtil {
|
|||
tokenEnd: startIndex,
|
||||
hideContent: false,
|
||||
selected: (isSelected?.call(token) ?? false) && !hasHiddenContent,
|
||||
isHighlighted: isHighlighted?.call(token) ?? false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -91,6 +121,7 @@ class MessageTextUtil {
|
|||
selected: (isSelected?.call(token) ?? false) &&
|
||||
!hideContent &&
|
||||
!hasHiddenContent,
|
||||
isHighlighted: isHighlighted?.call(token) ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,15 @@ enum InstructionsEnum {
|
|||
missingVoice,
|
||||
clickBestOption,
|
||||
completeActivitiesToUnlock,
|
||||
lemmaMeaning,
|
||||
chooseLemmaMeaning,
|
||||
activityPlannerOverview,
|
||||
ttsDisabled,
|
||||
chooseEmoji,
|
||||
chooseWordAudio,
|
||||
chooseMorphs,
|
||||
analyticsVocabList,
|
||||
morphAnalyticsList,
|
||||
readingAssistanceOverview,
|
||||
}
|
||||
|
||||
extension InstructionsEnumExtension on InstructionsEnum {
|
||||
|
|
@ -43,6 +46,7 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
return l10n.missingVoiceTitle;
|
||||
case InstructionsEnum.ttsDisabled:
|
||||
return l10n.ttsDisbledTitle;
|
||||
case InstructionsEnum.chooseWordAudio:
|
||||
case InstructionsEnum.chooseEmoji:
|
||||
case InstructionsEnum.activityPlannerOverview:
|
||||
case InstructionsEnum.clickAgainToDeselect:
|
||||
|
|
@ -51,9 +55,11 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
case InstructionsEnum.translationChoices:
|
||||
case InstructionsEnum.clickBestOption:
|
||||
case InstructionsEnum.completeActivitiesToUnlock:
|
||||
case InstructionsEnum.lemmaMeaning:
|
||||
case InstructionsEnum.chooseLemmaMeaning:
|
||||
case InstructionsEnum.chooseMorphs:
|
||||
case InstructionsEnum.analyticsVocabList:
|
||||
case InstructionsEnum.morphAnalyticsList:
|
||||
case InstructionsEnum.readingAssistanceOverview:
|
||||
ErrorHandler.logError(
|
||||
e: Exception("No title for this instruction"),
|
||||
m: 'InstructionsEnumExtension.title',
|
||||
|
|
@ -66,6 +72,21 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
}
|
||||
}
|
||||
|
||||
// IconData? get icon {
|
||||
// switch (this) {
|
||||
// case InstructionsEnum.itInstructions:
|
||||
// return Icons.translate;
|
||||
// case InstructionsEnum.clickMessage:
|
||||
// return Icons.touch_app;
|
||||
// case InstructionsEnum.blurMeansTranslate:
|
||||
// return Icons.blur_on;
|
||||
// case InstructionsEnum.tooltipInstructions:
|
||||
// return Icons.help;
|
||||
// case InstructionsEnum.missingVoice:
|
||||
// return Icons.mic_off;
|
||||
// }
|
||||
// }
|
||||
|
||||
String body(L10n l10n) {
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
|
|
@ -92,18 +113,24 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
return l10n.clickBestOption;
|
||||
case InstructionsEnum.completeActivitiesToUnlock:
|
||||
return l10n.completeActivitiesToUnlock;
|
||||
case InstructionsEnum.lemmaMeaning:
|
||||
return l10n.lemmaMeaningInstructionsBody;
|
||||
case InstructionsEnum.chooseLemmaMeaning:
|
||||
return l10n.chooseLemmaMeaningInstructionsBody;
|
||||
case InstructionsEnum.activityPlannerOverview:
|
||||
return l10n.activityPlannerOverviewInstructionsBody;
|
||||
case InstructionsEnum.chooseEmoji:
|
||||
return l10n.chooseEmojiInstructionsBody;
|
||||
case InstructionsEnum.ttsDisabled:
|
||||
return l10n.ttsDisabledBody;
|
||||
case InstructionsEnum.chooseWordAudio:
|
||||
return l10n.chooseWordAudioInstructionsBody;
|
||||
case InstructionsEnum.chooseMorphs:
|
||||
return l10n.chooseMorphsInstructionsBody;
|
||||
case InstructionsEnum.analyticsVocabList:
|
||||
return l10n.analyticsVocabListBody;
|
||||
case InstructionsEnum.morphAnalyticsList:
|
||||
return l10n.morphAnalyticsListBody;
|
||||
case InstructionsEnum.readingAssistanceOverview:
|
||||
return l10n.readingAssistanceOverviewBody;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class InstructionsInlineTooltip extends StatefulWidget {
|
||||
final InstructionsEnum instructionsEnum;
|
||||
final bool bold;
|
||||
|
||||
const InstructionsInlineTooltip({
|
||||
super.key,
|
||||
required this.instructionsEnum,
|
||||
this.bold = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -20,14 +20,27 @@ class InstructionsInlineTooltip extends StatefulWidget {
|
|||
}
|
||||
|
||||
class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with TickerProviderStateMixin {
|
||||
bool _isToggledOff = true;
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant InstructionsInlineTooltip oldWidget) {
|
||||
debugPrint("InstructionsInlineTooltip didUpdateWidget");
|
||||
if (oldWidget.instructionsEnum != widget.instructionsEnum) {
|
||||
setToggled();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setToggled();
|
||||
}
|
||||
|
||||
void setToggled() {
|
||||
_isToggledOff = widget.instructionsEnum.isToggledOff;
|
||||
|
||||
// Initialize AnimationController and Animation
|
||||
|
|
@ -43,6 +56,8 @@ class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
|
|||
|
||||
// Start in correct state
|
||||
if (!_isToggledOff) _controller.forward();
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -69,7 +84,7 @@ class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
|
|||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(5),
|
||||
color: AppConfig.gold.withAlpha(widget.bold ? 80 : 10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
|
|
@ -87,7 +102,8 @@ class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
|
|||
child: Center(
|
||||
child: Text(
|
||||
widget.instructionsEnum.body(L10n.of(context)),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
style: Theme.of(context).textTheme.titleLarge ??
|
||||
Theme.of(context).textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
51
lib/pangea/instructions/reset_instructions_list_tile.dart
Normal file
51
lib/pangea/instructions/reset_instructions_list_tile.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
|
||||
|
||||
class ResetInstructionsListTile extends StatelessWidget {
|
||||
const ResetInstructionsListTile({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final SettingsLearningController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//TODO: add to L10n
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.lightbulb),
|
||||
title: const Text(
|
||||
"Reset instruction tooltips",
|
||||
),
|
||||
subtitle: const Text(
|
||||
"Click to show instruction tooltips like for a brand new user.",
|
||||
),
|
||||
onTap: () => showAdaptiveDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
"Reset instruction tooltips?",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.resetInstructionTooltips();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text("Reset"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'package:country_picker/country_picker.dart';
|
|||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instruction_settings.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning_view.dart';
|
||||
|
|
@ -118,6 +119,20 @@ class SettingsLearningController extends State<SettingsLearning> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> resetInstructionTooltips() async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async => pangeaController.userController.updateProfile(
|
||||
(profile) {
|
||||
profile.instructionSettings = InstructionSettings();
|
||||
return profile;
|
||||
},
|
||||
waitForDataInSync: true,
|
||||
),
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> setSelectedLanguage({
|
||||
LanguageModel? sourceLanguage,
|
||||
LanguageModel? targetLanguage,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:app_settings/app_settings.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
|
||||
import 'package:fluffychat/pangea/instructions/reset_instructions_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/widgets/country_picker_tile.dart';
|
||||
|
|
@ -19,6 +14,10 @@ import 'package:fluffychat/pangea/learning_settings/widgets/p_settings_switch_li
|
|||
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SettingsLearningView extends StatelessWidget {
|
||||
final SettingsLearningController controller;
|
||||
|
|
@ -255,6 +254,7 @@ class SettingsLearningView extends StatelessWidget {
|
|||
activeColor: AppConfig.activeToggleColor,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
ResetInstructionsListTile(controller: controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
73
lib/pangea/lemmas/lemma_emoji_row.dart
Normal file
73
lib/pangea/lemmas/lemma_emoji_row.dart
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
|
||||
class LemmaEmojiRow extends StatelessWidget {
|
||||
final ConstructIdentifier cId;
|
||||
final VoidCallback onTap;
|
||||
final bool isSelected;
|
||||
|
||||
/// if a setState is defined then we're in a context where
|
||||
/// we allow removing an emoji
|
||||
/// later we'll probably want to allow this everywhere
|
||||
final void Function()? removeCallback;
|
||||
|
||||
const LemmaEmojiRow({
|
||||
required this.cId,
|
||||
required this.onTap,
|
||||
required this.removeCallback,
|
||||
this.isSelected = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
List<String> get emojis => cId.userSetEmoji;
|
||||
|
||||
Future<void> onEmojiTap(String toRemove) async {
|
||||
await cId.setUserLemmaInfo(
|
||||
UserSetLemmaInfo(
|
||||
emojis: emojis.where((e) => e != toRemove).toList(),
|
||||
),
|
||||
);
|
||||
removeCallback!();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
for (var i = 0; i < maxEmojisPerLemma; i++)
|
||||
Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
alignment: Alignment.center,
|
||||
child: i < emojis.length
|
||||
? GestureDetector(
|
||||
onTap: removeCallback == null
|
||||
? null
|
||||
: () => onEmojiTap(emojis[i]),
|
||||
child: Text(
|
||||
emojis[i],
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
)
|
||||
: WordZoomActivityButton(
|
||||
icon: Icon(
|
||||
Icons.add_reaction_outlined,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
isSelected: isSelected,
|
||||
onPressed: onTap,
|
||||
opacity: isSelected ? 1 : 0.4,
|
||||
tooltip: MessageMode.wordEmoji.title(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
import 'package:fluffychat/pangea/events/models/content_feedback.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
class LemmaInfoRepo {
|
||||
static final GetStorage _lemmaStorage = GetStorage('lemma_storage');
|
||||
|
|
@ -20,38 +23,22 @@ class LemmaInfoRepo {
|
|||
_lemmaStorage.write(request.storageKey, response.toJson());
|
||||
}
|
||||
|
||||
static Future<LemmaInfoResponse> get(
|
||||
LemmaInfoRequest request, [
|
||||
String? feedback,
|
||||
bool useExpireAt = false,
|
||||
]) async {
|
||||
static Future<LemmaInfoResponse> _fetch(LemmaInfoRequest request) async {
|
||||
final cachedJson = _lemmaStorage.read(request.storageKey);
|
||||
|
||||
final cached =
|
||||
cachedJson == null ? null : LemmaInfoResponse.fromJson(cachedJson);
|
||||
|
||||
if (cached != null) {
|
||||
if (feedback == null) {
|
||||
// at this point we have a cache without feedback
|
||||
if (!useExpireAt) {
|
||||
// return cache as is if we're not using expireAt
|
||||
return cached;
|
||||
} else if (cached.expireAt != null) {
|
||||
if (DateTime.now().isBefore(cached.expireAt!)) {
|
||||
// return cache as is if we're using expireAt and it's set but not expired
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
// we intentionally do not handle the case of expired at not set because
|
||||
// old caches won't have them set, and we want to trigger a new
|
||||
// choreo call
|
||||
if (DateTime.now().isBefore(cached.expireAt!)) {
|
||||
// return cache as is if we're using expireAt and it's set but not expired
|
||||
// debugPrint(
|
||||
// 'using cached data for ${request.lemma} ${cached.toJson()}',
|
||||
// );
|
||||
return cached;
|
||||
} else {
|
||||
// we're adding this within the service to avoid needing to have the widgets
|
||||
// save state including the bad response
|
||||
request.feedback = ContentFeedback(
|
||||
cached,
|
||||
feedback,
|
||||
);
|
||||
// if it's expired, remove it
|
||||
_lemmaStorage.remove(request.storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +57,47 @@ class LemmaInfoRepo {
|
|||
|
||||
set(request, response);
|
||||
|
||||
// debugPrint(
|
||||
// 'fetched data for ${request.lemma} ${response.toJson()}',
|
||||
// );
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// Get lemma info, prefering user set data over fetched data
|
||||
static Future<LemmaInfoResponse> get(LemmaInfoRequest request) async {
|
||||
try {
|
||||
// if the user has either emojis or meaning in the past, use those first
|
||||
final UserSetLemmaInfo? userSetLemmaInfo = request.cId.userLemmaInfo;
|
||||
|
||||
final List<String> emojis = userSetLemmaInfo?.emojis ?? [];
|
||||
String? meaning = userSetLemmaInfo?.meaning;
|
||||
|
||||
// if the user has not set these, fetch from the server
|
||||
if (emojis.length < maxEmojisPerLemma || meaning == null) {
|
||||
final LemmaInfoResponse fetched = await _fetch(request);
|
||||
|
||||
while (emojis.length < maxEmojisPerLemma && fetched.emoji.isNotEmpty) {
|
||||
final maybeToAdd = fetched.emoji.removeAt(0);
|
||||
if (!emojis.contains(maybeToAdd)) {
|
||||
emojis.add(maybeToAdd);
|
||||
}
|
||||
}
|
||||
meaning ??= fetched.meaning;
|
||||
} else {
|
||||
// debugPrint(
|
||||
// 'using user set data for ${request.lemma} ${userSetLemmaInfo?.toJson()}',
|
||||
// );
|
||||
}
|
||||
|
||||
return LemmaInfoResponse(
|
||||
emoji: emojis,
|
||||
meaning: meaning,
|
||||
);
|
||||
} catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, data: request.toJson());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/models/content_feedback.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
|
||||
|
|
@ -44,4 +46,10 @@ class LemmaInfoRequest {
|
|||
String get storageKey {
|
||||
return 'l:$lemma,p:$partOfSpeech,lang:$lemmaLang,l1:$userL1';
|
||||
}
|
||||
|
||||
ConstructIdentifier get cId => ConstructIdentifier(
|
||||
lemma: lemma,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
category: partOfSpeech,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:fluffychat/pangea/events/models/content_feedback.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
|
||||
class LemmaInfoResponse implements JsonSerializable {
|
||||
final List<String> emoji;
|
||||
|
|
@ -13,7 +14,12 @@ class LemmaInfoResponse implements JsonSerializable {
|
|||
|
||||
factory LemmaInfoResponse.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaInfoResponse(
|
||||
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
// NOTE: This is a workaround for the fact that the server sometimes sends more than 3 emojis
|
||||
emoji: (json['emoji'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList()
|
||||
.take(maxEmojisPerLemma)
|
||||
.toList(),
|
||||
meaning: json['meaning'] as String,
|
||||
expireAt: json['expireAt'] == null
|
||||
? null
|
||||
|
|
|
|||
34
lib/pangea/lemmas/user_set_lemma_info.dart
Normal file
34
lib/pangea/lemmas/user_set_lemma_info.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
class UserSetLemmaInfo {
|
||||
final String? meaning;
|
||||
final List<String>? emojis;
|
||||
|
||||
UserSetLemmaInfo({
|
||||
this.emojis,
|
||||
this.meaning,
|
||||
});
|
||||
|
||||
factory UserSetLemmaInfo.fromJson(Map<String, dynamic> json) {
|
||||
return UserSetLemmaInfo(
|
||||
emojis: json["emojis"] != null ? List<String>.from(json["emojis"]) : null,
|
||||
meaning: json['meaning'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'emojis': emojis,
|
||||
'meaning': meaning,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is UserSetLemmaInfo &&
|
||||
runtimeType == other.runtimeType &&
|
||||
emojis == other.emojis &&
|
||||
meaning == other.meaning;
|
||||
|
||||
@override
|
||||
int get hashCode => emojis.hashCode ^ meaning.hashCode;
|
||||
}
|
||||
46
lib/pangea/message_token_text/hidden_text.dart
Normal file
46
lib/pangea/message_token_text/hidden_text.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class HiddenText extends StatelessWidget {
|
||||
final String text;
|
||||
final TextStyle style;
|
||||
|
||||
const HiddenText({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.style,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(text: text, style: style),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
final textWidth = textPainter.size.width;
|
||||
final textHeight = textPainter.size.height;
|
||||
|
||||
textPainter.dispose();
|
||||
|
||||
return SizedBox(
|
||||
height: textHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: textWidth,
|
||||
height: 1,
|
||||
color: style.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
241
lib/pangea/message_token_text/message_token_button.dart
Normal file
241
lib/pangea/message_token_text/message_token_button.dart
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
const double tokenButtonHeight = 40.0;
|
||||
const double tokenButtonDefaultFontSize = 10;
|
||||
const int maxEmojisPerLemma = 1;
|
||||
const double estimatedEmojiWidthRatio = 2;
|
||||
const double estimatedEmojiHeightRatio = 1.3;
|
||||
|
||||
class MessageTokenButton extends StatefulWidget {
|
||||
final MessageOverlayController? overlayController;
|
||||
final PangeaToken token;
|
||||
final TextStyle textStyle;
|
||||
final double width;
|
||||
final bool animate;
|
||||
final TargetTokensAndActivityType? activity;
|
||||
|
||||
const MessageTokenButton({
|
||||
super.key,
|
||||
required this.overlayController,
|
||||
required this.token,
|
||||
required this.textStyle,
|
||||
required this.width,
|
||||
required this.activity,
|
||||
this.animate = false,
|
||||
});
|
||||
|
||||
@override
|
||||
MessageTokenButtonState createState() => MessageTokenButtonState();
|
||||
}
|
||||
|
||||
class MessageTokenButtonState extends State<MessageTokenButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _heightAnimation;
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(
|
||||
milliseconds: AppConfig.overlayAnimationDuration,
|
||||
// seconds: 5,
|
||||
),
|
||||
);
|
||||
|
||||
_heightAnimation = Tween<double>(
|
||||
begin: 0,
|
||||
end: tokenButtonHeight,
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
||||
|
||||
if (widget.animate) {
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
double get topPadding => 10.0;
|
||||
|
||||
double get height =>
|
||||
widget.animate ? _heightAnimation.value : tokenButtonHeight;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessageTokenButton oldWidget) {
|
||||
if (oldWidget.overlayController?.toolbarMode !=
|
||||
widget.overlayController?.toolbarMode ||
|
||||
oldWidget.overlayController?.selectedToken !=
|
||||
widget.overlayController?.selectedToken ||
|
||||
oldWidget.overlayController?.selectedMorph !=
|
||||
widget.overlayController?.selectedMorph ||
|
||||
oldWidget.activity != widget.activity) {
|
||||
setState(() {});
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double get textSize =>
|
||||
widget.textStyle.fontSize ?? tokenButtonDefaultFontSize;
|
||||
|
||||
double get emojiSize => textSize * estimatedEmojiWidthRatio;
|
||||
|
||||
TextStyle get emojiStyle => widget.textStyle.copyWith(
|
||||
fontSize: textSize + 4,
|
||||
);
|
||||
|
||||
TargetTokensAndActivityType? get activity => widget.activity;
|
||||
|
||||
Widget get emojiView {
|
||||
// if (widget.token.text.content.length == 1 || maxEmojisPerLemma == 1) {
|
||||
return ShrinkableText(
|
||||
text: widget.token.vocabConstructID.userSetEmoji.firstOrNull ?? '',
|
||||
maxWidth: widget.width,
|
||||
style: emojiStyle,
|
||||
);
|
||||
// }
|
||||
// return Stack(
|
||||
// alignment: Alignment.center,
|
||||
// children: widget.token.vocabConstructID.userSetEmoji
|
||||
// .take(maxEmojisPerLemma)
|
||||
// .mapIndexed(
|
||||
// (index, emoji) => Positioned(
|
||||
// left: min(
|
||||
// index /
|
||||
// widget.token.vocabConstructID.userSetEmoji.length *
|
||||
// totalAvailableWidth,
|
||||
// index * emojiSize,
|
||||
// ),
|
||||
// child: Text(
|
||||
// emoji,
|
||||
// style: emojiStyle,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList()
|
||||
// .reversed
|
||||
// .toList(),
|
||||
// );
|
||||
}
|
||||
|
||||
Widget get content {
|
||||
final tokenActivity = activity;
|
||||
if (tokenActivity == null) {
|
||||
if (MessageMode.wordEmoji == widget.overlayController?.toolbarMode) {
|
||||
return SizedBox(height: height, child: emojiView);
|
||||
}
|
||||
return SizedBox(height: height);
|
||||
}
|
||||
|
||||
if (MessageMode.wordMorph == widget.overlayController?.toolbarMode) {
|
||||
if (activity?.morphFeature == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return SizedBox(height: height);
|
||||
}
|
||||
return InkWell(
|
||||
onHover: (isHovered) => setState(() => _isHovered = isHovered),
|
||||
onTap: () => widget.overlayController!
|
||||
.onMorphActivitySelect(widget.token, activity!.morphFeature!),
|
||||
borderRadius: borderRadius,
|
||||
child: Container(
|
||||
height: height,
|
||||
width: min(widget.width, height),
|
||||
alignment: Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: (widget.overlayController?.selectedToken == widget.token &&
|
||||
widget.overlayController?.selectedMorph ==
|
||||
activity?.morphFeature) ||
|
||||
_isHovered
|
||||
? 1.0
|
||||
: 0.5,
|
||||
child: Icon(
|
||||
Symbols.toys_and_games,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
// MorphIcon(morphFeature: activity!.morphFeature!, morphTag: null),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return DragTarget<ConstructForm>(
|
||||
builder: (BuildContext context, accepted, rejected) {
|
||||
final double colorAlpha = 0.3 +
|
||||
(widget.overlayController?.selectedChoice != null ? 0.3 : 0.0);
|
||||
|
||||
return InkWell(
|
||||
onHover: (isHovered) => setState(() => _isHovered = isHovered),
|
||||
onTap: widget.overlayController?.selectedChoice != null
|
||||
? () => widget.overlayController!.onMatchAttempt(
|
||||
widget.token,
|
||||
widget.overlayController!.selectedChoice!,
|
||||
)
|
||||
: null,
|
||||
borderRadius: borderRadius,
|
||||
child: Container(
|
||||
height: height,
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
width:
|
||||
MessageMode.wordMeaning == widget.overlayController?.toolbarMode
|
||||
? widget.width
|
||||
: min(widget.width, height),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha((colorAlpha * 255).toInt()),
|
||||
borderRadius: borderRadius,
|
||||
border: accepted.isNotEmpty ||
|
||||
(widget.overlayController?.selectedChoice != null &&
|
||||
_isHovered)
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAcceptWithDetails: (details) =>
|
||||
widget.overlayController!.onMatchAttempt(widget.token, details.data),
|
||||
);
|
||||
}
|
||||
|
||||
static final borderRadius = BorderRadius.circular(AppConfig.borderRadius - 4);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.overlayController == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (!widget.animate) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _heightAnimation,
|
||||
builder: (context, child) => content,
|
||||
);
|
||||
}
|
||||
}
|
||||
29
lib/pangea/message_token_text/token_position_model.dart
Normal file
29
lib/pangea/message_token_text/token_position_model.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
|
||||
class TokenPositionModel {
|
||||
/// Start index of the full substring in the message
|
||||
final int start;
|
||||
|
||||
/// End index of the full substring in the message
|
||||
final int end;
|
||||
|
||||
/// Start index of the token in the message
|
||||
final int tokenStart;
|
||||
|
||||
/// End index of the token in the message
|
||||
final int tokenEnd;
|
||||
|
||||
final bool selected;
|
||||
final bool hideContent;
|
||||
final PangeaToken? token;
|
||||
|
||||
const TokenPositionModel({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.tokenStart,
|
||||
required this.tokenEnd,
|
||||
required this.hideContent,
|
||||
required this.selected,
|
||||
this.token,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
// TODO Use the icons that Khue is creating
|
||||
IconData getIconForMorphFeature(String feature) {
|
||||
// Define a function to get the icon based on the universal dependency morphological feature (key)
|
||||
switch (feature.toLowerCase()) {
|
||||
case 'number':
|
||||
// google material 123 icon
|
||||
return Icons.format_list_numbered;
|
||||
case 'gender':
|
||||
return Icons.wc;
|
||||
case 'tense':
|
||||
return Icons.access_time;
|
||||
case 'mood':
|
||||
return Icons.mood;
|
||||
case 'person':
|
||||
return Icons.person;
|
||||
case 'case':
|
||||
return Icons.format_list_bulleted;
|
||||
case 'degree':
|
||||
return Icons.trending_up;
|
||||
case 'verbform':
|
||||
return Icons.text_format;
|
||||
case 'voice':
|
||||
return Icons.record_voice_over;
|
||||
case 'aspect':
|
||||
return Icons.aspect_ratio;
|
||||
case 'prontype':
|
||||
return Icons.text_fields;
|
||||
case 'numtype':
|
||||
return Icons.format_list_numbered;
|
||||
case 'poss':
|
||||
return Icons.account_balance;
|
||||
case 'reflex':
|
||||
return Icons.refresh;
|
||||
case 'foreign':
|
||||
return Icons.language;
|
||||
case 'abbr':
|
||||
return Icons.text_format;
|
||||
case 'nountype':
|
||||
return Symbols.abc;
|
||||
case 'pos':
|
||||
return Symbols.toys_and_games;
|
||||
case 'polarity':
|
||||
return Icons.swap_vert;
|
||||
case 'definite':
|
||||
return Icons.check_circle_outline;
|
||||
case 'prepcase':
|
||||
return Icons.location_on_outlined;
|
||||
case 'conjtype':
|
||||
return Icons.compare_arrows;
|
||||
default:
|
||||
return Icons.help_outline;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
||||
enum MorphologicalCategories {
|
||||
Pos,
|
||||
AdvType,
|
||||
Aspect,
|
||||
Case,
|
||||
ConjType,
|
||||
Definite,
|
||||
Degree,
|
||||
Evident,
|
||||
Foreign,
|
||||
Gender,
|
||||
Mood,
|
||||
NounType,
|
||||
NumForm,
|
||||
NumType,
|
||||
Number,
|
||||
NumberPsor,
|
||||
Person,
|
||||
Polarity,
|
||||
Polite,
|
||||
Poss,
|
||||
PrepCase,
|
||||
PronType,
|
||||
PunctSide,
|
||||
PunctType,
|
||||
Reflex,
|
||||
Tense,
|
||||
VerbForm,
|
||||
VerbType,
|
||||
Voice,
|
||||
}
|
||||
|
||||
extension MorphologicalCategoriesExtension on MorphologicalCategories {
|
||||
/// Convert enum to string
|
||||
String toShortString() {
|
||||
return toString().split('.').last.toLowerCase();
|
||||
}
|
||||
|
||||
/// Convert string to enum
|
||||
static MorphologicalCategories? fromString(String category) {
|
||||
final morph = MorphologicalCategories.values.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.toShortString() ==
|
||||
category.toLowerCase().replaceAll(RegExp(r'[,\[\]]'), ''),
|
||||
);
|
||||
if (morph == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "Missing morphological category",
|
||||
s: StackTrace.current,
|
||||
data: {"category": category},
|
||||
);
|
||||
}
|
||||
return morph;
|
||||
}
|
||||
|
||||
String getDisplayCopy(BuildContext context) {
|
||||
switch (this) {
|
||||
case MorphologicalCategories.Pos:
|
||||
return L10n.of(context).grammarCopyPOS;
|
||||
case MorphologicalCategories.AdvType:
|
||||
return L10n.of(context).grammarCopyADVTYPE;
|
||||
case MorphologicalCategories.Aspect:
|
||||
return L10n.of(context).grammarCopyASPECT;
|
||||
case MorphologicalCategories.Case:
|
||||
return L10n.of(context).grammarCopyCASE;
|
||||
case MorphologicalCategories.ConjType:
|
||||
return L10n.of(context).grammarCopyCONJTYPE;
|
||||
case MorphologicalCategories.Definite:
|
||||
return L10n.of(context).grammarCopyDEFINITE;
|
||||
case MorphologicalCategories.Degree:
|
||||
return L10n.of(context).grammarCopyDEGREE;
|
||||
case MorphologicalCategories.Evident:
|
||||
return L10n.of(context).grammarCopyEVIDENT;
|
||||
case MorphologicalCategories.Foreign:
|
||||
return L10n.of(context).grammarCopyFOREIGN;
|
||||
case MorphologicalCategories.Gender:
|
||||
return L10n.of(context).grammarCopyGENDER;
|
||||
case MorphologicalCategories.Mood:
|
||||
return L10n.of(context).grammarCopyMOOD;
|
||||
case MorphologicalCategories.NounType:
|
||||
return L10n.of(context).grammarCopyNOUNTYPE;
|
||||
case MorphologicalCategories.NumForm:
|
||||
return L10n.of(context).grammarCopyNUMFORM;
|
||||
case MorphologicalCategories.NumType:
|
||||
return L10n.of(context).grammarCopyNUMTYPE;
|
||||
case MorphologicalCategories.Number:
|
||||
return L10n.of(context).grammarCopyNUMBER;
|
||||
case MorphologicalCategories.NumberPsor:
|
||||
return L10n.of(context).grammarCopyNUMBERPSOR;
|
||||
case MorphologicalCategories.Person:
|
||||
return L10n.of(context).grammarCopyPERSON;
|
||||
case MorphologicalCategories.Polarity:
|
||||
return L10n.of(context).grammarCopyPOLARITY;
|
||||
case MorphologicalCategories.Polite:
|
||||
return L10n.of(context).grammarCopyPOLITE;
|
||||
case MorphologicalCategories.Poss:
|
||||
return L10n.of(context).grammarCopyPOSS;
|
||||
case MorphologicalCategories.PrepCase:
|
||||
return L10n.of(context).grammarCopyPREPCASE;
|
||||
case MorphologicalCategories.PronType:
|
||||
return L10n.of(context).grammarCopyPRONTYPE;
|
||||
case MorphologicalCategories.PunctSide:
|
||||
return L10n.of(context).grammarCopyPUNCTSIDE;
|
||||
case MorphologicalCategories.PunctType:
|
||||
return L10n.of(context).grammarCopyPUNCTTYPE;
|
||||
case MorphologicalCategories.Reflex:
|
||||
return L10n.of(context).grammarCopyREFLEX;
|
||||
case MorphologicalCategories.Tense:
|
||||
return L10n.of(context).grammarCopyTENSE;
|
||||
case MorphologicalCategories.VerbForm:
|
||||
return L10n.of(context).grammarCopyVERBFORM;
|
||||
case MorphologicalCategories.VerbType:
|
||||
return L10n.of(context).grammarCopyVERBTYPE;
|
||||
case MorphologicalCategories.Voice:
|
||||
return L10n.of(context).grammarCopyVOICE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? getMorphologicalCategoryCopy(
|
||||
String categoryName,
|
||||
BuildContext context,
|
||||
) {
|
||||
final MorphologicalCategories? category =
|
||||
MorphologicalCategoriesExtension.fromString(categoryName);
|
||||
|
||||
if (category == null) {
|
||||
return null;
|
||||
}
|
||||
return category.getDisplayCopy(context);
|
||||
}
|
||||
|
|
@ -1,18 +1,14 @@
|
|||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
|
||||
class MorphFeatureDisplay extends StatelessWidget {
|
||||
const MorphFeatureDisplay({
|
||||
MorphFeatureDisplay({
|
||||
super.key,
|
||||
required String morphFeature,
|
||||
required String morphTag,
|
||||
}) : _morphFeature = morphFeature,
|
||||
_morphTag = morphTag;
|
||||
}) : _morphFeature = MorphFeaturesEnumExtension.fromString(morphFeature);
|
||||
|
||||
final String _morphFeature;
|
||||
final String _morphTag;
|
||||
final MorphFeaturesEnum _morphFeature;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -20,22 +16,17 @@ class MorphFeatureDisplay extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 32.0,
|
||||
height: 32.0,
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: MorphIcon(
|
||||
morphFeature: _morphFeature,
|
||||
morphTag: _morphTag,
|
||||
morphTag: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
Text(
|
||||
getGrammarCopy(
|
||||
category: _morphFeature,
|
||||
lemma: _morphTag,
|
||||
context: context,
|
||||
) ??
|
||||
_morphTag,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
_morphFeature.getDisplayCopy(context),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
224
lib/pangea/morphs/morph_features_enum.dart
Normal file
224
lib/pangea/morphs/morph_features_enum.dart
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
enum MorphFeaturesEnum {
|
||||
Pos,
|
||||
AdvType,
|
||||
Aspect,
|
||||
Case,
|
||||
ConjType,
|
||||
Definite,
|
||||
Degree,
|
||||
Evident,
|
||||
Foreign,
|
||||
Gender,
|
||||
Mood,
|
||||
NounType,
|
||||
NumForm,
|
||||
NumType,
|
||||
Number,
|
||||
NumberPsor,
|
||||
Person,
|
||||
Polarity,
|
||||
Polite,
|
||||
Poss,
|
||||
PrepCase,
|
||||
PronType,
|
||||
PunctSide,
|
||||
PunctType,
|
||||
Reflex,
|
||||
Tense,
|
||||
VerbForm,
|
||||
VerbType,
|
||||
Voice,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
extension MorphFeaturesEnumExtension on MorphFeaturesEnum {
|
||||
/// Convert enum to string
|
||||
String toShortString() {
|
||||
return toString().split('.').last.toLowerCase();
|
||||
}
|
||||
|
||||
/// Convert string to enum
|
||||
static MorphFeaturesEnum fromString(String category) {
|
||||
final morph = MorphFeaturesEnum.values.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.toShortString() ==
|
||||
category.toLowerCase().replaceAll(RegExp(r'[,\[\]]'), ''),
|
||||
);
|
||||
if (morph == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "Missing morphological category",
|
||||
s: StackTrace.current,
|
||||
data: {"category": category},
|
||||
);
|
||||
return MorphFeaturesEnum.Unknown;
|
||||
}
|
||||
return morph;
|
||||
}
|
||||
|
||||
String getDisplayCopy(BuildContext context) {
|
||||
switch (this) {
|
||||
case MorphFeaturesEnum.Pos:
|
||||
return L10n.of(context).grammarCopyPOS;
|
||||
case MorphFeaturesEnum.AdvType:
|
||||
return L10n.of(context).grammarCopyADVTYPE;
|
||||
case MorphFeaturesEnum.Aspect:
|
||||
return L10n.of(context).grammarCopyASPECT;
|
||||
case MorphFeaturesEnum.Case:
|
||||
return L10n.of(context).grammarCopyCASE;
|
||||
case MorphFeaturesEnum.ConjType:
|
||||
return L10n.of(context).grammarCopyCONJTYPE;
|
||||
case MorphFeaturesEnum.Definite:
|
||||
return L10n.of(context).grammarCopyDEFINITE;
|
||||
case MorphFeaturesEnum.Degree:
|
||||
return L10n.of(context).grammarCopyDEGREE;
|
||||
case MorphFeaturesEnum.Evident:
|
||||
return L10n.of(context).grammarCopyEVIDENT;
|
||||
case MorphFeaturesEnum.Foreign:
|
||||
return L10n.of(context).grammarCopyFOREIGN;
|
||||
case MorphFeaturesEnum.Gender:
|
||||
return L10n.of(context).grammarCopyGENDER;
|
||||
case MorphFeaturesEnum.Mood:
|
||||
return L10n.of(context).grammarCopyMOOD;
|
||||
case MorphFeaturesEnum.NounType:
|
||||
return L10n.of(context).grammarCopyNOUNTYPE;
|
||||
case MorphFeaturesEnum.NumForm:
|
||||
return L10n.of(context).grammarCopyNUMFORM;
|
||||
case MorphFeaturesEnum.NumType:
|
||||
return L10n.of(context).grammarCopyNUMTYPE;
|
||||
case MorphFeaturesEnum.Number:
|
||||
return L10n.of(context).grammarCopyNUMBER;
|
||||
case MorphFeaturesEnum.NumberPsor:
|
||||
return L10n.of(context).grammarCopyNUMBERPSOR;
|
||||
case MorphFeaturesEnum.Person:
|
||||
return L10n.of(context).grammarCopyPERSON;
|
||||
case MorphFeaturesEnum.Polarity:
|
||||
return L10n.of(context).grammarCopyPOLARITY;
|
||||
case MorphFeaturesEnum.Polite:
|
||||
return L10n.of(context).grammarCopyPOLITE;
|
||||
case MorphFeaturesEnum.Poss:
|
||||
return L10n.of(context).grammarCopyPOSS;
|
||||
case MorphFeaturesEnum.PrepCase:
|
||||
return L10n.of(context).grammarCopyPREPCASE;
|
||||
case MorphFeaturesEnum.PronType:
|
||||
return L10n.of(context).grammarCopyPRONTYPE;
|
||||
case MorphFeaturesEnum.PunctSide:
|
||||
return L10n.of(context).grammarCopyPUNCTSIDE;
|
||||
case MorphFeaturesEnum.PunctType:
|
||||
return L10n.of(context).grammarCopyPUNCTTYPE;
|
||||
case MorphFeaturesEnum.Reflex:
|
||||
return L10n.of(context).grammarCopyREFLEX;
|
||||
case MorphFeaturesEnum.Tense:
|
||||
return L10n.of(context).grammarCopyTENSE;
|
||||
case MorphFeaturesEnum.VerbForm:
|
||||
return L10n.of(context).grammarCopyVERBFORM;
|
||||
case MorphFeaturesEnum.VerbType:
|
||||
return L10n.of(context).grammarCopyVERBTYPE;
|
||||
case MorphFeaturesEnum.Voice:
|
||||
return L10n.of(context).grammarCopyVOICE;
|
||||
case MorphFeaturesEnum.Unknown:
|
||||
return L10n.of(context).grammarCopyUNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/// the subset of morphological categories that are important to practice for learning the language
|
||||
/// by order of importance
|
||||
static List<MorphFeaturesEnum> get eligibleForPractice => [
|
||||
MorphFeaturesEnum.Pos,
|
||||
MorphFeaturesEnum.Tense,
|
||||
MorphFeaturesEnum.VerbForm,
|
||||
MorphFeaturesEnum.VerbType,
|
||||
MorphFeaturesEnum.Voice,
|
||||
MorphFeaturesEnum.AdvType,
|
||||
MorphFeaturesEnum.Aspect,
|
||||
MorphFeaturesEnum.Case,
|
||||
MorphFeaturesEnum.ConjType,
|
||||
MorphFeaturesEnum.Definite,
|
||||
MorphFeaturesEnum.Degree,
|
||||
MorphFeaturesEnum.Evident,
|
||||
MorphFeaturesEnum.Gender,
|
||||
MorphFeaturesEnum.Mood,
|
||||
MorphFeaturesEnum.NounType,
|
||||
MorphFeaturesEnum.NumForm,
|
||||
MorphFeaturesEnum.NumType,
|
||||
MorphFeaturesEnum.Number,
|
||||
MorphFeaturesEnum.NumberPsor,
|
||||
MorphFeaturesEnum.Person,
|
||||
MorphFeaturesEnum.Polarity,
|
||||
MorphFeaturesEnum.Polite,
|
||||
MorphFeaturesEnum.Poss,
|
||||
MorphFeaturesEnum.PrepCase,
|
||||
MorphFeaturesEnum.PronType,
|
||||
MorphFeaturesEnum.Reflex,
|
||||
];
|
||||
|
||||
bool get isEligibleForPractice {
|
||||
return eligibleForPractice.contains(this);
|
||||
}
|
||||
|
||||
IconData get fallbackIcon {
|
||||
switch (this) {
|
||||
case MorphFeaturesEnum.Number:
|
||||
// google material 123 icon
|
||||
return Icons.format_list_numbered;
|
||||
case MorphFeaturesEnum.Gender:
|
||||
return Icons.wc;
|
||||
case MorphFeaturesEnum.Tense:
|
||||
return Icons.access_time;
|
||||
case MorphFeaturesEnum.Mood:
|
||||
return Icons.mood;
|
||||
case MorphFeaturesEnum.Person:
|
||||
return Icons.person;
|
||||
case MorphFeaturesEnum.Case:
|
||||
return Icons.format_list_bulleted;
|
||||
case MorphFeaturesEnum.Degree:
|
||||
return Icons.trending_up;
|
||||
case MorphFeaturesEnum.VerbForm:
|
||||
return Icons.text_format;
|
||||
case MorphFeaturesEnum.Voice:
|
||||
return Icons.record_voice_over;
|
||||
case MorphFeaturesEnum.Aspect:
|
||||
return Icons.aspect_ratio;
|
||||
case MorphFeaturesEnum.PronType:
|
||||
return Icons.text_fields;
|
||||
case MorphFeaturesEnum.NumType:
|
||||
return Icons.format_list_numbered;
|
||||
case MorphFeaturesEnum.Poss:
|
||||
return Icons.account_balance;
|
||||
case MorphFeaturesEnum.Reflex:
|
||||
return Icons.refresh;
|
||||
case MorphFeaturesEnum.Foreign:
|
||||
return Icons.language;
|
||||
case MorphFeaturesEnum.NounType:
|
||||
return Symbols.abc;
|
||||
case MorphFeaturesEnum.Pos:
|
||||
return Symbols.toys_and_games;
|
||||
case MorphFeaturesEnum.Polarity:
|
||||
return Icons.swap_vert;
|
||||
case MorphFeaturesEnum.Definite:
|
||||
return Icons.check_circle_outline;
|
||||
case MorphFeaturesEnum.PrepCase:
|
||||
return Icons.location_on_outlined;
|
||||
case MorphFeaturesEnum.ConjType:
|
||||
return Icons.compare_arrows;
|
||||
default:
|
||||
return Icons.help_outline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? getMorphologicalCategoryCopy(
|
||||
String categoryName,
|
||||
BuildContext context,
|
||||
) {
|
||||
final MorphFeaturesEnum category =
|
||||
MorphFeaturesEnumExtension.fromString(categoryName);
|
||||
return category.getDisplayCopy(context);
|
||||
}
|
||||
|
|
@ -1,37 +1,55 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_icon_for_morph_feature.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_svg_link.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/utils/color_value.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MorphIcon extends StatelessWidget {
|
||||
const MorphIcon({
|
||||
super.key,
|
||||
required this.morphFeature,
|
||||
required this.morphTag,
|
||||
this.size,
|
||||
this.showTooltip = false,
|
||||
});
|
||||
|
||||
final String morphFeature;
|
||||
final MorphFeaturesEnum morphFeature;
|
||||
final String? morphTag;
|
||||
final bool showTooltip;
|
||||
final Size? size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// debugPrint("MorphIcon: morphFeature: $morphFeature, morphTag: $morphTag");
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
return CustomizedSvg(
|
||||
svgUrl: getMorphSvgLink(
|
||||
morphFeature: morphFeature,
|
||||
morphTag: morphTag,
|
||||
context: context,
|
||||
return Tooltip(
|
||||
message: morphTag == null
|
||||
? morphFeature.getDisplayCopy(context)
|
||||
: getGrammarCopy(
|
||||
category: morphFeature.name,
|
||||
lemma: morphTag!,
|
||||
context: context,
|
||||
),
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
child: CustomizedSvg(
|
||||
svgUrl: getMorphSvgLink(
|
||||
morphFeature: morphFeature.name,
|
||||
morphTag: morphTag,
|
||||
context: context,
|
||||
),
|
||||
colorReplacements: theme.brightness == Brightness.dark
|
||||
? {
|
||||
"white": theme.cardColor.hexValue.toString(),
|
||||
"black": "white",
|
||||
}
|
||||
: {},
|
||||
errorIcon: Icon(morphFeature.fallbackIcon),
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
),
|
||||
colorReplacements: theme.brightness == Brightness.dark
|
||||
? {
|
||||
"white": theme.cardColor.hexValue.toString(),
|
||||
"black": "white",
|
||||
}
|
||||
: {},
|
||||
errorIcon: Icon(getIconForMorphFeature(morphFeature)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MorphTagDisplay extends StatelessWidget {
|
||||
const MorphTagDisplay({
|
||||
super.key,
|
||||
required String morphFeature,
|
||||
required MorphFeaturesEnum morphFeature,
|
||||
required String morphTag,
|
||||
required this.textColor,
|
||||
}) : _morphFeature = morphFeature;
|
||||
}) : _morphFeature = morphFeature,
|
||||
_morphTag = morphTag;
|
||||
|
||||
final String _morphFeature;
|
||||
final MorphFeaturesEnum _morphFeature;
|
||||
final String _morphTag;
|
||||
final Color textColor;
|
||||
|
||||
@override
|
||||
|
|
@ -22,20 +23,19 @@ class MorphTagDisplay extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: MorphIcon(morphFeature: _morphFeature, morphTag: null),
|
||||
width: 32.0,
|
||||
height: 32.0,
|
||||
child: MorphIcon(morphFeature: _morphFeature, morphTag: _morphTag),
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
Text(
|
||||
_morphFeature.toLowerCase() == "other"
|
||||
? L10n.of(context).other
|
||||
: ConstructTypeEnum.morph.getDisplayCopy(
|
||||
_morphFeature,
|
||||
context,
|
||||
) ??
|
||||
_morphFeature,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
getGrammarCopy(
|
||||
category: _morphFeature.name,
|
||||
lemma: _morphTag,
|
||||
context: context,
|
||||
) ??
|
||||
_morphTag,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
/// list ordered by priority
|
||||
enum PartOfSpeechEnum {
|
||||
//Content tokens
|
||||
noun,
|
||||
verb,
|
||||
adj,
|
||||
adv,
|
||||
|
||||
enum GrammarCopyPOS {
|
||||
//Function tokens
|
||||
sconj,
|
||||
num,
|
||||
verb,
|
||||
affix,
|
||||
part,
|
||||
adj,
|
||||
cconj,
|
||||
punct,
|
||||
adv,
|
||||
aux,
|
||||
space,
|
||||
sym,
|
||||
|
|
@ -22,19 +29,18 @@ enum GrammarCopyPOS {
|
|||
pron,
|
||||
adp,
|
||||
propn,
|
||||
noun,
|
||||
intj,
|
||||
x,
|
||||
}
|
||||
|
||||
extension GrammarCopyPOSExtension on GrammarCopyPOS {
|
||||
extension PartOfSpeechEnumExtensions on PartOfSpeechEnum {
|
||||
/// Convert enum to string
|
||||
String toShortString() {
|
||||
return toString().split('.').last.toLowerCase();
|
||||
}
|
||||
|
||||
GrammarCopyPOS? fromString(String categoryName) {
|
||||
final pos = GrammarCopyPOS.values.firstWhereOrNull(
|
||||
static PartOfSpeechEnum? fromString(String categoryName) {
|
||||
final pos = PartOfSpeechEnum.values.firstWhereOrNull(
|
||||
(pos) => pos.toShortString() == categoryName.toLowerCase(),
|
||||
);
|
||||
if (pos == null) {
|
||||
|
|
@ -49,50 +55,103 @@ extension GrammarCopyPOSExtension on GrammarCopyPOS {
|
|||
|
||||
String getDisplayCopy(BuildContext context) {
|
||||
switch (this) {
|
||||
case GrammarCopyPOS.sconj:
|
||||
case PartOfSpeechEnum.sconj:
|
||||
return L10n.of(context).grammarCopyPOSsconj;
|
||||
case GrammarCopyPOS.num:
|
||||
case PartOfSpeechEnum.num:
|
||||
return L10n.of(context).grammarCopyPOSnum;
|
||||
case GrammarCopyPOS.verb:
|
||||
case PartOfSpeechEnum.verb:
|
||||
return L10n.of(context).grammarCopyPOSverb;
|
||||
case GrammarCopyPOS.affix:
|
||||
case PartOfSpeechEnum.affix:
|
||||
return L10n.of(context).grammarCopyPOSaffix;
|
||||
case GrammarCopyPOS.part:
|
||||
case PartOfSpeechEnum.part:
|
||||
return L10n.of(context).grammarCopyPOSpart;
|
||||
case GrammarCopyPOS.adj:
|
||||
case PartOfSpeechEnum.adj:
|
||||
return L10n.of(context).grammarCopyPOSadj;
|
||||
case GrammarCopyPOS.cconj:
|
||||
case PartOfSpeechEnum.cconj:
|
||||
return L10n.of(context).grammarCopyPOScconj;
|
||||
case GrammarCopyPOS.punct:
|
||||
case PartOfSpeechEnum.punct:
|
||||
return L10n.of(context).grammarCopyPOSpunct;
|
||||
case GrammarCopyPOS.adv:
|
||||
case PartOfSpeechEnum.adv:
|
||||
return L10n.of(context).grammarCopyPOSadv;
|
||||
case GrammarCopyPOS.aux:
|
||||
case PartOfSpeechEnum.aux:
|
||||
return L10n.of(context).grammarCopyPOSaux;
|
||||
case GrammarCopyPOS.space:
|
||||
case PartOfSpeechEnum.space:
|
||||
return L10n.of(context).grammarCopyPOSspace;
|
||||
case GrammarCopyPOS.sym:
|
||||
case PartOfSpeechEnum.sym:
|
||||
return L10n.of(context).grammarCopyPOSsym;
|
||||
case GrammarCopyPOS.det:
|
||||
case PartOfSpeechEnum.det:
|
||||
return L10n.of(context).grammarCopyPOSdet;
|
||||
case GrammarCopyPOS.pron:
|
||||
case PartOfSpeechEnum.pron:
|
||||
return L10n.of(context).grammarCopyPOSpron;
|
||||
case GrammarCopyPOS.adp:
|
||||
case PartOfSpeechEnum.adp:
|
||||
return L10n.of(context).grammarCopyPOSadp;
|
||||
case GrammarCopyPOS.propn:
|
||||
case PartOfSpeechEnum.propn:
|
||||
return L10n.of(context).grammarCopyPOSpropn;
|
||||
case GrammarCopyPOS.noun:
|
||||
case PartOfSpeechEnum.noun:
|
||||
return L10n.of(context).grammarCopyPOSnoun;
|
||||
case GrammarCopyPOS.intj:
|
||||
case PartOfSpeechEnum.intj:
|
||||
return L10n.of(context).grammarCopyPOSintj;
|
||||
case GrammarCopyPOS.x:
|
||||
case PartOfSpeechEnum.x:
|
||||
return L10n.of(context).grammarCopyPOSx;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isContentWord => [
|
||||
PartOfSpeechEnum.noun,
|
||||
PartOfSpeechEnum.verb,
|
||||
PartOfSpeechEnum.adj,
|
||||
PartOfSpeechEnum.adv,
|
||||
].contains(this);
|
||||
|
||||
bool get canBeDefined => [
|
||||
PartOfSpeechEnum.noun,
|
||||
PartOfSpeechEnum.verb,
|
||||
PartOfSpeechEnum.adj,
|
||||
PartOfSpeechEnum.adv,
|
||||
PartOfSpeechEnum.propn,
|
||||
PartOfSpeechEnum.intj,
|
||||
PartOfSpeechEnum.det,
|
||||
PartOfSpeechEnum.pron,
|
||||
PartOfSpeechEnum.sconj,
|
||||
PartOfSpeechEnum.cconj,
|
||||
PartOfSpeechEnum.adp,
|
||||
PartOfSpeechEnum.aux,
|
||||
PartOfSpeechEnum.num,
|
||||
].contains(this);
|
||||
|
||||
bool get canBeHeard => [
|
||||
PartOfSpeechEnum.noun,
|
||||
PartOfSpeechEnum.verb,
|
||||
PartOfSpeechEnum.adj,
|
||||
PartOfSpeechEnum.adv,
|
||||
PartOfSpeechEnum.propn,
|
||||
PartOfSpeechEnum.intj,
|
||||
PartOfSpeechEnum.det,
|
||||
PartOfSpeechEnum.pron,
|
||||
PartOfSpeechEnum.sconj,
|
||||
PartOfSpeechEnum.cconj,
|
||||
PartOfSpeechEnum.adp,
|
||||
PartOfSpeechEnum.aux,
|
||||
PartOfSpeechEnum.num,
|
||||
].contains(this);
|
||||
|
||||
bool eligibleForPractice(ActivityTypeEnum activityType) {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
case ActivityTypeEnum.morphId:
|
||||
return canBeDefined;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return canBeHeard;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? getVocabCategoryName(String category, BuildContext context) {
|
||||
return GrammarCopyPOS.values
|
||||
return PartOfSpeechEnum.values
|
||||
.firstWhereOrNull((pos) => pos.toShortString() == category.toLowerCase())
|
||||
?.getDisplayCopy(context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
enum ActivityTypeEnum {
|
||||
wordMeaning,
|
||||
|
|
@ -156,18 +152,22 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
}
|
||||
}
|
||||
|
||||
/// Filters out constructs that are not relevant to the activity type
|
||||
bool Function(ConstructIdentifier) get constructFilter {
|
||||
ConstructUseTypeEnum? get incorrectUse {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return ConstructUseTypeEnum.incPA;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return ConstructUseTypeEnum.incWL;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return ConstructUseTypeEnum.incHWL;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return ConstructUseTypeEnum.incL;
|
||||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
return (id) => id.type == ConstructTypeEnum.vocab;
|
||||
return null;
|
||||
case ActivityTypeEnum.morphId:
|
||||
return (id) => id.type == ConstructTypeEnum.morph;
|
||||
return ConstructUseTypeEnum.incM;
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
return ConstructUseTypeEnum.incMM;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
return Icons.translate;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return Icons.hearing;
|
||||
return Icons.volume_up;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return Symbols.dictionary;
|
||||
case ActivityTypeEnum.emoji:
|
||||
|
|
@ -1,17 +1,15 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
|
||||
class EmojiActivityGenerator {
|
||||
Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
|
|
@ -22,11 +20,16 @@ class EmojiActivityGenerator {
|
|||
final PangeaToken token = req.targetTokens.first;
|
||||
|
||||
final List<String> emojis = await token.getEmojiChoices();
|
||||
final tokenEmoji = token.getEmoji();
|
||||
if (tokenEmoji != null && !emojis.contains(tokenEmoji)) {
|
||||
final List<String> tokenEmojis = token.getEmoji();
|
||||
//TODO : fix this or delete the file
|
||||
if (tokenEmojis.isNotEmpty) {
|
||||
final Random random = Random();
|
||||
final int randomIndex = random.nextInt(emojis.length);
|
||||
emojis[randomIndex] = tokenEmoji;
|
||||
for (final emoji in tokenEmojis) {
|
||||
final emojiIndex = emojis.indexOf(emoji);
|
||||
if (emojiIndex != -1) continue;
|
||||
final int randomIndex = random.nextInt(emojis.length);
|
||||
emojis[randomIndex] = emoji;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - modify MultipleChoiceActivity flow to allow no correct answer
|
||||
|
|
@ -1,17 +1,15 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class LemmaActivityGenerator {
|
||||
Future<MessageActivityResponse> get(
|
||||
|
|
@ -1,24 +1,20 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/word_bank/vocab_bank_repo.dart';
|
||||
import 'package:fluffychat/pangea/word_bank/vocab_request.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class LemmaMeaningActivityGenerator {
|
||||
/// Cache whether a lemma has distractors for a given part of speech
|
||||
static final Map<String, bool> _hasDistractorsCache = {};
|
||||
static Timer? _cacheClearTimer;
|
||||
|
||||
Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
|
|
@ -38,7 +34,7 @@ class LemmaMeaningActivityGenerator {
|
|||
userL1: req.userL1,
|
||||
);
|
||||
|
||||
final res = await LemmaInfoRepo.get(lemmaDefReq, null, true);
|
||||
final res = await LemmaInfoRepo.get(lemmaDefReq);
|
||||
|
||||
final choices = await getDistractorMeanings(lemmaDefReq, 3);
|
||||
|
||||
|
|
@ -64,40 +60,25 @@ class LemmaMeaningActivityGenerator {
|
|||
);
|
||||
}
|
||||
|
||||
static List<ConstructUses> eligibleDistractors(String lemma, String pos) {
|
||||
final distractors =
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel
|
||||
.constructList(type: ConstructTypeEnum.vocab)
|
||||
.where(
|
||||
(c) =>
|
||||
c.lemma.isNotEmpty && // must not be empty strings
|
||||
c.lemma.toLowerCase() !=
|
||||
lemma.toLowerCase() && // must not be the lemma itself
|
||||
c.category.toLowerCase() ==
|
||||
pos.toLowerCase(), // must be same part of speech
|
||||
)
|
||||
.toList();
|
||||
|
||||
_hasDistractorsCache['${lemma.toLowerCase()}-${pos.toLowerCase()}'] =
|
||||
distractors.isNotEmpty;
|
||||
|
||||
_cacheClearTimer ??= Timer.periodic(const Duration(minutes: 2), (Timer t) {
|
||||
_hasDistractorsCache.clear();
|
||||
});
|
||||
|
||||
return distractors;
|
||||
}
|
||||
|
||||
/// From the cache, get a random set of cached definitions that are not for a specific lemma
|
||||
static Future<List<String>> getDistractorMeanings(
|
||||
LemmaInfoRequest req,
|
||||
int count,
|
||||
) async {
|
||||
final eligible = eligibleDistractors(req.lemma, req.partOfSpeech);
|
||||
eligible.shuffle();
|
||||
final eligible = await VocabRepo.getSemanticallySimilarWords(
|
||||
VocabRequest(
|
||||
langCode: req.lemmaLang,
|
||||
level: MatrixState
|
||||
.pangeaController.userController.profile.userSettings.cefrLevel,
|
||||
lemma: req.lemma,
|
||||
pos: req.partOfSpeech,
|
||||
count: count,
|
||||
),
|
||||
);
|
||||
eligible.vocab.shuffle();
|
||||
|
||||
final List<ConstructUses> distractorConstructUses =
|
||||
eligible.take(count).toList();
|
||||
final List<ConstructIdentifier> distractorConstructUses =
|
||||
eligible.vocab.take(count).toList();
|
||||
|
||||
final List<Future<LemmaInfoResponse>> futureDefs = [];
|
||||
for (final construct in distractorConstructUses) {
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
// includes feedback text and the bad activity model
|
||||
class ActivityQualityFeedback {
|
||||
283
lib/pangea/practice_activities/message_analytics_controller.dart
Normal file
283
lib/pangea/practice_activities/message_analytics_controller.dart
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class MessageAnalyticsEntry {
|
||||
final DateTime createdAt = DateTime.now();
|
||||
|
||||
late final List<PangeaToken> _tokens;
|
||||
|
||||
final Map<ActivityTypeEnum, List<TargetTokensAndActivityType>>
|
||||
_activityQueue = {};
|
||||
|
||||
final int _maxQueueLength = 5;
|
||||
|
||||
MessageAnalyticsEntry({
|
||||
required List<PangeaToken> tokens,
|
||||
required bool includeHiddenWordActivities,
|
||||
required PangeaMessageEvent pangeaMessageEvent,
|
||||
}) {
|
||||
_tokens = tokens;
|
||||
initialize();
|
||||
}
|
||||
|
||||
void _pushQueue(TargetTokensAndActivityType entry) {
|
||||
if (_activityQueue.containsKey(entry.activityType)) {
|
||||
_activityQueue[entry.activityType]!.insert(0, entry);
|
||||
} else {
|
||||
_activityQueue[entry.activityType] = [entry];
|
||||
}
|
||||
|
||||
// just in case we make a mistake and the queue gets too long
|
||||
if (_activityQueue[entry.activityType]!.length > _maxQueueLength) {
|
||||
debugger(when: kDebugMode);
|
||||
_activityQueue[entry.activityType]!.removeRange(
|
||||
_maxQueueLength,
|
||||
_activityQueue.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _filterActivityQueue(ActivityTypeEnum activityType) {
|
||||
_activityQueue[activityType]?.clear();
|
||||
}
|
||||
|
||||
void _clearAllQueue() {
|
||||
_activityQueue.clear();
|
||||
}
|
||||
|
||||
TargetTokensAndActivityType? nextActivity(ActivityTypeEnum a) =>
|
||||
_activityQueue[a]?.firstOrNull;
|
||||
|
||||
bool get hasHiddenWordActivity =>
|
||||
activities(ActivityTypeEnum.hiddenWordListening).isNotEmpty;
|
||||
|
||||
bool get hasMessageMeaningActivity =>
|
||||
activities(ActivityTypeEnum.messageMeaning).isNotEmpty;
|
||||
|
||||
int get numActivities => _activityQueue.length;
|
||||
|
||||
List<TargetTokensAndActivityType> activities(ActivityTypeEnum a) =>
|
||||
_activityQueue[a] ?? [];
|
||||
|
||||
// /// If there are more than 4 tokens that can be heard, we don't want to do word focus listening
|
||||
// /// Otherwise, we don't have enough distractors
|
||||
// bool get canDoWordFocusListening =>
|
||||
// _tokens.where((t) => t.canBeHeard).length > 4;
|
||||
|
||||
/// On initialization, we pick which tokens to do activities on and what types of activities to do
|
||||
void initialize() {
|
||||
final eligibleTokens = _tokens.where((t) => t.lemma.saveVocab);
|
||||
|
||||
// EMOJI
|
||||
// sort the tokens by the preference of them for an emoji activity
|
||||
// order from least to most recent
|
||||
// words that have never been used are counted as 1000 days
|
||||
// we preference content words over function words by multiplying the days since last use by 2
|
||||
// NOTE: for now, we put it at the end if it has no uses and basically just give them the answer
|
||||
// later on, we may introduce an emoji activity that is easier than the current matching one
|
||||
// i.e. we show them 3 good emojis and 1 bad one and ask them to pick the bad one
|
||||
_activityQueue[ActivityTypeEnum.emoji] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.emoji,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => a.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.emoji, null)
|
||||
.compareTo(
|
||||
b.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.emoji, null),
|
||||
),
|
||||
)
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// WORD MEANING
|
||||
// make word meaning activities
|
||||
// same as emojis for now
|
||||
_activityQueue[ActivityTypeEnum.wordMeaning] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => a.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null)
|
||||
.compareTo(
|
||||
b.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null),
|
||||
),
|
||||
)
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// WORD FOCUS LISTENING
|
||||
// make word focus listening activities
|
||||
// same as emojis for now
|
||||
_activityQueue[ActivityTypeEnum.wordFocusListening] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.wordFocusListening,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => a.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordFocusListening, null)
|
||||
.compareTo(
|
||||
b.tokens.first.activityPriorityScore(
|
||||
ActivityTypeEnum.wordFocusListening,
|
||||
null,
|
||||
),
|
||||
),
|
||||
)
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// GRAMMAR
|
||||
// build a list of TargetTokensAndActivityType for all tokens and all features in the message
|
||||
// limits to _maxQueueLength activities and only one per token
|
||||
final List<TargetTokensAndActivityType> candidates = eligibleTokens.expand(
|
||||
(t) {
|
||||
return t.morphsBasicallyEligibleForPracticeByPriority.map(
|
||||
(m) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.morphId,
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(m.category),
|
||||
),
|
||||
);
|
||||
},
|
||||
).sorted(
|
||||
(a, b) => a.tokens.first
|
||||
.activityPriorityScore(
|
||||
ActivityTypeEnum.morphId,
|
||||
a.morphFeature!.name,
|
||||
)
|
||||
.compareTo(
|
||||
b.tokens.first.activityPriorityScore(
|
||||
ActivityTypeEnum.morphId,
|
||||
b.morphFeature!.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
//pick from the top 5, only including one per token
|
||||
_activityQueue[ActivityTypeEnum.morphId] = [];
|
||||
for (final candidate in candidates) {
|
||||
if (_activityQueue[ActivityTypeEnum.morphId]!.length >= _maxQueueLength) {
|
||||
break;
|
||||
}
|
||||
if (_activityQueue[ActivityTypeEnum.morphId]?.any(
|
||||
(entry) => entry.tokens.contains(candidate.tokens.first),
|
||||
) ==
|
||||
false) {
|
||||
_activityQueue[ActivityTypeEnum.morphId]?.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool hasActivity(
|
||||
ActivityTypeEnum a,
|
||||
PangeaToken t, [
|
||||
MorphFeaturesEnum? morph,
|
||||
]) =>
|
||||
_activityQueue[a]?.any(
|
||||
(entry) =>
|
||||
entry.tokens.contains(t) &&
|
||||
(morph == null || entry.morphFeature == morph),
|
||||
) ==
|
||||
true;
|
||||
|
||||
/// Add a message meaning activity to the front of the queue
|
||||
/// And limits to _maxQueueLength activities
|
||||
void addMessageMeaningActivity() {
|
||||
final entry = TargetTokensAndActivityType(
|
||||
tokens: _tokens,
|
||||
activityType: ActivityTypeEnum.messageMeaning,
|
||||
);
|
||||
_pushQueue(entry);
|
||||
}
|
||||
|
||||
void onActivityComplete(ActivityTypeEnum a, PangeaToken? token) {
|
||||
_activityQueue[a]
|
||||
?.removeWhere((entry) => token == null || entry.tokens.contains(token));
|
||||
}
|
||||
|
||||
void exitPracticeFlow() => _activityQueue.clear();
|
||||
|
||||
void revealAllTokens() =>
|
||||
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.clear();
|
||||
|
||||
bool isTokenInHiddenWordActivity(PangeaToken token) =>
|
||||
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.isNotEmpty ?? false;
|
||||
}
|
||||
|
||||
/// computes TokenWithXP for given a pangeaMessageEvent and caches the result, according to the full text of the message
|
||||
/// listens for analytics updates and updates the cache accordingly
|
||||
class MessageAnalyticsController {
|
||||
static final Map<String, MessageAnalyticsEntry> _cache = {};
|
||||
|
||||
void dispose() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
// if over 300, remove oldest 5 entries by createdAt
|
||||
static void clean() {
|
||||
if (_cache.length > 300) {
|
||||
final sortedEntries = _cache.entries.toList()
|
||||
..sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
|
||||
for (var i = 0; i < 5; i++) {
|
||||
_cache.remove(sortedEntries[i].key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String _key(List<PangeaToken> tokens) =>
|
||||
PangeaToken.reconstructText(tokens);
|
||||
|
||||
static MessageAnalyticsEntry? get(
|
||||
List<PangeaToken> tokens,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) {
|
||||
final String key = _key(tokens);
|
||||
final entry = _cache[key];
|
||||
|
||||
// if cache is older than 1 day, then remove and recompute
|
||||
if (entry != null &&
|
||||
DateTime.now().difference(entry.createdAt).inDays > 1) {
|
||||
_cache.remove(key);
|
||||
}
|
||||
|
||||
if (entry != null) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
final bool includeHiddenWordActivities = !pangeaMessageEvent.ownMessage &&
|
||||
pangeaMessageEvent.messageDisplayRepresentation?.tokens != null &&
|
||||
pangeaMessageEvent.messageDisplayLangIsL2 &&
|
||||
!pangeaMessageEvent.event.isRichMessage;
|
||||
|
||||
_cache[key] = MessageAnalyticsEntry(
|
||||
tokens: tokens,
|
||||
includeHiddenWordActivities: includeHiddenWordActivities,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
);
|
||||
|
||||
clean();
|
||||
|
||||
return _cache[key];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
typedef MorphActivitySequence = Map<String, POSActivitySequence>;
|
||||
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
|
||||
class ActivityContent {
|
||||
final String question;
|
||||
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_display_instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_display_instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class CandidateMessage {
|
||||
final String msgId;
|
||||
|
|
@ -5,13 +5,12 @@
|
|||
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class PracticeActivityRecordModel {
|
||||
final String? question;
|
||||
|
|
@ -2,12 +2,6 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
|
|
@ -15,15 +9,19 @@ import 'package:fluffychat/pangea/common/network/urls.dart';
|
|||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/emoji_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/lemma_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/lemma_meaning_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/morph_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/repo/emoji_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/repo/lemma_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/repo/lemma_meaning_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/repo/morph_activity_generator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
class _RequestCacheItem {
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Picks which tokens to do activities on and what types of activities to do
|
||||
/// Caches result so that we don't have to recompute it
|
||||
/// Most importantly, we can't do this in the state of a message widget because the state is disposed of and recreated
|
||||
/// If we decided that the first token should have a hidden word listening, we need to remember that
|
||||
/// Otherwise, the user might leave the chat, return, and see a different word hidden
|
||||
|
||||
class TargetTokensAndActivityType {
|
||||
/// this is the tokens involved in the activity
|
||||
/// for most, this will be a single token
|
||||
final List<PangeaToken> tokens;
|
||||
final ActivityTypeEnum activityType;
|
||||
|
||||
// this is only defined for morphId activities
|
||||
final MorphFeaturesEnum? morphFeature;
|
||||
|
||||
TargetTokensAndActivityType({
|
||||
required this.tokens,
|
||||
required this.activityType,
|
||||
this.morphFeature,
|
||||
}) {
|
||||
if (ActivityTypeEnum.hiddenWordListening != activityType &&
|
||||
tokens.length != 1) {
|
||||
throw Exception("Only hiddenWordListening can have multiple tokens");
|
||||
}
|
||||
if (ActivityTypeEnum.morphId == activityType && morphFeature == null) {
|
||||
throw Exception("morphFeature must be defined for morphId activities");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TargetTokensAndActivityType &&
|
||||
listEquals(other.tokens, tokens) &&
|
||||
other.activityType == activityType &&
|
||||
other.morphFeature == morphFeature;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
tokens.hashCode ^ activityType.hashCode ^ morphFeature.hashCode;
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
|
||||
final wordMeaningStaticPracticeActivityModel = MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
|
||||
enum MessageMode {
|
||||
practiceActivity,
|
||||
|
||||
|
|
@ -16,7 +21,7 @@ enum MessageMode {
|
|||
// wordZoomSpeechToText,
|
||||
|
||||
messageMeaning,
|
||||
messageTextToSpeech,
|
||||
listening,
|
||||
messageSpeechToText,
|
||||
messageTranslation,
|
||||
|
||||
|
|
@ -28,9 +33,9 @@ extension MessageModeExtension on MessageMode {
|
|||
IconData get icon {
|
||||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return Icons.g_translate;
|
||||
case MessageMode.messageTextToSpeech:
|
||||
return Symbols.text_to_speech;
|
||||
return Icons.translate;
|
||||
case MessageMode.listening:
|
||||
return Icons.volume_up;
|
||||
case MessageMode.messageSpeechToText:
|
||||
return Symbols.speech_to_text;
|
||||
case MessageMode.practiceActivity:
|
||||
|
|
@ -43,7 +48,7 @@ extension MessageModeExtension on MessageMode {
|
|||
case MessageMode.messageMeaning:
|
||||
return Icons.star;
|
||||
case MessageMode.wordEmoji:
|
||||
return Icons.emoji_emotions;
|
||||
return Icons.add_reaction_outlined;
|
||||
case MessageMode.wordMorph:
|
||||
return Symbols.toys_and_games;
|
||||
}
|
||||
|
|
@ -53,7 +58,7 @@ extension MessageModeExtension on MessageMode {
|
|||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return L10n.of(context).translations;
|
||||
case MessageMode.messageTextToSpeech:
|
||||
case MessageMode.listening:
|
||||
return L10n.of(context).messageAudio;
|
||||
case MessageMode.messageSpeechToText:
|
||||
return L10n.of(context).speechToTextTooltip;
|
||||
|
|
@ -69,7 +74,7 @@ extension MessageModeExtension on MessageMode {
|
|||
case MessageMode.wordEmoji:
|
||||
return "Emoji";
|
||||
case MessageMode.wordMorph:
|
||||
return "Morph";
|
||||
return "Grammar";
|
||||
case MessageMode.wordMeaning:
|
||||
return "Meaning";
|
||||
}
|
||||
|
|
@ -79,8 +84,8 @@ extension MessageModeExtension on MessageMode {
|
|||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return L10n.of(context).translationTooltip;
|
||||
case MessageMode.messageTextToSpeech:
|
||||
return L10n.of(context).audioTooltip;
|
||||
case MessageMode.listening:
|
||||
return L10n.of(context).listen;
|
||||
case MessageMode.messageSpeechToText:
|
||||
return L10n.of(context).speechToTextTooltip;
|
||||
case MessageMode.practiceActivity:
|
||||
|
|
@ -95,42 +100,64 @@ extension MessageModeExtension on MessageMode {
|
|||
case MessageMode.wordEmoji:
|
||||
return "Emoji";
|
||||
case MessageMode.wordMorph:
|
||||
return "Morph";
|
||||
return "Grammar";
|
||||
case MessageMode.wordMeaning:
|
||||
return "Meaning";
|
||||
}
|
||||
}
|
||||
|
||||
InstructionsEnum? get instructionsEnum {
|
||||
switch (this) {
|
||||
case MessageMode.wordMorph:
|
||||
return InstructionsEnum.chooseMorphs;
|
||||
case MessageMode.messageSpeechToText:
|
||||
return InstructionsEnum.speechToText;
|
||||
case MessageMode.wordMeaning:
|
||||
return InstructionsEnum.chooseLemmaMeaning;
|
||||
case MessageMode.listening:
|
||||
return InstructionsEnum.chooseWordAudio;
|
||||
case MessageMode.wordEmoji:
|
||||
return InstructionsEnum.chooseEmoji;
|
||||
case MessageMode.noneSelected:
|
||||
return InstructionsEnum.readingAssistanceOverview;
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.practiceActivity:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
double get pointOnBar {
|
||||
switch (this) {
|
||||
case MessageMode.practiceActivity:
|
||||
return 0;
|
||||
case MessageMode.messageTextToSpeech:
|
||||
return 0.35;
|
||||
case MessageMode.messageTranslation:
|
||||
return 0.64;
|
||||
case MessageMode.messageMeaning:
|
||||
// case MessageMode.stats:
|
||||
// return 1;
|
||||
case MessageMode.noneSelected:
|
||||
return 1;
|
||||
case MessageMode.wordMorph:
|
||||
return 0.7;
|
||||
case MessageMode.wordMeaning:
|
||||
return 0.5;
|
||||
case MessageMode.listening:
|
||||
return 0.3;
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.messageSpeechToText:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.noneSelected:
|
||||
case MessageMode.wordEmoji:
|
||||
case MessageMode.wordMorph:
|
||||
case MessageMode.wordMeaning:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.practiceActivity:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool isUnlocked(
|
||||
double proportionOfActivitiesCompleted,
|
||||
bool totallyDone,
|
||||
MessageOverlayController overlayController,
|
||||
) {
|
||||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.messageTextToSpeech:
|
||||
return proportionOfActivitiesCompleted >= pointOnBar || totallyDone;
|
||||
return overlayController.isTranslationUnlocked;
|
||||
case MessageMode.practiceActivity:
|
||||
return !totallyDone;
|
||||
case MessageMode.listening:
|
||||
case MessageMode.messageSpeechToText:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.wordZoom:
|
||||
|
|
@ -144,28 +171,40 @@ extension MessageModeExtension on MessageMode {
|
|||
|
||||
bool get showButton => this != MessageMode.practiceActivity;
|
||||
|
||||
bool isModeDone(MessageOverlayController overlayController) {
|
||||
switch (this) {
|
||||
case MessageMode.messageTranslation:
|
||||
return overlayController.isTotallyDone;
|
||||
case MessageMode.listening:
|
||||
return overlayController.isListeningDone;
|
||||
case MessageMode.wordEmoji:
|
||||
return overlayController.isEmojiDone;
|
||||
case MessageMode.wordMorph:
|
||||
return overlayController.isMorphDone;
|
||||
case MessageMode.wordMeaning:
|
||||
return overlayController.isMeaningDone;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Color iconButtonColor(
|
||||
BuildContext context,
|
||||
MessageMode currentMode,
|
||||
double proportionOfActivitiesUnlocked,
|
||||
bool totallyDone,
|
||||
MessageOverlayController overlayController,
|
||||
) {
|
||||
if (this == MessageMode.practiceActivity && totallyDone) {
|
||||
if (overlayController.isTotallyDone) {
|
||||
return AppConfig.gold;
|
||||
}
|
||||
|
||||
//locked
|
||||
if (!isUnlocked(proportionOfActivitiesUnlocked, totallyDone)) {
|
||||
if (!isUnlocked(overlayController)) {
|
||||
return barAndLockedButtonColor(context);
|
||||
}
|
||||
|
||||
//unlocked and active
|
||||
if (this == currentMode) {
|
||||
return totallyDone ? AppConfig.gold : AppConfig.primaryColorLight;
|
||||
}
|
||||
|
||||
//unlocked and inactive
|
||||
return Theme.of(context).colorScheme.primaryContainer;
|
||||
//unlocked
|
||||
return isModeDone(overlayController)
|
||||
? AppConfig.gold
|
||||
: Theme.of(context).colorScheme.primaryContainer;
|
||||
}
|
||||
|
||||
static Color barAndLockedButtonColor(BuildContext context) {
|
||||
|
|
@ -173,4 +212,172 @@ extension MessageModeExtension on MessageMode {
|
|||
? Colors.grey[800]!
|
||||
: Colors.grey[200]!;
|
||||
}
|
||||
|
||||
ActivityTypeEnum? get associatedActivityType {
|
||||
switch (this) {
|
||||
case MessageMode.wordMeaning:
|
||||
return ActivityTypeEnum.wordMeaning;
|
||||
case MessageMode.listening:
|
||||
return ActivityTypeEnum.wordFocusListening;
|
||||
|
||||
case MessageMode.wordEmoji:
|
||||
return ActivityTypeEnum.emoji;
|
||||
|
||||
case MessageMode.wordMorph:
|
||||
return ActivityTypeEnum.morphId;
|
||||
|
||||
case MessageMode.noneSelected:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.messageSpeechToText:
|
||||
case MessageMode.practiceActivity:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// returns a nullable string of the current level of the message
|
||||
/// if string is null, then user has completed all levels
|
||||
/// should be resolvable into a part of speech or morph feature using fromString
|
||||
/// of the respective enum, PartOfSpeechEnum or MorphFeatureEnum
|
||||
String? currentChoiceMode(
|
||||
MessageOverlayController overlayController,
|
||||
PangeaMessageEvent pangeaMessage,
|
||||
) {
|
||||
switch (this) {
|
||||
case MessageMode.wordMeaning:
|
||||
case MessageMode.listening:
|
||||
case MessageMode.wordEmoji:
|
||||
// get the pos with some tokens left to practice, from most to least important for learning
|
||||
return pangeaMessage.messageDisplayRepresentation!
|
||||
.posSetToPractice(associatedActivityType!)
|
||||
.firstWhereOrNull(
|
||||
(pos) => pangeaMessage.messageDisplayRepresentation!.tokens!.any(
|
||||
(t) => t.vocabConstructID.isActivityProbablyLevelAppropriate(
|
||||
associatedActivityType!,
|
||||
t.text.content,
|
||||
),
|
||||
),
|
||||
)
|
||||
?.name;
|
||||
|
||||
case MessageMode.wordMorph:
|
||||
// get the morph feature with some tokens left to practice, from most to least important for learning
|
||||
return pangeaMessage
|
||||
.messageDisplayRepresentation!.morphFeatureSetToPractice
|
||||
.firstWhereOrNull(
|
||||
(feature) =>
|
||||
pangeaMessage.messageDisplayRepresentation!.tokens!.any((t) {
|
||||
final String? morphTag = t.getMorphTag(feature.name);
|
||||
|
||||
if (morphTag == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ConstructIdentifier(
|
||||
lemma: morphTag,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: feature.name,
|
||||
).isActivityProbablyLevelAppropriate(
|
||||
associatedActivityType!,
|
||||
t.text.content,
|
||||
);
|
||||
}),
|
||||
)
|
||||
?.name;
|
||||
|
||||
case MessageMode.noneSelected:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.messageSpeechToText:
|
||||
case MessageMode.practiceActivity:
|
||||
return null;
|
||||
}
|
||||
|
||||
// final feature = MorphFeaturesEnumExtension.fromString(overlayController);
|
||||
|
||||
// if (feature != null) {
|
||||
// for (int i; i < pangeaMessage.messageDisplayRepresentation!.morphFeatureSetToPractice.length; i++) {
|
||||
// if (pangeaMessage.messageDisplayRepresentation?.tagsByFeature(feature).isNotEmpty ?? false) {
|
||||
// return i;
|
||||
// }
|
||||
// }
|
||||
|
||||
// for (final feature in pangeaMessage.messageDisplayRepresentation?.tagsByFeature(feature)) ?? []) {
|
||||
// if (pangeaMessage.messageDisplayRepresentation?.tagsByFeature(feature).isNotEmpty ?? false) {
|
||||
// return feature.index;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// List<MessageModeChoiceLevelWidget> messageModeChoiceLevel(
|
||||
// MessageOverlayController overlayController,
|
||||
// PangeaMessageEvent pangeaMessage,
|
||||
// ) {
|
||||
// switch (this) {
|
||||
// case MessageMode.wordMorph:
|
||||
// final morphFeatureSet = pangeaMessage
|
||||
// .messageDisplayRepresentation?.morphFeatureSetToPractice;
|
||||
|
||||
// if (morphFeatureSet == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return [];
|
||||
// }
|
||||
|
||||
// // sort by the list of priority of parts of speech, defined by their order in the enum
|
||||
// morphFeatureSet.toList().sort((a, b) => a.index.compareTo(b.index));
|
||||
|
||||
// debugPrint(
|
||||
// "morphFeatureSet: ${morphFeatureSet.map((e) => e.name).toList()}",
|
||||
// );
|
||||
// return morphFeatureSet
|
||||
// .map(
|
||||
// (feature) => MessageModeChoiceLevelWidget(
|
||||
// overlayController: overlayController,
|
||||
// pangeaMessageEvent: pangeaMessage,
|
||||
// morphFeature: feature,
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
// case MessageMode.noneSelected:
|
||||
// case MessageMode.messageMeaning:
|
||||
// case MessageMode.messageTranslation:
|
||||
// case MessageMode.messageTextToSpeech:
|
||||
// case MessageMode.messageSpeechToText:
|
||||
// case MessageMode.practiceActivity:
|
||||
// case MessageMode.wordZoom:
|
||||
// case MessageMode.wordMeaning:
|
||||
// case MessageMode.wordEmoji:
|
||||
// if (associatedActivityType == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return [];
|
||||
// }
|
||||
// final posSet = pangeaMessage.messageDisplayRepresentation
|
||||
// ?.posSetToPractice(associatedActivityType!);
|
||||
|
||||
// if (posSet == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return [];
|
||||
// }
|
||||
|
||||
// // sort by the list of priority of parts of speech, defined by their order in the enum
|
||||
// posSet.toList().sort((a, b) => a.index.compareTo(b.index));
|
||||
|
||||
// debugPrint("posSet: ${posSet.map((e) => e.name).toList()}");
|
||||
|
||||
// final widgets = posSet
|
||||
// .map(
|
||||
// (pos) => MessageModeChoiceLevelWidget(
|
||||
// partOfSpeech: pos,
|
||||
// overlayController: overlayController,
|
||||
// pangeaMessageEvent: pangeaMessage,
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
|
||||
// return widgets;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import '../../events/constants/pangea_event_types.dart';
|
||||
|
||||
class PracticeActivityEvent {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_record_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_record_model.dart';
|
||||
import '../../events/constants/pangea_event_types.dart';
|
||||
|
||||
class PracticeActivityRecordEvent {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
|
||||
class MatchFeedback {
|
||||
ConstructForm form;
|
||||
bool isCorrect;
|
||||
|
||||
MatchFeedback({required this.form, required this.isCorrect});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is MatchFeedback &&
|
||||
other.form == form &&
|
||||
other.isCorrect == isCorrect;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => form.hashCode ^ isCorrect.hashCode;
|
||||
}
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_emojis.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_emoji_choice_item.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class MessageEmojiChoice extends StatelessWidget {
|
||||
final List<PangeaToken>? tokens;
|
||||
final ChatController controller;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const MessageEmojiChoice({
|
||||
super.key,
|
||||
required this.tokens,
|
||||
required this.controller,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
Future<void> redactReaction(BuildContext context, String emoji) {
|
||||
if (!context.mounted) {
|
||||
return Future.value();
|
||||
}
|
||||
final evt = allReactionEvents.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.senderId == e.room.client.userID &&
|
||||
e.content.tryGetMap('m.relates_to')?['key'] == emoji,
|
||||
);
|
||||
if (evt != null) {
|
||||
return showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => evt.redactEvent(),
|
||||
);
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
Iterable<Event> get allReactionEvents => controller.selectedEvents.first
|
||||
.aggregatedEvents(
|
||||
controller.timeline!,
|
||||
RelationshipTypes.reaction,
|
||||
)
|
||||
.where(
|
||||
(event) =>
|
||||
event.senderId == event.room.client.userID &&
|
||||
event.type == 'm.reaction',
|
||||
);
|
||||
|
||||
bool alreadyInReactions(String emoji) {
|
||||
for (final event in allReactionEvents) {
|
||||
try {
|
||||
if (event.content.tryGetMap('m.relates_to')!['key'] == emoji) {
|
||||
return true;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void onDoubleTapOrLongPress(BuildContext context, String emoji) {
|
||||
if (alreadyInReactions(emoji)) {
|
||||
redactReaction(context, emoji);
|
||||
} else {
|
||||
controller.sendEmojiAction(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> standardEmojiChoices(BuildContext context) => AppEmojis.emojis
|
||||
.map(
|
||||
(emoji) => MessageEmojiChoiceItem(
|
||||
content: emoji,
|
||||
onTap: () => alreadyInReactions(emoji)
|
||||
? redactReaction(context, emoji)
|
||||
: controller.sendEmojiAction(emoji),
|
||||
isSelected: false,
|
||||
onDoubleTap: () => onDoubleTapOrLongPress(context, emoji),
|
||||
onLongPress: () => onDoubleTapOrLongPress(context, emoji),
|
||||
greenHighlight: false,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
List<Widget> perTokenEmoji(BuildContext context) =>
|
||||
tokens!.where((token) => token.lemma.saveVocab).map((token) {
|
||||
final bool greenHighlight = token.shouldDoActivity(
|
||||
a: ActivityTypeEnum.wordMeaning,
|
||||
feature: null,
|
||||
tag: null,
|
||||
);
|
||||
if (!token.lemma.saveVocab) {
|
||||
return MessageEmojiChoiceItem(
|
||||
content: token.text.content,
|
||||
onTap: () => {},
|
||||
isSelected: overlayController.isTokenSelected(token),
|
||||
onDoubleTap: null,
|
||||
onLongPress: null,
|
||||
greenHighlight: greenHighlight,
|
||||
);
|
||||
}
|
||||
|
||||
final emoji = token.getEmoji();
|
||||
|
||||
if (emoji == null) {
|
||||
return MessageEmojiChoiceItem(
|
||||
topContent: token.vocabConstruct.constructLevel.icon(),
|
||||
content: token.text.content,
|
||||
onTap: () => overlayController.onClickOverlayMessageToken(token),
|
||||
onDoubleTap: null,
|
||||
onLongPress: null,
|
||||
isSelected: overlayController.isTokenSelected(token),
|
||||
contentOpacity: 0.1,
|
||||
greenHighlight: greenHighlight,
|
||||
);
|
||||
}
|
||||
|
||||
return MessageEmojiChoiceItem(
|
||||
topContent: Text(
|
||||
emoji,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
content: token.text.content,
|
||||
onTap: () => overlayController.onClickOverlayMessageToken(token),
|
||||
onDoubleTap: () => onDoubleTapOrLongPress(context, emoji),
|
||||
onLongPress: () => onDoubleTapOrLongPress(context, emoji),
|
||||
isSelected: overlayController.isTokenSelected(token),
|
||||
greenHighlight: greenHighlight,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
// spacing: 8.0, // Adjust spacing between items
|
||||
runSpacing: 0.0, // Adjust spacing between rows
|
||||
children: tokens == null ||
|
||||
tokens!.isEmpty ||
|
||||
!(overlayController
|
||||
.pangeaMessageEvent?.messageDisplayLangIsL2 ??
|
||||
false)
|
||||
? standardEmojiChoices(context)
|
||||
: perTokenEmoji(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
|
||||
const Size emojiButtonSize = Size(60, 60);
|
||||
BoxDecoration emojiButtonDecoration = BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
);
|
||||
|
||||
class MessageEmojiChoiceItem extends StatefulWidget {
|
||||
const MessageEmojiChoiceItem({
|
||||
|
|
@ -13,7 +20,8 @@ class MessageEmojiChoiceItem extends StatefulWidget {
|
|||
this.onLongPress,
|
||||
required this.isSelected,
|
||||
this.contentOpacity = 1.0,
|
||||
required this.greenHighlight,
|
||||
required this.isGold,
|
||||
required this.token,
|
||||
});
|
||||
|
||||
final Widget? topContent;
|
||||
|
|
@ -24,7 +32,8 @@ class MessageEmojiChoiceItem extends StatefulWidget {
|
|||
final bool isSelected;
|
||||
final double textSize;
|
||||
final double contentOpacity;
|
||||
final bool greenHighlight;
|
||||
final PangeaToken? token;
|
||||
final bool? isGold;
|
||||
|
||||
@override
|
||||
MessageEmojiChoiceItemState createState() => MessageEmojiChoiceItemState();
|
||||
|
|
@ -33,8 +42,56 @@ class MessageEmojiChoiceItem extends StatefulWidget {
|
|||
class MessageEmojiChoiceItemState extends State<MessageEmojiChoiceItem> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
didUpdateWidget(MessageEmojiChoiceItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.isSelected != widget.isSelected ||
|
||||
oldWidget.isGold != widget.isGold) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Color get color {
|
||||
if (widget.isSelected) {
|
||||
debugPrint('widget.isGold: ${widget.isGold}');
|
||||
if (widget.isGold == null) {
|
||||
return AppConfig.primaryColor.withAlpha((0.4 * 255).toInt());
|
||||
} else {
|
||||
return widget.isGold!
|
||||
? AppConfig.success.withAlpha((0.4 * 255).toInt())
|
||||
: AppConfig.warning.withAlpha((0.4 * 255).toInt());
|
||||
}
|
||||
}
|
||||
if (_isHovered) {
|
||||
return AppConfig.primaryColor.withAlpha((0.2 * 255).toInt());
|
||||
}
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
<<<<<<< HEAD
|
||||
return Opacity(
|
||||
opacity: widget.contentOpacity,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
onTap: widget.onTap,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: Container(
|
||||
height: emojiButtonSize.height,
|
||||
width: emojiButtonSize.width,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
child: Text(
|
||||
widget.content,
|
||||
style: TextStyle(fontSize: widget.textSize - 2),
|
||||
=======
|
||||
return IntrinsicWidth(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
|
|
@ -80,6 +137,7 @@ class MessageEmojiChoiceItemState extends State<MessageEmojiChoiceItem> {
|
|||
],
|
||||
),
|
||||
),
|
||||
>>>>>>> main
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_match_activity_item.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageMatchActivity extends StatelessWidget {
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const MessageMatchActivity({
|
||||
super.key,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
ActivityTypeEnum? get activityType =>
|
||||
overlayController.toolbarMode.associatedActivityType;
|
||||
|
||||
List<String> choices(TargetTokensAndActivityType a) {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.emoji:
|
||||
return overlayController
|
||||
.messageLemmaInfos![a.tokens.first.vocabConstructID]!.emoji
|
||||
.take(maxEmojisPerLemma)
|
||||
.toList();
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return [
|
||||
overlayController
|
||||
.messageLemmaInfos![a.tokens.first.vocabConstructID]!.meaning,
|
||||
];
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return [a.tokens.first.text.content];
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Widget choiceDisplayContent(TargetTokensAndActivityType a, String choice) {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.emoji:
|
||||
return Text(
|
||||
choice,
|
||||
style: const TextStyle(fontSize: 26),
|
||||
);
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return Text(
|
||||
choice,
|
||||
style: const TextStyle(fontSize: 26),
|
||||
);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return const Icon(
|
||||
Icons.volume_up,
|
||||
size: 26,
|
||||
);
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (overlayController.messageAnalyticsEntry == null ||
|
||||
overlayController.messageLemmaInfos == null ||
|
||||
activityType == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
if (overlayController.toolbarMode == MessageMode.listening &&
|
||||
overlayController.pangeaMessageEvent != null)
|
||||
MessageAudioCard(
|
||||
messageEvent: overlayController.pangeaMessageEvent!,
|
||||
overlayController: overlayController,
|
||||
setIsPlayingAudio: overlayController.setIsPlayingAudio,
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8.0, // Adjust spacing between items
|
||||
runSpacing: 8.0, // Adjust spacing between rows
|
||||
children: overlayController.messageAnalyticsEntry!
|
||||
.activities(activityType!)
|
||||
.expand(
|
||||
(TargetTokensAndActivityType a) {
|
||||
return choices(a).map(
|
||||
(choice) => MessageMatchActivityItem(
|
||||
constructForm: ConstructForm(
|
||||
choice,
|
||||
a.tokens.first.vocabConstructID,
|
||||
),
|
||||
content: choiceDisplayContent(a, choice),
|
||||
audioContent:
|
||||
overlayController.toolbarMode == MessageMode.listening
|
||||
? a.tokens.first.text.content
|
||||
: null,
|
||||
overlayController: overlayController,
|
||||
fixedSize: a.activityType == ActivityTypeEnum.wordMeaning
|
||||
? null
|
||||
: 60,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/match_feedback_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageMatchActivityItem extends StatefulWidget {
|
||||
const MessageMatchActivityItem({
|
||||
super.key,
|
||||
required this.content,
|
||||
required this.constructForm,
|
||||
this.audioContent,
|
||||
required this.overlayController,
|
||||
required this.fixedSize,
|
||||
});
|
||||
|
||||
final Widget content;
|
||||
final ConstructForm constructForm;
|
||||
final String? audioContent;
|
||||
final MessageOverlayController overlayController;
|
||||
final double? fixedSize;
|
||||
|
||||
@override
|
||||
MessageMatchActivityItemState createState() =>
|
||||
MessageMatchActivityItemState();
|
||||
}
|
||||
|
||||
class MessageMatchActivityItemState extends State<MessageMatchActivityItem> {
|
||||
bool _isHovered = false;
|
||||
bool _isPlaying = false;
|
||||
|
||||
TtsController get tts =>
|
||||
widget.overlayController.widget.chatController.choreographer.tts;
|
||||
|
||||
bool get isSelected =>
|
||||
widget.overlayController.selectedChoice == widget.constructForm;
|
||||
|
||||
Future<void> play() async {
|
||||
if (widget.audioContent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isPlaying) {
|
||||
await tts.stop();
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = true);
|
||||
}
|
||||
try {
|
||||
await tts.tryToSpeak(
|
||||
widget.audioContent!,
|
||||
context,
|
||||
targetID: 'word-audio-button',
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {"text": widget.audioContent},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MatchFeedback? get wasChosen => widget.overlayController.feedbackStates
|
||||
.firstWhereOrNull((e) => e.form == widget.constructForm);
|
||||
|
||||
Color color(BuildContext context) {
|
||||
final feedback = wasChosen;
|
||||
if (feedback != null) {
|
||||
return feedback.isCorrect ? AppConfig.success : AppConfig.warning;
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
return AppConfig.primaryColor;
|
||||
}
|
||||
|
||||
if (_isHovered) {
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
@override
|
||||
didUpdateWidget(MessageMatchActivityItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.overlayController.selectedChoice !=
|
||||
widget.overlayController.selectedChoice ||
|
||||
oldWidget.overlayController.selectedToken !=
|
||||
widget.overlayController.selectedToken ||
|
||||
widget.overlayController.feedbackStates
|
||||
.any((e) => e.form == widget.constructForm) !=
|
||||
oldWidget.overlayController.feedbackStates
|
||||
.any((e) => e.form == widget.constructForm)) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
IntrinsicWidth content(BuildContext context) {
|
||||
return IntrinsicWidth(
|
||||
child: Container(
|
||||
height: widget.fixedSize,
|
||||
width: widget.fixedSize,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color(context).withAlpha((0.4 * 255).toInt()),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
border: isSelected
|
||||
? Border.all(
|
||||
color: color(context),
|
||||
width: 2,
|
||||
)
|
||||
: Border.all(
|
||||
color: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: widget.content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LongPressDraggable<ConstructForm>(
|
||||
data: widget.constructForm,
|
||||
feedback: content(context),
|
||||
delay: const Duration(milliseconds: 100),
|
||||
onDragStarted: () {
|
||||
widget.overlayController.onChoiceSelect(widget.constructForm, true);
|
||||
},
|
||||
// onDragCompleted: () {
|
||||
// debugger(when: kDebugMode);
|
||||
// },
|
||||
// onDragEnd: (details) {
|
||||
// // debugger(when: kDebugMode);
|
||||
// },
|
||||
child: InkWell(
|
||||
onHover: (isHovered) => setState(() => _isHovered = isHovered),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
onTap: () {
|
||||
play();
|
||||
widget.overlayController.onChoiceSelect(widget.constructForm);
|
||||
},
|
||||
child: content(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_morph_choice_item.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
// this widget will handle the content of the input bar when mode == MessageMode.wordMorph
|
||||
|
||||
// if initializing, with a selectedToken then we should show an activity if one is available
|
||||
// in this case, we'll set selectedMorph to the first morph available for the selected token
|
||||
|
||||
// if initializing with a selectedMorph then we should show the first activity available for that morph
|
||||
// if no activity available for that morph, then we should just show the details of the feature and tag
|
||||
|
||||
// the details of a morph will allow the user to edit the morphological tag of that feature.
|
||||
|
||||
const int numberOfMorphDistractors = 3;
|
||||
|
||||
class MessageMorphInputBarContent extends StatefulWidget {
|
||||
final MessageOverlayController overlayController;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
|
||||
const MessageMorphInputBarContent({
|
||||
super.key,
|
||||
required this.overlayController,
|
||||
required this.pangeaMessageEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
MessageMorphInputBarContentState createState() =>
|
||||
MessageMorphInputBarContentState();
|
||||
}
|
||||
|
||||
class MessageMorphInputBarContentState
|
||||
extends State<MessageMorphInputBarContent> {
|
||||
// bool initialized = false;
|
||||
|
||||
String? selectedTag;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
MessageOverlayController get overlay => widget.overlayController;
|
||||
PangeaToken? get token => overlay.selectedToken;
|
||||
MorphFeaturesEnum? get morph => overlay.selectedMorph;
|
||||
|
||||
// void init() async {
|
||||
// initialized = false;
|
||||
// setState(() {});
|
||||
|
||||
// if (token != null && morph != null) {
|
||||
// morphChoices = MorphsRepo.cached.getDisplayTags(morph);
|
||||
// }
|
||||
// initialized = true;
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessageMorphInputBarContent oldWidget) {
|
||||
if (morph != oldWidget.overlayController.selectedMorph ||
|
||||
token != oldWidget.overlayController.selectedToken) {
|
||||
selectedTag = null;
|
||||
setState(() {});
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
List<String>? get choices {
|
||||
if (morph == null ||
|
||||
token == null ||
|
||||
overlay.messageAnalyticsEntry
|
||||
?.hasActivity(ActivityTypeEnum.morphId, token!, morph) ==
|
||||
false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final tag = token!.getMorphTag(morph!.name)!;
|
||||
|
||||
return MorphsRepo.cached
|
||||
.getDisplayTags(morph!.name)
|
||||
.where((other) => other.toLowerCase() != tag.toLowerCase())
|
||||
.toList()
|
||||
.take(numberOfMorphDistractors)
|
||||
.toList() +
|
||||
[tag];
|
||||
}
|
||||
|
||||
bool isCorrect(String tag) => token?.getMorphTag(morph!.name) == tag;
|
||||
|
||||
Future<void> onActivityChoice(
|
||||
String choice,
|
||||
) async {
|
||||
if (token == null || morph == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "Token or morph is null in onTokenSelectionWithSelectedMorphs",
|
||||
data: overlay.selectedToken?.toJson() ?? {},
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedTag = choice;
|
||||
|
||||
MatrixState.pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: overlay.pangeaMessageEvent!.eventId,
|
||||
roomId: overlay.pangeaMessageEvent!.room.id,
|
||||
constructs: [
|
||||
OneConstructUse(
|
||||
useType: isCorrect(choice)
|
||||
? ConstructUseTypeEnum.corM
|
||||
: ConstructUseTypeEnum.incM,
|
||||
lemma: choice,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: overlay.pangeaMessageEvent!.room.id,
|
||||
timeStamp: DateTime.now(),
|
||||
eventId: overlay.pangeaMessageEvent!.eventId,
|
||||
),
|
||||
category: morph!.name,
|
||||
form: token!.text.content,
|
||||
),
|
||||
],
|
||||
origin: AnalyticsUpdateOrigin.wordZoom,
|
||||
),
|
||||
);
|
||||
|
||||
setState(() => {});
|
||||
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: choiceArrayAnimationDuration),
|
||||
);
|
||||
|
||||
// this removes just one of the options
|
||||
// important because sometimes meanings are the same for different words
|
||||
if (isCorrect(choice)) {
|
||||
overlay.messageAnalyticsEntry
|
||||
?.onActivityComplete(ActivityTypeEnum.morphId, token);
|
||||
}
|
||||
|
||||
// kind of an odd way to do this, but should work
|
||||
overlay.setState(() {});
|
||||
// setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (choices != null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
MorphIcon(
|
||||
morphFeature: morph!,
|
||||
morphTag: null,
|
||||
size: const Size(30, 30),
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).whatIsTheMorphTag(
|
||||
morph!.getDisplayCopy(context),
|
||||
token!.text.content,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
spacing: 8.0, // Adjust spacing between items
|
||||
runSpacing: 8.0, // Adjust spacing between rows
|
||||
children: choices!
|
||||
.mapIndexed(
|
||||
(index, choice) => MessageMorphChoiceItem(
|
||||
cId: ConstructIdentifier(
|
||||
lemma: choice,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: morph!.name,
|
||||
),
|
||||
onTap: () => onActivityChoice(choice),
|
||||
isSelected: selectedTag == choice,
|
||||
isGold: selectedTag != null ? isCorrect(choice) : null,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (token != null && morph != null) {
|
||||
return MorphFocusWidget(
|
||||
token: token!,
|
||||
morphFeature: morph!.name,
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
overlayController: overlay,
|
||||
onEditDone: () => overlay.setState(() {}),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text("Select a grammar icon for activities and details."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageMorphChoiceItem extends StatefulWidget {
|
||||
const MessageMorphChoiceItem({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.isSelected,
|
||||
required this.isGold,
|
||||
required this.cId,
|
||||
});
|
||||
|
||||
final ConstructIdentifier cId;
|
||||
final void Function() onTap;
|
||||
final bool isSelected;
|
||||
final bool? isGold;
|
||||
|
||||
@override
|
||||
MessageMorphChoiceItemState createState() => MessageMorphChoiceItemState();
|
||||
}
|
||||
|
||||
class MessageMorphChoiceItemState extends State<MessageMorphChoiceItem> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessageMorphChoiceItem oldWidget) {
|
||||
if (oldWidget.isSelected != widget.isSelected ||
|
||||
oldWidget.isGold != widget.isGold) {
|
||||
setState(() {});
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Future<void> onTap() async {
|
||||
widget.onTap();
|
||||
}
|
||||
|
||||
Color get _color {
|
||||
if (widget.isSelected && widget.isGold != null) {
|
||||
return widget.isGold!
|
||||
? AppConfig.success.withAlpha((0.4 * 255).toInt())
|
||||
: AppConfig.warning.withAlpha((0.4 * 255).toInt());
|
||||
}
|
||||
if (widget.isSelected) {
|
||||
return AppConfig.primaryColor.withAlpha((0.4 * 255).toInt());
|
||||
}
|
||||
return _isHovered
|
||||
? AppConfig.primaryColor.withAlpha((0.2 * 255).toInt())
|
||||
: Colors.transparent;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onHover: (isHovered) => setState(() => _isHovered = isHovered),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
onTap: onTap,
|
||||
child: IntrinsicWidth(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: _color,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: MorphIcon(
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(
|
||||
widget.cId.category,
|
||||
),
|
||||
morphTag: widget.cId.lemma,
|
||||
size: const Size(40, 40),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
getGrammarCopy(
|
||||
category: widget.cId.category,
|
||||
lemma: widget.cId.lemma,
|
||||
context: context,
|
||||
) ??
|
||||
widget.cId.lemma,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/word_emoji_choice.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_match_activity.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart';
|
||||
import 'message_emoji_choice.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ReadingAssistanceInputBar extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
|
@ -55,93 +55,42 @@ class ReadingAssistanceInputBar extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget barContent(BuildContext context) {
|
||||
if (token == null ||
|
||||
!(overlayController.pangeaMessageEvent?.messageDisplayLangIsL2 ??
|
||||
false)) {
|
||||
return MessageEmojiChoice(
|
||||
tokens: overlayController
|
||||
.pangeaMessageEvent?.messageDisplayRepresentation?.tokens ??
|
||||
[],
|
||||
controller: controller,
|
||||
overlayController: overlayController,
|
||||
);
|
||||
}
|
||||
|
||||
switch (overlayController.toolbarMode) {
|
||||
// message meaning will not use the input bar (for now at least)
|
||||
// maybe we move some choices there later
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.messageTranslation:
|
||||
case MessageMode.messageTextToSpeech:
|
||||
case MessageMode.messageSpeechToText:
|
||||
case MessageMode.practiceActivity:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.noneSelected:
|
||||
return MessageEmojiChoice(
|
||||
tokens: overlayController
|
||||
.pangeaMessageEvent?.messageDisplayRepresentation?.tokens ??
|
||||
[],
|
||||
controller: controller,
|
||||
//TODO: show all emojis for the lemmas and allow sending normal reactions
|
||||
return const SizedBox.shrink();
|
||||
// return MessageEmojiChoice(
|
||||
// controller: controller,
|
||||
// overlayController: overlayController,
|
||||
// );
|
||||
|
||||
case MessageMode.messageTranslation:
|
||||
if (overlayController.isTranslationUnlocked) {
|
||||
return MessageTranslationCard(
|
||||
messageEvent: overlayController.pangeaMessageEvent!,
|
||||
);
|
||||
} else {
|
||||
return MessageModeLockedCard(controller: overlayController);
|
||||
}
|
||||
|
||||
case MessageMode.wordEmoji:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.wordMeaning:
|
||||
case MessageMode.listening:
|
||||
return MessageMatchActivity(
|
||||
overlayController: overlayController,
|
||||
);
|
||||
|
||||
case MessageMode.wordEmoji:
|
||||
return WordEmojiChoice(
|
||||
form: overlayController.selectedToken!.text.content,
|
||||
onEmojiChosen: () =>
|
||||
overlayController.onActivityFinish(ActivityTypeEnum.emoji),
|
||||
constructID: overlayController.selectedToken!.vocabConstructID,
|
||||
);
|
||||
|
||||
case MessageMode.wordMeaning:
|
||||
return getPracticeActivityCard(ActivityTypeEnum.wordMeaning);
|
||||
|
||||
case MessageMode.wordMorph:
|
||||
if (overlayController.selectedMorphFeature != null) {
|
||||
if (!token!.shouldDoActivity(
|
||||
a: ActivityTypeEnum.morphId,
|
||||
feature: overlayController.selectedMorphFeature,
|
||||
tag: token!.getMorphTag(overlayController.selectedMorphFeature!),
|
||||
)) {
|
||||
return MorphFocusWidget(
|
||||
token: token!,
|
||||
morphFeature: overlayController.selectedMorphFeature!,
|
||||
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
|
||||
overlayController: overlayController,
|
||||
onEditDone: () => overlayController.initializeTokensAndMode(),
|
||||
);
|
||||
} else {
|
||||
return getPracticeActivityCard(
|
||||
ActivityTypeEnum.morphId,
|
||||
overlayController.selectedMorphFeature,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
/// we're not supposed to be here actually
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "selectedMorphFeature is null in wordMorph mode",
|
||||
s: StackTrace.current,
|
||||
data: token?.toJson() ?? {},
|
||||
);
|
||||
final String? nextFeature = overlayController
|
||||
.selectedToken?.nextMorphFeatureEligibleForActivity;
|
||||
if (nextFeature != null) {
|
||||
return getPracticeActivityCard(
|
||||
ActivityTypeEnum.morphId,
|
||||
nextFeature,
|
||||
);
|
||||
} else {
|
||||
// morph center widget with feature = "pos"
|
||||
return MorphFocusWidget(
|
||||
token: token!,
|
||||
morphFeature: "pos",
|
||||
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
|
||||
overlayController: overlayController,
|
||||
onEditDone: () => overlayController.initializeTokensAndMode(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return MessageMorphInputBarContent(
|
||||
overlayController: overlayController,
|
||||
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,9 +107,10 @@ class ReadingAssistanceInputBar extends StatelessWidget {
|
|||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Flexible(
|
||||
return Expanded(
|
||||
child: Container(
|
||||
height: AppConfig.readingAssistanceInputBarHeight,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.all(
|
||||
|
|
@ -168,7 +118,9 @@ class ReadingAssistanceInputBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: barContent(context),
|
||||
child: SingleChildScrollView(
|
||||
child: barContent(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
43
lib/pangea/toolbar/widgets/measure_render_box.dart
Normal file
43
lib/pangea/toolbar/widgets/measure_render_box.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class MeasureRenderBox extends StatefulWidget {
|
||||
final Widget child;
|
||||
final ValueChanged<RenderBox>? onChange;
|
||||
|
||||
const MeasureRenderBox({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onChange,
|
||||
});
|
||||
|
||||
@override
|
||||
MeasureRenderBoxState createState() => MeasureRenderBoxState();
|
||||
}
|
||||
|
||||
class MeasureRenderBoxState extends State<MeasureRenderBox> {
|
||||
Offset? _lastOffset;
|
||||
Size? _lastSize;
|
||||
|
||||
void _updateOffset() {
|
||||
if (widget.onChange == null) return;
|
||||
final renderBox = context.findRenderObject() as RenderBox?;
|
||||
if (renderBox != null) {
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
if (_lastOffset == null ||
|
||||
_lastOffset != offset ||
|
||||
_lastSize == null ||
|
||||
_lastSize != renderBox.size) {
|
||||
_lastOffset = offset;
|
||||
_lastSize = renderBox.size;
|
||||
widget.onChange!(renderBox);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOffset());
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -12,17 +11,13 @@ import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.da
|
|||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
|
||||
|
||||
class MessageAudioCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final PangeaTokenText? selection;
|
||||
final TtsController tts;
|
||||
final Function(bool) setIsPlayingAudio;
|
||||
final VoidCallback? onError;
|
||||
|
||||
|
|
@ -30,9 +25,7 @@ class MessageAudioCard extends StatefulWidget {
|
|||
super.key,
|
||||
required this.messageEvent,
|
||||
required this.overlayController,
|
||||
required this.tts,
|
||||
required this.setIsPlayingAudio,
|
||||
this.selection,
|
||||
this.onError,
|
||||
});
|
||||
|
||||
|
|
@ -44,92 +37,10 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
bool _isLoading = false;
|
||||
PangeaAudioFile? audioFile;
|
||||
|
||||
int? sectionStartMS;
|
||||
int? sectionEndMS;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetchAudio();
|
||||
|
||||
// initializeTTS();
|
||||
}
|
||||
|
||||
// initializeTTS() async {
|
||||
// tts.setupTTS().then((value) => setState(() {}));
|
||||
// }
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
// if (oldWidget.selection != widget.selection && widget.selection != null) {
|
||||
// debugPrint('selection changed');
|
||||
// setSectionStartAndEndFromSelection();
|
||||
// playSelectionAudio();
|
||||
// }
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void setSectionStartAndEnd(int? start, int? end) => mounted
|
||||
? setState(() {
|
||||
sectionStartMS = start;
|
||||
sectionEndMS = end;
|
||||
})
|
||||
: null;
|
||||
|
||||
void setSectionStartAndEndFromSelection() async {
|
||||
if (audioFile == null) {
|
||||
// should never happen but just in case
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFile!.duration == null) {
|
||||
// should never happen but just in case
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: 'audioFile duration is null in MessageAudioCardState',
|
||||
data: {
|
||||
'audioFile': audioFile,
|
||||
},
|
||||
);
|
||||
return setSectionStartAndEnd(null, null);
|
||||
}
|
||||
|
||||
// if there is no selection, we don't need to do anything
|
||||
// but clear the section start and end
|
||||
if (widget.selection == null) {
|
||||
return setSectionStartAndEnd(null, null);
|
||||
}
|
||||
|
||||
final PangeaTokenText selection = widget.selection!;
|
||||
final List<TTSToken> tokens = audioFile!.tokens;
|
||||
|
||||
// find the token that corresponds to the selection
|
||||
// set the start to the start of the token
|
||||
// set the end to the start of the next token or to the duration of the audio if
|
||||
// if there is no next token
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
final TTSToken ttsToken = tokens[i];
|
||||
if (ttsToken.text.offset == selection.offset) {
|
||||
return setSectionStartAndEnd(
|
||||
max(ttsToken.startMS - 150, 0),
|
||||
min(ttsToken.endMS + 150, audioFile!.duration!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't find the token, we should pause if debug and log an error
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: 'could not find token for selection in MessageAudioCardState',
|
||||
data: {
|
||||
'selection': selection,
|
||||
'tokens': tokens,
|
||||
'sttTokens': audioFile!.tokens,
|
||||
},
|
||||
);
|
||||
|
||||
setSectionStartAndEnd(null, null);
|
||||
}
|
||||
|
||||
Future<void> fetchAudio() async {
|
||||
|
|
@ -151,7 +62,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
);
|
||||
}
|
||||
debugPrint("audio file is now: $audioFile. setting starts and ends...");
|
||||
setSectionStartAndEndFromSelection();
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
} catch (e, s) {
|
||||
widget.onError?.call();
|
||||
|
|
@ -182,8 +92,8 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
? AudioPlayerWidget(
|
||||
null,
|
||||
matrixFile: audioFile,
|
||||
sectionStartMS: sectionStartMS,
|
||||
sectionEndMS: sectionEndMS,
|
||||
sectionStartMS: null,
|
||||
sectionEndMS: null,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
setIsPlayingAudio: widget.setIsPlayingAudio,
|
||||
fontSize:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageModeLockedCard extends StatelessWidget {
|
||||
final MessageOverlayController controller;
|
||||
|
|
@ -12,32 +10,24 @@ class MessageModeLockedCard extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
if (!InstructionsEnum.completeActivitiesToUnlock.isToggledOff) ...[
|
||||
const SizedBox(height: 8),
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.completeActivitiesToUnlock,
|
||||
),
|
||||
],
|
||||
],
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
if (!InstructionsEnum.completeActivitiesToUnlock.isToggledOff) ...[
|
||||
const SizedBox(height: 8),
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.completeActivitiesToUnlock,
|
||||
bold: true,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,37 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/match_feedback_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Controls data at the top level of the toolbar (mainly token / toolbar mode selection)
|
||||
class MessageSelectionOverlay extends StatefulWidget {
|
||||
|
|
@ -52,54 +62,51 @@ class MessageSelectionOverlay extends StatefulWidget {
|
|||
|
||||
class MessageOverlayController extends State<MessageSelectionOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Event get event => widget._event;
|
||||
/////////////////////////////////////
|
||||
/// Variables
|
||||
/////////////////////////////////////
|
||||
MessageMode toolbarMode = MessageMode.noneSelected;
|
||||
|
||||
/// If doing a morphological activity, this is the selected morph feature.
|
||||
String? selectedMorphFeature;
|
||||
Map<ConstructIdentifier, LemmaInfoResponse>? messageLemmaInfos;
|
||||
|
||||
MorphFeaturesEnum? selectedMorph;
|
||||
ConstructForm? selectedChoice;
|
||||
PangeaTokenText? _selectedSpan;
|
||||
|
||||
List<PangeaTokenText>? _highlightedTokens;
|
||||
bool initialized = false;
|
||||
bool isPlayingAudio = false;
|
||||
|
||||
/// The text that the toolbar should target
|
||||
/// If there is no selectedSpan, then the whole message is the target
|
||||
/// If there is a selectedSpan, then the target is the selected text
|
||||
String get targetText {
|
||||
if (_selectedSpan == null || pangeaMessageEvent == null) {
|
||||
return pangeaMessageEvent?.messageDisplayText ?? widget._event.body;
|
||||
}
|
||||
/// temporary stores of which matches have been made and
|
||||
/// whether correct or not. allows user to quickly match
|
||||
/// choices while we're still displaying feedback for previous
|
||||
/// choices
|
||||
final List<MatchFeedback> feedbackStates = [];
|
||||
|
||||
return pangeaMessageEvent!.messageDisplayText.substring(
|
||||
_selectedSpan!.offset,
|
||||
_selectedSpan!.offset + _selectedSpan!.length,
|
||||
);
|
||||
}
|
||||
final GlobalKey<ReadingAssistanceContentState> wordZoomKey = GlobalKey();
|
||||
|
||||
PangeaToken? get selectedToken =>
|
||||
pangeaMessageEvent?.messageDisplayRepresentation?.tokens
|
||||
?.firstWhereOrNull(isTokenSelected);
|
||||
|
||||
/// Whether the overlay is currently displaying a selection
|
||||
bool get isSelection => _selectedSpan != null || _highlightedTokens != null;
|
||||
|
||||
PangeaTokenText? get selectedSpan => _selectedSpan;
|
||||
// either a MorphFeatureEnum or a PartOfSpeechEnum
|
||||
// String? modeLevel;
|
||||
|
||||
/////////////////////////////////////
|
||||
/// Lifecycle
|
||||
/////////////////////////////////////
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializeTokensAndMode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> initializeTokensAndMode() async {
|
||||
try {
|
||||
debugPrint("what");
|
||||
RepresentationEvent? repEvent =
|
||||
pangeaMessageEvent?.messageDisplayRepresentation;
|
||||
repEvent ??= await _fetchNewRepEvent();
|
||||
|
|
@ -130,7 +137,24 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
choreo: pangeaMessageEvent!.originalSent?.choreo,
|
||||
);
|
||||
}
|
||||
|
||||
// Get all the lemma infos
|
||||
final messageVocabConstructIds = pangeaMessageEvent!
|
||||
.messageDisplayRepresentation!.tokensToSave
|
||||
.map((e) => e.vocabConstructID)
|
||||
.toList();
|
||||
final List<Future<LemmaInfoResponse>> lemmaInfoFutures =
|
||||
messageVocabConstructIds
|
||||
.map((token) => token.getLemmaInfo())
|
||||
.toList();
|
||||
final List<LemmaInfoResponse> lemmaInfos =
|
||||
await Future.wait(lemmaInfoFutures);
|
||||
messageLemmaInfos = Map.fromIterables(
|
||||
messageVocabConstructIds,
|
||||
lemmaInfos,
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
|
|
@ -148,21 +172,20 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
Future<void> _setInitialToolbarMode() async {
|
||||
if (pangeaMessageEvent?.isAudioMessage ?? false) {
|
||||
updateToolbarMode(MessageMode.messageSpeechToText);
|
||||
updateToolbarMode(MessageMode.listening);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) if we have a hidden word activity, then we should start with that
|
||||
if (messageAnalyticsEntry?.nextActivity?.activityType ==
|
||||
ActivityTypeEnum.hiddenWordListening) {
|
||||
if (messageAnalyticsEntry?.hasHiddenWordActivity ?? false) {
|
||||
updateToolbarMode(MessageMode.practiceActivity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedToken != null) {
|
||||
updateToolbarMode(selectedToken!.modeForToken);
|
||||
return;
|
||||
}
|
||||
// if (selectedToken != null) {
|
||||
// updateToolbarMode(selectedToken!.modeForToken);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Note: this setting is now hidden so this will always be false
|
||||
// leaving this here in case we want to bring it back
|
||||
|
|
@ -183,18 +206,17 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
// should not already be involved in a hidden word activity
|
||||
final isInHiddenWordActivity =
|
||||
messageAnalyticsEntry!.isTokenInHiddenWordActivity(
|
||||
widget._initialSelectedToken!,
|
||||
);
|
||||
// final isInHiddenWordActivity =
|
||||
// messageAnalyticsEntry!.isTokenInHiddenWordActivity(
|
||||
// widget._initialSelectedToken!,
|
||||
// );
|
||||
|
||||
// whether the activity should generally be involved in an activity
|
||||
final selected =
|
||||
!isInHiddenWordActivity ? widget._initialSelectedToken : null;
|
||||
|
||||
if (selected != null) {
|
||||
_updateSelectedSpan(selected.text);
|
||||
// // whether the activity should generally be involved in an activity
|
||||
if (messageAnalyticsEntry?.hasHiddenWordActivity == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateSelectedSpan(widget._initialSelectedToken!.text);
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
|
|
@ -206,12 +228,21 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
/// This is a workaround to prevent that error
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
// if (pangeaMessageEvent != null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// modeLevel = toolbarMode.currentChoiceMode(this, pangeaMessageEvent!);
|
||||
// } else {
|
||||
// debugger(when: kDebugMode);
|
||||
// }
|
||||
|
||||
final phase = SchedulerBinding.instance.schedulerPhase;
|
||||
if (mounted &&
|
||||
(phase == SchedulerPhase.idle ||
|
||||
phase == SchedulerPhase.postFrameCallbacks)) {
|
||||
// It's safe to call setState immediately
|
||||
try {
|
||||
wordZoomKey.currentState?.setState(() {});
|
||||
|
||||
super.setState(fn);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -236,114 +267,121 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
}
|
||||
}
|
||||
|
||||
/// Update to [selectedSpan]
|
||||
/// [forceMode] is used to force a specific mode
|
||||
void _updateSelectedSpan(
|
||||
PangeaTokenText selectedSpan, [
|
||||
MessageMode? forceMode,
|
||||
]) {
|
||||
if (forceMode == null && selectedSpan == _selectedSpan) {
|
||||
_selectedSpan = null;
|
||||
updateToolbarMode(MessageMode.noneSelected);
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSpan = selectedSpan;
|
||||
|
||||
if (!(messageAnalyticsEntry?.hasHiddenWordActivity ?? false)) {
|
||||
widget.chatController.choreographer.tts.tryToSpeak(
|
||||
selectedSpan.content,
|
||||
context,
|
||||
targetID: null,
|
||||
/// Update [selectedSpan]
|
||||
void _updateSelectedSpan(PangeaTokenText selectedSpan, [bool force = false]) {
|
||||
// close overlay of previous token
|
||||
if (selectedToken != null) {
|
||||
MatrixState.pAnyState.disposeByWidgetKey(
|
||||
selectedToken!.text.uniqueKey,
|
||||
);
|
||||
}
|
||||
|
||||
final nextModeForToken = forceMode ?? selectedToken!.modeForToken;
|
||||
if (toolbarMode != nextModeForToken) {
|
||||
debugPrint("_updateSelectedSpan: setting toolbarMode to wordZoom");
|
||||
updateToolbarMode(nextModeForToken);
|
||||
if (selectedSpan == _selectedSpan && !force) {
|
||||
_selectedSpan = null;
|
||||
} else {
|
||||
_selectedSpan = selectedSpan;
|
||||
}
|
||||
|
||||
if (selectedToken != null) {
|
||||
final entry = ReadingAssistanceContent(
|
||||
key: wordZoomKey,
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
overlayController: this,
|
||||
);
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
cardToShow: entry,
|
||||
transformTargetId: selectedToken!.text.uniqueKey,
|
||||
closePrevOverlay: false,
|
||||
backDropToDismiss: false,
|
||||
addBorder: false,
|
||||
overlayKey: selectedToken!.text.uniqueKey,
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void updateToolbarMode(MessageMode mode, [String? feature]) => setState(() {
|
||||
debugger(
|
||||
when: kDebugMode &&
|
||||
selectedToken == null &&
|
||||
[
|
||||
MessageMode.wordMorph,
|
||||
MessageMode.wordZoom,
|
||||
MessageMode.wordEmoji,
|
||||
MessageMode.wordZoom,
|
||||
].contains(mode),
|
||||
);
|
||||
|
||||
if (toolbarMode == mode) {
|
||||
if (selectedToken == null) {
|
||||
toolbarMode = MessageMode.noneSelected;
|
||||
selectedMorphFeature = null;
|
||||
} else if (mode != MessageMode.wordMorph) {
|
||||
debugPrint('toolbarMode == mode: setting toolbarMode to wordZoom');
|
||||
toolbarMode = MessageMode.wordZoom;
|
||||
} else {
|
||||
if (selectedMorphFeature != feature) {
|
||||
selectedMorphFeature = feature;
|
||||
} else {
|
||||
selectedMorphFeature = null;
|
||||
toolbarMode = MessageMode.wordZoom;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (mode) {
|
||||
case MessageMode.noneSelected:
|
||||
selectedMorphFeature = null;
|
||||
break;
|
||||
case MessageMode.wordMorph:
|
||||
selectedMorphFeature =
|
||||
feature ?? selectedToken?.nextMorphFeatureEligibleForActivity;
|
||||
if (selectedMorphFeature == null) {
|
||||
updateToolbarMode(MessageMode.wordZoom);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case MessageMode.wordZoom:
|
||||
selectedMorphFeature = null;
|
||||
break;
|
||||
case MessageMode.wordEmoji:
|
||||
selectedMorphFeature = null;
|
||||
break;
|
||||
case MessageMode.wordMeaning:
|
||||
selectedMorphFeature = null;
|
||||
break;
|
||||
case MessageMode.practiceActivity:
|
||||
break;
|
||||
case MessageMode.messageTextToSpeech:
|
||||
if (isPlayingAudio) {
|
||||
//TODO stop audio
|
||||
} else {
|
||||
// Play audio
|
||||
}
|
||||
break;
|
||||
case MessageMode.messageSpeechToText:
|
||||
_selectedSpan = null;
|
||||
break;
|
||||
case MessageMode.messageTranslation:
|
||||
_selectedSpan = null;
|
||||
break;
|
||||
case MessageMode.messageMeaning:
|
||||
_selectedSpan = null;
|
||||
break;
|
||||
}
|
||||
|
||||
toolbarMode = mode;
|
||||
if (mode != MessageMode.messageTextToSpeech) {
|
||||
_highlightedTokens = null;
|
||||
}
|
||||
}
|
||||
void updateToolbarMode(MessageMode mode) => setState(() {
|
||||
selectedChoice = null;
|
||||
toolbarMode = mode;
|
||||
});
|
||||
|
||||
void onChoiceSelect(ConstructForm choice, [bool force = false]) {
|
||||
if (selectedChoice == choice && !force) {
|
||||
selectedChoice = null;
|
||||
} else {
|
||||
selectedChoice = choice;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void onMorphActivitySelect(PangeaToken token, MorphFeaturesEnum morph) {
|
||||
if (toolbarMode != MessageMode.wordMorph) {
|
||||
updateToolbarMode(MessageMode.wordMorph);
|
||||
}
|
||||
selectedMorph = morph;
|
||||
_updateSelectedSpan(token.text, true);
|
||||
}
|
||||
|
||||
// only used for word meaning, emoji, and word focus listening atm
|
||||
Future<void> onMatchAttempt(
|
||||
PangeaToken token,
|
||||
ConstructForm choice,
|
||||
) async {
|
||||
final ActivityTypeEnum activityType = toolbarMode.associatedActivityType!;
|
||||
|
||||
final bool isCorrect = token.vocabConstructID == choice.cId;
|
||||
|
||||
final ConstructUseTypeEnum? useType =
|
||||
isCorrect ? activityType.correctUse : activityType.incorrectUse;
|
||||
|
||||
if (useType != null) {
|
||||
MatrixState.pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: pangeaMessageEvent!.eventId,
|
||||
roomId: pangeaMessageEvent!.room.id,
|
||||
constructs: [
|
||||
OneConstructUse(
|
||||
useType: useType,
|
||||
lemma: token.vocabConstructID.lemma,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: pangeaMessageEvent!.room.id,
|
||||
timeStamp: DateTime.now(),
|
||||
eventId: pangeaMessageEvent!.eventId,
|
||||
),
|
||||
category: token.vocabConstructID.category,
|
||||
form: token.text.content,
|
||||
),
|
||||
],
|
||||
origin: AnalyticsUpdateOrigin.wordZoom,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
feedbackStates.add(MatchFeedback(form: choice, isCorrect: isCorrect));
|
||||
|
||||
selectedChoice = null;
|
||||
|
||||
setState(() => {});
|
||||
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 2000),
|
||||
);
|
||||
|
||||
if (isCorrect) {
|
||||
messageAnalyticsEntry?.onActivityComplete(activityType, token);
|
||||
}
|
||||
|
||||
feedbackStates.removeWhere((e) => e.form == choice);
|
||||
|
||||
setState(() => {});
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
/// Getters
|
||||
////////////////////////////////////
|
||||
|
|
@ -363,9 +401,43 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
(pangeaMessageEvent?.proportionOfActivitiesCompleted ?? 1) >= 1 ||
|
||||
!messageInUserL2;
|
||||
|
||||
// bool get isEmojiDone =>
|
||||
// pangeaMessageEvent?.messageDisplayRepresentation?.tokensToSave
|
||||
// .every((token) => token.vocabConstructID.userSetEmoji.isNotEmpty) ??
|
||||
// false;
|
||||
|
||||
bool get isEmojiDone =>
|
||||
messageAnalyticsEntry?.activities(ActivityTypeEnum.emoji).isEmpty == true;
|
||||
|
||||
bool get isMeaningDone =>
|
||||
messageAnalyticsEntry?.activities(ActivityTypeEnum.wordMeaning).isEmpty ==
|
||||
true;
|
||||
|
||||
bool get isListeningDone =>
|
||||
messageAnalyticsEntry
|
||||
?.activities(ActivityTypeEnum.wordFocusListening)
|
||||
.isEmpty ==
|
||||
true;
|
||||
|
||||
bool get isMorphDone =>
|
||||
messageAnalyticsEntry?.activities(ActivityTypeEnum.morphId).isEmpty ==
|
||||
true;
|
||||
|
||||
/// you have to complete one of the mode mini-games to unlock translation
|
||||
bool get isTranslationUnlocked =>
|
||||
!messageInUserL2 ||
|
||||
(messageLemmaInfos?.isEmpty ?? false) ||
|
||||
isEmojiDone ||
|
||||
isMeaningDone ||
|
||||
isListeningDone ||
|
||||
isMorphDone;
|
||||
|
||||
bool get isTotallyDone =>
|
||||
isEmojiDone && isMeaningDone && isListeningDone && isMorphDone;
|
||||
|
||||
MessageAnalyticsEntry? get messageAnalyticsEntry =>
|
||||
pangeaMessageEvent?.messageDisplayRepresentation?.tokens != null
|
||||
? MatrixState.pangeaController.getAnalytics.perMessage.get(
|
||||
? MessageAnalyticsController.get(
|
||||
pangeaMessageEvent!.messageDisplayRepresentation!.tokens!,
|
||||
pangeaMessageEvent!,
|
||||
)
|
||||
|
|
@ -375,6 +447,15 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
pangeaMessageEvent?.messageDisplayLangCode ==
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
PangeaToken? get selectedToken =>
|
||||
pangeaMessageEvent?.messageDisplayRepresentation?.tokens
|
||||
?.firstWhereOrNull(isTokenSelected);
|
||||
|
||||
/// Whether the overlay is currently displaying a selection
|
||||
bool get isSelection => _selectedSpan != null || _highlightedTokens != null;
|
||||
|
||||
PangeaTokenText? get selectedSpan => _selectedSpan;
|
||||
|
||||
///////////////////////////////////
|
||||
/// User action handlers
|
||||
/////////////////////////////////////
|
||||
|
|
@ -394,27 +475,27 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
}
|
||||
}
|
||||
|
||||
void onNextActivityRequest() {
|
||||
if (pangeaMessageEvent?.messageDisplayRepresentation?.tokens == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: "Tokens are null in onNextActivityRequest",
|
||||
data: {},
|
||||
);
|
||||
return;
|
||||
}
|
||||
// // void onNextActivityRequest() {
|
||||
// // if (pangeaMessageEvent?.messageDisplayRepresentation?.tokens == null) {
|
||||
// // debugger(when: kDebugMode);
|
||||
// // ErrorHandler.logError(
|
||||
// // e: "Tokens are null in onNextActivityRequest",
|
||||
// // data: {},
|
||||
// // );
|
||||
// // return;
|
||||
// // }
|
||||
|
||||
for (final token in pangeaMessageEvent!
|
||||
.messageDisplayRepresentation!.tokens!
|
||||
.where((t) => t.lemma.saveVocab)) {
|
||||
final MessageMode nextActivityMode = token.modeForToken;
|
||||
if (nextActivityMode != MessageMode.wordZoom) {
|
||||
_selectedSpan = token.text;
|
||||
_updateSelectedSpan(token.text, nextActivityMode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// // for (final token in pangeaMessageEvent!
|
||||
// // .messageDisplayRepresentation!.tokens!
|
||||
// // .where((t) => t.lemma.saveVocab)) {
|
||||
// // final MessageMode nextActivityMode = token.modeForToken;
|
||||
// // if (nextActivityMode != MessageMode.wordZoom) {
|
||||
// // _selectedSpan = token.text;
|
||||
// // _updateSelectedSpan(token.text);
|
||||
// // return;
|
||||
// // }
|
||||
// // }
|
||||
// // }
|
||||
|
||||
///////////////////////////////////
|
||||
/// Functions
|
||||
|
|
@ -460,14 +541,14 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
/// When an activity is completed, we need to update the state
|
||||
/// and check if the toolbar should be unlocked
|
||||
void onActivityFinish(ActivityTypeEnum activityType) {
|
||||
messageAnalyticsEntry!.onActivityComplete();
|
||||
void onActivityFinish(ActivityTypeEnum activityType, PangeaToken? token) {
|
||||
messageAnalyticsEntry!.onActivityComplete(activityType, null);
|
||||
|
||||
if (selectedToken == null) {
|
||||
updateToolbarMode(MessageMode.noneSelected);
|
||||
}
|
||||
|
||||
updateToolbarMode(selectedToken!.modeForToken);
|
||||
// updateToolbarMode(selectedToken!.modeForToken);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
|
|
@ -483,14 +564,44 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
void onClickOverlayMessageToken(
|
||||
PangeaToken token,
|
||||
) {
|
||||
if (toolbarMode == MessageMode.practiceActivity &&
|
||||
messageAnalyticsEntry?.nextActivity?.activityType ==
|
||||
ActivityTypeEnum.hiddenWordListening) {
|
||||
if (messageAnalyticsEntry?.hasHiddenWordActivity == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
/// we don't want to associate the audio with the text in this mode
|
||||
if (messageAnalyticsEntry?.hasActivity(
|
||||
ActivityTypeEnum.wordFocusListening,
|
||||
token,
|
||||
) ==
|
||||
false) {
|
||||
widget.chatController.choreographer.tts.tryToSpeak(
|
||||
token.text.content,
|
||||
context,
|
||||
targetID: null,
|
||||
);
|
||||
}
|
||||
|
||||
_updateSelectedSpan(token.text);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void onDragTargetClick(PangeaToken token) {
|
||||
if (toolbarMode.associatedActivityType == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// supporting highlighting of the target then a click on an option is possible
|
||||
// however, a user might want to click an option to hear the audio then
|
||||
// accidentally submit. for this reason, holding off on supporting that flow
|
||||
// to see if it feels needed
|
||||
if (selectedChoice == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
onMatchAttempt(
|
||||
token,
|
||||
selectedChoice!,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether the given token is currently selected or highlighted
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button_and_progress_row.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button_column.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
|
@ -51,20 +53,34 @@ class MessageSelectionPositioner extends StatefulWidget {
|
|||
class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
Animation<double>? _overlayPositionAnimation;
|
||||
Animation<Offset>? _overlayOffsetAnimation;
|
||||
Animation<Offset>? _buttonsOffsetAnimation;
|
||||
Animation<Size>? _messageSizeAnimation;
|
||||
|
||||
StreamSubscription? _reactionSubscription;
|
||||
|
||||
/// if the message height is too tall to fit with the tools, adjust the message height
|
||||
double? _adjustedMessageHeight;
|
||||
|
||||
Offset? _centeredMessageOffset;
|
||||
Size? _centeredMessageSize;
|
||||
final Completer _centeredMessageCompleter = Completer();
|
||||
|
||||
Offset? _centeredButtonsOffset;
|
||||
Size? _centeredButtonsSize;
|
||||
final Completer _centeredButtonsCompleter = Completer();
|
||||
|
||||
bool _finishedAnimation = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration:
|
||||
const Duration(milliseconds: AppConfig.overlayAnimationDuration),
|
||||
duration: const Duration(
|
||||
milliseconds: AppConfig.overlayAnimationDuration,
|
||||
// seconds: 5,
|
||||
),
|
||||
);
|
||||
|
||||
_reactionSubscription =
|
||||
|
|
@ -86,27 +102,72 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
);
|
||||
},
|
||||
).listen((_) => setState(() {}));
|
||||
|
||||
Future.wait([
|
||||
_centeredMessageCompleter.future,
|
||||
_centeredButtonsCompleter.future,
|
||||
]).then((_) => _startAnimation());
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MessageSelectionPositioner oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.overlayController.toolbarMode !=
|
||||
widget.overlayController.toolbarMode) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_reactionSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setCenteredMessageSize(RenderBox renderBox) {
|
||||
if (_finishedAnimation) return;
|
||||
_centeredMessageSize = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
_centeredMessageOffset = Offset(
|
||||
offset.dx - _columnWidth - _horizontalPadding - 2.0,
|
||||
_mediaQuery!.size.height -
|
||||
offset.dy -
|
||||
renderBox.size.height -
|
||||
_reactionsHeight,
|
||||
);
|
||||
setState(() {});
|
||||
|
||||
if (!_centeredMessageCompleter.isCompleted) {
|
||||
_centeredMessageCompleter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
void _setCenteredButtonsSize(RenderBox renderBox) {
|
||||
if (_finishedAnimation) return;
|
||||
_centeredButtonsSize = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
_centeredButtonsOffset = Offset(
|
||||
offset.dx - _columnWidth - _horizontalPadding - 2.0,
|
||||
offset.dy,
|
||||
);
|
||||
setState(() {});
|
||||
|
||||
if (!_centeredButtonsCompleter.isCompleted) {
|
||||
_centeredButtonsCompleter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
void _startAnimation() {
|
||||
if (_mediaQuery == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool hasHeaderOverflow =
|
||||
_remainingTopSpace < AppConfig.toolbarSpacing;
|
||||
final bool hasFooterOverflow =
|
||||
_remainingBottomSpace < AppConfig.toolbarSpacing;
|
||||
|
||||
if (!hasHeaderOverflow && !hasFooterOverflow || _mediaQuery == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double adjustedBottomOffset = _totalToolbarBottomOffset;
|
||||
double scrollOffset = 0;
|
||||
|
||||
// if the message height is too tall to fit, adjust the message height
|
||||
if (_totalVerticalSpace! < _maxTotalToolbarHeight) {
|
||||
_adjustedMessageHeight = _totalVerticalSpace! -
|
||||
|
|
@ -117,22 +178,13 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
_adjustedMessageHeight = max(_adjustedMessageHeight!, 0);
|
||||
}
|
||||
|
||||
// if the overlay could have header overflow if the message wasn't shifted, we want to shift
|
||||
// it down so the bottom to give it enough space.
|
||||
if (hasHeaderOverflow) {
|
||||
// what is the distance between the current top offset of the toolbar and the desired top offset?
|
||||
final double neededShift =
|
||||
(_headerHeight - _totalToolbarTopOffset) + AppConfig.toolbarSpacing;
|
||||
adjustedBottomOffset = _totalToolbarBottomOffset - neededShift;
|
||||
} else if (hasFooterOverflow) {
|
||||
adjustedBottomOffset = _footerHeight + AppConfig.toolbarSpacing;
|
||||
}
|
||||
|
||||
scrollOffset = adjustedBottomOffset - _totalToolbarBottomOffset;
|
||||
|
||||
_overlayPositionAnimation = Tween<double>(
|
||||
begin: _totalToolbarBottomOffset,
|
||||
end: adjustedBottomOffset,
|
||||
_overlayOffsetAnimation = Tween<Offset>(
|
||||
begin: Offset(
|
||||
_ownMessage ? _messageRightOffset : _messageLeftOffset,
|
||||
_totalToolbarBottomOffset,
|
||||
),
|
||||
// For own messages, dx is the right offset. For other's messages, dx is the left offset.
|
||||
end: _centeredMessageOffset,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
|
|
@ -140,20 +192,48 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
),
|
||||
);
|
||||
|
||||
widget.chatController.scrollController.animateTo(
|
||||
widget.chatController.scrollController.offset - scrollOffset,
|
||||
duration:
|
||||
const Duration(milliseconds: AppConfig.overlayAnimationDuration),
|
||||
curve: FluffyThemes.animationCurve,
|
||||
_buttonsOffsetAnimation = Tween<Offset>(
|
||||
begin: Offset(
|
||||
_ownMessage
|
||||
? (_centeredButtonsSize!.width * -1)
|
||||
: _centeredButtonsSize!.width + _mediaQuery!.size.width,
|
||||
_totalToolbarBottomOffset,
|
||||
),
|
||||
end: _centeredButtonsOffset,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
);
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_reactionSubscription?.cancel();
|
||||
super.dispose();
|
||||
_messageSizeAnimation = Tween<Size>(
|
||||
begin: Size(
|
||||
_messageSize.width,
|
||||
_messageHeight,
|
||||
),
|
||||
end: _centeredMessageSize,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
);
|
||||
|
||||
// _contentSizeAnimation = Tween<double>(
|
||||
// begin: 0,
|
||||
// end: 1,
|
||||
// ).animate(
|
||||
// CurvedAnimation(
|
||||
// parent: _animationController,
|
||||
// curve: FluffyThemes.animationCurve,
|
||||
// ),
|
||||
// );
|
||||
|
||||
_animationController.forward().then((_) {
|
||||
_finishedAnimation = true;
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
T _runWithLogging<T>(
|
||||
|
|
@ -175,7 +255,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
}
|
||||
}
|
||||
|
||||
bool get showDetails =>
|
||||
bool get _showDetails =>
|
||||
(Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ??
|
||||
false) &&
|
||||
FluffyThemes.isThreeColumnMode(context) &&
|
||||
|
|
@ -221,7 +301,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
//TODO: figure out where the 16 and 8 come from and use references instead of hard-coded values
|
||||
static const _messageDefaultLeftMargin = Avatar.defaultSize + 16 + 8;
|
||||
|
||||
double get _messageMaxWidth {
|
||||
double get _toolbarMaxWidth {
|
||||
final double messageMargin =
|
||||
widget.event.isActivityMessage ? 0 : Avatar.defaultSize + 16 + 8;
|
||||
final bool showingDetails = widget.chatController.displayChatDetailsColumn;
|
||||
|
|
@ -257,21 +337,16 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
);
|
||||
}
|
||||
|
||||
double get _messageTopOffset {
|
||||
return _messageOffset.dy -
|
||||
(_mediaQuery?.padding.top ?? 0) +
|
||||
(_mediaQuery?.viewPadding.top ?? 0);
|
||||
}
|
||||
|
||||
double get _messageBottomOffset =>
|
||||
_mediaQuery!.size.height - _messageOffset.dy - _messageHeight;
|
||||
|
||||
double get _messageLeftOffset =>
|
||||
_messageOffset.dx - _columnWidth - _horizontalPadding;
|
||||
double get _messageLeftOffset => max(
|
||||
_messageOffset.dx - _columnWidth - _horizontalPadding,
|
||||
0,
|
||||
);
|
||||
|
||||
double get _messageRightOffset {
|
||||
if (_mediaQuery == null ||
|
||||
widget.event.senderId != widget.event.room.client.userID) {
|
||||
if (_mediaQuery == null || !_ownMessage) {
|
||||
return 0;
|
||||
}
|
||||
return _mediaQuery!.size.width -
|
||||
|
|
@ -327,28 +402,13 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
AppConfig.toolbarSpacing +
|
||||
AppConfig.toolbarMaxHeight;
|
||||
|
||||
double get _maxTotalToolbarHeight {
|
||||
return max(AppConfig.toolbarButtonsColumnHeight, _maxTotalCenterHeight);
|
||||
}
|
||||
|
||||
double get _totalToolbarTopOffset {
|
||||
final topOffset = _messageTopOffset -
|
||||
(AppConfig.toolbarSpacing + AppConfig.toolbarMaxHeight);
|
||||
final addedColumnOffset =
|
||||
max(AppConfig.toolbarButtonsColumnHeight - _maxTotalCenterHeight, 0);
|
||||
return topOffset - addedColumnOffset;
|
||||
}
|
||||
double get _maxTotalToolbarHeight => _maxTotalCenterHeight;
|
||||
|
||||
double get _totalToolbarBottomOffset =>
|
||||
_messageBottomOffset - _reactionsHeight;
|
||||
|
||||
/// The remaining space between the top of the screen and the top of the toolbar.
|
||||
/// Negative if the toolbar is overflowing the top of the screen.
|
||||
double get _remainingTopSpace => _totalToolbarTopOffset - _headerHeight;
|
||||
|
||||
/// The remaining space between the bottom of the screen and the bottom of the toolbar.
|
||||
/// Negative if the toolbar is overflowing the bottom of the screen.
|
||||
double get _remainingBottomSpace => _totalToolbarBottomOffset - _footerHeight;
|
||||
bool get _ownMessage =>
|
||||
widget.event.senderId == widget.event.room.client.userID;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -362,58 +422,75 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
right: _horizontalPadding,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _overlayPositionAnimation ?? _animationController,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
// subtract width of
|
||||
left: widget.event.senderId != widget.event.room.client.userID
|
||||
? max(
|
||||
_messageLeftOffset -
|
||||
AppConfig.toolbarButtonsColumnWidth,
|
||||
0,
|
||||
)
|
||||
: null,
|
||||
right: widget.event.senderId == widget.event.room.client.userID
|
||||
? _messageRightOffset
|
||||
: _messageRightOffset,
|
||||
bottom: _overlayPositionAnimation?.value ??
|
||||
_totalToolbarBottomOffset,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
widget.event.senderId == widget.event.room.client.userID
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// fixed height and width container
|
||||
|
||||
ToolbarButtonAndProgressColumn(
|
||||
event: widget.event,
|
||||
overlayController: widget.overlayController,
|
||||
shouldShowToolbarButtons: showToolbarButtons,
|
||||
width: AppConfig.toolbarButtonsColumnWidth,
|
||||
height: AppConfig.toolbarButtonsColumnHeight,
|
||||
),
|
||||
OverlayCenterContent(
|
||||
messageHeight: _messageHeight,
|
||||
messageWidth: _messageSize.width,
|
||||
maxWidth: _messageMaxWidth,
|
||||
event: widget.event,
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
nextEvent: widget.nextEvent,
|
||||
prevEvent: widget.prevEvent,
|
||||
overlayController: widget.overlayController,
|
||||
chatController: widget.chatController,
|
||||
hasReactions: _hasReactions,
|
||||
shouldShowToolbarButtons: showToolbarButtons,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
Opacity(
|
||||
opacity: _finishedAnimation ? 1.0 : 0.0,
|
||||
child: OverlayCenterContent(
|
||||
event: widget.event,
|
||||
messageHeight: _messageHeight,
|
||||
messageWidth: _messageSize.width,
|
||||
toolbarMaxWidth: _toolbarMaxWidth,
|
||||
overlayController: widget.overlayController,
|
||||
chatController: widget.chatController,
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
nextEvent: widget.nextEvent,
|
||||
prevEvent: widget.prevEvent,
|
||||
showToolbarButtons: showToolbarButtons,
|
||||
hasReactions: _hasReactions,
|
||||
onChangeMessageSize: _setCenteredMessageSize,
|
||||
onChangeButtonsSize: _setCenteredButtonsSize,
|
||||
isTransitionAnimation: false,
|
||||
),
|
||||
),
|
||||
if (!_finishedAnimation)
|
||||
AnimatedBuilder(
|
||||
animation: _overlayOffsetAnimation ?? _animationController,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
left: _ownMessage
|
||||
? null
|
||||
: (_overlayOffsetAnimation?.value)?.dx ??
|
||||
_messageLeftOffset,
|
||||
right: _ownMessage
|
||||
? (_overlayOffsetAnimation?.value)?.dx ??
|
||||
_messageRightOffset
|
||||
: null,
|
||||
bottom: (_overlayOffsetAnimation?.value)?.dy ??
|
||||
_totalToolbarBottomOffset,
|
||||
child: OverlayCenterContent(
|
||||
event: widget.event,
|
||||
messageHeight: _messageHeight,
|
||||
messageWidth: _messageSize.width,
|
||||
toolbarMaxWidth: _toolbarMaxWidth,
|
||||
overlayController: widget.overlayController,
|
||||
chatController: widget.chatController,
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
nextEvent: widget.nextEvent,
|
||||
prevEvent: widget.prevEvent,
|
||||
showToolbarButtons: showToolbarButtons,
|
||||
hasReactions: _hasReactions,
|
||||
sizeAnimation: _messageSizeAnimation,
|
||||
isTransitionAnimation: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!_finishedAnimation && _buttonsOffsetAnimation != null)
|
||||
AnimatedBuilder(
|
||||
animation: _buttonsOffsetAnimation!,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
left: (_buttonsOffsetAnimation?.value)!.dx,
|
||||
top: _centeredButtonsOffset?.dy,
|
||||
child: ToolbarButtonRow(
|
||||
event: widget.event,
|
||||
overlayController: widget.overlayController,
|
||||
shouldShowToolbarButtons: showToolbarButtons,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Row(
|
||||
|
|
@ -423,6 +500,11 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ToolbarButtonRow(
|
||||
// event: widget.overlayController.pangeaMessageEvent!.event,
|
||||
// overlayController: widget.overlayController,
|
||||
// shouldShowToolbarButtons: showToolbarButtons,
|
||||
// ),
|
||||
OverlayFooter(
|
||||
controller: widget.chatController,
|
||||
overlayController: widget.overlayController,
|
||||
|
|
@ -431,7 +513,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
],
|
||||
),
|
||||
),
|
||||
if (showDetails)
|
||||
if (_showDetails)
|
||||
const SizedBox(
|
||||
width: FluffyThemes.columnWidth,
|
||||
),
|
||||
|
|
@ -444,6 +526,18 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
children: [
|
||||
SizedBox(height: _mediaQuery?.padding.top ?? 0),
|
||||
OverlayHeader(controller: widget.chatController),
|
||||
widget.overlayController.toolbarMode.instructionsEnum != null
|
||||
? Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 400,
|
||||
),
|
||||
child: InstructionsInlineTooltip(
|
||||
instructionsEnum: widget
|
||||
.overlayController.toolbarMode.instructionsEnum!,
|
||||
bold: true,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,25 +1,32 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/utils/message_text_util.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
/// Question - does this need to be stateful or does this work?
|
||||
/// Need to test.
|
||||
///
|
||||
|
||||
class MessageTokenText extends StatelessWidget {
|
||||
final PangeaMessageEvent _pangeaMessageEvent;
|
||||
final TextStyle _style;
|
||||
|
||||
final bool Function(PangeaToken)? _isSelected;
|
||||
final void Function(PangeaToken)? _onClick;
|
||||
final bool Function(PangeaToken)? _isHighlighted;
|
||||
final MessageMode? _messageMode;
|
||||
final MessageOverlayController? _overlayController;
|
||||
final bool _isTransitionAnimation;
|
||||
|
||||
const MessageTokenText({
|
||||
super.key,
|
||||
|
|
@ -28,16 +35,24 @@ class MessageTokenText extends StatelessWidget {
|
|||
required TextStyle style,
|
||||
required void Function(PangeaToken)? onClick,
|
||||
bool Function(PangeaToken)? isSelected,
|
||||
bool Function(PangeaToken)? isHighlighted,
|
||||
MessageMode? messageMode,
|
||||
MessageOverlayController? overlayController,
|
||||
bool isTransitionAnimation = false,
|
||||
}) : _onClick = onClick,
|
||||
_isSelected = isSelected,
|
||||
_style = style,
|
||||
_pangeaMessageEvent = pangeaMessageEvent;
|
||||
_pangeaMessageEvent = pangeaMessageEvent,
|
||||
_messageMode = messageMode,
|
||||
_isHighlighted = isHighlighted,
|
||||
_overlayController = overlayController,
|
||||
_isTransitionAnimation = isTransitionAnimation;
|
||||
|
||||
List<PangeaToken>? get _tokens =>
|
||||
_pangeaMessageEvent.messageDisplayRepresentation?.tokens;
|
||||
|
||||
MessageAnalyticsEntry? get messageAnalyticsEntry => _tokens != null
|
||||
? MatrixState.pangeaController.getAnalytics.perMessage.get(
|
||||
? MessageAnalyticsController.get(
|
||||
_tokens!,
|
||||
_pangeaMessageEvent,
|
||||
)
|
||||
|
|
@ -60,42 +75,18 @@ class MessageTokenText extends StatelessWidget {
|
|||
|
||||
return MessageTextWidget(
|
||||
pangeaMessageEvent: _pangeaMessageEvent,
|
||||
style: _style,
|
||||
existingStyle: _style,
|
||||
messageAnalyticsEntry: messageAnalyticsEntry,
|
||||
isSelected: _isSelected,
|
||||
onClick: callOnClick,
|
||||
messageMode: _messageMode,
|
||||
isHighlighted: _isHighlighted,
|
||||
overlayController: _overlayController,
|
||||
isTransitionAnimation: _isTransitionAnimation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenPosition {
|
||||
/// Start index of the full substring in the message
|
||||
final int start;
|
||||
|
||||
/// End index of the full substring in the message
|
||||
final int end;
|
||||
|
||||
/// Start index of the token in the message
|
||||
final int tokenStart;
|
||||
|
||||
/// End index of the token in the message
|
||||
final int tokenEnd;
|
||||
|
||||
final bool selected;
|
||||
final bool hideContent;
|
||||
final PangeaToken? token;
|
||||
|
||||
const TokenPosition({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.tokenStart,
|
||||
required this.tokenEnd,
|
||||
required this.hideContent,
|
||||
required this.selected,
|
||||
this.token,
|
||||
});
|
||||
}
|
||||
|
||||
class HiddenText extends StatelessWidget {
|
||||
final String text;
|
||||
final TextStyle style;
|
||||
|
|
@ -143,27 +134,72 @@ class HiddenText extends StatelessWidget {
|
|||
|
||||
class MessageTextWidget extends StatelessWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final TextStyle style;
|
||||
final TextStyle existingStyle;
|
||||
final MessageAnalyticsEntry? messageAnalyticsEntry;
|
||||
final bool Function(PangeaToken)? isSelected;
|
||||
final void Function(TokenPosition tokenPosition)? onClick;
|
||||
final bool Function(PangeaToken)? isHighlighted;
|
||||
|
||||
final bool? softWrap;
|
||||
final int? maxLines;
|
||||
final TextOverflow? overflow;
|
||||
final MessageMode? messageMode;
|
||||
|
||||
final Animation<double>? contentSizeAnimation;
|
||||
final MessageOverlayController? overlayController;
|
||||
final bool isTransitionAnimation;
|
||||
|
||||
const MessageTextWidget({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.style,
|
||||
required this.existingStyle,
|
||||
this.messageAnalyticsEntry,
|
||||
this.isSelected,
|
||||
this.onClick,
|
||||
this.softWrap,
|
||||
this.maxLines,
|
||||
this.overflow,
|
||||
this.messageMode,
|
||||
this.isHighlighted,
|
||||
this.contentSizeAnimation,
|
||||
this.overlayController,
|
||||
this.isTransitionAnimation = false,
|
||||
});
|
||||
|
||||
TextStyle get style => overlayController != null
|
||||
? existingStyle.copyWith(
|
||||
fontSize: 22,
|
||||
)
|
||||
: existingStyle;
|
||||
|
||||
/// for some reason, this isn't the same as tokenTextWidth
|
||||
double tokenTextWidthForContainer(PangeaToken token) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: token.text.content, style: style),
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
return textPainter.width;
|
||||
}
|
||||
|
||||
Color backgroundColor(TokenPosition tokenPosition) {
|
||||
final hideTokenHighlights = messageAnalyticsEntry != null &&
|
||||
(messageAnalyticsEntry!.hasHiddenWordActivity ||
|
||||
messageAnalyticsEntry!.hasMessageMeaningActivity);
|
||||
|
||||
Color backgroundColor = Colors.transparent;
|
||||
|
||||
if (!hideTokenHighlights) {
|
||||
if (tokenPosition.selected) {
|
||||
backgroundColor = AppConfig.primaryColor;
|
||||
}
|
||||
// else if (tokenPosition.isHighlighted) {
|
||||
// backgroundColor = AppConfig.success.withAlpha(80);
|
||||
// }
|
||||
}
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Characters messageCharacters =
|
||||
|
|
@ -173,6 +209,7 @@ class MessageTextWidget extends StatelessWidget {
|
|||
pangeaMessageEvent,
|
||||
messageAnalyticsEntry: messageAnalyticsEntry,
|
||||
isSelected: isSelected,
|
||||
isHighlighted: isHighlighted,
|
||||
);
|
||||
|
||||
if (tokenPositions == null) {
|
||||
|
|
@ -185,10 +222,6 @@ class MessageTextWidget extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final hideTokenHighlights = messageAnalyticsEntry != null &&
|
||||
(messageAnalyticsEntry!.hasHiddenWordActivity ||
|
||||
messageAnalyticsEntry!.hasMessageMeaningActivity);
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final ownMessage =
|
||||
pangeaMessageEvent.senderId == Matrix.of(context).client.userID;
|
||||
|
|
@ -205,24 +238,13 @@ class MessageTextWidget extends StatelessWidget {
|
|||
text: TextSpan(
|
||||
children:
|
||||
tokenPositions.mapIndexed((int i, TokenPosition tokenPosition) {
|
||||
final shouldDo = pangeaMessageEvent.shouldDoActivity(
|
||||
token: tokenPosition.token,
|
||||
a: ActivityTypeEnum.wordMeaning,
|
||||
feature: null,
|
||||
tag: null,
|
||||
);
|
||||
|
||||
final didMeaningActivity =
|
||||
tokenPosition.token?.didActivitySuccessfully(
|
||||
ActivityTypeEnum.wordMeaning,
|
||||
) ??
|
||||
true;
|
||||
|
||||
final substring = messageCharacters
|
||||
.skip(tokenPosition.start)
|
||||
.take(tokenPosition.end - tokenPosition.start)
|
||||
.toString();
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
Color backgroundColor = Colors.transparent;
|
||||
if (!hideTokenHighlights) {
|
||||
if (tokenPosition.selected) {
|
||||
|
|
@ -235,6 +257,7 @@ class MessageTextWidget extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
>>>>>>> main
|
||||
if (tokenPosition.token?.pos == 'SPACE') {
|
||||
return const TextSpan(text: '\n');
|
||||
}
|
||||
|
|
@ -257,68 +280,121 @@ class MessageTextWidget extends StatelessWidget {
|
|||
.take(endSplitIndex - startSplitIndex)
|
||||
.toString();
|
||||
|
||||
final token = tokenPosition.token!;
|
||||
|
||||
final tokenWidth = tokenTextWidthForContainer(token);
|
||||
|
||||
return WidgetSpan(
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onClick != null
|
||||
? () => onClick?.call(tokenPosition)
|
||||
: null,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
if (start.isNotEmpty)
|
||||
LinkifySpan(
|
||||
text: start,
|
||||
style: style,
|
||||
linkStyle: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: linkColor,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
tokenPosition.hideContent
|
||||
? WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: onClick != null
|
||||
? () => onClick?.call(tokenPosition)
|
||||
: null,
|
||||
child: HiddenText(
|
||||
text: middle,
|
||||
style: style,
|
||||
),
|
||||
),
|
||||
child: CompositedTransformTarget(
|
||||
link: overlayController == null || isTransitionAnimation
|
||||
? LayerLinkAndKey(token.hashCode.toString()).link
|
||||
: MatrixState.pAnyState
|
||||
.layerLinkAndKey(token.text.uniqueKey)
|
||||
.link,
|
||||
child: Column(
|
||||
key: overlayController == null || isTransitionAnimation
|
||||
? null
|
||||
: MatrixState.pAnyState
|
||||
.layerLinkAndKey(token.text.uniqueKey)
|
||||
.key,
|
||||
children: [
|
||||
MessageTokenButton(
|
||||
token: token,
|
||||
overlayController: overlayController,
|
||||
textStyle: style,
|
||||
width: tokenWidth,
|
||||
animate: isTransitionAnimation,
|
||||
activity: overlayController
|
||||
?.toolbarMode.associatedActivityType !=
|
||||
null
|
||||
? overlayController?.messageAnalyticsEntry
|
||||
?.activities(
|
||||
overlayController!
|
||||
.toolbarMode.associatedActivityType!,
|
||||
)
|
||||
: LinkifySpan(
|
||||
text: middle,
|
||||
style: style.merge(
|
||||
TextStyle(
|
||||
backgroundColor: backgroundColor,
|
||||
),
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: linkColor,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
if (end.isNotEmpty)
|
||||
LinkifySpan(
|
||||
text: end,
|
||||
style: style,
|
||||
linkStyle: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: linkColor,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
],
|
||||
.firstWhereOrNull((a) => a.tokens.contains(token))
|
||||
: null,
|
||||
),
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onClick != null
|
||||
? () => onClick?.call(tokenPosition)
|
||||
: null,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
if (start.isNotEmpty)
|
||||
LinkifySpan(
|
||||
text: start,
|
||||
style: style,
|
||||
linkStyle: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: linkColor,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
tokenPosition.hideContent
|
||||
? WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: onClick != null
|
||||
? () => onClick?.call(tokenPosition)
|
||||
: null,
|
||||
child: HiddenText(
|
||||
text: middle,
|
||||
style: style,
|
||||
),
|
||||
),
|
||||
)
|
||||
: LinkifySpan(
|
||||
text: middle,
|
||||
// style: style.merge(
|
||||
// TextStyle(
|
||||
// backgroundColor: backgroundColor(tokenPosition)
|
||||
// ),
|
||||
// ),
|
||||
style: style,
|
||||
linkStyle: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: linkColor,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url)
|
||||
.launchUrl(),
|
||||
),
|
||||
if (end.isNotEmpty)
|
||||
LinkifySpan(
|
||||
text: end,
|
||||
style: style,
|
||||
linkStyle: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: linkColor,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(
|
||||
milliseconds: AppConfig.overlayAnimationDuration,
|
||||
),
|
||||
height:
|
||||
overlayController != null && !isTransitionAnimation
|
||||
? 4
|
||||
: 0,
|
||||
width: tokenWidth,
|
||||
child: Container(
|
||||
color: backgroundColor(tokenPosition),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
|
@ -9,6 +7,7 @@ import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
|||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageTranslationCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
|
|
@ -28,7 +27,6 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
debugPrint('MessageTranslationCard initState');
|
||||
super.initState();
|
||||
loadTranslation();
|
||||
}
|
||||
|
|
@ -99,42 +97,24 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
repEvent!.text,
|
||||
style: AppConfig.messageTextStyle(
|
||||
widget.messageEvent.event,
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (notGoingToTranslate &&
|
||||
!InstructionsEnum.l1Translation.isToggledOff)
|
||||
const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.l1Translation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Text(
|
||||
repEvent!.text,
|
||||
style: AppConfig.messageTextStyle(
|
||||
widget.messageEvent.event,
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (notGoingToTranslate)
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.l1Translation,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,93 +2,121 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_reactions.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button_column.dart';
|
||||
|
||||
class OverlayCenterContent extends StatelessWidget {
|
||||
final double messageHeight;
|
||||
final double messageWidth;
|
||||
final double maxWidth;
|
||||
|
||||
final Event event;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
|
||||
final bool hasReactions;
|
||||
final bool shouldShowToolbarButtons;
|
||||
|
||||
final PangeaMessageEvent? pangeaMessageEvent;
|
||||
|
||||
final MessageOverlayController overlayController;
|
||||
final ChatController chatController;
|
||||
|
||||
final Animation<Size>? sizeAnimation;
|
||||
final void Function(RenderBox)? onChangeMessageSize;
|
||||
final void Function(RenderBox)? onChangeContentSize;
|
||||
final void Function(RenderBox)? onChangeButtonsSize;
|
||||
|
||||
final double messageHeight;
|
||||
final double messageWidth;
|
||||
final double toolbarMaxWidth;
|
||||
|
||||
final bool showToolbarButtons;
|
||||
final bool hasReactions;
|
||||
final bool isTransitionAnimation;
|
||||
|
||||
const OverlayCenterContent({
|
||||
super.key,
|
||||
required this.event,
|
||||
required this.messageHeight,
|
||||
required this.messageWidth,
|
||||
required this.maxWidth,
|
||||
required this.event,
|
||||
required this.toolbarMaxWidth,
|
||||
required this.overlayController,
|
||||
required this.chatController,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.nextEvent,
|
||||
required this.prevEvent,
|
||||
required this.showToolbarButtons,
|
||||
required this.hasReactions,
|
||||
required this.shouldShowToolbarButtons,
|
||||
this.pangeaMessageEvent,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
this.onChangeMessageSize,
|
||||
this.onChangeContentSize,
|
||||
this.onChangeButtonsSize,
|
||||
this.sizeAnimation,
|
||||
this.isTransitionAnimation = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: event.senderId == event.room.client.userID
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (pangeaMessageEvent != null &&
|
||||
pangeaMessageEvent!.shouldShowToolbar)
|
||||
ReadingAssistanceContentCard(
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
overlayController: overlayController,
|
||||
),
|
||||
const SizedBox(height: AppConfig.toolbarSpacing),
|
||||
SizedBox(
|
||||
height: messageHeight,
|
||||
child: OverlayMessage(
|
||||
event,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: chatController.choreographer.immersionMode,
|
||||
controller: chatController,
|
||||
overlayController: overlayController,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
timeline: chatController.timeline!,
|
||||
messageWidth: messageWidth,
|
||||
messageHeight: messageHeight,
|
||||
),
|
||||
),
|
||||
if (hasReactions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
child: MessageReactions(
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: toolbarMaxWidth),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: event.senderId == event.room.client.userID
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MeasureRenderBox(
|
||||
onChange: onChangeMessageSize,
|
||||
child: OverlayMessage(
|
||||
event,
|
||||
chatController.timeline!,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: chatController.choreographer.immersionMode,
|
||||
controller: chatController,
|
||||
overlayController: overlayController,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
timeline: chatController.timeline!,
|
||||
sizeAnimation: sizeAnimation,
|
||||
// there's a split seconds between when the transition animation starts and
|
||||
// when the sizeAnimation is set when the original dimensions need to be enforced
|
||||
messageWidth: sizeAnimation == null && isTransitionAnimation
|
||||
? messageWidth
|
||||
: null,
|
||||
messageHeight:
|
||||
sizeAnimation == null && isTransitionAnimation
|
||||
? messageHeight
|
||||
: null,
|
||||
isTransitionAnimation: isTransitionAnimation,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (hasReactions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
child: MessageReactions(
|
||||
event,
|
||||
chatController.timeline!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isTransitionAnimation)
|
||||
MeasureRenderBox(
|
||||
onChange: onChangeButtonsSize,
|
||||
child: ToolbarButtonRow(
|
||||
event: event,
|
||||
overlayController: overlayController,
|
||||
shouldShowToolbarButtons: showToolbarButtons,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -22,8 +23,12 @@ class OverlayMessage extends StatelessWidget {
|
|||
final Event? prevEvent;
|
||||
final Timeline timeline;
|
||||
final bool immersionMode;
|
||||
final double messageWidth;
|
||||
final double messageHeight;
|
||||
|
||||
final Animation<Size>? sizeAnimation;
|
||||
final double? messageWidth;
|
||||
final double? messageHeight;
|
||||
|
||||
final bool isTransitionAnimation;
|
||||
|
||||
const OverlayMessage(
|
||||
this.event, {
|
||||
|
|
@ -36,6 +41,8 @@ class OverlayMessage extends StatelessWidget {
|
|||
this.pangeaMessageEvent,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
this.sizeAnimation,
|
||||
this.isTransitionAnimation = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -112,127 +119,143 @@ class OverlayMessage extends StatelessWidget {
|
|||
? ThemeData.dark().colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface;
|
||||
|
||||
final content = SingleChildScrollView(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
width: messageWidth,
|
||||
height: messageHeight,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (event.relationshipType == RelationshipTypes.reply)
|
||||
FutureBuilder<Event?>(
|
||||
future: event.getReplyEvent(
|
||||
timeline,
|
||||
),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
snapshot,
|
||||
) {
|
||||
final replyEvent = snapshot.hasData
|
||||
? snapshot.data!
|
||||
: Event(
|
||||
eventId: event.relationshipEventId!,
|
||||
content: {
|
||||
'msgtype': 'm.text',
|
||||
'body': '...',
|
||||
},
|
||||
senderId: event.senderId,
|
||||
type: 'm.room.message',
|
||||
room: event.room,
|
||||
status: EventStatus.sent,
|
||||
originServerTs: DateTime.now(),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: ReplyContent.borderRadius,
|
||||
onTap: () => controller.scrollToEventId(
|
||||
replyEvent.eventId,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage: ownMessage,
|
||||
timeline: timeline,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MessageContent(
|
||||
event.getDisplayEvent(timeline),
|
||||
textColor: textColor,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
overlayController: overlayController,
|
||||
controller: controller,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
borderRadius: borderRadius,
|
||||
timeline: timeline,
|
||||
linkColor: theme.brightness == Brightness.light
|
||||
? theme.colorScheme.primary
|
||||
: ownMessage
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
isTransitionAnimation: isTransitionAnimation,
|
||||
),
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)) ...[
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor.withAlpha(164),
|
||||
size: 14,
|
||||
),
|
||||
Text(
|
||||
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(
|
||||
164,
|
||||
),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
width: messageWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (event.relationshipType == RelationshipTypes.reply)
|
||||
FutureBuilder<Event?>(
|
||||
future: event.getReplyEvent(
|
||||
timeline,
|
||||
),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
snapshot,
|
||||
) {
|
||||
final replyEvent = snapshot.hasData
|
||||
? snapshot.data!
|
||||
: Event(
|
||||
eventId: event.relationshipEventId!,
|
||||
content: {
|
||||
'msgtype': 'm.text',
|
||||
'body': '...',
|
||||
},
|
||||
senderId: event.senderId,
|
||||
type: 'm.room.message',
|
||||
room: event.room,
|
||||
status: EventStatus.sent,
|
||||
originServerTs: DateTime.now(),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: ReplyContent.borderRadius,
|
||||
onTap: () => controller.scrollToEventId(
|
||||
replyEvent.eventId,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage: ownMessage,
|
||||
timeline: timeline,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MessageContent(
|
||||
event.getDisplayEvent(timeline),
|
||||
textColor: textColor,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
overlayController: overlayController,
|
||||
controller: controller,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
borderRadius: borderRadius,
|
||||
timeline: timeline,
|
||||
linkColor: theme.brightness == Brightness.light
|
||||
? theme.colorScheme.primary
|
||||
: ownMessage
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)) ...[
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor.withAlpha(164),
|
||||
size: 14,
|
||||
),
|
||||
Text(
|
||||
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(
|
||||
164,
|
||||
),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: sizeAnimation != null
|
||||
? AnimatedBuilder(
|
||||
animation: sizeAnimation!,
|
||||
builder: (context, child) {
|
||||
return SizedBox(
|
||||
height: sizeAnimation!.value.height,
|
||||
width: sizeAnimation!.value.width,
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
)
|
||||
: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/emojis/emoji_stack.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
|
||||
|
|
@ -19,10 +20,10 @@ class EmojiPracticeButton extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final emoji = token.getEmoji();
|
||||
return WordZoomActivityButton(
|
||||
icon: emoji == null
|
||||
icon: emoji.isEmpty
|
||||
? const Icon(Icons.add_reaction_outlined)
|
||||
: Text(
|
||||
emoji,
|
||||
: EmojiStack(
|
||||
emoji: emoji,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
isSelected: isSelected,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// The multiple choice activity view
|
||||
class MultipleChoiceActivity extends StatefulWidget {
|
||||
|
|
@ -122,13 +120,15 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
// If the selected choice is correct, send the record
|
||||
if (widget.currentActivity.content.isCorrect(value, index)) {
|
||||
// If the activity is an emoji activity, set the emoji value
|
||||
if (widget.currentActivity.activityType == ActivityTypeEnum.emoji) {
|
||||
if (widget.currentActivity.targetTokens?.length != 1) {
|
||||
debugger(when: kDebugMode);
|
||||
} else {
|
||||
widget.currentActivity.targetTokens!.first.setEmoji(value);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this widget is deprecated for use with emoji activities
|
||||
// if (widget.currentActivity.activityType == ActivityTypeEnum.emoji) {
|
||||
// if (widget.currentActivity.targetTokens?.length != 1) {
|
||||
// debugger(when: kDebugMode);
|
||||
// } else {
|
||||
// widget.currentActivity.targetTokens!.first.setEmoji(value);
|
||||
// }
|
||||
// }
|
||||
|
||||
// The next entry in the analytics stream should be from the above putAnalytics.setState.
|
||||
// So we can wait for the stream to update before calling onActivityFinish.
|
||||
|
|
@ -219,7 +219,6 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
messageEvent:
|
||||
widget.practiceCardController.widget.pangeaMessageEvent,
|
||||
overlayController: widget.overlayController,
|
||||
tts: tts,
|
||||
setIsPlayingAudio: widget.overlayController.setIsPlayingAudio,
|
||||
onError: widget.onError,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_repo.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/repo/practice_repo.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// The wrapper for practice activity content.
|
||||
/// Handles the activities associated with a message,
|
||||
|
|
@ -261,7 +260,8 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
|
||||
// wait for savor the joy before popping from the activity queue
|
||||
// to keep the completed activity on screen for a moment
|
||||
widget.overlayController.onActivityFinish(currentActivity!.activityType);
|
||||
widget.overlayController
|
||||
.onActivityFinish(currentActivity!.activityType, null);
|
||||
widget.overlayController.widget.chatController.choreographer.tts.stop();
|
||||
} catch (e, s) {
|
||||
_onError();
|
||||
|
|
@ -298,18 +298,19 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.morphId:
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
final selectedChoice =
|
||||
currentActivity?.activityType == ActivityTypeEnum.emoji &&
|
||||
(currentActivity?.targetTokens?.isNotEmpty ?? false)
|
||||
? currentActivity?.targetTokens?.first.getEmoji()
|
||||
: null;
|
||||
// final selectedChoice =
|
||||
// currentActivity?.activityType == ActivityTypeEnum.emoji &&
|
||||
// (currentActivity?.targetTokens?.isNotEmpty ?? false)
|
||||
// ? currentActivity?.targetTokens?.first.getEmoji()
|
||||
// : null;
|
||||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity!,
|
||||
event: widget.pangeaMessageEvent.event,
|
||||
onError: _onError,
|
||||
overlayController: widget.overlayController,
|
||||
initialSelectedChoice: selectedChoice,
|
||||
// initialSelectedChoice: selectedChoice,
|
||||
initialSelectedChoice: null,
|
||||
clearResponsesOnUpdate:
|
||||
currentActivity?.activityType == ActivityTypeEnum.emoji,
|
||||
);
|
||||
|
|
@ -354,6 +355,15 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
: null,
|
||||
),
|
||||
],
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
child: IconButton(
|
||||
onPressed: () => widget.overlayController
|
||||
.updateToolbarMode(MessageMode.wordEmoji),
|
||||
icon: const Icon(Icons.lightbulb),
|
||||
),
|
||||
),
|
||||
// Flag button in the top right corner
|
||||
// Positioned(
|
||||
// top: 0,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class WordAudioButton extends StatefulWidget {
|
||||
final String text;
|
||||
final double size;
|
||||
final bool isSelected;
|
||||
final double baseOpacity;
|
||||
|
||||
/// If defined, this callback will be called instead of the default one
|
||||
final void Function()? callbackOverride;
|
||||
|
||||
const WordAudioButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.size = 24,
|
||||
this.isSelected = false,
|
||||
this.baseOpacity = 1,
|
||||
this.callbackOverride,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -24,6 +30,15 @@ class WordAudioButtonState extends State<WordAudioButton> {
|
|||
final TtsController tts = TtsController();
|
||||
bool _isPlaying = false;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant WordAudioButton oldWidget) {
|
||||
if (oldWidget.isSelected != widget.isSelected ||
|
||||
oldWidget.callbackOverride != widget.callbackOverride) {
|
||||
setState(() {});
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tts.dispose();
|
||||
|
|
@ -33,47 +48,56 @@ class WordAudioButtonState extends State<WordAudioButton> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState.layerLinkAndKey('word-audio-button').link,
|
||||
child: IconButton(
|
||||
key: MatrixState.pAnyState.layerLinkAndKey('word-audio-button').key,
|
||||
icon: const Icon(Icons.play_arrow_outlined),
|
||||
isSelected: _isPlaying,
|
||||
selectedIcon: const Icon(Icons.pause_outlined),
|
||||
color: _isPlaying ? Colors.white : null,
|
||||
tooltip:
|
||||
_isPlaying ? L10n.of(context).stop : L10n.of(context).playAudio,
|
||||
iconSize: widget.size,
|
||||
onPressed: () async {
|
||||
if (_isPlaying) {
|
||||
await tts.stop();
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = true);
|
||||
}
|
||||
try {
|
||||
await tts.tryToSpeak(
|
||||
widget.text,
|
||||
context,
|
||||
targetID: 'word-audio-button',
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"text": widget.text,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // Disable button if language isn't supported
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey('word-audio-butto${widget.text}')
|
||||
.link,
|
||||
child: Opacity(
|
||||
opacity: !widget.isSelected ? widget.baseOpacity : 1,
|
||||
child: IconButton(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey('word-audio-butto${widget.text}')
|
||||
.key,
|
||||
icon: const Icon(Icons.volume_up),
|
||||
isSelected: _isPlaying,
|
||||
selectedIcon: const Icon(Icons.pause_outlined),
|
||||
color:
|
||||
widget.isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
tooltip:
|
||||
_isPlaying ? L10n.of(context).stop : L10n.of(context).playAudio,
|
||||
iconSize: widget.size,
|
||||
onPressed: widget.callbackOverride ??
|
||||
() async {
|
||||
if (_isPlaying) {
|
||||
await tts.stop();
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = true);
|
||||
}
|
||||
try {
|
||||
await tts.tryToSpeak(
|
||||
widget.text,
|
||||
context,
|
||||
targetID: 'word-audio-button',
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"text": widget.text,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // Disable button if language isn't supported
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class WordAudioButtonState extends State<WordTextWithAudioButton> {
|
|||
widget.text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: _isPlaying
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
fontSize: textSize,
|
||||
),
|
||||
|
|
@ -136,8 +136,11 @@ class WordAudioButtonState extends State<WordTextWithAudioButton> {
|
|||
)
|
||||
else
|
||||
Icon(
|
||||
_isPlaying ? Icons.play_arrow : Icons.play_arrow_outlined,
|
||||
_isPlaying ? Icons.volume_up : Icons.pause_outlined,
|
||||
size: textSize,
|
||||
color: _isPlaying
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,11 +24,12 @@ class WordZoomActivityButton extends StatelessWidget {
|
|||
icon: icon,
|
||||
iconSize: 24,
|
||||
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: isSelected
|
||||
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.25)
|
||||
: Colors.transparent,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
// style: IconButton.styleFrom(
|
||||
// backgroundColor: isSelected
|
||||
// ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.25)
|
||||
// : Colors.transparent,
|
||||
// ),
|
||||
);
|
||||
|
||||
if (opacity != null) {
|
||||
|
|
|
|||
|
|
@ -1,40 +1,42 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_meaning_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_speech_to_text_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_unsubscribed_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
||||
|
||||
const double minCardHeight = 70;
|
||||
|
||||
class ReadingAssistanceContentCard extends StatelessWidget {
|
||||
class ReadingAssistanceContent extends StatefulWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final Duration animationDuration;
|
||||
|
||||
const ReadingAssistanceContentCard({
|
||||
const ReadingAssistanceContent({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overlayController,
|
||||
this.animationDuration = FluffyThemes.animationDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
ReadingAssistanceContentState createState() =>
|
||||
ReadingAssistanceContentState();
|
||||
}
|
||||
|
||||
class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
|
||||
TtsController get ttsController =>
|
||||
overlayController.widget.chatController.choreographer.tts;
|
||||
widget.overlayController.widget.chatController.choreographer.tts;
|
||||
|
||||
Widget? toolbarContent(BuildContext context) {
|
||||
final bool? subscribed =
|
||||
|
|
@ -42,69 +44,76 @@ class ReadingAssistanceContentCard extends StatelessWidget {
|
|||
|
||||
if (subscribed != null && !subscribed) {
|
||||
return MessageUnsubscribedCard(
|
||||
controller: overlayController,
|
||||
controller: widget.overlayController,
|
||||
);
|
||||
}
|
||||
|
||||
if ((overlayController.messageAnalyticsEntry?.hasHiddenWordActivity ??
|
||||
false) ||
|
||||
(overlayController.messageAnalyticsEntry?.hasMessageMeaningActivity ??
|
||||
false)) {
|
||||
if (widget.overlayController.messageAnalyticsEntry?.hasHiddenWordActivity ??
|
||||
false) {
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
overlayController: overlayController,
|
||||
targetTokensAndActivityType:
|
||||
overlayController.messageAnalyticsEntry!.nextActivity!,
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
overlayController: widget.overlayController,
|
||||
targetTokensAndActivityType: widget
|
||||
.overlayController.messageAnalyticsEntry!
|
||||
.nextActivity(ActivityTypeEnum.hiddenWordListening)!,
|
||||
location: AnalyticsUpdateOrigin.practiceActivity,
|
||||
);
|
||||
}
|
||||
|
||||
if (!overlayController.initialized) {
|
||||
if (widget.overlayController.messageAnalyticsEntry
|
||||
?.hasMessageMeaningActivity ??
|
||||
false) {
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
overlayController: widget.overlayController,
|
||||
targetTokensAndActivityType: widget
|
||||
.overlayController.messageAnalyticsEntry!
|
||||
.nextActivity(ActivityTypeEnum.messageMeaning)!,
|
||||
location: AnalyticsUpdateOrigin.practiceActivity,
|
||||
);
|
||||
}
|
||||
|
||||
if (!widget.overlayController.initialized) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
final unlocked = overlayController.toolbarMode.isUnlocked(
|
||||
overlayController.pangeaMessageEvent!.proportionOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
);
|
||||
// final unlocked = widget.overlayController.toolbarMode
|
||||
// .isUnlocked(widget.overlayController);
|
||||
|
||||
if (!unlocked) {
|
||||
return MessageModeLockedCard(controller: overlayController);
|
||||
}
|
||||
// if (!unlocked) {
|
||||
// return MessageModeLockedCard(controller: widget.overlayController);
|
||||
// }
|
||||
|
||||
switch (overlayController.toolbarMode) {
|
||||
switch (widget.overlayController.toolbarMode) {
|
||||
case MessageMode.messageTranslation:
|
||||
return MessageTranslationCard(
|
||||
messageEvent: pangeaMessageEvent,
|
||||
);
|
||||
case MessageMode.messageTextToSpeech:
|
||||
return MessageAudioCard(
|
||||
messageEvent: pangeaMessageEvent,
|
||||
overlayController: overlayController,
|
||||
selection: overlayController.selectedSpan,
|
||||
tts: ttsController,
|
||||
setIsPlayingAudio: overlayController.setIsPlayingAudio,
|
||||
);
|
||||
// return MessageTranslationCard(
|
||||
// messageEvent: widget.pangeaMessageEvent,
|
||||
// );
|
||||
case MessageMode.messageSpeechToText:
|
||||
return MessageSpeechToTextCard(
|
||||
messageEvent: pangeaMessageEvent,
|
||||
);
|
||||
// return MessageSpeechToTextCard(
|
||||
// messageEvent: widget.pangeaMessageEvent,
|
||||
// );
|
||||
case MessageMode.noneSelected:
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
L10n.of(context).clickWordsInstructions,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(8),
|
||||
// child: Text(
|
||||
// L10n.of(context).clickWordsInstructions,
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// );
|
||||
case MessageMode.messageMeaning:
|
||||
return MessageMeaningCard(controller: overlayController);
|
||||
// return MessageMeaningCard(controller: widget.overlayController);
|
||||
case MessageMode.listening:
|
||||
// return MessageAudioCard(
|
||||
// messageEvent: widget.overlayController.pangeaMessageEvent!,
|
||||
// overlayController: widget.overlayController,
|
||||
// setIsPlayingAudio: widget.overlayController.setIsPlayingAudio);
|
||||
case MessageMode.practiceActivity:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.wordEmoji:
|
||||
case MessageMode.wordMorph:
|
||||
case MessageMode.wordMeaning:
|
||||
if (overlayController.selectedToken == null) {
|
||||
if (widget.overlayController.selectedToken == null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
|
|
@ -114,10 +123,10 @@ class ReadingAssistanceContentCard extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
return WordZoomWidget(
|
||||
token: overlayController.selectedToken!,
|
||||
messageEvent: overlayController.pangeaMessageEvent!,
|
||||
token: widget.overlayController.selectedToken!,
|
||||
messageEvent: widget.overlayController.pangeaMessageEvent!,
|
||||
tts: ttsController,
|
||||
overlayController: overlayController,
|
||||
overlayController: widget.overlayController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +134,7 @@ class ReadingAssistanceContentCard extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (![MessageTypes.Text, MessageTypes.Audio].contains(
|
||||
pangeaMessageEvent.event.messageType,
|
||||
widget.pangeaMessageEvent.event.messageType,
|
||||
)) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
|
@ -149,7 +158,7 @@ class ReadingAssistanceContentCard extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
duration: widget.animationDuration,
|
||||
child: toolbarContent(context),
|
||||
),
|
||||
],
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ToolbarButton extends StatelessWidget {
|
||||
final MessageMode mode;
|
||||
|
|
@ -21,15 +20,12 @@ class ToolbarButton extends StatelessWidget {
|
|||
|
||||
Color color(BuildContext context) => mode.iconButtonColor(
|
||||
context,
|
||||
overlayController.toolbarMode,
|
||||
overlayController.pangeaMessageEvent!.proportionOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
overlayController,
|
||||
);
|
||||
|
||||
bool get enabled => mode.isUnlocked(
|
||||
overlayController.pangeaMessageEvent!.proportionOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
);
|
||||
bool get enabled => mode == MessageMode.messageTranslation
|
||||
? overlayController.isTranslationUnlocked
|
||||
: true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -37,7 +33,7 @@ class ToolbarButton extends StatelessWidget {
|
|||
message: mode.tooltip(context),
|
||||
child: PressableButton(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
depressed: mode == overlayController.toolbarMode || !enabled,
|
||||
depressed: mode == overlayController.toolbarMode,
|
||||
color: color(context),
|
||||
onPressed: () => onPressed(mode),
|
||||
playSound: true,
|
||||
|
|
|
|||
|
|
@ -8,19 +8,16 @@ import 'package:fluffychat/config/app_config.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button.dart';
|
||||
|
||||
class ToolbarButtonAndProgressColumn extends StatelessWidget {
|
||||
final Event event;
|
||||
final MessageOverlayController overlayController;
|
||||
final bool shouldShowToolbarButtons;
|
||||
final double height;
|
||||
final double width;
|
||||
|
||||
const ToolbarButtonAndProgressColumn({
|
||||
required this.event,
|
||||
required this.overlayController,
|
||||
required this.shouldShowToolbarButtons,
|
||||
required this.height,
|
||||
required this.width,
|
||||
super.key,
|
||||
|
|
@ -37,7 +34,6 @@ class ToolbarButtonAndProgressColumn extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (event.messageType == MessageTypes.Audio ||
|
||||
!shouldShowToolbarButtons ||
|
||||
!(overlayController.pangeaMessageEvent?.messageDisplayLangIsL2 ??
|
||||
false)) {
|
||||
return SizedBox(height: height, width: width);
|
||||
|
|
@ -77,8 +73,9 @@ class ToolbarButtonAndProgressColumn extends StatelessWidget {
|
|||
margin: barMargin,
|
||||
),
|
||||
Positioned(
|
||||
bottom:
|
||||
height * MessageMode.messageMeaning.pointOnBar - buttonSize,
|
||||
bottom: height * MessageMode.noneSelected.pointOnBar -
|
||||
buttonSize / 2 -
|
||||
barMargin.vertical / 2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: overlayController.isPracticeComplete
|
||||
|
|
@ -98,36 +95,6 @@ class ToolbarButtonAndProgressColumn extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: height * MessageMode.messageTranslation.pointOnBar -
|
||||
buttonSize / 2,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.messageTranslation,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: height * MessageMode.messageTextToSpeech.pointOnBar -
|
||||
buttonSize / 2,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.messageTextToSpeech,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: height * MessageMode.practiceActivity.pointOnBar,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.practiceActivity,
|
||||
overlayController: overlayController,
|
||||
onPressed: (mode) =>
|
||||
overlayController.onNextActivityRequest(),
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
85
lib/pangea/toolbar/widgets/toolbar_button_column.dart
Normal file
85
lib/pangea/toolbar/widgets/toolbar_button_column.dart
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class ToolbarButtonRow extends StatelessWidget {
|
||||
final Event event;
|
||||
final MessageOverlayController overlayController;
|
||||
final bool shouldShowToolbarButtons;
|
||||
|
||||
const ToolbarButtonRow({
|
||||
required this.event,
|
||||
required this.overlayController,
|
||||
required this.shouldShowToolbarButtons,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const double iconWidth = 36.0;
|
||||
static const double buttonSize = 40.0;
|
||||
static const barMargin =
|
||||
EdgeInsets.symmetric(horizontal: iconWidth / 2, vertical: buttonSize / 2);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (event.messageType == MessageTypes.Audio ||
|
||||
!shouldShowToolbarButtons ||
|
||||
!(overlayController.pangeaMessageEvent?.messageDisplayLangIsL2 ??
|
||||
false)) {
|
||||
return const SizedBox(
|
||||
height: 50.0,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ToolbarButton(
|
||||
mode: MessageMode.messageTranslation,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
ToolbarButton(
|
||||
mode: MessageMode.wordMorph,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
ToolbarButton(
|
||||
mode: MessageMode.wordMeaning,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
ToolbarButton(
|
||||
mode: MessageMode.listening,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
ToolbarButton(
|
||||
mode: MessageMode.wordEmoji,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
|
|
@ -13,11 +7,15 @@ import 'package:fluffychat/pangea/learning_settings/constants/language_constants
|
|||
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class LemmaMeaningWidget extends StatefulWidget {
|
||||
final ConstructUses constructUse;
|
||||
|
|
@ -27,7 +25,7 @@ class LemmaMeaningWidget extends StatefulWidget {
|
|||
|
||||
/// These are not present if this widget is used outside the chat
|
||||
/// (e.g. in the vocab details view)
|
||||
/// we're going to punt on letting the user assign the meaning in the vocab details view
|
||||
/// TODO: let the user assign the meaning in the vocab details view
|
||||
final MessageOverlayController? controller;
|
||||
final PangeaToken? token;
|
||||
|
||||
|
|
@ -48,6 +46,9 @@ class LemmaMeaningWidget extends StatefulWidget {
|
|||
class LemmaMeaningWidgetState extends State<LemmaMeaningWidget> {
|
||||
bool _editMode = false;
|
||||
late TextEditingController _controller;
|
||||
LemmaInfoResponse? _lemmaInfo;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
String get _lemma => widget.constructUse.lemma;
|
||||
|
||||
|
|
@ -55,6 +56,16 @@ class LemmaMeaningWidgetState extends State<LemmaMeaningWidget> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
_fetchLemmaMeaning();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LemmaMeaningWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.constructUse != widget.constructUse ||
|
||||
oldWidget.langCode != widget.langCode) {
|
||||
_fetchLemmaMeaning();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -74,29 +85,42 @@ class LemmaMeaningWidgetState extends State<LemmaMeaningWidget> {
|
|||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
Future<LemmaInfoResponse> _lemmaMeaning() => LemmaInfoRepo.get(_request);
|
||||
Future<void> _fetchLemmaMeaning() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
_lemmaInfo = await LemmaInfoRepo.get(_request);
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleEditMode(bool value) => setState(() => _editMode = value);
|
||||
|
||||
Future<void> editLemmaMeaning(String userEdit) async {
|
||||
final originalMeaning = await _lemmaMeaning();
|
||||
final originalMeaning = _lemmaInfo;
|
||||
|
||||
LemmaInfoRepo.set(
|
||||
_request,
|
||||
LemmaInfoResponse(emoji: originalMeaning.emoji, meaning: userEdit),
|
||||
);
|
||||
if (originalMeaning != null) {
|
||||
LemmaInfoRepo.set(
|
||||
_request,
|
||||
LemmaInfoResponse(emoji: originalMeaning.emoji, meaning: userEdit),
|
||||
);
|
||||
|
||||
_toggleEditMode(false);
|
||||
_toggleEditMode(false);
|
||||
_fetchLemmaMeaning();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.token != null &&
|
||||
widget.token!.shouldDoActivity(
|
||||
a: ActivityTypeEnum.wordMeaning,
|
||||
feature: null,
|
||||
tag: null,
|
||||
)) {
|
||||
widget.controller?.messageAnalyticsEntry != null &&
|
||||
widget.controller!.messageAnalyticsEntry!
|
||||
.hasActivity(ActivityTypeEnum.wordMeaning, widget.token!)) {
|
||||
return WordZoomActivityButton(
|
||||
icon: const Icon(Symbols.dictionary),
|
||||
isSelected: widget.controller?.toolbarMode == MessageMode.wordMeaning,
|
||||
|
|
@ -106,107 +130,110 @@ class LemmaMeaningWidgetState extends State<LemmaMeaningWidget> {
|
|||
widget.controller!.updateToolbarMode(MessageMode.wordMeaning);
|
||||
}
|
||||
: () => {},
|
||||
opacity:
|
||||
widget.controller?.toolbarMode == MessageMode.wordMeaning ? 1 : 0.4,
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder<LemmaInfoResponse>(
|
||||
future: _lemmaMeaning(),
|
||||
builder: (context, snapshot) {
|
||||
if (_editMode) {
|
||||
_controller.text = snapshot.data?.meaning ?? "";
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
if (_isLoading) {
|
||||
return const TextLoadingShimmer();
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
debugger(when: kDebugMode);
|
||||
return Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
if (_editMode) {
|
||||
_controller.text = _lemmaInfo?.meaning ?? "";
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(_lemma, widget.constructUse.category)}",
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextField(
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: _lemmaInfo?.meaning,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(_lemma, widget.constructUse.category)}",
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextField(
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: snapshot.data!.meaning,
|
||||
ElevatedButton(
|
||||
onPressed: () => _toggleEditMode(false),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
),
|
||||
child: Text(L10n.of(context).cancel),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => _toggleEditMode(false),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
),
|
||||
child: Text(L10n.of(context).cancel),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () => _controller.text != _lemmaInfo?.meaning &&
|
||||
_controller.text.isNotEmpty
|
||||
? editLemmaMeaning(_controller.text)
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
_controller.text != snapshot.data!.meaning &&
|
||||
_controller.text.isNotEmpty
|
||||
? editLemmaMeaning(_controller.text)
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
),
|
||||
child: Text(L10n.of(context).saveChanges),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
),
|
||||
child: Text(L10n.of(context).saveChanges),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const TextLoadingShimmer();
|
||||
}
|
||||
|
||||
if (snapshot.hasError || snapshot.data == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _toggleEditMode(true),
|
||||
onDoubleTap: () => _toggleEditMode(true),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: widget.style,
|
||||
children: [
|
||||
if (widget.leading != null) widget.leading!,
|
||||
if (widget.leading != null) const TextSpan(text: ' '),
|
||||
TextSpan(text: snapshot.data!.meaning),
|
||||
],
|
||||
),
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _toggleEditMode(true),
|
||||
onDoubleTap: () => _toggleEditMode(true),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: widget.style?.copyWith(
|
||||
color: widget.controller?.toolbarMode ==
|
||||
MessageMode.wordMeaning
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
children: [
|
||||
if (widget.leading != null) widget.leading!,
|
||||
if (widget.leading != null) const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: _lemmaInfo?.meaning,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
|
|
@ -9,8 +5,12 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
|||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class LemmaWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
|
|
@ -18,6 +18,7 @@ class LemmaWidget extends StatefulWidget {
|
|||
final VoidCallback onEdit;
|
||||
final VoidCallback onEditDone;
|
||||
final TtsController tts;
|
||||
final MessageOverlayController? overlayController;
|
||||
|
||||
const LemmaWidget({
|
||||
super.key,
|
||||
|
|
@ -26,6 +27,7 @@ class LemmaWidget extends StatefulWidget {
|
|||
required this.onEdit,
|
||||
required this.onEditDone,
|
||||
required this.tts,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -162,16 +164,39 @@ class LemmaWidgetState extends State<LemmaWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _toggleEditMode(true),
|
||||
onDoubleTap: () => _toggleEditMode(true),
|
||||
child: WordTextWithAudioButton(
|
||||
text: widget.token.lemma.text,
|
||||
return Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _toggleEditMode(true),
|
||||
onDoubleTap: () => _toggleEditMode(true),
|
||||
child: Text(
|
||||
widget.token.lemma.text,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.token.lemma.text.toLowerCase() ==
|
||||
widget.token.text.content.toLowerCase())
|
||||
WordAudioButton(
|
||||
text: widget.token.text.content,
|
||||
isSelected:
|
||||
MessageMode.listening == widget.overlayController?.toolbarMode,
|
||||
baseOpacity: 0.4,
|
||||
callbackOverride:
|
||||
widget.overlayController?.messageAnalyticsEntry?.hasActivity(
|
||||
MessageMode.listening.associatedActivityType!,
|
||||
widget.token,
|
||||
) ==
|
||||
true
|
||||
? () => widget.overlayController
|
||||
?.updateToolbarMode(MessageMode.listening)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
// stateful widget that displays morphological label and a shimmer effect while the text is loading
|
||||
// takes a token and morphological feature as input
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
|
@ -19,10 +16,13 @@ import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart';
|
|||
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_feature_display.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_tag_display.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class MorphFocusWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
|
|
@ -54,17 +54,18 @@ class MorphFocusWidgetState extends State<MorphFocusWidget> {
|
|||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
void resetMorphTag() => setState(
|
||||
() => selectedMorphTag = widget.token.morph[widget.morphFeature]!,
|
||||
() => selectedMorphTag =
|
||||
widget.token.getMorphTag(widget.morphFeature) ?? "X",
|
||||
);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MorphFocusWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.token != oldWidget.token ||
|
||||
widget.morphFeature != oldWidget.morphFeature) {
|
||||
resetMorphTag();
|
||||
setState(() => editMode = false);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -171,40 +172,51 @@ class MorphFocusWidgetState extends State<MorphFocusWidget> {
|
|||
children: [
|
||||
MorphFeatureDisplay(
|
||||
morphFeature: widget.morphFeature,
|
||||
morphTag: selectedMorphTag,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: enterEditMode,
|
||||
onDoubleTap: enterEditMode,
|
||||
child: MorphTagDisplay(
|
||||
morphFeature: widget.morphFeature,
|
||||
textColor: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? id.constructUses.lemmaCategory.darkColor(context)
|
||||
: id.constructUses.lemmaCategory.color(context),
|
||||
if (widget.token.getMorphTag(widget.morphFeature) != null) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: enterEditMode,
|
||||
onDoubleTap: enterEditMode,
|
||||
child: MorphTagDisplay(
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(
|
||||
widget.morphFeature,
|
||||
),
|
||||
morphTag:
|
||||
widget.token.getMorphTag(widget.morphFeature) ??
|
||||
L10n.of(context).nan,
|
||||
textColor: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? id.constructUses.lemmaCategory.darkColor(context)
|
||||
: id.constructUses.lemmaCategory.color(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
ConstructXpWidget(
|
||||
id: id,
|
||||
onTap: () => showDialog<AnalyticsPopupWrapper>(
|
||||
context: context,
|
||||
builder: (context) => AnalyticsPopupWrapper(
|
||||
constructZoom: id,
|
||||
view: ConstructTypeEnum.morph,
|
||||
const SizedBox(width: 6),
|
||||
ConstructXpWidget(
|
||||
id: id,
|
||||
onTap: () => showDialog<AnalyticsPopupWrapper>(
|
||||
context: context,
|
||||
builder: (context) => AnalyticsPopupWrapper(
|
||||
constructZoom: id,
|
||||
view: ConstructTypeEnum.morph,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
MorphMeaningWidget(
|
||||
feature: widget.morphFeature,
|
||||
tag: widget.token.getMorphTag(widget.morphFeature)!,
|
||||
),
|
||||
] else
|
||||
Text(L10n.of(context).nan),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -220,91 +232,72 @@ class MorphFocusWidgetState extends State<MorphFocusWidget> {
|
|||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 50),
|
||||
child: Scrollbar(
|
||||
controller: _scrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: FutureBuilder(
|
||||
future: MorphsRepo.get(),
|
||||
builder: (context, snapshot) {
|
||||
final allMorphTagsForEdit = snapshot.data
|
||||
?.getDisplayTags(widget.morphFeature) ??
|
||||
defaultMorphMapping
|
||||
.getDisplayTags(widget.morphFeature);
|
||||
FutureBuilder(
|
||||
future: MorphsRepo.get(),
|
||||
builder: (context, snapshot) {
|
||||
final allMorphTagsForEdit =
|
||||
snapshot.data?.getDisplayTags(widget.morphFeature) ??
|
||||
defaultMorphMapping.getDisplayTags(widget.morphFeature);
|
||||
|
||||
return snapshot.connectionState == ConnectionState.done
|
||||
? Row(
|
||||
children: allMorphTagsForEdit.map((tag) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
border: Border.all(
|
||||
color: selectedMorphTag == tag
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
: Colors.transparent,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 7,
|
||||
),
|
||||
),
|
||||
backgroundColor:
|
||||
WidgetStateProperty.all<Color>(
|
||||
selectedMorphTag == tag
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha(50)
|
||||
: Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => selectedMorphTag = tag);
|
||||
},
|
||||
child: Text(
|
||||
getGrammarCopy(
|
||||
category: widget.morphFeature,
|
||||
lemma: tag,
|
||||
context: context,
|
||||
) ??
|
||||
tag,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
children: allMorphTagsForEdit.map((tag) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
border: Border.all(
|
||||
color: selectedMorphTag == tag
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 7,
|
||||
),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
selectedMorphTag == tag
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha(50)
|
||||
: Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => selectedMorphTag = tag);
|
||||
},
|
||||
child: Text(
|
||||
getGrammarCopy(
|
||||
category: widget.morphFeature,
|
||||
lemma: tag,
|
||||
context: context,
|
||||
) ??
|
||||
tag,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -1,59 +1,74 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
|
||||
class MorphologicalListItem extends StatelessWidget {
|
||||
final Function(String) onPressed;
|
||||
final String morphFeature;
|
||||
final String morphTag;
|
||||
|
||||
final bool isUnlocked;
|
||||
final bool isSelected;
|
||||
final MorphFeaturesEnum morphFeature;
|
||||
final PangeaToken token;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const MorphologicalListItem({
|
||||
required this.onPressed,
|
||||
required this.morphFeature,
|
||||
required this.morphTag,
|
||||
this.isUnlocked = true,
|
||||
this.isSelected = false,
|
||||
required this.token,
|
||||
required this.overlayController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
bool get shouldDoActivity =>
|
||||
overlayController.messageAnalyticsEntry?.hasActivity(
|
||||
ActivityTypeEnum.morphId,
|
||||
token,
|
||||
morphFeature,
|
||||
) ==
|
||||
true;
|
||||
|
||||
bool get isSelected => overlayController.toolbarMode == MessageMode.wordMorph;
|
||||
|
||||
String get morphTag => token.getMorphTag(morphFeature.name) ?? "X";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: WordZoomActivityButton(
|
||||
icon: isUnlocked
|
||||
// if unlocked, we show the specific icon for the morphological feature/tag
|
||||
? MorphIcon(morphFeature: morphFeature, morphTag: morphTag)
|
||||
// we show general feature icon if the morph is locked
|
||||
// : MorphIcon(morphFeature: morphFeature, morphTag: null),
|
||||
// or maybe we should the general grammar icon to show the
|
||||
// connection between these and the grammar icon in the progress header
|
||||
: const Icon(Symbols.toys_and_games),
|
||||
icon: shouldDoActivity
|
||||
? const Icon(Symbols.toys_and_games)
|
||||
: MorphIcon(
|
||||
morphFeature: morphFeature,
|
||||
morphTag: token.getMorphTag(morphFeature.name),
|
||||
size: const Size(24, 24),
|
||||
),
|
||||
isSelected: isSelected,
|
||||
onPressed: () => onPressed(morphFeature),
|
||||
tooltip: isUnlocked
|
||||
? getGrammarCopy(
|
||||
category: morphFeature,
|
||||
// onPressed: shouldDoActivity
|
||||
// ? () => overlayController.updateToolbarMode(MessageMode.wordMorph)
|
||||
// : () => (feature) => showDialog<AnalyticsPopupWrapper>(
|
||||
// context: context,
|
||||
// builder: (context) => AnalyticsPopupWrapper(
|
||||
// constructZoom: token.morphIdByFeature(feature),
|
||||
// view: ConstructTypeEnum.vocab,
|
||||
// ),
|
||||
// ),
|
||||
onPressed: () =>
|
||||
overlayController.onMorphActivitySelect(token, morphFeature),
|
||||
tooltip: shouldDoActivity
|
||||
? morphFeature.getDisplayCopy(context)
|
||||
: getGrammarCopy(
|
||||
category: morphFeature.name,
|
||||
lemma: morphTag,
|
||||
context: context,
|
||||
)
|
||||
: getMorphologicalCategoryCopy(
|
||||
morphFeature,
|
||||
context,
|
||||
),
|
||||
opacity: isSelected
|
||||
? 1
|
||||
: !isUnlocked
|
||||
? 0.2
|
||||
: shouldDoActivity
|
||||
? 0.4
|
||||
: 1,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
|
||||
enum WordZoomSelection {
|
||||
meaning,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue