feat: show lemma under token, give lemma activity first if applicable (#1347)

pull/1593/head
ggurdin 10 months ago committed by GitHub
parent 317cf88aca
commit 14a1ae2287
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4659,5 +4659,6 @@
"publicProfileTitle": "Allow my profile to be found in search", "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", "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.", "clickWordsInstructions": "Click on individual words for more activities.",
"chooseBestDefinition": "Choose the best definition" "chooseBestDefinition": "Choose the best definition",
"chooseBaseForm": "Choose the base form"
} }

@ -217,6 +217,8 @@ IconData getIconForMorphFeature(String feature) {
return Icons.swap_vert; return Icons.swap_vert;
case 'definite': case 'definite':
return Icons.check_circle_outline; return Icons.check_circle_outline;
case 'prepcase':
return Icons.location_on_outlined;
default: default:
debugger(when: kDebugMode); debugger(when: kDebugMode);
return Icons.help_outline; return Icons.help_outline;

@ -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/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class LemmaActivityGenerator { class LemmaActivityGenerator {
Future<MessageActivityResponse> get( Future<MessageActivityResponse> get(
MessageActivityRequest req, MessageActivityRequest req,
BuildContext context,
) async { ) async {
debugger(when: kDebugMode && req.targetTokens.length != 1); debugger(when: kDebugMode && req.targetTokens.length != 1);
@ -26,7 +29,7 @@ class LemmaActivityGenerator {
tgtConstructs: [token.vocabConstructID], tgtConstructs: [token.vocabConstructID],
langCode: req.userL2, langCode: req.userL2,
content: ActivityContent( content: ActivityContent(
question: "", question: L10n.of(context).chooseBaseForm,
choices: choices, choices: choices,
answers: [token.lemma.text], answers: [token.lemma.text],
spanDisplayDetails: null, spanDisplayDetails: null,

@ -127,7 +127,7 @@ class PracticeGenerationController {
case ActivityTypeEnum.emoji: case ActivityTypeEnum.emoji:
return _emoji.get(req); return _emoji.get(req);
case ActivityTypeEnum.lemmaId: case ActivityTypeEnum.lemmaId:
return _lemma.get(req); return _lemma.get(req, context);
case ActivityTypeEnum.morphId: case ActivityTypeEnum.morphId:
return _morph.get(req); return _morph.get(req);
case ActivityTypeEnum.wordMeaning: case ActivityTypeEnum.wordMeaning:

@ -1,25 +1,41 @@
import 'package:fluffychat/pangea/models/pangea_token_model.dart'; 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'; import 'package:flutter/material.dart';
class LemmaWidget extends StatelessWidget { class LemmaWidget extends StatelessWidget {
final PangeaToken token; final PangeaToken token;
final VoidCallback onPressed;
final bool isSelected;
const LemmaWidget({ const LemmaWidget({
super.key, super.key,
required this.token, required this.token,
required this.onPressed,
this.isSelected = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WordZoomActivityButton( return Padding(
icon: Text(token.xpEmoji), padding: const EdgeInsets.all(4.0),
isSelected: isSelected, child: Text("${token.lemma.text} ${token.xpEmoji}"),
onPressed: onPressed,
); );
} }
} }
// 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,
// );
// }
// }

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.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/repo/lemma_definition_repo.dart';
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.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/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/chat/tts_controller.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/emoji_practice_button.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/emoji_practice_button.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
@ -59,17 +62,17 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
/// The currently selected word zoom activity type. /// The currently selected word zoom activity type.
/// If an activity should be shown for this type, shows that activity. /// If an activity should be shown for this type, shows that activity.
/// If not, shows the info related to that activity type. /// If not, shows the info related to that activity type.
/// Defaults to the lemma translation. /// Null if not set yet, since it takes a second to determine if the lemma activity can be shown.
WordZoomSelection _selectionType = WordZoomSelection.translation; WordZoomSelection? _selectionType;
/// If doing a morphological activity, this is the selected morph feature. /// If doing a morphological activity, this is the selected morph feature.
String? _selectedMorphFeature; 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. /// Used to show the practice activity card's savor the joy animation.
/// (Analytics sending triggers the point gain animation, do also /// (Analytics sending triggers the point gain animation, do also
/// causes shouldDoActivity to be false. This is a workaround.) /// causes shouldDoActivity to be false. This is a workaround.)
bool _forceShowActivity = false; Completer<void>? _activityLock;
// The function to determine if lemma distractors can be generated // The function to determine if lemma distractors can be generated
// is computationally expensive, so we only do it once // is computationally expensive, so we only do it once
@ -84,7 +87,7 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_setCanGenerateLemmaActivity(); _setInitialSelectionType();
} }
@override @override
@ -92,51 +95,74 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.token != oldWidget.token) { if (widget.token != oldWidget.token) {
_clean(); _clean();
_setCanGenerateLemmaActivity(); _setInitialSelectionType();
} }
} }
void _clean() { void _clean() {
if (mounted) { if (mounted) {
setState(() { setState(() {
_selectionType = WordZoomSelection.translation; _activityLock = null;
_selectionType = null;
_selectedMorphFeature = null; _selectedMorphFeature = null;
_forceShowActivity = false;
}); });
} }
} }
void _setCanGenerateLemmaActivity() { Future<void> _setCanGenerateLemmaActivity() async {
widget.token.canGenerateDistractors(ActivityTypeEnum.lemmaId).then((value) { final canGenerate =
if (mounted) setState(() => _canGenerateLemmaActivity = value); await widget.token.canGenerateDistractors(ActivityTypeEnum.lemmaId);
}); if (mounted) setState(() => _canGenerateLemmaActivity = canGenerate);
}
Future<void> _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<void> _setSelectionType(
WordZoomSelection type, {
String? feature,
}) async {
WordZoomSelection newSelectedType = type; WordZoomSelection newSelectedType = type;
String? newSelectedFeature; String? newSelectedFeature;
if (type != WordZoomSelection.morph) { if (type != WordZoomSelection.morph) {
// if setting selectionType to non-morph activity, either set it if it's not // if setting selectionType to non-morph activity, either set it if it's not
// already selected, or reset to it the default type // already selected, or reset to it the default type
newSelectedType = newSelectedType = _selectionType == type ? _defaultSelectionType : type;
_selectionType == type ? WordZoomSelection.translation : type;
} else { } else {
// otherwise (because there could be multiple different morph features), check // 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 the feature is already selected, and if so, reset to the default type.
// if not, set the selectionType and feature // if not, set the selectionType and feature
newSelectedFeature = _selectedMorphFeature == feature ? null : feature; newSelectedFeature = _selectedMorphFeature == feature ? null : feature;
newSelectedType = newSelectedFeature == null newSelectedType = newSelectedFeature == null
? WordZoomSelection.translation ? _defaultSelectionType
: WordZoomSelection.morph; : WordZoomSelection.morph;
} }
// wait for savor the joy animation to finish before changing the selection type
if (_activityLock != null) await _activityLock!.future;
_selectionType = newSelectedType; _selectionType = newSelectedType;
_selectedMorphFeature = newSelectedFeature; _selectedMorphFeature = newSelectedFeature;
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
void _setForceShowActivity(bool showActivity) { void _lockActivity() {
if (mounted) setState(() => _forceShowActivity = showActivity); 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 /// This function should be called before overlayController.onActivityFinish to
@ -145,79 +171,27 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
void onActivityFinish({ void onActivityFinish({
Duration savorTheJoyDuration = const Duration(seconds: 1), Duration savorTheJoyDuration = const Duration(seconds: 1),
}) { }) {
_setForceShowActivity(true); _lockActivity();
Future.delayed(savorTheJoyDuration, () { Future.delayed(savorTheJoyDuration, () {
_setForceShowActivity(false); if (_selectionType == WordZoomSelection.lemma) {
_setSelectionType(WordZoomSelection.translation);
}
_unlockActivity();
}); });
} }
Widget get _wordZoomCenterWidget { bool _shouldShowActivity(WordZoomSelection selection) =>
final showActivity = widget.token.shouldDoActivity( widget.token.shouldDoActivity(
a: _selectionType.activityType, a: selection.activityType,
feature: _selectedMorphFeature, feature: _selectedMorphFeature,
tag: _selectedMorphFeature == null tag: _selectedMorphFeature == null
? null ? null
: widget.token.morph[_selectedMorphFeature], : widget.token.morph[_selectedMorphFeature],
) && ) &&
(_selectionType != WordZoomSelection.lemma || (selection != WordZoomSelection.lemma || _canGenerateLemmaActivity) &&
_canGenerateLemmaActivity) && (selection != WordZoomSelection.translation ||
(_selectionType != WordZoomSelection.translation ||
_canGenerateDefintionActivity); _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();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ConstrainedBox( return ConstrainedBox(
@ -245,6 +219,7 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
EmojiPracticeButton( EmojiPracticeButton(
token: widget.token, token: widget.token,
@ -252,21 +227,37 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
_setSelectionType(WordZoomSelection.emoji), _setSelectionType(WordZoomSelection.emoji),
isSelected: _selectionType == WordZoomSelection.emoji, isSelected: _selectionType == WordZoomSelection.emoji,
), ),
Column(
mainAxisSize: MainAxisSize.min,
children: [
WordTextWithAudioButton( WordTextWithAudioButton(
text: widget.token.text.content, text: widget.token.text.content,
ttsController: widget.tts, ttsController: widget.tts,
eventID: widget.messageEvent.eventId, 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( LemmaWidget(
token: widget.token, token: widget.token,
onPressed: () =>
_setSelectionType(WordZoomSelection.lemma),
isSelected: _selectionType == WordZoomSelection.lemma,
), ),
], ],
), ),
const SizedBox(width: 40),
],
),
),
WordZoomCenterWidget(
selectionType: _selectionType,
selectedMorphFeature: _selectedMorphFeature,
shouldDoActivity: _selectionType != null
? _shouldShowActivity(_selectionType!)
: false,
locked:
_activityLock != null && !_activityLock!.isCompleted,
wordDetailsController: this,
), ),
_wordZoomCenterWidget,
MorphologicalListWidget( MorphologicalListWidget(
token: widget.token, token: widget.token,
setMorphFeature: (feature) => _setSelectionType( setMorphFeature: (feature) => _setSelectionType(
@ -284,3 +275,101 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
); );
} }
} }
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,
),
],
),
);
}
}

Loading…
Cancel
Save