From bf29b283643a8a2018be80557c493e599ba693d2 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:04:33 -0400 Subject: [PATCH 01/10] feat: underline new tokens and animate collecting them on click --- lib/l10n/intl_en.arb | 1 + lib/pages/chat/events/html_message.dart | 27 ++-- .../construct_use_type_enum.dart | 14 +- .../toolbar/utils/token_rendering_util.dart | 9 +- .../widgets/message_selection_overlay.dart | 67 +++++++++- .../widgets/reading_assistance_content.dart | 8 +- .../widgets/word_zoom/new_word_overlay.dart | 121 ++++++++++++++++++ .../widgets/word_zoom/word_zoom_widget.dart | 18 ++- 8 files changed, 237 insertions(+), 28 deletions(-) create mode 100644 lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0f6b2899e..33b2205ea 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 877ddf428..4fbe27a49 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -1,14 +1,4 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:flutter_highlighter/flutter_highlighter.dart'; -import 'package:flutter_highlighter/themes/shades-of-purple.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:html/dom.dart' as dom; -import 'package:html/parser.dart' as parser; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/common/utils/any_state_holder.dart'; @@ -24,6 +14,15 @@ import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_highlighter/flutter_highlighter.dart'; +import 'package:flutter_highlighter/themes/shades-of-purple.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:html/dom.dart' as dom; +import 'package:html/parser.dart' as parser; +import 'package:matrix/matrix.dart'; + import '../../../utils/url_launcher.dart'; class HtmlMessage extends StatelessWidget { @@ -324,6 +323,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, @@ -352,6 +355,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, selected, + isNew, ), ), width: tokenWidth, @@ -386,6 +390,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, selected, + isNew, ), ), linkStyle: linkStyle, @@ -528,6 +533,7 @@ class HtmlMessage extends StatelessWidget { color: renderer.backgroundColor( context, false, + false, ), ), ), @@ -543,6 +549,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..7cda1e003 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_downloads/space_analytics_summary_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; +import 'package:flutter/material.dart'; enum ConstructUseTypeEnum { /// produced in chat by user, igc was run, and we've judged it to be a correct use @@ -66,6 +65,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 +137,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 +189,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.nan: return Icons.help; + case ConstructUseTypeEnum.click: + return Icons.format_color_text; } } @@ -211,6 +217,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corIGC: case ConstructUseTypeEnum.corL: + case ConstructUseTypeEnum.click: return 2; case ConstructUseTypeEnum.corIt: @@ -279,6 +286,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incMM: case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.em: + case ConstructUseTypeEnum.click: case ConstructUseTypeEnum.nan: return false; } @@ -318,6 +326,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 +373,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/toolbar/utils/token_rendering_util.dart b/lib/pangea/toolbar/utils/token_rendering_util.dart index 8efbeeedc..f5eae4603 100644 --- a/lib/pangea/toolbar/utils/token_rendering_util.dart +++ b/lib/pangea/toolbar/utils/token_rendering_util.dart @@ -1,8 +1,8 @@ -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'; +import 'package:flutter/material.dart'; class TokenRenderingUtil { final PangeaMessageEvent? pangeaMessageEvent; @@ -85,7 +85,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 daaecd2ae..93d0ea081 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -2,15 +2,13 @@ import 'dart:async'; import 'dart:developer'; import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; - import 'package:collection/collection.dart'; -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'; @@ -35,6 +33,10 @@ import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_sel import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart'; import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:matrix/matrix.dart'; /// Controls data at the top level of the toolbar (mainly token / toolbar mode selection) class MessageSelectionOverlay extends StatefulWidget { @@ -103,6 +105,8 @@ class MessageOverlayController extends State double maxWidth = AppConfig.toolbarMinWidth; + List newTokens = []; + ///////////////////////////////////// /// Lifecycle ///////////////////////////////////// @@ -114,6 +118,12 @@ 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; + }).toList() ?? + []; } @override @@ -569,6 +579,42 @@ class MessageOverlayController extends State } updateSelectedSpan(token.text); + + Future.delayed(const Duration(seconds: 2), () { + if (isNewToken(token)) { + 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, + ), + ); + // Remove the token from newTokens so it is no longer highlighted as "new" + setState(() { + newTokens.removeWhere( + (t) => + t.text.offset == token.text.offset && + t.text.length == token.text.length, + ); + debugPrint("$token.text has been removed from newTokens list."); + }); + } + }); } /// Whether the given token is currently selected or highlighted @@ -578,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 0edc58849..8cfe06562 100644 --- a/lib/pangea/toolbar/widgets/reading_assistance_content.dart +++ b/lib/pangea/toolbar/widgets/reading_assistance_content.dart @@ -1,9 +1,5 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix_api_lite/model/message_types.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -16,6 +12,8 @@ import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_act import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix_api_lite/model/message_types.dart'; const double minCardHeight = 70; @@ -120,6 +118,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/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart new file mode 100644 index 000000000..c5222daeb --- /dev/null +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -0,0 +1,121 @@ +import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; +import 'package:flutter/material.dart'; + +class NewWordOverlay extends StatefulWidget { + final Widget child; + final bool show; + final Color overlayColor; + final VoidCallback? onComplete; + + const NewWordOverlay({ + super.key, + required this.child, + required this.show, + required this.overlayColor, + this.onComplete, + }); + + @override + State createState() => _NewWordOverlayState(); +} + +class _NewWordOverlayState extends State + with TickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _xpScaleAnim; + late final Animation _fadeAnim; + late final Animation _alignmentAnim; + late final Animation _offsetAnim; + final bool _showOverlay = true; + Widget xpSeedWidget = const SizedBox(); + Widget? get svg => ConstructLevelEnum.seeds.icon(); + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2500), + ); + + _xpScaleAnim = CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.bounceOut), + ); + _fadeAnim = CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeOut), + ); + + // Alignment animation: stays center until 0.5, then animates to topRight + _alignmentAnim = AlignmentTween( + begin: Alignment.center, + end: Alignment.topRight, + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), + ), + ); + + // Offset animation: stays at Offset.zero, then moves up and right + _offsetAnim = Tween( + begin: Offset.zero, + end: const Offset(-0.1, -0.1), + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), + ), + ); + + xpSeedWidget = Container( + child: svg, + ); + + if (mounted) { + _controller.forward(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!_showOverlay) return widget.child; + return Stack( + children: [ + widget.child, + Positioned.fill( + child: FadeTransition( + opacity: ReverseAnimation(_fadeAnim), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + color: widget.overlayColor, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Align( + alignment: _alignmentAnim.value, + child: FractionalTranslation( + translation: _offsetAnim.value, + child: ScaleTransition( + scale: _xpScaleAnim, + child: xpSeedWidget, + ), + ), + ); + }, + ), + ), + ), + ), + ), + ], + ); + } +} 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 dcb86b6eb..da137881b 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart'; @@ -15,18 +13,22 @@ 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'; +import 'package:flutter/material.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,7 +43,9 @@ class WordZoomWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + final isNewWord = wordIsNew; + final overlayColor = Theme.of(context).scaffoldBackgroundColor; + Widget card = Container( padding: const EdgeInsets.all(12.0), constraints: const BoxConstraints( minHeight: AppConfig.toolbarMinHeight - 8, @@ -241,5 +245,13 @@ class WordZoomWidget extends StatelessWidget { ), ), ); + if (isNewWord) { + card = NewWordOverlay( + show: true, + overlayColor: overlayColor, + child: card, + ); + } + return card; } } From 7d92e81f56d1aba0c3ece7c53f086227fb3da50a Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:56:19 -0400 Subject: [PATCH 02/10] fix: only give user vocab in their L2 and animation tweaks --- lib/pangea/toolbar/widgets/message_selection_overlay.dart | 4 ++-- .../toolbar/widgets/word_zoom/new_word_overlay.dart | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 93d0ea081..7a9753de0 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -121,7 +121,8 @@ class MessageOverlayController extends State newTokens = pangeaMessageEvent?.messageDisplayRepresentation?.tokens ?.where((token) { return token.lemma.saveVocab == true && - token.vocabConstruct.uses.isEmpty; + token.vocabConstruct.uses.isEmpty && + messageInUserL2; }).toList() ?? []; } @@ -611,7 +612,6 @@ class MessageOverlayController extends State t.text.offset == token.text.offset && t.text.length == token.text.length, ); - debugPrint("$token.text has been removed from newTokens list."); }); } }); diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index c5222daeb..f0317405d 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -43,10 +43,9 @@ class _NewWordOverlayState extends State ); _fadeAnim = CurvedAnimation( parent: _controller, - curve: const Interval(0.7, 1.0, curve: Curves.easeOut), + curve: const Interval(0.5, 1.0, curve: Curves.easeOut), ); - // Alignment animation: stays center until 0.5, then animates to topRight _alignmentAnim = AlignmentTween( begin: Alignment.center, end: Alignment.topRight, @@ -105,7 +104,10 @@ class _NewWordOverlayState extends State translation: _offsetAnim.value, child: ScaleTransition( scale: _xpScaleAnim, - child: xpSeedWidget, + child: Transform.scale( + scale: 2 * (1 - _fadeAnim.value), + child: xpSeedWidget, + ), ), ), ); From 5205ec8775dec1d12a5cd7c708cb4447f6810fad Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:05:28 -0400 Subject: [PATCH 03/10] Fix word card resize on load and add points animation --- .../widgets/word_zoom/new_word_overlay.dart | 77 ++++++++++++------- .../widgets/word_zoom/word_zoom_widget.dart | 17 ++-- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index f0317405d..1127bfd41 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; import 'package:flutter/material.dart'; @@ -26,7 +27,7 @@ class _NewWordOverlayState extends State late final Animation _fadeAnim; late final Animation _alignmentAnim; late final Animation _offsetAnim; - final bool _showOverlay = true; + bool pointsBlast = false; Widget xpSeedWidget = const SizedBox(); Widget? get svg => ConstructLevelEnum.seeds.icon(); @override @@ -34,7 +35,7 @@ class _NewWordOverlayState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 2500), + duration: const Duration(milliseconds: 2000), ); _xpScaleAnim = CurvedAnimation( @@ -43,7 +44,7 @@ class _NewWordOverlayState extends State ); _fadeAnim = CurvedAnimation( parent: _controller, - curve: const Interval(0.5, 1.0, curve: Curves.easeOut), + curve: const Interval(0.7, 1.0, curve: Curves.easeOut), ); _alignmentAnim = AlignmentTween( @@ -67,6 +68,14 @@ class _NewWordOverlayState extends State ), ); + _controller.addListener(() { + if (!pointsBlast && _controller.value >= 0.6) { + setState(() { + pointsBlast = true; + }); + } + }); + xpSeedWidget = Container( child: svg, ); @@ -84,37 +93,51 @@ class _NewWordOverlayState extends State @override Widget build(BuildContext context) { - if (!_showOverlay) return widget.child; + if (!widget.show) return widget.child; return Stack( children: [ widget.child, - Positioned.fill( - child: FadeTransition( - opacity: ReverseAnimation(_fadeAnim), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - color: widget.overlayColor, - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Align( - alignment: _alignmentAnim.value, - child: FractionalTranslation( - translation: _offsetAnim.value, - child: ScaleTransition( - scale: _xpScaleAnim, - child: Transform.scale( - scale: 2 * (1 - _fadeAnim.value), - child: xpSeedWidget, + Positioned( + left: 5, + right: 5, + top: 5, + bottom: 5, + child: Stack( + children: [ + FadeTransition( + opacity: ReverseAnimation(_fadeAnim), + child: Container( + color: widget.overlayColor, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Align( + alignment: _alignmentAnim.value, + child: FractionalTranslation( + translation: _offsetAnim.value, + child: ScaleTransition( + scale: _xpScaleAnim, + child: Transform.scale( + scale: 2 * (.8 - _fadeAnim.value), + child: xpSeedWidget, + ), ), ), - ), - ); - }, + ); + }, + ), ), ), - ), + pointsBlast + ? const Align( + alignment: Alignment.bottomCenter, + child: PointsGainedAnimation( + points: 10, + targetID: "", + ), + ) + : const SizedBox.shrink(), + ], ), ), ], 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 da137881b..ad5957fed 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -43,9 +43,8 @@ class WordZoomWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final isNewWord = wordIsNew; final overlayColor = Theme.of(context).scaffoldBackgroundColor; - Widget card = Container( + final Widget card = Container( padding: const EdgeInsets.all(12.0), constraints: const BoxConstraints( minHeight: AppConfig.toolbarMinHeight - 8, @@ -245,13 +244,11 @@ class WordZoomWidget extends StatelessWidget { ), ), ); - if (isNewWord) { - card = NewWordOverlay( - show: true, - overlayColor: overlayColor, - child: card, - ); - } - return card; + // Only wrap in NewWordOverlay if wordIsNew is true + return NewWordOverlay( + show: wordIsNew, + overlayColor: overlayColor, + child: card, + ); } } From 16ea5ea56338ee9c0bd4b514bb2a99c24912d837 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:49:42 -0400 Subject: [PATCH 04/10] Progress indicators rise and fade when increased for more emphasis --- .../analytics_summary/progress_indicator.dart | 99 +++++++++++++++++-- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/lib/pangea/analytics_summary/progress_indicator.dart b/lib/pangea/analytics_summary/progress_indicator.dart index 130ad0650..9ecc74f1f 100644 --- a/lib/pangea/analytics_summary/progress_indicator.dart +++ b/lib/pangea/analytics_summary/progress_indicator.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; +import 'package:flutter/material.dart'; /// A badge that represents one learning progress indicator (i.e., construct uses) class ProgressIndicatorBadge extends StatelessWidget { @@ -30,13 +29,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 +45,89 @@ class ProgressIndicatorBadge extends StatelessWidget { ); } } + +class _AnimatedFloatingNumber extends StatefulWidget { + final int number; + final ProgressIndicatorEnum indicator; + final Duration duration; + + const _AnimatedFloatingNumber({ + required this.number, + required this.indicator, + this.duration = const Duration(milliseconds: 900), + }); + + @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: widget.duration); + _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, + ), + ], + ); + } +} From 9afee1cca13ef58dfcf479bc2aedc5dae0e55a1e Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:03:33 -0400 Subject: [PATCH 05/10] seed spins in and fades to top left to draw attention to vocab increment + minor refactoring --- .../analytics_summary/progress_indicator.dart | 5 +- .../widgets/word_zoom/new_word_overlay.dart | 189 +++++++++--------- .../widgets/word_zoom/word_zoom_widget.dart | 5 +- 3 files changed, 106 insertions(+), 93 deletions(-) diff --git a/lib/pangea/analytics_summary/progress_indicator.dart b/lib/pangea/analytics_summary/progress_indicator.dart index 9ecc74f1f..ddd2af6e3 100644 --- a/lib/pangea/analytics_summary/progress_indicator.dart +++ b/lib/pangea/analytics_summary/progress_indicator.dart @@ -49,12 +49,10 @@ class ProgressIndicatorBadge extends StatelessWidget { class _AnimatedFloatingNumber extends StatefulWidget { final int number; final ProgressIndicatorEnum indicator; - final Duration duration; const _AnimatedFloatingNumber({ required this.number, required this.indicator, - this.duration = const Duration(milliseconds: 900), }); @override @@ -73,7 +71,8 @@ class _AnimatedFloatingNumberState extends State<_AnimatedFloatingNumber> @override void initState() { super.initState(); - _controller = AnimationController(vsync: this, duration: widget.duration); + _controller = AnimationController( + vsync: this, duration: const Duration(milliseconds: 900)); _fadeAnim = CurvedAnimation(parent: _controller, curve: Curves.easeOut); _offsetAnim = Tween( begin: const Offset(0, 0), diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index 1127bfd41..cf16dfbcf 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -1,4 +1,5 @@ -import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; +import 'dart:math'; + import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; import 'package:flutter/material.dart'; @@ -6,14 +7,14 @@ class NewWordOverlay extends StatefulWidget { final Widget child; final bool show; final Color overlayColor; - final VoidCallback? onComplete; + final GlobalKey cardKey; const NewWordOverlay({ super.key, required this.child, required this.show, required this.overlayColor, - this.onComplete, + required this.cardKey, }); @override @@ -22,125 +23,135 @@ class NewWordOverlay extends StatefulWidget { class _NewWordOverlayState extends State with TickerProviderStateMixin { - late final AnimationController _controller; - late final Animation _xpScaleAnim; - late final Animation _fadeAnim; - late final Animation _alignmentAnim; - late final Animation _offsetAnim; - bool pointsBlast = false; - Widget xpSeedWidget = const SizedBox(); + AnimationController? _controller; + Animation? _xpScaleAnim; + Animation? _fadeAnim; + Size size = const Size(0, 0); + Offset position = const Offset(0, 0); + OverlayEntry? _overlayEntry; + bool _animationStarted = false; + Widget? get svg => ConstructLevelEnum.seeds.icon(); - @override - void initState() { - super.initState(); + + void _initAndStartAnimation() { _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), ); - _xpScaleAnim = CurvedAnimation( - parent: _controller, - curve: const Interval(0.0, 0.5, curve: Curves.bounceOut), + parent: _controller!, + curve: const Interval(0.0, 0.6, curve: Curves.easeInOut), ); _fadeAnim = CurvedAnimation( - parent: _controller, + parent: _controller!, curve: const Interval(0.7, 1.0, curve: Curves.easeOut), ); - _alignmentAnim = AlignmentTween( - begin: Alignment.center, - end: Alignment.topRight, - ).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), - ), - ); - - // Offset animation: stays at Offset.zero, then moves up and right - _offsetAnim = Tween( - begin: Offset.zero, - end: const Offset(-0.1, -0.1), - ).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), - ), - ); - - _controller.addListener(() { - if (!pointsBlast && _controller.value >= 0.6) { - setState(() { - pointsBlast = true; - }); - } + WidgetsBinding.instance.addPostFrameCallback((_) { + calculateSizeAndPosition(); + _showFlyingWidget(); + _controller?.forward(); }); + } - xpSeedWidget = Container( - child: svg, - ); - - if (mounted) { - _controller.forward(); + @override + void initState() { + super.initState(); + if (widget.show) { + _initAndStartAnimation(); + _animationStarted = true; } } @override void dispose() { - _controller.dispose(); + _overlayEntry?.remove(); + _controller?.dispose(); super.dispose(); } + void calculateSizeAndPosition() { + final RenderBox? box = + widget.cardKey.currentContext?.findRenderObject() as RenderBox?; + if (box != null) { + setState(() { + position = box.localToGlobal(const Offset(-455, 0)); + size = box.size; + }); + } + } + + void _showFlyingWidget() { + 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); + // Calculate t for move to top left after 0.7 + double t = 0.0; + if ((_controller!.value) >= 0.7) { + t = ((_controller!.value) - 0.7) / 0.3; + t = t.clamp(0.0, 1.0); + } + // Start position: center of card, End position: top left (0,0) + final startX = position.dx + size.width / 2 - (37 * scale); + final startY = position.dy + size.height / 2 + 20 - (37 * scale); + const endX = 0.0; + const endY = 0.0; + final currentX = startX * (1 - t) + endX * t; + final currentY = startY * (1 - t) + endY * t; + return Positioned( + left: currentX, + top: currentY, + child: Opacity( + opacity: fade, + child: Transform.rotate( + angle: scale * 2 * pi, + child: SizedBox( + width: 75 * scale, + height: 75 * scale, + 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) { - if (!widget.show) return widget.child; + if (!widget.show && !_animationStarted) return widget.child; return Stack( children: [ widget.child, Positioned( left: 5, right: 5, - top: 5, + top: 50, bottom: 5, - child: Stack( - children: [ - FadeTransition( - opacity: ReverseAnimation(_fadeAnim), - child: Container( - color: widget.overlayColor, - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Align( - alignment: _alignmentAnim.value, - child: FractionalTranslation( - translation: _offsetAnim.value, - child: ScaleTransition( - scale: _xpScaleAnim, - child: Transform.scale( - scale: 2 * (.8 - _fadeAnim.value), - child: xpSeedWidget, - ), - ), - ), - ); - }, - ), - ), - ), - pointsBlast - ? const Align( - alignment: Alignment.bottomCenter, - child: PointsGainedAnimation( - points: 10, - targetID: "", - ), - ) - : const SizedBox.shrink(), - ], + child: FadeTransition( + opacity: ReverseAnimation(_fadeAnim ?? kAlwaysCompleteAnimation), + child: Container( + color: widget.overlayColor, + ), ), ), ], ); } } + +const kAlwaysCompleteAnimation = AlwaysStoppedAnimation(1.0); 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 ad5957fed..64d3f8f1b 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -43,8 +43,10 @@ class WordZoomWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final GlobalKey cardKey = GlobalKey(); final overlayColor = Theme.of(context).scaffoldBackgroundColor; final Widget card = Container( + key: cardKey, padding: const EdgeInsets.all(12.0), constraints: const BoxConstraints( minHeight: AppConfig.toolbarMinHeight - 8, @@ -244,10 +246,11 @@ class WordZoomWidget extends StatelessWidget { ), ), ); - // Only wrap in NewWordOverlay if wordIsNew is true + return NewWordOverlay( show: wordIsNew, overlayColor: overlayColor, + cardKey: cardKey, child: card, ); } From 1a08dda0ad37fe197616150474c7ec167edd35d9 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:02:17 -0400 Subject: [PATCH 06/10] add slightly different animation for smaller screens when not in column mode vocab seed goes towards top right of card rather than top left of chat view, since there are no vocab stats on small screens --- .../widgets/word_zoom/new_word_overlay.dart | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index cf16dfbcf..b07f8d3f4 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; import 'package:flutter/material.dart'; @@ -30,7 +31,7 @@ class _NewWordOverlayState extends State Offset position = const Offset(0, 0); OverlayEntry? _overlayEntry; bool _animationStarted = false; - + bool columnMode = false; Widget? get svg => ConstructLevelEnum.seeds.icon(); void _initAndStartAnimation() { @@ -48,6 +49,7 @@ class _NewWordOverlayState extends State ); WidgetsBinding.instance.addPostFrameCallback((_) { + columnMode = FluffyThemes.isColumnMode(context); calculateSizeAndPosition(); _showFlyingWidget(); _controller?.forward(); @@ -71,12 +73,17 @@ class _NewWordOverlayState extends State } void calculateSizeAndPosition() { - final RenderBox? box = + //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?; - if (box != null) { + 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(() { - position = box.localToGlobal(const Offset(-455, 0)); - size = box.size; + position = cardGlobal - overlayGlobal; + size = cardBox.size; }); } } @@ -91,19 +98,18 @@ class _NewWordOverlayState extends State builder: (context, child) { final scale = _xpScaleAnim!.value; final fade = 1.0 - (_fadeAnim!.value); - // Calculate t for move to top left after 0.7 double t = 0.0; if ((_controller!.value) >= 0.7) { t = ((_controller!.value) - 0.7) / 0.3; t = t.clamp(0.0, 1.0); } - // Start position: center of card, End position: top left (0,0) final startX = position.dx + size.width / 2 - (37 * scale); final startY = position.dy + size.height / 2 + 20 - (37 * scale); - const endX = 0.0; - const endY = 0.0; + final endX = (columnMode) ? 0.0 : position.dx + size.width; + final endY = (columnMode) ? 0.0 : position.dy + 30; final currentX = startX * (1 - t) + endX * t; final currentY = startY * (1 - t) + endY * t; + return Positioned( left: currentX, top: currentY, @@ -112,8 +118,9 @@ class _NewWordOverlayState extends State child: Transform.rotate( angle: scale * 2 * pi, child: SizedBox( - width: 75 * scale, - height: 75 * scale, + //if going to top right, shrinks as it moves to match word card seed size + width: 75 * scale * ((!columnMode) ? fade : 1), + height: 75 * scale * ((!columnMode) ? fade : 1), child: svg ?? const SizedBox(), ), ), From cec627386bd8036db464975bad7a2db10c1b37a8 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:58:41 -0400 Subject: [PATCH 07/10] small fixes and refactoring Change how NewWordOverlay is called and remove redundant variables, take repeat tokens/lemma out of newTokens on click --- .../widgets/message_selection_overlay.dart | 7 +++-- .../widgets/word_zoom/new_word_overlay.dart | 30 ++++++++----------- .../widgets/word_zoom/word_zoom_widget.dart | 13 ++++---- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index fc05ad1bd..ede1c3c5e 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -569,7 +569,7 @@ class MessageOverlayController extends State updateSelectedSpan(token.text); - Future.delayed(const Duration(seconds: 2), () { + Future.delayed(const Duration(milliseconds: 1700), () { if (isNewToken(token)) { MatrixState.pangeaController.putAnalytics.setState( AnalyticsStream( @@ -593,13 +593,16 @@ class MessageOverlayController extends State targetID: token.text.uniqueKey, ), ); - // Remove the token from newTokens so it is no longer highlighted as "new" + // Remove the token (and all tokens of same lemma but different form) from newTokens setState(() { newTokens.removeWhere( (t) => t.text.offset == token.text.offset && t.text.length == token.text.length, ); + newTokens.removeWhere( + (t) => t.lemma.text.equals(token.lemma.text), + ); }); } }); diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index b07f8d3f4..1a376b5f9 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -6,14 +6,12 @@ import 'package:flutter/material.dart'; class NewWordOverlay extends StatefulWidget { final Widget child; - final bool show; final Color overlayColor; final GlobalKey cardKey; const NewWordOverlay({ super.key, required this.child, - required this.show, required this.overlayColor, required this.cardKey, }); @@ -30,14 +28,15 @@ class _NewWordOverlayState extends State Size size = const Size(0, 0); Offset position = const Offset(0, 0); OverlayEntry? _overlayEntry; - bool _animationStarted = false; bool columnMode = false; Widget? get svg => ConstructLevelEnum.seeds.icon(); - void _initAndStartAnimation() { + @override + void initState() { + super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 2000), + duration: const Duration(milliseconds: 1700), ); _xpScaleAnim = CurvedAnimation( parent: _controller!, @@ -56,15 +55,6 @@ class _NewWordOverlayState extends State }); } - @override - void initState() { - super.initState(); - if (widget.show) { - _initAndStartAnimation(); - _animationStarted = true; - } - } - @override void dispose() { _overlayEntry?.remove(); @@ -89,6 +79,7 @@ class _NewWordOverlayState extends State } void _showFlyingWidget() { + _overlayEntry?.remove(); // Remove any existing overlay if (_controller == null || _xpScaleAnim == null || _fadeAnim == null) { return; } @@ -103,12 +94,16 @@ class _NewWordOverlayState extends State 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 startX = position.dx + size.width / 2 - (37 * scale); final startY = position.dy + size.height / 2 + 20 - (37 * scale); + //end is top left if column mode (going towards vocab stats) or top right of card otherwise final endX = (columnMode) ? 0.0 : position.dx + size.width; final endY = (columnMode) ? 0.0 : position.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 + final seedSize = 75 * scale * ((!columnMode) ? fade : 1); return Positioned( left: currentX, @@ -118,9 +113,9 @@ class _NewWordOverlayState extends State child: Transform.rotate( angle: scale * 2 * pi, child: SizedBox( - //if going to top right, shrinks as it moves to match word card seed size - width: 75 * scale * ((!columnMode) ? fade : 1), - height: 75 * scale * ((!columnMode) ? fade : 1), + //if going to card top right, shrinks as it moves to match word card seed size + width: seedSize, + height: seedSize, child: svg ?? const SizedBox(), ), ), @@ -140,7 +135,6 @@ class _NewWordOverlayState extends State @override Widget build(BuildContext context) { - if (!widget.show && !_animationStarted) return widget.child; return Stack( children: [ widget.child, 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 a164cfd4e..8c00bcdcb 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -245,11 +245,12 @@ class WordZoomWidget extends StatelessWidget { ), ); - return NewWordOverlay( - show: wordIsNew, - overlayColor: overlayColor, - cardKey: cardKey, - child: card, - ); + return wordIsNew + ? NewWordOverlay( + overlayColor: overlayColor, + cardKey: cardKey, + child: card, + ) + : card; } } From 71e1423fd945389d1a9fbb26b62fce75d6301b75 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:44:59 -0400 Subject: [PATCH 08/10] make overlay separate/stacked on top of word card --- .../widgets/word_zoom/new_word_overlay.dart | 17 +- .../widgets/word_zoom/word_zoom_widget.dart | 391 +++++++++--------- 2 files changed, 208 insertions(+), 200 deletions(-) diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index 1a376b5f9..d2a23282d 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -5,13 +5,11 @@ import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; import 'package:flutter/material.dart'; class NewWordOverlay extends StatefulWidget { - final Widget child; final Color overlayColor; final GlobalKey cardKey; const NewWordOverlay({ super.key, - required this.child, required this.overlayColor, required this.cardKey, }); @@ -137,17 +135,20 @@ class _NewWordOverlayState extends State Widget build(BuildContext context) { return Stack( children: [ - widget.child, + Container( + height: size.height, + width: size.width, + color: Colors.transparent, + ), Positioned( left: 5, right: 5, top: 50, bottom: 5, - child: FadeTransition( - opacity: ReverseAnimation(_fadeAnim ?? kAlwaysCompleteAnimation), - child: Container( - color: widget.overlayColor, - ), + child: Container( + height: size.height, + width: size.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 8c00bcdcb..8f7c4702c 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -45,212 +45,219 @@ class WordZoomWidget extends StatelessWidget { Widget build(BuildContext context) { final GlobalKey cardKey = GlobalKey(); final overlayColor = Theme.of(context).scaffoldBackgroundColor; - final Widget card = 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: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + 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: () => AnalyticsPopupWrapper.show( - context, - constructZoom: token.vocabConstructID, - view: ConstructTypeEnum.vocab, - ), - ), - ], - ), - 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: () => AnalyticsPopupWrapper.show( + context, + constructZoom: token.vocabConstructID, + view: ConstructTypeEnum.vocab, + ), + ), + ], + ), + 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(), + ], ); - - return wordIsNew - ? NewWordOverlay( - overlayColor: overlayColor, - cardKey: cardKey, - child: card, - ) - : card; } } From 15b1e68b7417f82af20fce9c5f7c51dda4585a1a Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:46:16 -0400 Subject: [PATCH 09/10] clarify variable names and format code --- .../widgets/word_zoom/new_word_overlay.dart | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index d2a23282d..0c5dcc945 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -23,8 +23,8 @@ class _NewWordOverlayState extends State AnimationController? _controller; Animation? _xpScaleAnim; Animation? _fadeAnim; - Size size = const Size(0, 0); - Offset position = const Offset(0, 0); + Size cardSize = const Size(0, 0); + Offset cardPosition = const Offset(0, 0); OverlayEntry? _overlayEntry; bool columnMode = false; Widget? get svg => ConstructLevelEnum.seeds.icon(); @@ -70,8 +70,8 @@ class _NewWordOverlayState extends State final cardGlobal = cardBox.localToGlobal(Offset.zero); final overlayGlobal = overlayBox.localToGlobal(Offset.zero); setState(() { - position = cardGlobal - overlayGlobal; - size = cardBox.size; + cardPosition = cardGlobal - overlayGlobal; + cardSize = cardBox.size; }); } } @@ -93,15 +93,15 @@ class _NewWordOverlayState extends State t = t.clamp(0.0, 1.0); } //move starting position as seed grows so it stays centered - final startX = position.dx + size.width / 2 - (37 * scale); - final startY = position.dy + size.height / 2 + 20 - (37 * scale); + 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 : position.dx + size.width; - final endY = (columnMode) ? 0.0 : position.dy + 30; + 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 - final seedSize = 75 * scale * ((!columnMode) ? fade : 1); return Positioned( left: currentX, @@ -136,8 +136,8 @@ class _NewWordOverlayState extends State return Stack( children: [ Container( - height: size.height, - width: size.width, + height: cardSize.height, + width: cardSize.width, color: Colors.transparent, ), Positioned( @@ -146,8 +146,8 @@ class _NewWordOverlayState extends State top: 50, bottom: 5, child: Container( - height: size.height, - width: size.width, + height: cardSize.height, + width: cardSize.width, color: widget.overlayColor, ), ), From 491c13b28f1e499a65e4b04f6cea7487d62b0382 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 3 Jul 2025 15:45:36 -0400 Subject: [PATCH 10/10] chore: fix jerking animation in word card --- .../widgets/message_selection_overlay.dart | 30 +++++++++---------- .../widgets/word_zoom/new_word_overlay.dart | 2 -- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index a8a6cde1c..1b1412f88 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -571,8 +571,8 @@ class MessageOverlayController extends State updateSelectedSpan(token.text); - Future.delayed(const Duration(milliseconds: 1700), () { - if (isNewToken(token)) { + if (isNewToken(token)) { + Future.delayed(const Duration(milliseconds: 1700), () { MatrixState.pangeaController.putAnalytics.setState( AnalyticsStream( eventId: event.eventId, @@ -595,19 +595,19 @@ class MessageOverlayController extends State targetID: token.text.uniqueKey, ), ); - // Remove the token (and all tokens of same lemma but different form) from newTokens - setState(() { - newTokens.removeWhere( - (t) => - t.text.offset == token.text.offset && - t.text.length == token.text.length, - ); - newTokens.removeWhere( - (t) => t.lemma.text.equals(token.lemma.text), - ); - }); - } - }); + + 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) { diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index 37bbe6891..a1b893b53 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -156,5 +156,3 @@ class _NewWordOverlayState extends State ); } } - -const kAlwaysCompleteAnimation = AlwaysStoppedAnimation(1.0);