diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 03f46373c..edf406046 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -4589,6 +4589,7 @@ "constructUseIncMDesc": "Incorrect in grammar activity", "constructUseIgnMDesc": "Ignored in grammar activity", "constructUseEmojiDesc": "Correct in emoji activity", + "constructUseCollected": "Collected in chat", "constructUseNanDesc": "Not applicable", "xpIntoLevel": "{currentXP} / {maxXP} XP", "@xpIntoLevel": { diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index eae69be7b..160b34857 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -388,6 +388,10 @@ class HtmlMessage extends StatelessWidget { ? isSelected!.call(token) : false; + final isNew = token != null && + overlayController != null && + overlayController!.isNewToken(token); + final tokenWidth = renderer.tokenTextWidthForContainer( context, node.text, @@ -419,6 +423,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, selected, + isNew, ), ), width: tokenWidth, @@ -442,6 +447,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, selected, + isNew, ), ), linkStyle: linkStyle, @@ -578,6 +584,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, false, + false, ), ), ), @@ -593,6 +600,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, false, + false, ), ), // Pangea# diff --git a/lib/pangea/analytics_misc/construct_use_type_enum.dart b/lib/pangea/analytics_misc/construct_use_type_enum.dart index ef29da935..741480bca 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -66,6 +66,9 @@ enum ConstructUseTypeEnum { incMM, ignMM, + /// lemma collected by clicking on it + click, + /// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client nan } @@ -135,6 +138,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { return L10n.of(context).constructUseIncMmDesc; case ConstructUseTypeEnum.ignMM: return L10n.of(context).constructUseIgnMmDesc; + case ConstructUseTypeEnum.click: + return L10n.of(context).constructUseCollected; case ConstructUseTypeEnum.nan: return L10n.of(context).constructUseNanDesc; } @@ -185,6 +190,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.nan: return Icons.help; + case ConstructUseTypeEnum.click: + return Icons.format_color_text; } } @@ -211,6 +218,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corIGC: case ConstructUseTypeEnum.corL: + case ConstructUseTypeEnum.click: return 2; case ConstructUseTypeEnum.corIt: @@ -279,6 +287,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incMM: case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.em: + case ConstructUseTypeEnum.click: case ConstructUseTypeEnum.nan: return false; } @@ -318,6 +327,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incMM: case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.em: + case ConstructUseTypeEnum.click: return LearningSkillsEnum.reading; case ConstructUseTypeEnum.pvm: return LearningSkillsEnum.speaking; @@ -364,6 +374,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.ignL: case ConstructUseTypeEnum.ignM: case ConstructUseTypeEnum.ignMM: + case ConstructUseTypeEnum.click: case ConstructUseTypeEnum.nan: return null; } diff --git a/lib/pangea/analytics_summary/learning_progress_indicators.dart b/lib/pangea/analytics_summary/learning_progress_indicators.dart index a10829812..6c0002db0 100644 --- a/lib/pangea/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/analytics_summary/learning_progress_indicators.dart @@ -1,5 +1,9 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart'; @@ -9,8 +13,6 @@ import 'package:fluffychat/pangea/analytics_summary/progress_indicator.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; /// A summary of "My Analytics" shown at the top of the chat list /// It shows a variety of progress indicators such as diff --git a/lib/pangea/analytics_summary/progress_indicator.dart b/lib/pangea/analytics_summary/progress_indicator.dart index 130ad0650..1ededda3c 100644 --- a/lib/pangea/analytics_summary/progress_indicator.dart +++ b/lib/pangea/analytics_summary/progress_indicator.dart @@ -30,13 +30,9 @@ class ProgressIndicatorBadge extends StatelessWidget { ), const SizedBox(width: 6.0), !loading - ? Text( - points.toString(), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: indicator.color(context), - ), + ? _AnimatedFloatingNumber( + number: points, + indicator: indicator, ) : const SizedBox( height: 8, @@ -50,3 +46,88 @@ class ProgressIndicatorBadge extends StatelessWidget { ); } } + +class _AnimatedFloatingNumber extends StatefulWidget { + final int number; + final ProgressIndicatorEnum indicator; + + const _AnimatedFloatingNumber({ + required this.number, + required this.indicator, + }); + + @override + State<_AnimatedFloatingNumber> createState() => + _AnimatedFloatingNumberState(); +} + +class _AnimatedFloatingNumberState extends State<_AnimatedFloatingNumber> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnim; + late Animation _offsetAnim; + int? _lastNumber; + int? _floatingNumber; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, duration: const Duration(milliseconds: 900)); + _fadeAnim = CurvedAnimation(parent: _controller, curve: Curves.easeOut); + _offsetAnim = Tween( + begin: const Offset(0, 0), + end: const Offset(0, -0.7), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + _lastNumber = widget.number; + } + + @override + void didUpdateWidget(covariant _AnimatedFloatingNumber oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.number > _lastNumber!) { + _floatingNumber = widget.number; + _controller.forward(from: 0.0).then((_) { + setState(() { + _lastNumber = widget.number; + _floatingNumber = null; + }); + }); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final TextStyle indicatorStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: widget.indicator.color(context), + ); + return Stack( + alignment: Alignment.center, + children: [ + if (_floatingNumber != null) + SlideTransition( + position: _offsetAnim, + child: FadeTransition( + opacity: ReverseAnimation(_fadeAnim), + child: Text( + "$_floatingNumber", + style: indicatorStyle, + ), + ), + ), + Text( + widget.number.toString(), + style: indicatorStyle, + ), + ], + ); + } +} diff --git a/lib/pangea/toolbar/utils/token_rendering_util.dart b/lib/pangea/toolbar/utils/token_rendering_util.dart index 8efbeeedc..2a61659a5 100644 --- a/lib/pangea/toolbar/utils/token_rendering_util.dart +++ b/lib/pangea/toolbar/utils/token_rendering_util.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; @@ -85,7 +86,10 @@ class TokenRenderingUtil { } } - Color backgroundColor(BuildContext context, bool selected) { + Color backgroundColor(BuildContext context, bool selected, bool isNew) { + if (isNew) { + return AppConfig.success; + } return selected ? Theme.of(context).colorScheme.primary : Colors.white.withAlpha(0); diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 221719490..1b1412f88 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -11,6 +11,10 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.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'; @@ -104,6 +108,8 @@ class MessageOverlayController extends State double maxWidth = AppConfig.toolbarMinWidth; + List newTokens = []; + ///////////////////////////////////// /// Lifecycle ///////////////////////////////////// @@ -115,6 +121,13 @@ class MessageOverlayController extends State WidgetsBinding.instance.addPostFrameCallback( (_) => widget.chatController.setSelectedEvent(event), ); + newTokens = pangeaMessageEvent?.messageDisplayRepresentation?.tokens + ?.where((token) { + return token.lemma.saveVocab == true && + token.vocabConstruct.uses.isEmpty && + messageInUserL2; + }).toList() ?? + []; } @override @@ -557,6 +570,44 @@ class MessageOverlayController extends State } updateSelectedSpan(token.text); + + if (isNewToken(token)) { + Future.delayed(const Duration(milliseconds: 1700), () { + MatrixState.pangeaController.putAnalytics.setState( + AnalyticsStream( + eventId: event.eventId, + roomId: event.room.id, + constructs: [ + OneConstructUse( + useType: ConstructUseTypeEnum.click, + lemma: token.lemma.text, + constructType: ConstructTypeEnum.vocab, + metadata: ConstructUseMetaData( + roomId: event.room.id, + timeStamp: DateTime.now(), + eventId: event.eventId, + ), + category: token.pos, + form: token.text.content, + xp: ConstructUseTypeEnum.click.pointValue, + ), + ], + targetID: token.text.uniqueKey, + ), + ); + + if (mounted) { + setState(() { + newTokens.removeWhere( + (t) => + t.text.offset == token.text.offset && + t.text.length == token.text.length && + t.lemma.text.equals(token.lemma.text), + ); + }); + } + }); + } } PracticeTarget? practiceTargetForToken(PangeaToken token) { @@ -573,6 +624,15 @@ class MessageOverlayController extends State return isSelected; } + bool isNewToken(PangeaToken token) { + if (newTokens.isEmpty) return false; + return newTokens.any( + (t) => + t.text.offset == token.text.offset && + t.text.length == token.text.length, + ); + } + bool isTokenHighlighted(PangeaToken token) { if (_highlightedTokens == null) return false; return _highlightedTokens!.any( diff --git a/lib/pangea/toolbar/widgets/reading_assistance_content.dart b/lib/pangea/toolbar/widgets/reading_assistance_content.dart index 81f74d405..ff0cadfd9 100644 --- a/lib/pangea/toolbar/widgets/reading_assistance_content.dart +++ b/lib/pangea/toolbar/widgets/reading_assistance_content.dart @@ -120,6 +120,8 @@ class ReadingAssistanceContentState extends State { token: widget.overlayController.selectedToken!, messageEvent: widget.overlayController.pangeaMessageEvent!, overlayController: widget.overlayController, + wordIsNew: widget.overlayController + .isNewToken(widget.overlayController.selectedToken!), ); } } diff --git a/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart b/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart index aae00b6e9..579c65e1c 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart @@ -1,5 +1,11 @@ import 'dart:developer'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -20,10 +26,6 @@ import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_sel import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:material_symbols_icons/symbols.dart'; class MorphologicalListItem extends StatelessWidget { final MorphFeaturesEnum morphFeature; diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart new file mode 100644 index 000000000..a1b893b53 --- /dev/null +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -0,0 +1,158 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; + +class NewWordOverlay extends StatefulWidget { + final Color overlayColor; + final GlobalKey cardKey; + + const NewWordOverlay({ + super.key, + required this.overlayColor, + required this.cardKey, + }); + + @override + State createState() => _NewWordOverlayState(); +} + +class _NewWordOverlayState extends State + with TickerProviderStateMixin { + AnimationController? _controller; + Animation? _xpScaleAnim; + Animation? _fadeAnim; + Size cardSize = const Size(0, 0); + Offset cardPosition = const Offset(0, 0); + OverlayEntry? _overlayEntry; + bool columnMode = false; + Widget? get svg => ConstructLevelEnum.seeds.icon(); + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1700), + ); + _xpScaleAnim = CurvedAnimation( + parent: _controller!, + curve: const Interval(0.0, 0.6, curve: Curves.easeInOut), + ); + _fadeAnim = CurvedAnimation( + parent: _controller!, + curve: const Interval(0.7, 1.0, curve: Curves.easeOut), + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + columnMode = FluffyThemes.isColumnMode(context); + calculateSizeAndPosition(); + _showFlyingWidget(); + _controller?.forward(); + }); + } + + @override + void dispose() { + _overlayEntry?.remove(); + _controller?.dispose(); + super.dispose(); + } + + void calculateSizeAndPosition() { + //find position of word card and overlaybox(chat view) to figure out where seed should start + final RenderBox? cardBox = + widget.cardKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? overlayBox = + Overlay.of(context).context.findRenderObject() as RenderBox?; + if (cardBox != null && overlayBox != null) { + final cardGlobal = cardBox.localToGlobal(Offset.zero); + final overlayGlobal = overlayBox.localToGlobal(Offset.zero); + setState(() { + cardPosition = cardGlobal - overlayGlobal; + cardSize = cardBox.size; + }); + } + } + + void _showFlyingWidget() { + _overlayEntry?.remove(); // Remove any existing overlay + if (_controller == null || _xpScaleAnim == null || _fadeAnim == null) { + return; + } + _overlayEntry = OverlayEntry( + builder: (context) => AnimatedBuilder( + animation: _controller!, + builder: (context, child) { + final scale = _xpScaleAnim!.value; + final fade = 1.0 - (_fadeAnim!.value); + double t = 0.0; + if ((_controller!.value) >= 0.7) { + t = ((_controller!.value) - 0.7) / 0.3; + t = t.clamp(0.0, 1.0); + } + //move starting position as seed grows so it stays centered + final seedSize = 75 * scale * ((!columnMode) ? fade : 1); + final startX = cardPosition.dx + cardSize.width / 2 - seedSize; + final startY = cardPosition.dy + cardSize.height / 2 + 20 - seedSize; + //end is top left if column mode (going towards vocab stats) or top right of card otherwise + final endX = (columnMode) ? 0.0 : cardPosition.dx + cardSize.width; + final endY = (columnMode) ? 0.0 : cardPosition.dy + 30; + final currentX = startX * (1 - t) + endX * t; + final currentY = startY * (1 - t) + endY * t; + //Grows into frame, and then shrinks if going to top right so it matches card seed size + + return Positioned( + left: currentX, + top: currentY, + child: Opacity( + opacity: fade, + child: Transform.rotate( + angle: scale * 2 * pi, + child: SizedBox( + //if going to card top right, shrinks as it moves to match word card seed size + width: seedSize, + height: seedSize, + child: svg ?? const SizedBox(), + ), + ), + ), + ); + }, + ), + ); + Overlay.of(context).insert(_overlayEntry!); + _controller?.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _overlayEntry?.remove(); + _overlayEntry = null; + } + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + height: cardSize.height, + width: cardSize.width, + color: Colors.transparent, + ), + Positioned( + left: 5, + right: 5, + top: 50, + bottom: 5, + child: Container( + height: cardSize.height, + width: cardSize.width, + color: widget.overlayColor, + ), + ), + ], + ); + } +} diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index f320523c3..11f3376b2 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -15,18 +15,21 @@ import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart'; import 'package:fluffychat/widgets/matrix.dart'; class WordZoomWidget extends StatelessWidget { final PangeaToken token; final PangeaMessageEvent messageEvent; final MessageOverlayController overlayController; + final bool wordIsNew; const WordZoomWidget({ super.key, required this.token, required this.messageEvent, required this.overlayController, + required this.wordIsNew, }); PangeaToken get _selectedToken => overlayController.selectedToken!; @@ -41,202 +44,220 @@ class WordZoomWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(12.0), - constraints: const BoxConstraints( - minHeight: AppConfig.toolbarMinHeight - 8, - maxHeight: AppConfig.toolbarMaxHeight - 8, - maxWidth: AppConfig.toolbarMinWidth, - ), - child: SingleChildScrollView( - child: Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + final GlobalKey cardKey = GlobalKey(); + final overlayColor = Theme.of(context).scaffoldBackgroundColor; + return Stack( + children: [ + Container( + key: cardKey, + padding: const EdgeInsets.all(12.0), + constraints: const BoxConstraints( + minHeight: AppConfig.toolbarMinHeight - 8, + maxHeight: AppConfig.toolbarMaxHeight - 8, + maxWidth: AppConfig.toolbarMinWidth, + ), + child: SingleChildScrollView( + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: 24.0, - height: 24.0, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => overlayController.updateSelectedSpan( - token.text, - ), - child: const Icon( - Icons.close, - size: 16.0, - ), - ), - ), - ), - Flexible( - child: Text( - token.text.content, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 32.0, - fontWeight: FontWeight.w600, - height: 1.2, - color: Theme.of(context).brightness == Brightness.light - ? AppConfig.yellowDark - : AppConfig.yellowLight, - ), - ), - ), - ConstructXpWidget( - id: token.vocabConstructID, - onTap: () => context.go( - "/rooms/analytics?mode=vocab", - extra: token.vocabConstructID, - ), - ), - ], - ), - LemmaMeaningBuilder( - langCode: messageEvent.messageDisplayLangCode, - constructId: token.vocabConstructID, - builder: (context, controller) { - if (controller.editMode) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning( - token.vocabConstructID.lemma, - token.vocabConstructID.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, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 24.0, + height: 24.0, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => overlayController.updateSelectedSpan( + token.text, + ), + child: const Icon( + Icons.close, + size: 16.0, ), ), ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, + ), + Flexible( + child: Text( + token.text.content, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 32.0, + fontWeight: FontWeight.w600, + height: 1.2, + color: + Theme.of(context).brightness == Brightness.light + ? AppConfig.yellowDark + : AppConfig.yellowLight, + ), + ), + ), + ConstructXpWidget( + id: token.vocabConstructID, + onTap: () => context.go( + "/rooms/analytics?mode=vocab", + extra: token.vocabConstructID, + ), + ), + ], + ), + LemmaMeaningBuilder( + langCode: messageEvent.messageDisplayLangCode, + constructId: token.vocabConstructID, + builder: (context, controller) { + if (controller.editMode) { + return Column( + mainAxisSize: MainAxisSize.min, 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), + Text( + "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning( + token.vocabConstructID.lemma, + token.vocabConstructID.category, + )}", + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), ), - 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), + 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, ), - padding: - const EdgeInsets.symmetric(horizontal: 10), ), - child: Text(L10n.of(context).saveChanges), + ), + 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), + ), + ], ), ], - ), - ], - ); - } + ); + } - return Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - children: [ - if (MatrixState - .pangeaController.languageController.showTrancription) - PhoneticTranscriptionWidget( - text: token.text.content, - textLanguage: PLanguageStore.byLangCode( - messageEvent.messageDisplayLangCode, - ) ?? - LanguageModel.unknown, - style: const TextStyle(fontSize: 14.0), - iconSize: 24.0, - ) - else - WordAudioButton( - text: token.text.content, - uniqueID: "lemma-content-${token.text.content}", - langCode: messageEvent.messageDisplayLangCode, - iconSize: 24.0, - ), - LemmaReactionPicker( - cId: _selectedToken.vocabConstructID, - controller: overlayController.widget.chatController, - ), - if (controller.error != null) - Text( - L10n.of(context).oopsSomethingWentWrong, - textAlign: TextAlign.center, - ) - else if (controller.isLoading || - controller.lemmaInfo == null) - const CircularProgressIndicator.adaptive() - else - GestureDetector( - onLongPress: () => controller.toggleEditMode(true), - onDoubleTap: () => controller.toggleEditMode(true), - child: token.lemma.text == token.text.content - ? Text( - controller.lemmaInfo!.meaning, - style: const TextStyle(fontSize: 14.0), - textAlign: TextAlign.center, - ) - : RichText( - text: TextSpan( - style: DefaultTextStyle.of(context) - .style - .copyWith( - fontSize: 14.0, - ), - children: [ - TextSpan(text: token.lemma.text), - const WidgetSpan( - child: SizedBox(width: 8.0), + return Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + children: [ + if (MatrixState.pangeaController.languageController + .showTrancription) + PhoneticTranscriptionWidget( + text: token.text.content, + textLanguage: PLanguageStore.byLangCode( + messageEvent.messageDisplayLangCode, + ) ?? + LanguageModel.unknown, + style: const TextStyle(fontSize: 14.0), + iconSize: 24.0, + ) + else + WordAudioButton( + text: token.text.content, + uniqueID: "lemma-content-${token.text.content}", + langCode: messageEvent.messageDisplayLangCode, + iconSize: 24.0, + ), + LemmaReactionPicker( + cId: _selectedToken.vocabConstructID, + controller: overlayController.widget.chatController, + ), + if (controller.error != null) + Text( + L10n.of(context).oopsSomethingWentWrong, + textAlign: TextAlign.center, + ) + else if (controller.isLoading || + controller.lemmaInfo == null) + const CircularProgressIndicator.adaptive() + else + GestureDetector( + onLongPress: () => controller.toggleEditMode(true), + onDoubleTap: () => controller.toggleEditMode(true), + child: token.lemma.text == token.text.content + ? Text( + controller.lemmaInfo!.meaning, + style: const TextStyle(fontSize: 14.0), + textAlign: TextAlign.center, + ) + : RichText( + text: TextSpan( + style: DefaultTextStyle.of(context) + .style + .copyWith( + fontSize: 14.0, + ), + children: [ + TextSpan(text: token.lemma.text), + const WidgetSpan( + child: SizedBox(width: 8.0), + ), + const TextSpan(text: ":"), + const WidgetSpan( + child: SizedBox(width: 8.0), + ), + TextSpan( + text: controller.lemmaInfo!.meaning, + ), + ], ), - const TextSpan(text: ":"), - const WidgetSpan( - child: SizedBox(width: 8.0), - ), - TextSpan( - text: controller.lemmaInfo!.meaning, - ), - ], - ), - ), - ), - ], - ); - }, + ), + ), + ], + ); + }, + ), + ], ), - ], + ), ), - ), + wordIsNew + ? NewWordOverlay( + overlayColor: overlayColor, + cardKey: cardKey, + ) + : const SizedBox.shrink(), + ], ); } }