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 <ggurdin@gmail.com>
Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
pull/2245/head
avashilling 4 months ago committed by GitHub
parent 593a0cc9c2
commit 8e5dc610f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -77,8 +77,8 @@ class ConstructXpWidgetState extends State<ConstructXpWidget>
setState(() { setState(() {
constructLemmaCategory = constructUse?.lemmaCategory; constructLemmaCategory = constructUse?.lemmaCategory;
didChange = true; didChange = true;
_controller.reset(); //_controller.reset();
_controller.forward(); //_controller.forward();
}); });
} }
}); });
@ -113,18 +113,20 @@ class ConstructXpWidgetState extends State<ConstructXpWidget>
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
AnimatedSwitcher( //replaces rise animation, remove 116 and uncomment everything to revert
duration: const Duration(milliseconds: 1000), svg != null ? svg! : const SizedBox.shrink(),
child: svg, // AnimatedSwitcher(
), // duration: const Duration(milliseconds: 1000),
if (didChange) // child: svg,
SlideTransition( // ),
position: _offsetAnimation, // if (didChange)
child: FadeTransition( // SlideTransition(
opacity: _fadeAnimation, // position: _offsetAnimation,
child: svg, // child: FadeTransition(
), // opacity: _fadeAnimation,
), // child: svg,
// ),
// ),
], ],
), ),
), ),

