3680 emoji population in vocab page (#3754)

* more consistent emojis and emoji selection in vocab page

- Makes emoji row always visible in vocab page and highlights selection
- selects one by default so more emojis show on the page
- Saves spot in vocab page on navigation
- Doesn't override emoji choice from emoji activity

* code and import formatting

* reduce calls to lemma_definition, remove unused widget file, prevent copy-related errors, don't show emoji activities for messages with less-than 2 relevant tokens

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
pull/2245/head
avashilling 3 months ago committed by GitHub
parent 598820295f
commit bae5765a97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4843,7 +4843,7 @@
"autocorrectNotAvailable": "Unfortunately your platform is not currently supported for this feature. Stay tuned for further development!",
"pleaseUpdateApp": "Please update the app to continue.",
"chooseEmojiInstructionsBody": "Match emojis with the words they best represent. Don't worry! No points off for disagreeing. 😅",
"pickAnEmojiFor": "Pick an emoji for ${lemma}",
"pickAnEmojiFor": "Pick an emoji for {lemma}",
"@pickAnEmojiFor": {
"type": "String",
"placeholders": {

@ -7,13 +7,14 @@ import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popu
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart';
import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.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/phonetic_transcription/phonetic_transcription_widget.dart';
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.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_builder.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -50,136 +51,143 @@ class VocabDetailsView extends StatelessWidget {
? _construct.lemmaCategory.color(context)
: _construct.lemmaCategory.darkColor(context));
return AnalyticsDetailsViewContent(
title: Column(
children: [
LayoutBuilder(
builder: (context, constraints) {
return ShrinkableText(
text: _construct.lemma,
maxWidth: constraints.maxWidth - 40.0,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: textColor,
),
);
},
),
if (MatrixState.pangeaController.languageController.showTrancription)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: PhoneticTranscriptionWidget(
text: _construct.lemma,
textLanguage:
MatrixState.pangeaController.languageController.userL2!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: textColor.withAlpha((0.7 * 255).toInt()),
fontSize: 18,
),
iconSize: _iconSize * 0.8,
return LemmaMeaningBuilder(
langCode: _userL2!,
constructId: _construct.id,
builder: (context, controller) {
return AnalyticsDetailsViewContent(
title: Column(
children: [
LayoutBuilder(
builder: (context, constraints) {
return ShrinkableText(
text: _construct.lemma,
maxWidth: constraints.maxWidth - 40.0,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: textColor,
),
);
},
),
),
],
),
subtitle: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
if (MatrixState
.pangeaController.languageController.showTrancription)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: PhoneticTranscriptionWidget(
text: _construct.lemma,
textLanguage:
MatrixState.pangeaController.languageController.userL2!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: textColor.withAlpha((0.7 * 255).toInt()),
fontSize: 18,
),
iconSize: _iconSize * 0.8,
),
),
],
),
subtitle: Column(
children: [
Text(
getGrammarCopy(
category: "POS",
lemma: _construct.category,
context: context,
) ??
_construct.lemma,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor,
Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
Text(
getGrammarCopy(
category: "POS",
lemma: _construct.category,
context: context,
) ??
_construct.lemma,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor,
),
),
SizedBox(
width: _iconSize,
height: _iconSize,
child: MorphIcon(
morphFeature: MorphFeaturesEnum.Pos,
morphTag: _construct.category,
),
),
],
),
SizedBox(
width: _iconSize,
height: _iconSize,
child: MorphIcon(
morphFeature: MorphFeaturesEnum.Pos,
morphTag: _construct.category,
),
const SizedBox(height: 16.0),
LemmmaHighlightEmojiRow(
controller: controller,
isSelected: false,
cId: constructId,
onTapOverride: null,
iconSize: _iconSize,
),
],
),
const SizedBox(height: 16.0),
LemmaEmojiRow(
isSelected: false,
shouldShowEmojis: true,
cId: constructId,
onTapOverride: null,
emojiSetCallback: () {
debugPrint('Emoji set callback');
},
iconSize: _iconSize,
),
],
),
headerContent: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
spacing: 8.0,
children: [
Align(
alignment: Alignment.topLeft,
child: _userL2 == null
? Text(L10n.of(context).meaningNotFound)
: LemmaMeaningWidget(
constructUse: _construct,
langCode: _userL2!,
style: Theme.of(context).textTheme.bodyLarge,
leading: TextSpan(
text: L10n.of(context).meaningSectionHeader,
headerContent: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
spacing: 8.0,
children: [
Align(
alignment: Alignment.topLeft,
child: _userL2 == null
? Text(L10n.of(context).meaningNotFound)
: LemmaMeaningWidget(
controller: controller,
constructUse: _construct,
style: Theme.of(context).textTheme.bodyLarge,
leading: TextSpan(
text: L10n.of(context).meaningSectionHeader,
style:
Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
),
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,
),
),
),
),
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,
),
),
const SizedBox(width: 6.0),
...forms.mapIndexed(
(i, form) => Row(
mainAxisSize: MainAxisSize.min,
children: [
WordTextWithAudioButton(
text: form,
style:
Theme.of(context).textTheme.bodyLarge?.copyWith(
const SizedBox(width: 6.0),
...forms.mapIndexed(
(i, form) => Row(
mainAxisSize: MainAxisSize.min,
children: [
WordTextWithAudioButton(
text: form,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
color: textColor,
),
uniqueID: "$form-${_construct.lemma}-$i",
langCode: _userL2!,
uniqueID: "$form-${_construct.lemma}-$i",
langCode: _userL2!,
),
if (i != forms.length - 1) const Text(", "),
],
),
if (i != forms.length - 1) const Text(", "),
],
),
),
],
),
],
),
),
],
),
],
),
),
xpIcon: _construct.lemmaCategory.icon(_iconSize + 6.0),
constructId: constructId,
),
xpIcon: _construct.lemmaCategory.icon(_iconSize + 6.0),
constructId: constructId,
);
},
);
}
}

