diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart
index d484a6093..581e6de77 100644
--- a/lib/pages/chat/events/html_message.dart
+++ b/lib/pages/chat/events/html_message.dart
@@ -315,7 +315,7 @@ class HtmlMessage extends StatelessWidget {
),
),
width: tokenWidth,
- animate: isTransitionAnimation,
+ animateIn: isTransitionAnimation,
practiceTarget:
overlayController?.toolbarMode.associatedActivityType !=
null
diff --git a/lib/pangea/choreographer/widgets/choice_animation.dart b/lib/pangea/choreographer/widgets/choice_animation.dart
index a557568d1..d6fbbbc20 100644
--- a/lib/pangea/choreographer/widgets/choice_animation.dart
+++ b/lib/pangea/choreographer/widgets/choice_animation.dart
@@ -32,10 +32,6 @@ class ChoiceAnimationWidgetState extends State
duration: const Duration(milliseconds: choiceArrayAnimationDuration),
vsync: this,
);
-
- if (widget.isSelected) {
- _controller.forward().then((_) => _controller.reset());
- }
}
@override
diff --git a/lib/pangea/message_token_text/message_token_button.dart b/lib/pangea/message_token_text/message_token_button.dart
index 4750dab8f..7b393c6b6 100644
--- a/lib/pangea/message_token_text/message_token_button.dart
+++ b/lib/pangea/message_token_text/message_token_button.dart
@@ -25,14 +25,13 @@ const double tokenButtonHeight = 40.0;
const double tokenButtonDefaultFontSize = 10;
const int maxEmojisPerLemma = 1;
const double estimatedEmojiWidthRatio = 2;
-const double estimatedEmojiHeightRatio = 1.3;
class MessageTokenButton extends StatefulWidget {
final MessageOverlayController? overlayController;
final PangeaToken token;
final TextStyle textStyle;
final double width;
- final bool animate;
+ final bool animateIn;
final PracticeTarget? practiceTarget;
const MessageTokenButton({
@@ -42,7 +41,7 @@ class MessageTokenButton extends StatefulWidget {
required this.textStyle,
required this.width,
required this.practiceTarget,
- this.animate = false,
+ this.animateIn = false,
});
@override
@@ -59,14 +58,20 @@ class MessageTokenButtonState extends State
late Animation _iconSizeAnimation;
bool _isHovered = false;
+ bool _isSelected = false;
+ bool _finishedInitialAnimation = false;
+ bool _wasEmpty = false;
@override
void initState() {
super.initState();
+ _setSelected();
+
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
+ // seconds: 5,
),
);
@@ -88,29 +93,29 @@ class MessageTokenButtonState extends State
CurvedAnimation(parent: _iconSizeController, curve: Curves.easeInOut),
);
- if (widget.animate) {
- _controller.forward();
+ _wasEmpty = _isEmpty;
+
+ if (!_isEmpty) {
+ _controller.forward().then((_) {
+ if (mounted) setState(() => _finishedInitialAnimation = true);
+ });
+ } else {
+ setState(() => _finishedInitialAnimation = true);
}
}
- double get topPadding => 10.0;
-
- double get height =>
- widget.animate ? _heightAnimation.value : tokenButtonHeight;
-
@override
void didUpdateWidget(covariant MessageTokenButton oldWidget) {
- if (oldWidget.overlayController?.toolbarMode !=
- widget.overlayController?.toolbarMode ||
- oldWidget.overlayController?.selectedToken !=
- widget.overlayController?.selectedToken ||
- oldWidget.overlayController?.selectedMorph !=
- widget.overlayController?.selectedMorph ||
- widget.token.vocabConstructID.constructUses.points !=
- widget.token.vocabConstructID.constructUses.points) {
- setState(() {});
- }
super.didUpdateWidget(oldWidget);
+ _setSelected();
+ if (_isEmpty != _wasEmpty) {
+ if (_isEmpty && _animate) {
+ _controller.reverse();
+ } else if (!_isEmpty && _animate) {
+ _controller.forward();
+ }
+ setState(() => _wasEmpty = _isEmpty);
+ }
}
@override
@@ -120,18 +125,51 @@ class MessageTokenButtonState extends State
super.dispose();
}
- double get textSize =>
- widget.textStyle.fontSize ?? tokenButtonDefaultFontSize;
+ bool get _animate => widget.animateIn || _finishedInitialAnimation;
- double get emojiSize => textSize * estimatedEmojiWidthRatio;
+ PracticeTarget? get _activity => widget.practiceTarget;
- TextStyle get emojiStyle => widget.textStyle.copyWith(
- fontSize: textSize + 4,
- );
+ bool get _isActivityCompleteForToken =>
+ _activity?.isCompleteByToken(
+ widget.token,
+ _activity!.morphFeature,
+ ) ==
+ true;
- PracticeTarget? get activity => widget.practiceTarget;
+ void _setSelected() {
+ final selected =
+ widget.overlayController?.selectedMorph?.token == widget.token &&
+ widget.overlayController?.selectedMorph?.morph ==
+ _activity?.morphFeature;
- onMatch(PracticeChoice form) {
+ if (selected != _isSelected) {
+ setState(() {
+ _isSelected = selected;
+ });
+
+ _isSelected
+ ? _iconSizeController.forward()
+ : _iconSizeController.reverse();
+ }
+ }
+
+ void _setHovered(bool isHovered) {
+ if (isHovered != _isHovered) {
+ setState(() {
+ _isHovered = isHovered;
+ });
+
+ if (!_isHovered && _isSelected) {
+ return;
+ }
+
+ _isHovered
+ ? _iconSizeController.forward()
+ : _iconSizeController.reverse();
+ }
+ }
+
+ void _onMatch(PracticeChoice form) {
if (widget.overlayController?.activity == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
@@ -140,9 +178,7 @@ class MessageTokenButtonState extends State
);
return;
}
- widget.overlayController!.selectedChoice = null;
- widget.overlayController!.setState(() {});
-
+ widget.overlayController!.onChoiceSelect(null);
widget.overlayController!.activity!.onMatch(
widget.token,
form,
@@ -151,65 +187,136 @@ class MessageTokenButtonState extends State
);
}
- Widget get emojiView {
- // if (widget.token.text.content.length == 1 || maxEmojisPerLemma == 1) {
- return ShrinkableText(
- text: widget.token.vocabConstructID.userSetEmoji.firstOrNull ?? '',
- maxWidth: widget.width,
- style: emojiStyle,
- );
- // }
- // return Stack(
- // alignment: Alignment.center,
- // children: widget.token.vocabConstructID.userSetEmoji
- // .take(maxEmojisPerLemma)
- // .mapIndexed(
- // (index, emoji) => Positioned(
- // left: min(
- // index /
- // widget.token.vocabConstructID.userSetEmoji.length *
- // totalAvailableWidth,
- // index * emojiSize,
- // ),
- // child: Text(
- // emoji,
- // style: emojiStyle,
- // ),
- // ),
- // )
- // .toList()
- // .reversed
- // .toList(),
- // );
+ bool get _isEmpty {
+ final mode = widget.overlayController?.toolbarMode;
+ return _activity == null ||
+ (_isActivityCompleteForToken &&
+ ![MessageMode.wordEmoji, MessageMode.wordMorph].contains(mode)) ||
+ (MessageMode.wordMorph == mode && _activity?.morphFeature == null);
}
- bool get isActivityCompleteForToken =>
- activity?.isCompleteByToken(
- widget.token,
- activity!.morphFeature,
- ) ==
- true;
+ @override
+ Widget build(BuildContext context) {
+ if (widget.overlayController == null) {
+ return const SizedBox.shrink();
+ }
- Color get color {
+ if (!_animate) {
+ return MessageTokenButtonContent(
+ activity: _activity,
+ messageMode: widget.overlayController!.toolbarMode,
+ token: widget.token,
+ selectedChoice: widget.overlayController?.selectedChoice,
+ isComplete: _isActivityCompleteForToken,
+ isSelected: _isSelected,
+ height: tokenButtonHeight,
+ width: widget.width,
+ textStyle: widget.textStyle,
+ sizeAnimation: _iconSizeAnimation,
+ onHover: _setHovered,
+ onTap: () => widget.overlayController!.onMorphActivitySelect(
+ MorphSelection(widget.token, _activity!.morphFeature!),
+ ),
+ onMatch: _onMatch,
+ );
+ }
+
+ return AnimatedBuilder(
+ animation: _heightAnimation,
+ builder: (context, child) {
+ return MessageTokenButtonContent(
+ activity: _activity,
+ messageMode: widget.overlayController!.toolbarMode,
+ token: widget.token,
+ selectedChoice: widget.overlayController?.selectedChoice,
+ isComplete: _isActivityCompleteForToken,
+ isSelected: _isSelected,
+ height: _heightAnimation.value,
+ width: widget.width,
+ textStyle: widget.textStyle,
+ sizeAnimation: _iconSizeAnimation,
+ onHover: _setHovered,
+ onTap: () => widget.overlayController!.onMorphActivitySelect(
+ MorphSelection(widget.token, _activity!.morphFeature!),
+ ),
+ onMatch: _onMatch,
+ );
+ },
+ );
+ }
+}
+
+class MessageTokenButtonContent extends StatelessWidget {
+ final PracticeTarget? activity;
+ final MessageMode messageMode;
+ final PangeaToken token;
+ final PracticeChoice? selectedChoice;
+
+ final bool isComplete;
+ final bool isSelected;
+ final double height;
+ final double width;
+ final TextStyle textStyle;
+ final Animation sizeAnimation;
+
+ final Function(bool)? onHover;
+ final Function()? onTap;
+ final Function(PracticeChoice)? onMatch;
+
+ const MessageTokenButtonContent({
+ super.key,
+ required this.activity,
+ required this.messageMode,
+ required this.token,
+ required this.selectedChoice,
+ required this.isComplete,
+ required this.isSelected,
+ required this.height,
+ required this.width,
+ required this.textStyle,
+ required this.sizeAnimation,
+ this.onHover,
+ this.onTap,
+ this.onMatch,
+ });
+
+ TextStyle get _emojiStyle => textStyle.copyWith(
+ fontSize: (textStyle.fontSize ?? tokenButtonDefaultFontSize) + 4,
+ );
+
+ static final _borderRadius =
+ BorderRadius.circular(AppConfig.borderRadius - 4);
+
+ Color _color(BuildContext context) {
if (activity == null) {
return Theme.of(context).colorScheme.primary;
}
- if (isActivityCompleteForToken) {
+ if (isComplete) {
return AppConfig.gold;
}
return Theme.of(context).colorScheme.primary;
}
- Widget get content {
- final tokenActivity = activity;
- if (tokenActivity == null || isActivityCompleteForToken) {
- if (MessageMode.wordEmoji == widget.overlayController?.toolbarMode) {
- return SizedBox(height: height, child: emojiView);
+ @override
+ Widget build(BuildContext context) {
+ if (activity == null) {
+ return SizedBox(height: height);
+ }
+
+ if (isComplete) {
+ if (MessageMode.wordEmoji == messageMode) {
+ return SizedBox(
+ height: height,
+ child: ShrinkableText(
+ text: token.vocabConstructID.userSetEmoji.firstOrNull ?? '',
+ maxWidth: width,
+ style: _emojiStyle,
+ ),
+ );
}
- if (MessageMode.wordMorph == widget.overlayController?.toolbarMode &&
- activity?.morphFeature != null) {
+ if (MessageMode.wordMorph == messageMode) {
final morphFeature = activity!.morphFeature!;
- final morphTag = widget.token.morphIdByFeature(morphFeature);
+ final morphTag = token.morphIdByFeature(morphFeature);
if (morphTag != null) {
return Tooltip(
message: getGrammarCopy(
@@ -218,7 +325,7 @@ class MessageTokenButtonState extends State
context: context,
),
child: SizedBox(
- width: widget.width,
+ width: width,
height: height,
child: Center(
child: MorphIcon(
@@ -234,43 +341,28 @@ class MessageTokenButtonState extends State
return SizedBox(height: height);
}
- if (MessageMode.wordMorph == widget.overlayController?.toolbarMode) {
+ if (MessageMode.wordMorph == messageMode) {
if (activity?.morphFeature == null) {
return SizedBox(height: height);
}
- final bool isSelected =
- (widget.overlayController?.selectedMorph?.token == widget.token &&
- widget.overlayController?.selectedMorph?.morph ==
- activity?.morphFeature) ||
- _isHovered;
-
- // Trigger the icon size animation based on hover or selection
- if (isSelected) {
- _iconSizeController.forward();
- } else {
- _iconSizeController.reverse();
- }
-
return InkWell(
- onHover: (isHovered) => setState(() => _isHovered = isHovered),
- onTap: () => widget.overlayController!.onMorphActivitySelect(
- MorphSelection(widget.token, activity!.morphFeature!),
- ),
- borderRadius: borderRadius,
+ onHover: onHover,
+ onTap: onTap,
+ borderRadius: _borderRadius,
child: Container(
height: height,
- width: min(widget.width, height),
+ width: min(width, height),
alignment: Alignment.center,
child: Opacity(
opacity: isSelected ? 1.0 : 0.4,
child: AnimatedBuilder(
- animation: _iconSizeAnimation,
+ animation: sizeAnimation,
builder: (context, child) {
return Icon(
Symbols.toys_and_games,
- color: color,
- size: _iconSizeAnimation.value, // Use the new animation
+ color: _color(context),
+ size: sizeAnimation.value, // Use the new animation
);
},
),
@@ -282,61 +374,41 @@ class MessageTokenButtonState extends State
return DragTarget(
builder: (BuildContext context, accepted, rejected) {
final double colorAlpha = 0.3 +
- (widget.overlayController?.selectedChoice != null ? 0.4 : 0.0) +
- (accepted.isNotEmpty || _isHovered ? 0.3 : 0.0);
+ (selectedChoice != null ? 0.4 : 0.0) +
+ (accepted.isNotEmpty ? 0.3 : 0.0);
return InkWell(
- onHover: (isHovered) => setState(() => _isHovered = isHovered),
- onTap: widget.overlayController?.selectedChoice != null
- ? () => onMatch(widget.overlayController!.selectedChoice!)
+ onTap: selectedChoice != null
+ ? () => onMatch?.call(selectedChoice!)
: null,
- borderRadius: borderRadius,
+ borderRadius: _borderRadius,
child: CustomPaint(
painter: DottedBorderPainter(
color: Theme.of(context)
.colorScheme
.primary
.withAlpha((colorAlpha * 255).toInt()),
- borderRadius: borderRadius,
+ borderRadius: _borderRadius,
),
child: Container(
height: height,
- padding: EdgeInsets.only(top: topPadding),
- width: MessageMode.wordMeaning ==
- widget.overlayController?.toolbarMode
- ? widget.width
- : min(widget.width, height),
+ padding: const EdgeInsets.only(top: 10.0),
+ width: MessageMode.wordMeaning == messageMode
+ ? width
+ : min(width, height),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withAlpha((max(0, colorAlpha - 0.7) * 255).toInt()),
- borderRadius: borderRadius,
+ borderRadius: _borderRadius,
),
),
),
);
},
- onAcceptWithDetails: (details) => onMatch(details.data),
- );
- }
-
- static final borderRadius = BorderRadius.circular(AppConfig.borderRadius - 4);
-
- @override
- Widget build(BuildContext context) {
- if (widget.overlayController == null) {
- return const SizedBox.shrink();
- }
-
- if (!widget.animate) {
- return content;
- }
-
- return AnimatedBuilder(
- animation: _heightAnimation,
- builder: (context, child) => content,
+ onAcceptWithDetails: (details) => onMatch?.call(details.data),
);
}
}
diff --git a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart
index 9ef6ffb2a..183b5cfee 100644
--- a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart
+++ b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart
@@ -35,10 +35,6 @@ class MatchActivityCard extends StatelessWidget {
) {
switch (activityType) {
case ActivityTypeEnum.emoji:
- return Text(
- choice,
- style: TextStyle(fontSize: fontSize),
- );
case ActivityTypeEnum.wordMeaning:
return Text(
choice,
diff --git a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart
index ea7aa6084..c547e0c3c 100644
--- a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart
+++ b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart
@@ -107,31 +107,6 @@ class PracticeMatchItemState extends State {
}
}
- IntrinsicWidth content(BuildContext context) {
- return IntrinsicWidth(
- child: Container(
- height: widget.fixedSize,
- width: widget.fixedSize,
- alignment: Alignment.center,
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- color: color(context).withAlpha((0.4 * 255).toInt()),
- borderRadius: BorderRadius.circular(AppConfig.borderRadius),
- border: isSelected
- ? Border.all(
- color: color(context),
- width: 2,
- )
- : Border.all(
- color: Colors.transparent,
- width: 2,
- ),
- ),
- child: widget.content,
- ),
- );
- }
-
void onTap() {
play();
isCorrect == null || !isCorrect! || widget.token == null
@@ -141,11 +116,37 @@ class PracticeMatchItemState extends State {
@override
Widget build(BuildContext context) {
+ final content = Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ height: widget.fixedSize,
+ width: widget.fixedSize,
+ alignment: Alignment.center,
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: color(context).withAlpha((0.4 * 255).toInt()),
+ borderRadius: BorderRadius.circular(AppConfig.borderRadius),
+ border: isSelected
+ ? Border.all(
+ color: color(context),
+ width: 2,
+ )
+ : Border.all(
+ color: Colors.transparent,
+ width: 2,
+ ),
+ ),
+ child: widget.content,
+ ),
+ ],
+ );
+
return LongPressDraggable(
data: widget.constructForm,
feedback: Material(
type: MaterialType.transparency,
- child: content(context),
+ child: content,
),
delay: const Duration(milliseconds: 50),
onDragStarted: onTap,
@@ -153,7 +154,7 @@ class PracticeMatchItemState extends State {
onHover: (isHovered) => setState(() => _isHovered = isHovered),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
onTap: onTap,
- child: content(context),
+ child: content,
),
);
}
diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart
index ad4a16dfb..4ec8c1667 100644
--- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart
+++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart
@@ -370,7 +370,7 @@ class MessageOverlayController extends State
}
}
- void onChoiceSelect(PracticeChoice choice, [bool force = false]) {
+ void onChoiceSelect(PracticeChoice? choice, [bool force = false]) {
if (selectedChoice == choice && !force) {
selectedChoice = null;
} else {
diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart
index 5f270098d..95c298461 100644
--- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart
+++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart
@@ -145,6 +145,8 @@ class MessageSelectionPositionerState extends State
}
void _setCenteredMessageSize(RenderBox renderBox) {
+ if (_centeredMessageCompleter.isCompleted) return;
+
_centeredMessageSize = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
_centeredMessageOffset = Offset(
@@ -406,12 +408,17 @@ class MessageSelectionPositionerState extends State
if (hasHeaderOverflow) {
final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing);
+ double newBottomOffset = _mediaQuery!.size.height -
+ _originalMessageOffset.dy +
+ difference -
+ _originalMessageSize.height;
+ if (newBottomOffset < _footerHeight + AppConfig.toolbarSpacing) {
+ newBottomOffset = _footerHeight + AppConfig.toolbarSpacing;
+ }
+
return Offset(
_ownMessage ? _messageRightOffset : _messageLeftOffset,
- _mediaQuery!.size.height -
- _originalMessageOffset.dy +
- difference -
- _originalMessageSize.height,
+ newBottomOffset,
);
} else {
final difference =
diff --git a/lib/pangea/toolbar/widgets/message_token_text.dart b/lib/pangea/toolbar/widgets/message_token_text.dart
index 35c71a4d2..e9d1843ec 100644
--- a/lib/pangea/toolbar/widgets/message_token_text.dart
+++ b/lib/pangea/toolbar/widgets/message_token_text.dart
@@ -225,7 +225,7 @@ class MessageTextWidget extends StatelessWidget {
overlayController: overlayController,
textStyle: renderer.style(context),
width: tokenWidth,
- animate: isTransitionAnimation,
+ animateIn: isTransitionAnimation,
practiceTarget: overlayController
?.toolbarMode.associatedActivityType !=
null
diff --git a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart
index 2ab32a3c6..cc7bf514b 100644
--- a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart
+++ b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart
@@ -312,7 +312,7 @@ class PracticeActivityCardState extends State {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- if (activityWidget != null) activityWidget!,
+ if (activityWidget != null && !fetchingActivity) activityWidget!,
// Conditionally show the darkening and progress indicator based on the loading state
if (!savoringTheJoy && fetchingActivity) ...[
// Circular progress indicator in the center