@ -279,9 +279,11 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
_selectedSpan = selectedSpan; _selectedSpan = selectedSpan;
if (mounted) setState(() {}); 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() { void _showReadingAssistanceContent() {
@ -556,43 +558,41 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
updateSelectedSpan(token.text); updateSelectedSpan(token.text);
} }
void _onSelectNewToken(PangeaToken token) { void onSelectNewToken(PangeaToken token) {
if (!isNewToken(token)) return; if (!isNewToken(token)) return;
Future.delayed(const Duration(milliseconds: 1700), () { MatrixState.pangeaController.putAnalytics.setState(
MatrixState.pangeaController.putAnalytics.setState( AnalyticsStream(
AnalyticsStream( eventId: event.eventId,
eventId: event.eventId, roomId: event.room.id,
roomId: event.room.id, constructs: [
constructs: [ OneConstructUse(
OneConstructUse( useType: ConstructUseTypeEnum.click,
useType: ConstructUseTypeEnum.click, lemma: token.lemma.text,
lemma: token.lemma.text, constructType: ConstructTypeEnum.vocab,
constructType: ConstructTypeEnum.vocab, metadata: ConstructUseMetaData(
metadata: ConstructUseMetaData( roomId: event.room.id,
roomId: event.room.id, timeStamp: DateTime.now(),
timeStamp: DateTime.now(), eventId: event.eventId,
eventId: event.eventId,
),
category: token.pos,
form: token.text.content,
xp: ConstructUseTypeEnum.click.pointValue,
), ),
], category: token.pos,
targetID: token.text.uniqueKey, form: token.text.content,
), xp: ConstructUseTypeEnum.click.pointValue,
); ),
],
targetID: token.text.uniqueKey,
),
);
if (mounted) { if (mounted) {
setState(() { setState(() {
newTokens.removeWhere( newTokens.removeWhere(
(t) => (t) =>
t.text.offset == token.text.offset && t.text.offset == token.text.offset &&
t.text.length == token.text.length && t.text.length == token.text.length &&
t.lemma.text.equals(token.lemma.text), t.lemma.text.equals(token.lemma.text),
); );
}); });
} }
});
} }
PracticeTarget? practiceTargetForToken(PangeaToken token) { PracticeTarget? practiceTargetForToken(PangeaToken token) {

@ -117,6 +117,11 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
); );
} }
return WordZoomWidget( return WordZoomWidget(
key: MatrixState.pAnyState
.layerLinkAndKey(
"word-zoom-card-${widget.overlayController.selectedToken!.text.uniqueKey}",
)
.key,
token: widget.overlayController.selectedToken!, token: widget.overlayController.selectedToken!,
messageEvent: widget.overlayController.pangeaMessageEvent!, messageEvent: widget.overlayController.pangeaMessageEvent!,
overlayController: widget.overlayController, overlayController: widget.overlayController,

@ -3,16 +3,24 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.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/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 { class NewWordOverlay extends StatefulWidget {
final Color overlayColor; final Color overlayColor;
final GlobalKey cardKey; final MessageOverlayController overlayController;
final PangeaToken token;
final String transformTargetId;
const NewWordOverlay({ const NewWordOverlay({
super.key, super.key,
required this.overlayColor, required this.overlayColor,
required this.cardKey, required this.overlayController,
required this.token,
required this.transformTargetId,
}); });
@override @override
@ -24,10 +32,9 @@ class _NewWordOverlayState extends State<NewWordOverlay>
AnimationController? _controller; AnimationController? _controller;
Animation<double>? _xpScaleAnim; Animation<double>? _xpScaleAnim;
Animation<double>? _fadeAnim; Animation<double>? _fadeAnim;
Size cardSize = const Size(0, 0); Animation<double>? _moveAnim;
Offset cardPosition = const Offset(0, 0);
OverlayEntry? _overlayEntry;
bool columnMode = false; bool columnMode = false;
Widget? get svg => ConstructLevelEnum.seeds.icon(); Widget? get svg => ConstructLevelEnum.seeds.icon();
@override @override
@ -35,20 +42,30 @@ class _NewWordOverlayState extends State<NewWordOverlay>
super.initState(); super.initState();
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 1700), duration: const Duration(milliseconds: 1850),
); )..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
dispose();
}
});
_xpScaleAnim = CurvedAnimation( _xpScaleAnim = CurvedAnimation(
parent: _controller!, parent: _controller!,
curve: const Interval(0.0, 0.6, curve: Curves.easeInOut), curve: const Interval(0.0, 0.5, curve: Curves.easeInOut),
); );
_fadeAnim = CurvedAnimation( _fadeAnim = CurvedAnimation(
parent: _controller!, 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((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
columnMode = FluffyThemes.isColumnMode(context); columnMode = FluffyThemes.isColumnMode(context);
calculateSizeAndPosition();
_showFlyingWidget(); _showFlyingWidget();
_controller?.forward(); _controller?.forward();
}); });
@ -56,63 +73,55 @@ class _NewWordOverlayState extends State<NewWordOverlay>
@override @override
void dispose() { void dispose() {
_overlayEntry?.remove(); widget.overlayController.onSelectNewToken(widget.token);
_controller?.dispose(); _controller?.dispose();
MatrixState.pAnyState.closeOverlay(widget.transformTargetId);
super.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() { void _showFlyingWidget() {
_overlayEntry?.remove(); // Remove any existing overlay if (_controller == null ||
if (_controller == null || _xpScaleAnim == null || _fadeAnim == null) { _xpScaleAnim == null ||
_fadeAnim == null ||
_moveAnim == null) {
return; 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!, animation: _controller!,
builder: (context, child) { builder: (context, child) {
final scale = _xpScaleAnim!.value; final scale = _xpScaleAnim!.value;
final fade = 1.0 - (_fadeAnim!.value); final fade = 1.0 - (_fadeAnim!.value);
double t = 0.0; final move = _moveAnim!.value;
if ((_controller!.value) >= 0.7) {
t = ((_controller!.value) - 0.7) / 0.3; final seedSize = 75 * scale * fade;
t = t.clamp(0.0, 1.0);
} // Calculate movement to top left if fullscreen, or top right of word card if mobile
//move starting position as seed grows so it stays centered final screenSize = MediaQuery.of(context).size;
final seedSize = 75 * scale * ((!columnMode) ? fade : 1); final moveX =
final startX = cardPosition.dx + cardSize.width / 2 - seedSize; columnMode ? -move * (screenSize.width / 2 - 50) : move * 130;
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 moveY =
final endX = (columnMode) ? 0.0 : cardPosition.dx + cardSize.width; columnMode ? -move * (screenSize.height / 2 - 50) : move * -120;
final endY = (columnMode) ? 0.0 : cardPosition.dy + 30;
final currentX = startX * (1 - t) + endX * t; return Transform.translate(
final currentY = startY * (1 - t) + endY * t; offset: Offset(moveX, moveY),
//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( child: Opacity(
opacity: fade, opacity: fade,
child: Transform.rotate( child: Transform.rotate(
angle: scale * 2 * pi, angle: scale * 2 * pi,
child: SizedBox( child: SizedBox(
//if going to card top right, shrinks as it moves to match word card seed size
width: seedSize, width: seedSize,
height: seedSize, height: seedSize,
child: svg ?? const SizedBox(), child: svg ?? const SizedBox(),
@ -123,36 +132,28 @@ class _NewWordOverlayState extends State<NewWordOverlay>
}, },
), ),
); );
Overlay.of(context).insert(_overlayEntry!);
_controller?.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_overlayEntry?.remove();
_overlayEntry = null;
}
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return AnimatedBuilder(
children: [ animation: _controller!,
Container( builder: (context, child) {
height: cardSize.height, return Positioned(
width: cardSize.width,
color: Colors.transparent,
),
Positioned(
left: 5, left: 5,
right: 5, right: 5,
top: 50, top: 50,
bottom: 5, bottom: 5,
child: Container( child: Opacity(
height: cardSize.height, opacity: 1 - _fadeAnim!.value,
width: cardSize.width, child: Container(
color: widget.overlayColor, height: double.infinity,
width: double.infinity,
color: widget.overlayColor,
),
), ),
), );
], },
); );
} }
} }