@ -132,6 +132,7 @@ class VocabAnalyticsListView extends StatelessWidget {
),
Expanded(
child: GridView.builder(
key: const PageStorageKey("vocab-analytics-list-view-page-key"),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100.0,
mainAxisExtent: 100.0,

@ -1,276 +0,0 @@
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/app_emojis.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaEmojiRow extends StatefulWidget {
final ConstructIdentifier cId;
final VoidCallback? onTapOverride;
final bool isSelected;
final bool shouldShowEmojis;
final double? iconSize;
/// 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()? emojiSetCallback;
const LemmaEmojiRow({
super.key,
required this.cId,
required this.onTapOverride,
required this.isSelected,
required this.shouldShowEmojis,
this.emojiSetCallback,
this.iconSize,
});
@override
LemmaEmojiRowState createState() => LemmaEmojiRowState();
}
class LemmaEmojiRowState extends State<LemmaEmojiRow> {
String? displayEmoji;
@override
void initState() {
super.initState();
displayEmoji = widget.cId.userSetEmoji.firstOrNull;
}
@override
didUpdateWidget(LemmaEmojiRow oldWidget) {
if (oldWidget.isSelected != widget.isSelected ||
widget.cId.userSetEmoji != oldWidget.cId.userSetEmoji) {
setState(() => displayEmoji = widget.cId.userSetEmoji.firstOrNull);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
MatrixState.pAnyState.closeOverlay(widget.cId.string);
super.dispose();
}
void openEmojiSetOverlay() async {
List<String> emojiChoices = [];
try {
final info = await widget.cId.getLemmaInfo();
emojiChoices = info.emoji;
} catch (e, s) {
for (int i = 0; i < 3; i++) {
emojiChoices
.add(AppEmojis.emojis[Random().nextInt(AppEmojis.emojis.length)]);
}
debugger(when: kDebugMode);
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
}
try {
OverlayUtil.showOverlay(
context: context,
child: EmojiEditOverlay(
cId: widget.cId,
onSelectEmoji: setEmoji,
emojis: emojiChoices,
),
transformTargetId: widget.cId.string,
backDropToDismiss: true,
blurBackground: false,
borderColor: Theme.of(context).colorScheme.primary,
closePrevOverlay: false,
followerAnchor: Alignment.topCenter,
targetAnchor: Alignment.bottomCenter,
);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
}
}
Future<void> setEmoji(String emoji) async {
try {
displayEmoji = emoji;
await widget.cId.setUserLemmaInfo(
UserSetLemmaInfo(
emojis: [emoji],
),
);
if (mounted) {
widget.emojiSetCallback?.call();
setState(() {});
}
MatrixState.pAnyState.closeOverlay();
widget.emojiSetCallback?.call();
setState(() {});
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
}
}
@override
Widget build(BuildContext context) {
return Material(
child: CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(
widget.cId.string,
)
.link,
child: Container(
key: MatrixState.pAnyState
.layerLinkAndKey(
widget.cId.string,
)
.key,
height: 50,
width: 50,
alignment: Alignment.center,
child: displayEmoji != null && widget.shouldShowEmojis
? InkWell(
hoverColor:
Theme.of(context).colorScheme.primary.withAlpha(50),
onTap: widget.onTapOverride ?? openEmojiSetOverlay,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
displayEmoji!,
style: TextStyle(fontSize: widget.iconSize ?? 20),
),
),
)
: WordZoomActivityButton(
icon: Icon(
Icons.add_reaction_outlined,
color: widget.isSelected
? Theme.of(context).colorScheme.primary
: null,
),
isSelected: widget.isSelected,
onPressed: widget.onTapOverride ?? openEmojiSetOverlay,
opacity: widget.isSelected ? 1 : 0.4,
tooltip: MessageMode.wordEmoji.title(context),
),
),
),
);
}
}
class EmojiEditOverlay extends StatelessWidget {
final Function(String) onSelectEmoji;
final ConstructIdentifier cId;
final List<String> emojis;
const EmojiEditOverlay({
super.key,
required this.onSelectEmoji,
required this.cId,
required this.emojis,
});
@override
Widget build(BuildContext context) {
return Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Container(
padding: const EdgeInsets.all(8),
height: 70,
width: 200,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.onSurface.withAlpha(50),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: emojis
.map(
(emoji) => EmojiChoiceItem(
emoji: emoji,
onSelectEmoji: onSelectEmoji,
),
)
.toList(),
),
),
),
);
}
}
class EmojiChoiceItem extends StatefulWidget {
final String emoji;
final Function(String) onSelectEmoji;
const EmojiChoiceItem({
super.key,
required this.emoji,
required this.onSelectEmoji,
});
@override
EmojiChoiceItemState createState() => EmojiChoiceItemState();
}
class EmojiChoiceItemState extends State<EmojiChoiceItem> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTap: () {
debugPrint('Selected emoji: ${widget.emoji}');
if (!mounted) {
return;
}
widget.onSelectEmoji(widget.emoji);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _isHovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
child: Text(
widget.emoji,
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
);
}
}

