diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index dbedd75ec..cf6842c1c 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4659,5 +4659,6 @@ "publicProfileTitle": "Allow my profile to be found in search", "publicProfileDesc": "By enabling this option, I confirm that I am of legal age in my country of residence", "clickWordsInstructions": "Click on individual words for more activities.", - "chooseBestDefinition": "Choose the best definition" + "chooseBestDefinition": "Choose the best definition", + "chooseBaseForm": "Choose the base form" } diff --git a/lib/pangea/constants/morph_categories_and_labels.dart b/lib/pangea/constants/morph_categories_and_labels.dart index f62d5ba31..e69491301 100644 --- a/lib/pangea/constants/morph_categories_and_labels.dart +++ b/lib/pangea/constants/morph_categories_and_labels.dart @@ -217,6 +217,8 @@ IconData getIconForMorphFeature(String feature) { return Icons.swap_vert; case 'definite': return Icons.check_circle_outline; + case 'prepcase': + return Icons.location_on_outlined; default: debugger(when: kDebugMode); return Icons.help_outline; diff --git a/lib/pangea/repo/practice/lemma_activity_generator.dart b/lib/pangea/repo/practice/lemma_activity_generator.dart index 39ee6e52a..81f1d8b37 100644 --- a/lib/pangea/repo/practice/lemma_activity_generator.dart +++ b/lib/pangea/repo/practice/lemma_activity_generator.dart @@ -6,10 +6,13 @@ import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choic import 'package:fluffychat/pangea/models/practice_activities.dart/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 get( MessageActivityRequest req, + BuildContext context, ) async { debugger(when: kDebugMode && req.targetTokens.length != 1); @@ -26,7 +29,7 @@ class LemmaActivityGenerator { tgtConstructs: [token.vocabConstructID], langCode: req.userL2, content: ActivityContent( - question: "", + question: L10n.of(context).chooseBaseForm, choices: choices, answers: [token.lemma.text], spanDisplayDetails: null, diff --git a/lib/pangea/repo/practice/practice_repo.dart b/lib/pangea/repo/practice/practice_repo.dart index f1caec063..e7e860f54 100644 --- a/lib/pangea/repo/practice/practice_repo.dart +++ b/lib/pangea/repo/practice/practice_repo.dart @@ -127,7 +127,7 @@ class PracticeGenerationController { case ActivityTypeEnum.emoji: return _emoji.get(req); case ActivityTypeEnum.lemmaId: - return _lemma.get(req); + return _lemma.get(req, context); case ActivityTypeEnum.morphId: return _morph.get(req); case ActivityTypeEnum.wordMeaning: diff --git a/lib/pangea/widgets/word_zoom/lemma_widget.dart b/lib/pangea/widgets/word_zoom/lemma_widget.dart index 9461ba759..44a80ca66 100644 --- a/lib/pangea/widgets/word_zoom/lemma_widget.dart +++ b/lib/pangea/widgets/word_zoom/lemma_widget.dart @@ -1,25 +1,41 @@ import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity/word_zoom_activity_button.dart'; import 'package:flutter/material.dart'; class LemmaWidget extends StatelessWidget { final PangeaToken token; - final VoidCallback onPressed; - final bool isSelected; const LemmaWidget({ super.key, required this.token, - required this.onPressed, - this.isSelected = false, }); @override Widget build(BuildContext context) { - return WordZoomActivityButton( - icon: Text(token.xpEmoji), - isSelected: isSelected, - onPressed: onPressed, + return Padding( + padding: const EdgeInsets.all(4.0), + child: Text("${token.lemma.text} ${token.xpEmoji}"), ); } } + +// class LemmaWidget extends StatelessWidget { +// final PangeaToken token; +// final VoidCallback onPressed; +// final bool isSelected; + +// const LemmaWidget({ +// super.key, +// required this.token, +// required this.onPressed, +// this.isSelected = false, +// }); + +// @override +// Widget build(BuildContext context) { +// return WordZoomActivityButton( +// icon: Text(token.xpEmoji), +// isSelected: isSelected, +// onPressed: onPressed, +// ); +// } +// } diff --git a/lib/pangea/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/widgets/word_zoom/word_zoom_widget.dart index 81b2da70c..0c260f58c 100644 --- a/lib/pangea/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/widgets/word_zoom/word_zoom_widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; @@ -6,6 +8,7 @@ import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart'; import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/emoji_practice_button.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; @@ -59,17 +62,17 @@ class WordZoomWidgetState extends State { /// The currently selected word zoom activity type. /// If an activity should be shown for this type, shows that activity. /// If not, shows the info related to that activity type. - /// Defaults to the lemma translation. - WordZoomSelection _selectionType = WordZoomSelection.translation; + /// Null if not set yet, since it takes a second to determine if the lemma activity can be shown. + WordZoomSelection? _selectionType; /// If doing a morphological activity, this is the selected morph feature. String? _selectedMorphFeature; - /// If true, the activity will be shown regardless of shouldDoActivity. + /// If non-null and not complete, the activity will be shown regardless of shouldDoActivity. /// Used to show the practice activity card's savor the joy animation. /// (Analytics sending triggers the point gain animation, do also /// causes shouldDoActivity to be false. This is a workaround.) - bool _forceShowActivity = false; + Completer? _activityLock; // The function to determine if lemma distractors can be generated // is computationally expensive, so we only do it once @@ -84,7 +87,7 @@ class WordZoomWidgetState extends State { @override void initState() { super.initState(); - _setCanGenerateLemmaActivity(); + _setInitialSelectionType(); } @override @@ -92,51 +95,74 @@ class WordZoomWidgetState extends State { super.didUpdateWidget(oldWidget); if (widget.token != oldWidget.token) { _clean(); - _setCanGenerateLemmaActivity(); + _setInitialSelectionType(); } } void _clean() { if (mounted) { setState(() { - _selectionType = WordZoomSelection.translation; + _activityLock = null; + _selectionType = null; _selectedMorphFeature = null; - _forceShowActivity = false; }); } } - void _setCanGenerateLemmaActivity() { - widget.token.canGenerateDistractors(ActivityTypeEnum.lemmaId).then((value) { - if (mounted) setState(() => _canGenerateLemmaActivity = value); - }); + Future _setCanGenerateLemmaActivity() async { + final canGenerate = + await widget.token.canGenerateDistractors(ActivityTypeEnum.lemmaId); + if (mounted) setState(() => _canGenerateLemmaActivity = canGenerate); + } + + Future _setInitialSelectionType() async { + if (_selectionType != null) return; + await _setCanGenerateLemmaActivity(); + _setSelectionType(_defaultSelectionType); } - void _setSelectionType(WordZoomSelection type, {String? feature}) { + WordZoomSelection get _defaultSelectionType => + _shouldShowActivity(WordZoomSelection.lemma) + ? WordZoomSelection.lemma + : WordZoomSelection.translation; + + Future _setSelectionType( + WordZoomSelection type, { + String? feature, + }) async { WordZoomSelection newSelectedType = type; String? newSelectedFeature; if (type != WordZoomSelection.morph) { // if setting selectionType to non-morph activity, either set it if it's not // already selected, or reset to it the default type - newSelectedType = - _selectionType == type ? WordZoomSelection.translation : type; + newSelectedType = _selectionType == type ? _defaultSelectionType : type; } else { // otherwise (because there could be multiple different morph features), check // if the feature is already selected, and if so, reset to the default type. // if not, set the selectionType and feature newSelectedFeature = _selectedMorphFeature == feature ? null : feature; newSelectedType = newSelectedFeature == null - ? WordZoomSelection.translation + ? _defaultSelectionType : WordZoomSelection.morph; } + // wait for savor the joy animation to finish before changing the selection type + if (_activityLock != null) await _activityLock!.future; + _selectionType = newSelectedType; _selectedMorphFeature = newSelectedFeature; if (mounted) setState(() {}); } - void _setForceShowActivity(bool showActivity) { - if (mounted) setState(() => _forceShowActivity = showActivity); + void _lockActivity() { + if (mounted) setState(() => _activityLock = Completer()); + } + + void _unlockActivity() { + if (_activityLock == null) return; + _activityLock!.complete(); + _activityLock = null; + if (mounted) setState(() {}); } /// This function should be called before overlayController.onActivityFinish to @@ -145,78 +171,26 @@ class WordZoomWidgetState extends State { void onActivityFinish({ Duration savorTheJoyDuration = const Duration(seconds: 1), }) { - _setForceShowActivity(true); + _lockActivity(); Future.delayed(savorTheJoyDuration, () { - _setForceShowActivity(false); + if (_selectionType == WordZoomSelection.lemma) { + _setSelectionType(WordZoomSelection.translation); + } + _unlockActivity(); }); } - Widget get _wordZoomCenterWidget { - final showActivity = widget.token.shouldDoActivity( - a: _selectionType.activityType, - feature: _selectedMorphFeature, - tag: _selectedMorphFeature == null - ? null - : widget.token.morph[_selectedMorphFeature], - ) && - (_selectionType != WordZoomSelection.lemma || - _canGenerateLemmaActivity) && - (_selectionType != WordZoomSelection.translation || - _canGenerateDefintionActivity); - - if (showActivity || _forceShowActivity) { - return PracticeActivityCard( - pangeaMessageEvent: widget.messageEvent, - targetTokensAndActivityType: TargetTokensAndActivityType( - tokens: [widget.token], - activityType: _selectionType.activityType, - ), - overlayController: widget.overlayController, - morphFeature: _selectedMorphFeature, - wordDetailsController: this, - ); - } - - if (_selectionType == WordZoomSelection.translation) { - return ContextualTranslationWidget( - token: widget.token, - langCode: widget.messageEvent.messageDisplayLangCode, - ); - } - - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [_activityAnswer], - ), - ); - } - - Widget get _activityAnswer { - switch (_selectionType) { - case WordZoomSelection.morph: - if (_selectedMorphFeature == null) { - return const Text("There should be a selected morph feature"); - } - final String morphTag = widget.token.morph[_selectedMorphFeature!]; - final copy = getGrammarCopy( - category: _selectedMorphFeature!, - lemma: morphTag, - context: context, - ); - return Text(copy ?? morphTag, textAlign: TextAlign.center); - case WordZoomSelection.lemma: - return Text(widget.token.lemma.text, textAlign: TextAlign.center); - case WordZoomSelection.emoji: - return widget.token.getEmoji() != null - ? Text(widget.token.getEmoji()!) - : const Text("emoji is null"); - case WordZoomSelection.translation: - return const SizedBox(); - } - } + bool _shouldShowActivity(WordZoomSelection selection) => + widget.token.shouldDoActivity( + a: selection.activityType, + feature: _selectedMorphFeature, + tag: _selectedMorphFeature == null + ? null + : widget.token.morph[_selectedMorphFeature], + ) && + (selection != WordZoomSelection.lemma || _canGenerateLemmaActivity) && + (selection != WordZoomSelection.translation || + _canGenerateDefintionActivity); @override Widget build(BuildContext context) { @@ -245,6 +219,7 @@ class WordZoomWidgetState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ EmojiPracticeButton( token: widget.token, @@ -252,21 +227,37 @@ class WordZoomWidgetState extends State { _setSelectionType(WordZoomSelection.emoji), isSelected: _selectionType == WordZoomSelection.emoji, ), - WordTextWithAudioButton( - text: widget.token.text.content, - ttsController: widget.tts, - eventID: widget.messageEvent.eventId, - ), - LemmaWidget( - token: widget.token, - onPressed: () => - _setSelectionType(WordZoomSelection.lemma), - isSelected: _selectionType == WordZoomSelection.lemma, + Column( + mainAxisSize: MainAxisSize.min, + children: [ + WordTextWithAudioButton( + text: widget.token.text.content, + ttsController: widget.tts, + eventID: widget.messageEvent.eventId, + ), + // if _selectionType is null, we don't know if the lemma activity + // can be shown yet, so we don't show the lemma definition + if (!_shouldShowActivity(WordZoomSelection.lemma) && + _selectionType != null) + LemmaWidget( + token: widget.token, + ), + ], ), + const SizedBox(width: 40), ], ), ), - _wordZoomCenterWidget, + WordZoomCenterWidget( + selectionType: _selectionType, + selectedMorphFeature: _selectedMorphFeature, + shouldDoActivity: _selectionType != null + ? _shouldShowActivity(_selectionType!) + : false, + locked: + _activityLock != null && !_activityLock!.isCompleted, + wordDetailsController: this, + ), MorphologicalListWidget( token: widget.token, setMorphFeature: (feature) => _setSelectionType( @@ -284,3 +275,101 @@ class WordZoomWidgetState extends State { ); } } + +class ActivityAnswerWidget extends StatelessWidget { + final PangeaToken token; + final WordZoomSelection selectionType; + final String? selectedMorphFeature; + + const ActivityAnswerWidget({ + super.key, + required this.token, + required this.selectionType, + required this.selectedMorphFeature, + }); + + @override + Widget build(BuildContext context) { + switch (selectionType) { + case WordZoomSelection.morph: + if (selectedMorphFeature == null) { + return const Text("There should be a selected morph feature"); + } + final String morphTag = token.morph[selectedMorphFeature!]; + final copy = getGrammarCopy( + category: selectedMorphFeature!, + lemma: morphTag, + context: context, + ); + return Text(copy ?? morphTag, textAlign: TextAlign.center); + case WordZoomSelection.lemma: + return Text(token.lemma.text, textAlign: TextAlign.center); + case WordZoomSelection.emoji: + return token.getEmoji() != null + ? Text(token.getEmoji()!) + : const Text("emoji is null"); + case WordZoomSelection.translation: + return const SizedBox(); + } + } +} + +class WordZoomCenterWidget extends StatelessWidget { + final WordZoomSelection? selectionType; + final String? selectedMorphFeature; + final bool shouldDoActivity; + final bool locked; + final WordZoomWidgetState wordDetailsController; + + const WordZoomCenterWidget({ + required this.selectionType, + required this.selectedMorphFeature, + required this.shouldDoActivity, + required this.locked, + required this.wordDetailsController, + super.key, + }); + + @override + Widget build(BuildContext context) { + if (selectionType == null) { + return const ToolbarContentLoadingIndicator(); + } + + if (shouldDoActivity || locked) { + return PracticeActivityCard( + pangeaMessageEvent: wordDetailsController.widget.messageEvent, + targetTokensAndActivityType: TargetTokensAndActivityType( + tokens: [wordDetailsController.widget.token], + activityType: selectionType!.activityType, + ), + overlayController: wordDetailsController.widget.overlayController, + morphFeature: selectedMorphFeature, + wordDetailsController: wordDetailsController, + ); + } + + if (selectionType == WordZoomSelection.translation) { + return ContextualTranslationWidget( + token: wordDetailsController.widget.token, + langCode: + wordDetailsController.widget.messageEvent.messageDisplayLangCode, + ); + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ActivityAnswerWidget( + token: wordDetailsController.widget.token, + selectionType: selectionType!, + selectedMorphFeature: selectedMorphFeature, + ), + ], + ), + ); + } +}