@ -43,222 +43,236 @@ class WordZoomWidget extends StatelessWidget {
true && true &&
overlayController.hideWordCardContent; overlayController.hideWordCardContent;
String get transformTargetId => "newer-word-overlay-${token.text.uniqueKey}";
LayerLink get layerLink =>
MatrixState.pAnyState.layerLinkAndKey(transformTargetId).link;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final GlobalKey cardKey = MatrixState.pAnyState // final GlobalKey cardKey = MatrixState.pAnyState
.layerLinkAndKey("word-zoom-card-${token.text.uniqueKey}") // .layerLinkAndKey("word-zoom-card-${token.text.uniqueKey}")
.key; // .key;
final overlayColor = Theme.of(context).scaffoldBackgroundColor; final overlayColor = Theme.of(context).scaffoldBackgroundColor;
return Stack( return Stack(
children: [ children: [
Container( Container(
key: cardKey,
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
constraints: const BoxConstraints( constraints: const BoxConstraints(
minHeight: AppConfig.toolbarMinHeight - 8, minHeight: AppConfig.toolbarMinHeight - 8,
maxHeight: AppConfig.toolbarMaxHeight - 8, maxHeight: AppConfig.toolbarMaxHeight - 8,
maxWidth: AppConfig.toolbarMinWidth, maxWidth: AppConfig.toolbarMinWidth,
), ),
child: SingleChildScrollView( child: CompositedTransformTarget(
child: Column( link: layerLink,
spacing: 12.0, child: SingleChildScrollView(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ spacing: 12.0,
Row( mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
SizedBox( mainAxisAlignment: MainAxisAlignment.spaceBetween,
width: 24.0, children: [
height: 24.0, SizedBox(
child: MouseRegion( width: 24.0,
cursor: SystemMouseCursors.click, height: 24.0,
child: GestureDetector( child: MouseRegion(
onTap: () => cursor: SystemMouseCursors.click,
overlayController.updateSelectedSpan(null), child: GestureDetector(
child: const Icon( onTap: () =>
Icons.close, overlayController.updateSelectedSpan(null),
size: 16.0, child: const Icon(
Icons.close,
size: 16.0,
),
), ),
), ),
), ),
), Flexible(
Flexible( child: Text(
child: Text( token.text.content,
token.text.content, textAlign: TextAlign.center,
textAlign: TextAlign.center, style: TextStyle(
style: TextStyle( fontSize: 28.0,
fontSize: 28.0, fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, height: 1.2,
height: 1.2, color:
color: Theme.of(context).brightness == Brightness.light
Theme.of(context).brightness == Brightness.light ? AppConfig.yellowDark
? AppConfig.yellowDark : AppConfig.yellowLight,
: AppConfig.yellowLight, ),
), ),
), ),
), ConstructXpWidget(
ConstructXpWidget( id: token.vocabConstructID,
id: token.vocabConstructID, onTap: () => context.go(
onTap: () => context.go( "/rooms/analytics?mode=vocab",
"/rooms/analytics?mode=vocab", extra: token.vocabConstructID,
extra: token.vocabConstructID, ),
), ),
), ],
], ),
), LemmaMeaningBuilder(
LemmaMeaningBuilder( langCode: messageEvent.messageDisplayLangCode,
langCode: messageEvent.messageDisplayLangCode, constructId: token.vocabConstructID,
constructId: token.vocabConstructID, builder: (context, controller) {
builder: (context, controller) { if (controller.editMode) {
if (controller.editMode) { return Column(
return Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Text(
Text( "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning( token.vocabConstructID.lemma,
token.vocabConstructID.lemma, token.vocabConstructID.category,
token.vocabConstructID.category, )}",
)}", textAlign: TextAlign.center,
textAlign: TextAlign.center, style:
style: const TextStyle(fontStyle: FontStyle.italic), const TextStyle(fontStyle: FontStyle.italic),
),
const SizedBox(height: 10),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0),
child: TextField(
minLines: 1,
maxLines: 3,
controller: controller.controller,
decoration: InputDecoration(
hintText: controller.lemmaInfo?.meaning,
),
), ),
), const SizedBox(height: 10),
const SizedBox(height: 10), Padding(
Row( padding:
mainAxisAlignment: MainAxisAlignment.center, const EdgeInsets.symmetric(horizontal: 16.0),
children: [ child: TextField(
ElevatedButton( minLines: 1,
onPressed: () => maxLines: 3,
controller.toggleEditMode(false), controller: controller.controller,
style: ElevatedButton.styleFrom( decoration: InputDecoration(
shape: RoundedRectangleBorder( hintText: controller.lemmaInfo?.meaning,
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
), ),
child: Text(L10n.of(context).cancel),
), ),
const SizedBox(width: 10), ),
ElevatedButton( const SizedBox(height: 10),
onPressed: () => controller.controller.text != Row(
controller.lemmaInfo?.meaning && mainAxisAlignment: MainAxisAlignment.center,
controller.controller.text.isNotEmpty children: [
? controller.editLemmaMeaning( ElevatedButton(
controller.controller.text, onPressed: () =>
) controller.toggleEditMode(false),
: null, style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0),
borderRadius: BorderRadius.circular(10.0), ),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
), ),
padding: const EdgeInsets.symmetric( child: Text(L10n.of(context).cancel),
horizontal: 10, ),
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( return Column(
spacing: 12.0, spacing: 12.0,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (MatrixState.pangeaController.languageController if (MatrixState.pangeaController.languageController
.showTrancription) .showTrancription)
PhoneticTranscriptionWidget( PhoneticTranscriptionWidget(
text: token.text.content, text: token.text.content,
textLanguage: PLanguageStore.byLangCode( textLanguage: PLanguageStore.byLangCode(
messageEvent.messageDisplayLangCode, messageEvent.messageDisplayLangCode,
) ?? ) ??
LanguageModel.unknown, LanguageModel.unknown,
style: const TextStyle(fontSize: 14.0), style: const TextStyle(fontSize: 14.0),
iconSize: 24.0, iconSize: 24.0,
) )
else else
WordAudioButton( WordAudioButton(
text: token.text.content, text: token.text.content,
uniqueID: "lemma-content-${token.text.content}", uniqueID: "lemma-content-${token.text.content}",
langCode: messageEvent.messageDisplayLangCode, langCode: messageEvent.messageDisplayLangCode,
iconSize: 24.0, iconSize: 24.0,
),
LemmaReactionPicker(
cId: _selectedToken.vocabConstructID,
event: messageEvent.event,
controller: overlayController.widget.chatController,
), ),
LemmaReactionPicker( if (controller.error != null)
cId: _selectedToken.vocabConstructID, ErrorIndicator(
event: messageEvent.event, message: L10n.of(context).errorFetchingDefinition,
controller: overlayController.widget.chatController, style: const TextStyle(fontSize: 14.0),
), )
if (controller.error != null) else if (controller.isLoading ||
ErrorIndicator( controller.lemmaInfo == null)
message: L10n.of(context).errorFetchingDefinition, const CircularProgressIndicator.adaptive()
style: const TextStyle(fontSize: 14.0), else
) GestureDetector(
else if (controller.isLoading || onLongPress: () =>
controller.lemmaInfo == null) controller.toggleEditMode(true),
const CircularProgressIndicator.adaptive() onDoubleTap: () =>
else controller.toggleEditMode(true),
GestureDetector( child: token.lemma.text.toLowerCase() ==
onLongPress: () => controller.toggleEditMode(true), token.text.content.toLowerCase()
onDoubleTap: () => controller.toggleEditMode(true), ? Text(
child: token.lemma.text.toLowerCase() == controller.lemmaInfo!.meaning,
token.text.content.toLowerCase() style: const TextStyle(fontSize: 14.0),
? Text( textAlign: TextAlign.center,
controller.lemmaInfo!.meaning, )
style: const TextStyle(fontSize: 14.0), : RichText(
textAlign: TextAlign.center, text: TextSpan(
) style: DefaultTextStyle.of(context)
: RichText( .style
text: TextSpan( .copyWith(
style: DefaultTextStyle.of(context) fontSize: 14.0,
.style ),
.copyWith( children: [
fontSize: 14.0, TextSpan(text: token.lemma.text),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
const TextSpan(text: ":"),
const WidgetSpan(
child: SizedBox(width: 8.0),
), ),
children: [ TextSpan(
TextSpan(text: token.lemma.text), text: controller.lemmaInfo!.meaning,
const WidgetSpan( ),
child: SizedBox(width: 8.0), ],
), ),
const TextSpan(text: ":"),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
TextSpan(
text: controller.lemmaInfo!.meaning,
),
],
), ),
), ),
), ],
], );
); },
}, ),
), ],
], ),
), ),
), ),
), ),
wordIsNew wordIsNew
? NewWordOverlay( ? NewWordOverlay(
key: ValueKey(transformTargetId),
token: token,
overlayColor: overlayColor, overlayColor: overlayColor,
cardKey: cardKey, overlayController: overlayController,
transformTargetId: transformTargetId,
//cardKey: cardKey,
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
], ],

Loading…
Cancel
Save