@ -0,0 +1,161 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart';
class LemmmaHighlightEmojiRow extends StatefulWidget {
final LemmaMeaningBuilderState controller;
final ConstructIdentifier cId;
final VoidCallback? onTapOverride;
final bool isSelected;
final double? iconSize;
const LemmmaHighlightEmojiRow({
super.key,
required this.controller,
required this.cId,
required this.onTapOverride,
required this.isSelected,
this.iconSize,
});
@override
LemmmaHighlightEmojiRowState createState() => LemmmaHighlightEmojiRowState();
}
class LemmmaHighlightEmojiRowState extends State<LemmmaHighlightEmojiRow> {
String? displayEmoji;
@override
void initState() {
super.initState();
displayEmoji = widget.cId.userSetEmoji.firstOrNull;
}
@override
didUpdateWidget(LemmmaHighlightEmojiRow oldWidget) {
if (oldWidget.isSelected != widget.isSelected ||
widget.cId.userSetEmoji != oldWidget.cId.userSetEmoji) {
setState(() => displayEmoji = widget.cId.userSetEmoji.firstOrNull);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
super.dispose();
}
Future<void> setEmoji(String emoji) async {
try {
setState(() => displayEmoji = emoji);
await widget.cId.setUserLemmaInfo(
UserSetLemmaInfo(
emojis: [emoji],
),
);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
}
}
@override
Widget build(BuildContext context) {
if (widget.controller.isLoading) {
return const CircularProgressIndicator.adaptive();
}
final emojis = widget.controller.lemmaInfo?.emoji;
if (widget.controller.error != null || emojis == null || emojis.isEmpty) {
return const SizedBox.shrink();
}
return Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Container(
padding: const EdgeInsets.all(8),
height: 80,
alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: emojis
.map(
(emoji) => EmojiChoiceItem(
emoji: emoji,
onSelectEmoji: () => setEmoji(emoji),
// will highlight selected emoji, or the first emoji if none are selected
isDisplay: (displayEmoji == emoji ||
(displayEmoji == null && emoji == emojis.first)),
),
)
.toList(),
),
),
),
);
}
}
class EmojiChoiceItem extends StatefulWidget {
final String emoji;
final VoidCallback onSelectEmoji;
final bool isDisplay;
const EmojiChoiceItem({
super.key,
required this.emoji,
required this.isDisplay,
required this.onSelectEmoji,
});
@override
EmojiChoiceItemState createState() => EmojiChoiceItemState();
}
class EmojiChoiceItemState extends State<EmojiChoiceItem> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTap: widget.onSelectEmoji,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _isHovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
border: widget.isDisplay
? Border.all(
color: AppConfig.goldLight,
width: 4,
)
: null,
),
child: Text(
widget.emoji,
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
),
);
}
}

