From bae5765a978157ce3d2f728698f29d6e12a0d3f7 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:12:01 -0400 Subject: [PATCH] 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 Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com> --- lib/l10n/intl_en.arb | 2 +- .../vocab_analytics_details_view.dart | 238 +++++++-------- .../vocab_analytics_list_view.dart | 1 + lib/pangea/lemmas/lemma_emoji_row.dart | 276 ------------------ .../lemmas/lemma_highlight_emoji_row.dart | 161 ++++++++++ .../activity_type_enum.dart | 2 +- .../emoji_activity_generator.dart | 71 ++--- .../practice_generation_repo.dart | 6 +- .../practice_selection.dart | 2 +- .../word_focus_listening_generator.dart | 35 +-- .../practice_activity_card.dart | 6 +- .../word_zoom/lemma_meaning_widget.dart | 189 ++++++------ 12 files changed, 422 insertions(+), 567 deletions(-) delete mode 100644 lib/pangea/lemmas/lemma_emoji_row.dart create mode 100644 lib/pangea/lemmas/lemma_highlight_emoji_row.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index f74c1feaa..132db1064 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -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": { diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index 5dd9ea0c5..75c751230 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -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, + ); + }, ); } } diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart index e602332b7..9e44e45e3 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart @@ -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, diff --git a/lib/pangea/lemmas/lemma_emoji_row.dart b/lib/pangea/lemmas/lemma_emoji_row.dart deleted file mode 100644 index e4b5ab4fe..000000000 --- a/lib/pangea/lemmas/lemma_emoji_row.dart +++ /dev/null @@ -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 { - 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 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 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 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 { - 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, - ), - ), - ), - ); - } -} diff --git a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart new file mode 100644 index 000000000..00d459d93 --- /dev/null +++ b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart @@ -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 { + 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 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 { + 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, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/practice_activities/activity_type_enum.dart b/lib/pangea/practice_activities/activity_type_enum.dart index c58bdd4e2..681bcf5db 100644 --- a/lib/pangea/practice_activities/activity_type_enum.dart +++ b/lib/pangea/practice_activities/activity_type_enum.dart @@ -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; diff --git a/lib/pangea/practice_activities/emoji_activity_generator.dart b/lib/pangea/practice_activities/emoji_activity_generator.dart index 5d1bb3956..95056d1f2 100644 --- a/lib/pangea/practice_activities/emoji_activity_generator.dart +++ b/lib/pangea/practice_activities/emoji_activity_generator.dart @@ -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 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 _favorite( + Future _matchActivity( MessageActivityRequest req, - BuildContext context, ) async { - final PangeaToken token = req.targetTokens.first; + final Map> matchInfo = {}; + final List>> tokensWithUserEmojis = []; + final List tokensNeedingServerEmojis = []; + //if user saved emojis, use those, otherwise generate. + for (final token in req.targetTokens) { + final List userSavedEmojis = token.vocabConstructID.userSetEmoji; - final List 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 _matchActivity( - MessageActivityRequest req, - BuildContext context, - ) async { - final List> lemmaInfoFutures = req.targetTokens - .map((token) => token.vocabConstructID.getLemmaInfo()) - .toList(); + if (tokensNeedingServerEmojis.isNotEmpty) { + final List> lemmaInfoFutures = + tokensNeedingServerEmojis + .map((token) => token.vocabConstructID.getLemmaInfo()) + .toList(); - final List lemmaInfos = - await Future.wait(lemmaInfoFutures); + final List lemmaInfos = + await Future.wait(lemmaInfoFutures); - final Map> 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( diff --git a/lib/pangea/practice_activities/practice_generation_repo.dart b/lib/pangea/practice_activities/practice_generation_repo.dart index e62bb835c..d0c983e09 100644 --- a/lib/pangea/practice_activities/practice_generation_repo.dart +++ b/lib/pangea/practice_activities/practice_generation_repo.dart @@ -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, diff --git a/lib/pangea/practice_activities/practice_selection.dart b/lib/pangea/practice_activities/practice_selection.dart index e7b60be33..a53180fe2 100644 --- a/lib/pangea/practice_activities/practice_selection.dart +++ b/lib/pangea/practice_activities/practice_selection.dart @@ -139,7 +139,7 @@ class PracticeSelection { } } - tokens.sorted( + tokens.sort( (a, b) { final bScore = b.activityPriorityScore(activityType, null) * (tokenIsIncludedInActivityOfAnyType(b) ? 1.1 : 1); diff --git a/lib/pangea/practice_activities/word_focus_listening_generator.dart b/lib/pangea/practice_activities/word_focus_listening_generator.dart index 7088648d1..2b76f32aa 100644 --- a/lib/pangea/practice_activities/word_focus_listening_generator.dart +++ b/lib/pangea/practice_activities/word_focus_listening_generator.dart @@ -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 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 _multipleChoiceActivity( - MessageActivityRequest req, - BuildContext context, - ) async { - final token = req.targetTokens.first; - final List 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 _matchActivity( MessageActivityRequest req, - BuildContext context, ) async { return MessageActivityResponse( activity: PracticeActivityModel( diff --git a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart index 506521be1..5ae92cb19 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart @@ -45,7 +45,7 @@ class PracticeActivityCard extends StatefulWidget { } class PracticeActivityCardState extends State { - bool fetchingActivity = false; + bool fetchingActivity = true; bool savoringTheJoy = false; Completer? currentActivityCompleter; @@ -62,8 +62,10 @@ class PracticeActivityCardState extends State { @override void initState() { - _fetchActivity(); super.initState(); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _fetchActivity(), + ); } @override diff --git a/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart index 057f73ad0..7ab3a8013 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart @@ -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, ), - ), + ], ), ), ), - ], - ); - }, + ), + ), + ], ); } }