feat: show lemma under token, give lemma activity first if applicable (#1347)
This commit is contained in:
parent
317cf88aca
commit
14a1ae2287
6 changed files with 220 additions and 109 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<MessageActivityResponse> 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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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<WordZoomWidget> {
|
|||
/// 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<void>? _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<WordZoomWidget> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setCanGenerateLemmaActivity();
|
||||
_setInitialSelectionType();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -92,51 +95,74 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
|
|||
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<void> _setCanGenerateLemmaActivity() async {
|
||||
final canGenerate =
|
||||
await widget.token.canGenerateDistractors(ActivityTypeEnum.lemmaId);
|
||||
if (mounted) setState(() => _canGenerateLemmaActivity = canGenerate);
|
||||
}
|
||||
|
||||
void _setSelectionType(WordZoomSelection type, {String? feature}) {
|
||||
Future<void> _setInitialSelectionType() async {
|
||||
if (_selectionType != null) return;
|
||||
await _setCanGenerateLemmaActivity();
|
||||
_setSelectionType(_defaultSelectionType);
|
||||
}
|
||||
|
||||
WordZoomSelection get _defaultSelectionType =>
|
||||
_shouldShowActivity(WordZoomSelection.lemma)
|
||||
? WordZoomSelection.lemma
|
||||
: WordZoomSelection.translation;
|
||||
|
||||
Future<void> _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<WordZoomWidget> {
|
|||
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<WordZoomWidget> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
EmojiPracticeButton(
|
||||
token: widget.token,
|
||||
|
|
@ -252,21 +227,37 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
|
|||
_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<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…
Add table
Reference in a new issue