From 8e5dc610f81dafc4db807317b005cd0d66ee8025 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:40:24 -0400 Subject: [PATCH] 3396 collect on selection animation not line up (#3468) * fix: switch to overlayUtil instead of manual overlay entry to fix alignment, and comment out second seed animation *still needs some work on the top right seed in word card * fix: fix construct xp widget bug and comment out previous animation also change message selection overlay to trigger update when animation is finished rather than on two conflicting timers * merge conflicts and code formatting * format --------- Co-authored-by: ggurdin Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com> --- lib/pangea/lemmas/construct_xp_widget.dart | 30 +- .../widgets/message_selection_overlay.dart | 74 ++-- .../widgets/reading_assistance_content.dart | 5 + .../widgets/word_zoom/new_word_overlay.dart | 145 +++---- .../widgets/word_zoom/word_zoom_widget.dart | 378 +++++++++--------- 5 files changed, 327 insertions(+), 305 deletions(-) diff --git a/lib/pangea/lemmas/construct_xp_widget.dart b/lib/pangea/lemmas/construct_xp_widget.dart index 22bb38d18..4ca5b80f2 100644 --- a/lib/pangea/lemmas/construct_xp_widget.dart +++ b/lib/pangea/lemmas/construct_xp_widget.dart @@ -77,8 +77,8 @@ class ConstructXpWidgetState extends State setState(() { constructLemmaCategory = constructUse?.lemmaCategory; didChange = true; - _controller.reset(); - _controller.forward(); + //_controller.reset(); + //_controller.forward(); }); } }); @@ -113,18 +113,20 @@ class ConstructXpWidgetState extends State child: Stack( alignment: Alignment.center, children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 1000), - child: svg, - ), - if (didChange) - SlideTransition( - position: _offsetAnimation, - child: FadeTransition( - opacity: _fadeAnimation, - child: svg, - ), - ), + //replaces rise animation, remove 116 and uncomment everything to revert + svg != null ? svg! : const SizedBox.shrink(), + // AnimatedSwitcher( + // duration: const Duration(milliseconds: 1000), + // child: svg, + // ), + // if (didChange) + // SlideTransition( + // position: _offsetAnimation, + // child: FadeTransition( + // opacity: _fadeAnimation, + // child: svg, + // ), + // ), ], ), ), diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 26f618da2..8d07a0c8d 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -279,9 +279,11 @@ class MessageOverlayController extends State _selectedSpan = selectedSpan; if (mounted) setState(() {}); - if (selectedToken != null && isNewToken(selectedToken!)) { - _onSelectNewToken(selectedToken!); - } + + //Commented out so onSelectNewTokens can be manually called after animation is finished + // if (selectedToken != null && isNewToken(selectedToken!)) { + // _onSelectNewToken(selectedToken!); + // } } void _showReadingAssistanceContent() { @@ -556,43 +558,41 @@ class MessageOverlayController extends State updateSelectedSpan(token.text); } - void _onSelectNewToken(PangeaToken token) { + void onSelectNewToken(PangeaToken token) { if (!isNewToken(token)) return; - 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, + 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, ), - ], - targetID: token.text.uniqueKey, - ), - ); + 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), - ); - }); - } - }); + 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/reading_assistance_content.dart b/lib/pangea/toolbar/widgets/reading_assistance_content.dart index ff0cadfd9..c9b0401b3 100644 --- a/lib/pangea/toolbar/widgets/reading_assistance_content.dart +++ b/lib/pangea/toolbar/widgets/reading_assistance_content.dart @@ -117,6 +117,11 @@ class ReadingAssistanceContentState extends State { ); } return WordZoomWidget( + key: MatrixState.pAnyState + .layerLinkAndKey( + "word-zoom-card-${widget.overlayController.selectedToken!.text.uniqueKey}", + ) + .key, token: widget.overlayController.selectedToken!, messageEvent: widget.overlayController.pangeaMessageEvent!, overlayController: widget.overlayController, 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 a1b893b53..ca7661843 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -3,16 +3,24 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class NewWordOverlay extends StatefulWidget { final Color overlayColor; - final GlobalKey cardKey; + final MessageOverlayController overlayController; + final PangeaToken token; + final String transformTargetId; const NewWordOverlay({ super.key, required this.overlayColor, - required this.cardKey, + required this.overlayController, + required this.token, + required this.transformTargetId, }); @override @@ -24,10 +32,9 @@ class _NewWordOverlayState extends State AnimationController? _controller; Animation? _xpScaleAnim; Animation? _fadeAnim; - Size cardSize = const Size(0, 0); - Offset cardPosition = const Offset(0, 0); - OverlayEntry? _overlayEntry; + Animation? _moveAnim; bool columnMode = false; + Widget? get svg => ConstructLevelEnum.seeds.icon(); @override @@ -35,20 +42,30 @@ class _NewWordOverlayState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 1700), - ); + duration: const Duration(milliseconds: 1850), + )..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + dispose(); + } + }); + _xpScaleAnim = CurvedAnimation( parent: _controller!, - curve: const Interval(0.0, 0.6, curve: Curves.easeInOut), + curve: const Interval(0.0, 0.5, curve: Curves.easeInOut), ); + _fadeAnim = CurvedAnimation( parent: _controller!, - curve: const Interval(0.7, 1.0, curve: Curves.easeOut), + curve: const Interval(0.5, 1.0, curve: Curves.easeOut), + ); + + _moveAnim = CurvedAnimation( + parent: _controller!, + curve: const Interval(0.5, 1.0, curve: Curves.easeOut), ); WidgetsBinding.instance.addPostFrameCallback((_) { columnMode = FluffyThemes.isColumnMode(context); - calculateSizeAndPosition(); _showFlyingWidget(); _controller?.forward(); }); @@ -56,63 +73,55 @@ class _NewWordOverlayState extends State @override void dispose() { - _overlayEntry?.remove(); + widget.overlayController.onSelectNewToken(widget.token); _controller?.dispose(); + MatrixState.pAnyState.closeOverlay(widget.transformTargetId); 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) { + if (_controller == null || + _xpScaleAnim == null || + _fadeAnim == null || + _moveAnim == null) { return; } - _overlayEntry = OverlayEntry( - builder: (context) => AnimatedBuilder( + + OverlayUtil.showOverlay( + context: context, + closePrevOverlay: false, + ignorePointer: true, + // onDismiss: () { + // MatrixState.pAnyState.closeOverlay(widget.transformTargetId); + // }, + offset: const Offset(0, 65), + targetAnchor: Alignment.center, + overlayKey: widget.transformTargetId, + transformTargetId: widget.transformTargetId, + child: 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, + final move = _moveAnim!.value; + + final seedSize = 75 * scale * fade; + + // Calculate movement to top left if fullscreen, or top right of word card if mobile + final screenSize = MediaQuery.of(context).size; + final moveX = + columnMode ? -move * (screenSize.width / 2 - 50) : move * 130; + + final moveY = + columnMode ? -move * (screenSize.height / 2 - 50) : move * -120; + + return Transform.translate( + offset: Offset(moveX, moveY), 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(), @@ -123,36 +132,28 @@ class _NewWordOverlayState extends State }, ), ); - 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( + return AnimatedBuilder( + animation: _controller!, + builder: (context, child) { + return Positioned( left: 5, right: 5, top: 50, bottom: 5, - child: Container( - height: cardSize.height, - width: cardSize.width, - color: widget.overlayColor, + child: Opacity( + opacity: 1 - _fadeAnim!.value, + child: Container( + height: double.infinity, + width: double.infinity, + 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 67884617a..feef01251 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -43,222 +43,236 @@ class WordZoomWidget extends StatelessWidget { true && overlayController.hideWordCardContent; + String get transformTargetId => "newer-word-overlay-${token.text.uniqueKey}"; + + LayerLink get layerLink => + MatrixState.pAnyState.layerLinkAndKey(transformTargetId).link; + @override Widget build(BuildContext context) { - final GlobalKey cardKey = MatrixState.pAnyState - .layerLinkAndKey("word-zoom-card-${token.text.uniqueKey}") - .key; + // final GlobalKey cardKey = MatrixState.pAnyState + // .layerLinkAndKey("word-zoom-card-${token.text.uniqueKey}") + // .key; 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: [ - 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(null), - child: const Icon( - Icons.close, - size: 16.0, + child: CompositedTransformTarget( + link: layerLink, + child: SingleChildScrollView( + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + children: [ + 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(null), + child: const Icon( + Icons.close, + size: 16.0, + ), ), ), ), - ), - Flexible( - child: Text( - token.text.content, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 28.0, - fontWeight: FontWeight.w600, - height: 1.2, - color: - Theme.of(context).brightness == Brightness.light - ? AppConfig.yellowDark - : AppConfig.yellowLight, + Flexible( + child: Text( + token.text.content, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 28.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, + 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, - ), + ], + ), + 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), - 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, - ), + 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, ), - 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), + ), + 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, + ), ), - 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), ), - 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, + 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, + event: messageEvent.event, + controller: overlayController.widget.chatController, ), - LemmaReactionPicker( - cId: _selectedToken.vocabConstructID, - event: messageEvent.event, - controller: overlayController.widget.chatController, - ), - if (controller.error != null) - ErrorIndicator( - message: L10n.of(context).errorFetchingDefinition, - style: const TextStyle(fontSize: 14.0), - ) - else if (controller.isLoading || - controller.lemmaInfo == null) - const CircularProgressIndicator.adaptive() - else - GestureDetector( - onLongPress: () => controller.toggleEditMode(true), - onDoubleTap: () => controller.toggleEditMode(true), - child: token.lemma.text.toLowerCase() == - token.text.content.toLowerCase() - ? 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, + if (controller.error != null) + ErrorIndicator( + message: L10n.of(context).errorFetchingDefinition, + style: const TextStyle(fontSize: 14.0), + ) + else if (controller.isLoading || + controller.lemmaInfo == null) + const CircularProgressIndicator.adaptive() + else + GestureDetector( + onLongPress: () => + controller.toggleEditMode(true), + onDoubleTap: () => + controller.toggleEditMode(true), + child: token.lemma.text.toLowerCase() == + token.text.content.toLowerCase() + ? 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), ), - 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, - ), - ], + TextSpan( + text: controller.lemmaInfo!.meaning, + ), + ], + ), ), - ), - ), - ], - ); - }, - ), - ], + ), + ], + ); + }, + ), + ], + ), ), ), ), wordIsNew ? NewWordOverlay( + key: ValueKey(transformTargetId), + token: token, overlayColor: overlayColor, - cardKey: cardKey, + overlayController: overlayController, + transformTargetId: transformTargetId, + //cardKey: cardKey, ) : const SizedBox.shrink(), ],