@ -198,9 +198,9 @@ extension ActivityTypeExtension on ActivityTypeEnum {
case ActivityTypeEnum.wordMeaning:
case ActivityTypeEnum.lemmaId:
case ActivityTypeEnum.wordFocusListening:
case ActivityTypeEnum.emoji:
return 2;
case ActivityTypeEnum.hiddenWordListening:
case ActivityTypeEnum.emoji:
case ActivityTypeEnum.morphId:
case ActivityTypeEnum.messageMeaning:
return 1;

@ -1,65 +1,56 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.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/practice_activities/practice_match.dart';
class EmojiActivityGenerator {
Future<MessageActivityResponse> get(
MessageActivityRequest req,
BuildContext context,
) async {
if (req.targetTokens.length == 1) {
return _favorite(req, context);
} else {
return _matchActivity(req, context);
if (req.targetTokens.length <= 1) {
throw Exception("Emoji activity requires at least 2 tokens");
}
return _matchActivity(req);
}
Future<MessageActivityResponse> _favorite(
Future<MessageActivityResponse> _matchActivity(
MessageActivityRequest req,
BuildContext context,
) async {
final PangeaToken token = req.targetTokens.first;
final Map<ConstructForm, List<String>> matchInfo = {};
final List<MapEntry<PangeaToken, List<String>>> tokensWithUserEmojis = [];
final List<PangeaToken> tokensNeedingServerEmojis = [];
//if user saved emojis, use those, otherwise generate.
for (final token in req.targetTokens) {
final List<String> userSavedEmojis = token.vocabConstructID.userSetEmoji;
final List<String> emojis = await token.getEmojiChoices();
if (userSavedEmojis.isNotEmpty) {
tokensWithUserEmojis.add(MapEntry(token, userSavedEmojis));
} else {
tokensNeedingServerEmojis.add(token);
}
}
return MessageActivityResponse(
activity: PracticeActivityModel(
activityType: ActivityTypeEnum.emoji,
targetTokens: [token],
langCode: req.userL2,
multipleChoiceContent: MultipleChoiceActivity(
question: L10n.of(context).pickAnEmojiFor(token.lemma.text),
choices: emojis,
answers: emojis,
spanDisplayDetails: null,
),
),
);
}
for (final entry in tokensWithUserEmojis) {
matchInfo[entry.key.vocabForm] = entry.value;
}
Future<MessageActivityResponse> _matchActivity(
MessageActivityRequest req,
BuildContext context,
) async {
final List<Future<LemmaInfoResponse>> lemmaInfoFutures = req.targetTokens
.map((token) => token.vocabConstructID.getLemmaInfo())
.toList();
if (tokensNeedingServerEmojis.isNotEmpty) {
final List<Future<LemmaInfoResponse>> lemmaInfoFutures =
tokensNeedingServerEmojis
.map((token) => token.vocabConstructID.getLemmaInfo())
.toList();
final List<LemmaInfoResponse> lemmaInfos =
await Future.wait(lemmaInfoFutures);
final List<LemmaInfoResponse> lemmaInfos =
await Future.wait(lemmaInfoFutures);
final Map<ConstructForm, List<String>> matchInfo = Map.fromIterables(
req.targetTokens.map((token) => token.vocabForm),
lemmaInfos.map((e) => e.emoji),
);
for (int i = 0; i < tokensNeedingServerEmojis.length; i++) {
matchInfo[tokensNeedingServerEmojis[i].vocabForm] = lemmaInfos[i].emoji;
}
}
return MessageActivityResponse(
activity: PracticeActivityModel(

@ -48,7 +48,7 @@ class PracticeRepo {
final _morph = MorphActivityGenerator();
final _emoji = EmojiActivityGenerator();
final _lemma = LemmaActivityGenerator();
final _wordFoocusListening = WordFocusListeningGenerator();
final _wordFocusListening = WordFocusListeningGenerator();
final _wordMeaning = LemmaMeaningActivityGenerator();
PracticeRepo() {
@ -129,7 +129,7 @@ class PracticeRepo {
// some activities we'll get from the server and others we'll generate locally
switch (req.targetType) {
case ActivityTypeEnum.emoji:
return _emoji.get(req, context);
return _emoji.get(req);
case ActivityTypeEnum.lemmaId:
return _lemma.get(req, context);
case ActivityTypeEnum.morphId:
@ -139,7 +139,7 @@ class PracticeRepo {
return _wordMeaning.get(req);
case ActivityTypeEnum.messageMeaning:
case ActivityTypeEnum.wordFocusListening:
return _wordFoocusListening.get(req, context);
return _wordFocusListening.get(req);
case ActivityTypeEnum.hiddenWordListening:
return _fetchFromServer(
accessToken: accessToken,

@ -139,7 +139,7 @@ class PracticeSelection {
}
}
tokens.sorted(
tokens.sort(
(a, b) {
final bScore = b.activityPriorityScore(activityType, null) *
(tokenIsIncludedInActivityOfAnyType(b) ? 1.1 : 1);

@ -1,13 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.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/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/practice_activities/practice_match.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -15,40 +12,18 @@ import 'package:fluffychat/widgets/matrix.dart';
class WordFocusListeningGenerator {
Future<MessageActivityResponse> get(
MessageActivityRequest req,
BuildContext context,
) async {
if (req.targetTokens.length == 1) {
return _multipleChoiceActivity(req, context);
} else {
return _matchActivity(req, context);
if (req.targetTokens.length <= 1) {
throw Exception(
"Word focus listening activity requires at least 2 tokens",
);
}
}
Future<MessageActivityResponse> _multipleChoiceActivity(
MessageActivityRequest req,
BuildContext context,
) async {
final token = req.targetTokens.first;
final List<String> choices = await lemmaActivityDistractors(token);
return MessageActivityResponse(
activity: PracticeActivityModel(
activityType: ActivityTypeEnum.wordFocusListening,
targetTokens: [token],
langCode: req.userL2,
multipleChoiceContent: MultipleChoiceActivity(
question: L10n.of(context).wordFocusListeningMultipleChoice,
choices: choices,
answers: [token.lemma.text],
spanDisplayDetails: null,
),
),
);
return _matchActivity(req);
}
Future<MessageActivityResponse> _matchActivity(
MessageActivityRequest req,
BuildContext context,
) async {
return MessageActivityResponse(
activity: PracticeActivityModel(

@ -45,7 +45,7 @@ class PracticeActivityCard extends StatefulWidget {
}
class PracticeActivityCardState extends State<PracticeActivityCard> {
bool fetchingActivity = false;
bool fetchingActivity = true;
bool savoringTheJoy = false;
Completer<PracticeActivityEvent?>? currentActivityCompleter;
@ -62,8 +62,10 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
@override
void initState() {
_fetchActivity();
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) => _fetchActivity(),
);
}
@override

@ -10,132 +10,125 @@ import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart';
class LemmaMeaningWidget extends StatelessWidget {
final LemmaMeaningBuilderState controller;
final ConstructUses constructUse;
final String langCode;
final TextStyle? style;
final InlineSpan? leading;
const LemmaMeaningWidget({
super.key,
required this.controller,
required this.constructUse,
required this.langCode,
this.style,
this.leading,
});
@override
Widget build(BuildContext context) {
return LemmaMeaningBuilder(
langCode: langCode,
constructId: constructUse.id,
builder: (context, controller) {
if (controller.isLoading) {
return const TextLoadingShimmer();
}
if (controller.isLoading) {
return const TextLoadingShimmer();
}
if (controller.error != null) {
debugger(when: kDebugMode);
return ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
style: style,
);
}
if (controller.error != null) {
debugger(when: kDebugMode);
return ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
style: style,
);
}
if (controller.editMode) {
controller.controller.text = controller.lemmaInfo?.meaning ?? "";
return Material(
type: MaterialType.transparency,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(
constructUse.lemma,
constructUse.category,
)}",
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic),
if (controller.editMode) {
controller.controller.text = controller.lemmaInfo?.meaning ?? "";
return Material(
type: MaterialType.transparency,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(
constructUse.lemma,
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.controller,
decoration: InputDecoration(
hintText: controller.lemmaInfo?.meaning,
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextField(
minLines: 1,
maxLines: 3,
controller: controller.controller,
decoration: InputDecoration(
hintText: controller.lemmaInfo?.meaning,
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => controller.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: () => controller.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.controller.text !=
controller.lemmaInfo?.meaning &&
controller.controller.text.isNotEmpty
? controller
.editLemmaMeaning(controller.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),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () => controller.controller.text !=
controller.lemmaInfo?.meaning &&
controller.controller.text.isNotEmpty
? controller.editLemmaMeaning(controller.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),
),
],
),
);
}
],
),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: L10n.of(context).doubleClickToEdit,
child: GestureDetector(
onLongPress: () => controller.toggleEditMode(true),
onDoubleTap: () => controller.toggleEditMode(true),
child: RichText(
textAlign:
leading == null ? TextAlign.center : TextAlign.start,
text: TextSpan(
style: style,
children: [
if (leading != null) leading!,
if (leading != null)
const WidgetSpan(child: SizedBox(width: 6.0)),
TextSpan(
text: controller.lemmaInfo?.meaning,
),
],
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: L10n.of(context).doubleClickToEdit,
child: GestureDetector(
onLongPress: () => controller.toggleEditMode(true),
onDoubleTap: () => controller.toggleEditMode(true),
child: RichText(
textAlign: leading == null ? TextAlign.center : TextAlign.start,
text: TextSpan(
style: style,
children: [
if (leading != null) leading!,
if (leading != null)
const WidgetSpan(child: SizedBox(width: 6.0)),
TextSpan(
text: controller.lemmaInfo?.meaning,
),
),
],
),
),
),
],
);
},
),
),
],
);
}
}

Loading…
Cancel
Save