diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart
index 43497b0dc..f0e1f2838 100644
--- a/lib/pages/chat/events/html_message.dart
+++ b/lib/pages/chat/events/html_message.dart
@@ -446,8 +446,10 @@ class HtmlMessage extends StatelessWidget {
enabled: token.lemma.saveVocab,
targetId: overlayController!.tokenEmojiPopupKey(token),
selectModeNotifier: overlayController!.selectedMode,
- selectedTokenNotifier:
- overlayController!.selectedTokenNotifier,
+ onTap: () =>
+ overlayController!.onClickOverlayMessageToken(token),
+ constructEmojiNotifier: overlayController!
+ .selectModeController.constructEmojiNotifier,
),
if (renderer.showCenterStyling &&
token != null &&
@@ -946,9 +948,10 @@ class HtmlMessage extends StatelessWidget {
// Use TokenEmojiButton to ensure consistent vertical alignment for non-token elements (e.g., emojis) in practice mode.
TokenEmojiButton(
selectModeNotifier: overlayController!.selectedMode,
- selectedTokenNotifier:
- overlayController!.selectedTokenNotifier,
+ onTap: () {},
enabled: false,
+ constructEmojiNotifier: overlayController!
+ .selectModeController.constructEmojiNotifier,
),
RichText(
text: TextSpan(
diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart
index 11e1d6f11..839579cae 100644
--- a/lib/pages/chat/events/message.dart
+++ b/lib/pages/chat/events/message.dart
@@ -601,7 +601,7 @@ class Message extends StatelessWidget {
color: color,
visible:
isButton && !noBubble,
- child:
+ builder: (context, _, __) =>
// Pangea#
Container(
decoration: BoxDecoration(
diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart
index e8457ebb4..6f43ba802 100644
--- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart
+++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart
@@ -4,13 +4,12 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart';
+import 'package:fluffychat/pangea/analytics_details_popup/vocab_details_emoji_selector.dart';
import 'package:fluffychat/pangea/analytics_details_popup/word_text_with_audio_button.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/common/widgets/shrinkable_text.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
-import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart';
-import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
import 'package:fluffychat/pangea/lemmas/lemma_meaning_widget.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
@@ -51,146 +50,136 @@ class VocabDetailsView extends StatelessWidget {
? _construct.lemmaCategory.color(context)
: _construct.lemmaCategory.darkColor(context));
- return LemmaMeaningBuilder(
- langCode: _userL2!,
- constructId: _construct.id,
- builder: (context, controller) {
- return AnalyticsDetailsViewContent(
- title: Column(
- children: [
- LayoutBuilder(
- builder: (context, constraints) {
- return ShrinkableText(
- text: _construct.lemma,
- maxWidth: constraints.maxWidth - 40.0,
- style: Theme.of(context).textTheme.headlineLarge?.copyWith(
- color: textColor,
- ),
- );
- },
- ),
- if (MatrixState.pangeaController.userController.showTranscription)
- Padding(
- padding: const EdgeInsets.only(top: 4.0),
- child: PhoneticTranscriptionWidget(
- text: _construct.lemma,
- textLanguage:
- MatrixState.pangeaController.userController.userL2!,
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- color: textColor.withAlpha((0.7 * 255).toInt()),
- fontSize: 18,
- ),
- iconSize: _iconSize * 0.8,
- ),
- ),
- ],
- ),
- subtitle: Column(
- children: [
- Row(
- mainAxisSize: MainAxisSize.min,
- spacing: 8.0,
- children: [
- Text(
- getGrammarCopy(
- category: "POS",
- lemma: _construct.category,
- context: context,
- ) ??
- _construct.lemma,
- style: Theme.of(context).textTheme.bodyLarge?.copyWith(
- color: textColor,
- ),
- ),
- SizedBox(
- width: _iconSize,
- height: _iconSize,
- child: MorphIcon(
- morphFeature: MorphFeaturesEnum.Pos,
- morphTag: _construct.category,
+ return AnalyticsDetailsViewContent(
+ title: Column(
+ children: [
+ LayoutBuilder(
+ builder: (context, constraints) {
+ return ShrinkableText(
+ text: _construct.lemma,
+ maxWidth: constraints.maxWidth - 40.0,
+ style: Theme.of(context).textTheme.headlineLarge?.copyWith(
+ color: textColor,
),
- ),
- ],
+ );
+ },
+ ),
+ if (MatrixState.pangeaController.userController.showTranscription)
+ Padding(
+ padding: const EdgeInsets.only(top: 4.0),
+ child: PhoneticTranscriptionWidget(
+ text: _construct.lemma,
+ textLanguage:
+ MatrixState.pangeaController.userController.userL2!,
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: textColor.withAlpha((0.7 * 255).toInt()),
+ fontSize: 18,
+ ),
+ iconSize: _iconSize * 0.8,
),
- const SizedBox(height: 16.0),
+ ),
+ ],
+ ),
+ subtitle: Column(
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 8.0,
+ children: [
Text(
- L10n.of(context).vocabEmoji,
- style: Theme.of(context).textTheme.labelMedium?.copyWith(
- fontStyle: FontStyle.italic,
+ getGrammarCopy(
+ category: "POS",
+ lemma: _construct.category,
+ context: context,
+ ) ??
+ _construct.lemma,
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor,
),
),
- LemmaHighlightEmojiRow(
- controller: controller,
- cId: constructId,
+ SizedBox(
+ width: _iconSize,
+ height: _iconSize,
+ child: MorphIcon(
+ morphFeature: MorphFeaturesEnum.Pos,
+ morphTag: _construct.category,
+ ),
),
],
),
- headerContent: Padding(
- padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
- child: Column(
- spacing: 8.0,
- children: [
- Align(
- alignment: Alignment.topLeft,
- child: _userL2 == null
- ? Text(L10n.of(context).meaningNotFound)
- : LemmaMeaningWidget(
- controller: controller,
- constructUse: _construct,
- style: Theme.of(context).textTheme.bodyLarge,
- leading: TextSpan(
- text: L10n.of(context).meaningSectionHeader,
- style:
- Theme.of(context).textTheme.bodyLarge?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
+ const SizedBox(height: 16.0),
+ Text(
+ L10n.of(context).vocabEmoji,
+ style: Theme.of(context).textTheme.labelMedium?.copyWith(
+ fontStyle: FontStyle.italic,
+ color: textColor,
),
- Align(
- alignment: Alignment.topLeft,
- child: Wrap(
- alignment: WrapAlignment.start,
- runAlignment: WrapAlignment.start,
- crossAxisAlignment: WrapCrossAlignment.center,
- children: [
- Text(
- L10n.of(context).formSectionHeader,
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: VocabDetailsEmojiSelector(constructId),
+ ),
+ ],
+ ),
+ headerContent: Padding(
+ padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
+ child: Column(
+ spacing: 8.0,
+ children: [
+ Align(
+ alignment: Alignment.topLeft,
+ child: _userL2 == null
+ ? Text(L10n.of(context).meaningNotFound)
+ : LemmaMeaningWidget(
+ constructId: constructId,
+ style: Theme.of(context).textTheme.bodyLarge,
+ leading: TextSpan(
+ text: L10n.of(context).meaningSectionHeader,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
- const SizedBox(width: 6.0),
- ...forms.mapIndexed(
- (i, form) => Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- WordTextWithAudioButton(
- text: form,
- style: Theme.of(context)
- .textTheme
- .bodyLarge
- ?.copyWith(
+ ),
+ ),
+ Align(
+ alignment: Alignment.topLeft,
+ child: Wrap(
+ alignment: WrapAlignment.start,
+ runAlignment: WrapAlignment.start,
+ crossAxisAlignment: WrapCrossAlignment.center,
+ children: [
+ Text(
+ L10n.of(context).formSectionHeader,
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(width: 6.0),
+ ...forms.mapIndexed(
+ (i, form) => Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ WordTextWithAudioButton(
+ text: form,
+ style:
+ Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor,
),
- uniqueID: "$form-${_construct.lemma}-$i",
- langCode: _userL2!,
- ),
- if (i != forms.length - 1) const Text(", "),
- ],
+ uniqueID: "$form-${_construct.lemma}-$i",
+ langCode: _userL2!,
),
- ),
- ],
+ if (i != forms.length - 1) const Text(", "),
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
- xpIcon: _construct.lemmaCategory.icon(_iconSize + 6.0),
- constructId: constructId,
- );
- },
+ ],
+ ),
+ ),
+ xpIcon: _construct.lemmaCategory.icon(_iconSize + 6.0),
+ constructId: constructId,
);
}
}
diff --git a/lib/pangea/analytics_details_popup/vocab_details_emoji_selector.dart b/lib/pangea/analytics_details_popup/vocab_details_emoji_selector.dart
new file mode 100644
index 000000000..85b99b602
--- /dev/null
+++ b/lib/pangea/analytics_details_popup/vocab_details_emoji_selector.dart
@@ -0,0 +1,68 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
+import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
+import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart';
+import 'package:fluffychat/widgets/matrix.dart';
+
+class VocabDetailsEmojiSelector extends StatefulWidget {
+ final ConstructIdentifier constructId;
+
+ const VocabDetailsEmojiSelector(
+ this.constructId, {
+ super.key,
+ });
+
+ @override
+ State createState() =>
+ VocabDetailsEmojiSelectorState();
+}
+
+class VocabDetailsEmojiSelectorState extends State
+ with LemmaEmojiSetter {
+ String? selectedEmoji;
+
+ @override
+ void initState() {
+ super.initState();
+ _setInitialEmoji();
+ }
+
+ @override
+ void didUpdateWidget(covariant VocabDetailsEmojiSelector oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.constructId != widget.constructId) {
+ _setInitialEmoji();
+ }
+ }
+
+ void _setInitialEmoji() {
+ setState(
+ () {
+ selectedEmoji = widget.constructId.userLemmaInfo.emojis?.firstOrNull;
+ },
+ );
+ }
+
+ Future _setEmoji(String emoji) async {
+ setState(() => selectedEmoji = emoji);
+ await setLemmaEmoji(
+ widget.constructId,
+ emoji,
+ "emoji-choice-item-$emoji-${widget.constructId.lemma}",
+ );
+ showLemmaEmojiSnackbar(context, widget.constructId, emoji);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return LemmaHighlightEmojiRow(
+ cId: widget.constructId,
+ langCode: MatrixState.pangeaController.userController.userL2Code!,
+ emoji: selectedEmoji,
+ onEmojiSelected: _setEmoji,
+ );
+ }
+}
diff --git a/lib/pangea/analytics_misc/lemma_emoji_setter_mixin.dart b/lib/pangea/analytics_misc/lemma_emoji_setter_mixin.dart
index 9e1b1d858..575fe1839 100644
--- a/lib/pangea/analytics_misc/lemma_emoji_setter_mixin.dart
+++ b/lib/pangea/analytics_misc/lemma_emoji_setter_mixin.dart
@@ -10,7 +10,7 @@ import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/widgets/matrix.dart';
-mixin LemmaEmojiSetter on State {
+mixin LemmaEmojiSetter {
Future setLemmaEmoji(
ConstructIdentifier constructId,
String emoji,
@@ -26,11 +26,13 @@ mixin LemmaEmojiSetter on State {
await constructId.setUserLemmaInfo(
constructId.userLemmaInfo.copyWith(emojis: [emoji]),
);
-
- _showSnackbar(constructId, emoji);
}
- void _showSnackbar(ConstructIdentifier constructId, String emoji) {
+ void showLemmaEmojiSnackbar(
+ BuildContext context,
+ ConstructIdentifier constructId,
+ String emoji,
+ ) {
if (InstructionsEnum.setLemmaEmoji.isToggledOff) return;
InstructionsEnum.setLemmaEmoji.setToggledOff(true);
diff --git a/lib/pangea/common/widgets/pressable_button.dart b/lib/pangea/common/widgets/pressable_button.dart
index dc0aaf475..2f662efcb 100644
--- a/lib/pangea/common/widgets/pressable_button.dart
+++ b/lib/pangea/common/widgets/pressable_button.dart
@@ -11,7 +11,8 @@ class PressableButton extends StatefulWidget {
final double buttonHeight;
final bool depressed;
final Color color;
- final Widget child;
+ final Widget Function(BuildContext context, bool depressed, Color shadowColor)
+ builder;
final void Function()? onPressed;
final Stream? triggerAnimation;
@@ -22,7 +23,7 @@ class PressableButton extends StatefulWidget {
const PressableButton({
required this.borderRadius,
- required this.child,
+ required this.builder,
required this.onPressed,
required this.color,
this.buttonHeight = 4,
@@ -137,8 +138,15 @@ class PressableButtonState extends State
@override
Widget build(BuildContext context) {
+ final shadowColor = Color.alphaBlend(
+ Colors.black.withAlpha(
+ (255 * widget.colorFactor).round(),
+ ),
+ widget.color,
+ );
+
if (!widget.visible) {
- return widget.child;
+ return widget.builder(context, _depressed, shadowColor);
}
return MouseRegion(
@@ -160,12 +168,7 @@ class PressableButtonState extends State
SizedBox(height: _tweenAnimation.value),
Container(
decoration: BoxDecoration(
- color: Color.alphaBlend(
- Colors.black.withAlpha(
- (255 * widget.colorFactor).round(),
- ),
- widget.color,
- ),
+ color: shadowColor,
borderRadius: widget.borderRadius,
),
padding: EdgeInsets.only(
@@ -173,7 +176,16 @@ class PressableButtonState extends State
? widget.buttonHeight - _tweenAnimation.value
: 0,
),
- child: child,
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: widget.borderRadius,
+ ),
+ child: widget.builder(
+ context,
+ _depressed || _tweenAnimation.value > 0,
+ shadowColor,
+ ),
+ ),
),
],
),
@@ -181,12 +193,6 @@ class PressableButtonState extends State
],
);
},
- child: Container(
- decoration: BoxDecoration(
- borderRadius: widget.borderRadius,
- ),
- child: widget.child,
- ),
),
),
);
diff --git a/lib/pangea/common/widgets/shimmer_background.dart b/lib/pangea/common/widgets/shimmer_background.dart
new file mode 100644
index 000000000..8a2e59a3a
--- /dev/null
+++ b/lib/pangea/common/widgets/shimmer_background.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/material.dart';
+
+import 'package:shimmer/shimmer.dart';
+
+import 'package:fluffychat/config/app_config.dart';
+
+class ShimmerBackground extends StatelessWidget {
+ final Widget child;
+ final Color shimmerColor;
+ final bool enabled;
+
+ const ShimmerBackground({
+ super.key,
+ required this.child,
+ this.shimmerColor = AppConfig.goldLight,
+ this.enabled = true,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ child,
+ if (enabled)
+ Positioned.fill(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(AppConfig.borderRadius),
+ child: Shimmer.fromColors(
+ baseColor: shimmerColor.withValues(alpha: 0.1),
+ highlightColor: shimmerColor.withValues(alpha: 0.6),
+ direction: ShimmerDirection.ltr,
+ child: Container(
+ decoration: BoxDecoration(
+ color: shimmerColor.withValues(alpha: 0.3),
+ borderRadius: BorderRadius.circular(AppConfig.borderRadius),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart
index 142a37753..c036f6633 100644
--- a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart
+++ b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart
@@ -1,41 +1,40 @@
import 'dart:async';
-import 'dart:developer';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-import 'package:shimmer/shimmer.dart';
-
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
-import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
-import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
+import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
+import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaHighlightEmojiRow extends StatefulWidget {
- final LemmaMeaningBuilderState controller;
final ConstructIdentifier cId;
+ final String langCode;
+
+ final Function(String) onEmojiSelected;
+
+ final String? emoji;
+ final Widget? selectedEmojiBadge;
const LemmaHighlightEmojiRow({
super.key,
- required this.controller,
required this.cId,
+ required this.langCode,
+ required this.onEmojiSelected,
+ this.emoji,
+ this.selectedEmojiBadge,
});
@override
- LemmaHighlightEmojiRowState createState() => LemmaHighlightEmojiRowState();
+ State createState() => LemmaHighlightEmojiRowState();
}
-class LemmaHighlightEmojiRowState extends State
- with LemmaEmojiSetter {
- bool _showShimmer = true;
- String? _selectedEmoji;
-
+class LemmaHighlightEmojiRowState extends State {
late StreamSubscription _analyticsSubscription;
- Timer? _shimmerTimer;
@override
void initState() {
@@ -43,157 +42,145 @@ class LemmaHighlightEmojiRowState extends State
_analyticsSubscription = MatrixState
.pangeaController.getAnalytics.analyticsStream.stream
.listen(_onAnalyticsUpdate);
- _setShimmer();
- }
-
- @override
- void didUpdateWidget(LemmaHighlightEmojiRow oldWidget) {
- if (oldWidget.cId != widget.cId) _setShimmer();
- super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_analyticsSubscription.cancel();
- _shimmerTimer?.cancel();
super.dispose();
}
- void _setShimmer() {
- setState(() {
- _selectedEmoji = widget.cId.userSetEmoji.firstOrNull;
- _showShimmer = _selectedEmoji == null;
-
- if (_showShimmer) {
- _shimmerTimer?.cancel();
- _shimmerTimer = Timer(const Duration(milliseconds: 1500), () {
- if (mounted) {
- setState(() {
- _showShimmer = false;
- _shimmerTimer?.cancel();
- _shimmerTimer = null;
- });
- }
- });
- }
- });
- }
-
void _onAnalyticsUpdate(AnalyticsStreamUpdate update) {
if (update.targetID != null) {
OverlayUtil.showPointsGained(update.targetID!, update.points, context);
}
}
- Future _setEmoji(String emoji, BuildContext context) async {
- try {
- setState(() => _selectedEmoji = emoji);
- await setLemmaEmoji(
- widget.cId,
- emoji,
- "emoji-choice-item-$emoji-${widget.cId.lemma}",
- );
- } catch (e, s) {
- debugger(when: kDebugMode);
- ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
- }
- }
-
@override
Widget build(BuildContext context) {
- if (widget.controller.isLoading) {
- return const CircularProgressIndicator.adaptive();
- }
+ return LemmaMeaningBuilder(
+ langCode: widget.langCode,
+ constructId: widget.cId,
+ builder: (context, controller) {
+ if (controller.isLoading) {
+ return const CircularProgressIndicator.adaptive();
+ }
- final emojis = widget.controller.lemmaInfo?.emoji;
- if (widget.controller.error != null || emojis == null || emojis.isEmpty) {
- return const SizedBox.shrink();
- }
+ final emojis = controller.lemmaInfo?.emoji;
+ if (controller.error != null || emojis == null || emojis.isEmpty) {
+ return const SizedBox.shrink();
+ }
- return Material(
- borderRadius: BorderRadius.circular(AppConfig.borderRadius),
- child: Container(
- padding: const EdgeInsets.all(8),
- height: 80,
- alignment: Alignment.center,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
+ return SizedBox(
+ height: 60.0,
child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: emojis
.map(
(emoji) => EmojiChoiceItem(
+ cId: widget.cId,
emoji: emoji,
- onSelectEmoji: () => _setEmoji(emoji, context),
- isDisplay: _selectedEmoji == emoji,
- showShimmer: _showShimmer,
+ onSelectEmoji: () => widget.onEmojiSelected(emoji),
+ selected: widget.emoji == emoji,
transformTargetId:
"emoji-choice-item-$emoji-${widget.cId.lemma}",
+ badge: widget.emoji == emoji
+ ? widget.selectedEmojiBadge
+ : null,
),
)
.toList(),
),
- ),
- ),
+ );
+ },
);
}
}
class EmojiChoiceItem extends StatefulWidget {
+ final ConstructIdentifier cId;
final String emoji;
final VoidCallback onSelectEmoji;
- final bool isDisplay;
- final bool showShimmer;
+ final bool selected;
final String transformTargetId;
+ final Widget? badge;
const EmojiChoiceItem({
super.key,
+ required this.cId,
required this.emoji,
- required this.isDisplay,
+ required this.selected,
required this.onSelectEmoji,
- required this.showShimmer,
required this.transformTargetId,
+ this.badge,
});
@override
- EmojiChoiceItemState createState() => EmojiChoiceItemState();
+ State createState() => EmojiChoiceItemState();
}
class EmojiChoiceItemState extends State {
- bool _isHovered = false;
+ bool shimmer = true;
+ Timer? _shimmerTimer;
+
+ @override
+ void initState() {
+ super.initState();
+ _showShimmer();
+ }
+
+ @override
+ void didUpdateWidget(covariant EmojiChoiceItem oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.emoji != widget.emoji) {
+ _showShimmer();
+ }
+ }
+
+ @override
+ void dispose() {
+ _shimmerTimer?.cancel();
+ super.dispose();
+ }
+
+ void _showShimmer() {
+ setState(() => shimmer = true);
+ _shimmerTimer?.cancel();
+ _shimmerTimer = Timer(const Duration(milliseconds: 1500), () {
+ if (mounted) setState(() => shimmer = false);
+ });
+ }
LayerLink get layerLink =>
MatrixState.pAnyState.layerLinkAndKey(widget.transformTargetId).link;
@override
Widget build(BuildContext context) {
- final shimmerColor = (Theme.of(context).brightness == Brightness.dark)
- ? Colors.white
- : Theme.of(context).colorScheme.primary;
- return MouseRegion(
- onEnter: (_) => setState(() => _isHovered = true),
- onExit: (_) => setState(() => _isHovered = false),
- child: GestureDetector(
+ return HoverBuilder(
+ builder: (context, hovered) => GestureDetector(
onTap: widget.onSelectEmoji,
- child: Padding(
- padding: const EdgeInsets.all(2.0),
- child: Stack(
- children: [
- CompositedTransformTarget(
+ child: Stack(
+ children: [
+ ShimmerBackground(
+ enabled: shimmer,
+ shimmerColor: (Theme.of(context).brightness == Brightness.dark)
+ ? Colors.white
+ : Theme.of(context).colorScheme.primary,
+ child: CompositedTransformTarget(
link: layerLink,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
- color: _isHovered
+ color: hovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
- border: widget.isDisplay
+ border: widget.selected
? Border.all(
- color: AppConfig.goldLight,
- width: 4,
+ color: AppConfig.goldLight.withAlpha(200),
+ width: 2,
)
: null,
),
@@ -203,26 +190,14 @@ class EmojiChoiceItemState extends State {
),
),
),
- if (widget.showShimmer)
- Positioned.fill(
- child: ClipRRect(
- borderRadius: BorderRadius.circular(AppConfig.borderRadius),
- child: Shimmer.fromColors(
- baseColor: shimmerColor.withValues(alpha: 0.1),
- highlightColor: shimmerColor.withValues(alpha: 0.6),
- direction: ShimmerDirection.ltr,
- child: Container(
- decoration: BoxDecoration(
- color: shimmerColor.withValues(alpha: 0.3),
- borderRadius:
- BorderRadius.circular(AppConfig.borderRadius),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
+ ),
+ if (widget.badge != null)
+ Positioned(
+ right: 0,
+ bottom: 0,
+ child: widget.badge!,
+ ),
+ ],
),
),
);
diff --git a/lib/pangea/lemmas/lemma_meaning_widget.dart b/lib/pangea/lemmas/lemma_meaning_widget.dart
index 29edac24e..191d4a161 100644
--- a/lib/pangea/lemmas/lemma_meaning_widget.dart
+++ b/lib/pangea/lemmas/lemma_meaning_widget.dart
@@ -1,71 +1,74 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
-import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
+import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaMeaningWidget extends StatelessWidget {
- final LemmaMeaningBuilderState controller;
-
- final ConstructUses constructUse;
+ final ConstructIdentifier constructId;
final TextStyle? style;
final InlineSpan? leading;
const LemmaMeaningWidget({
super.key,
- required this.controller,
- required this.constructUse,
+ required this.constructId,
this.style,
this.leading,
});
@override
Widget build(BuildContext context) {
- if (controller.isLoading) {
- return const TextLoadingShimmer();
- }
+ return LemmaMeaningBuilder(
+ langCode: MatrixState.pangeaController.userController.userL2!.langCode,
+ constructId: constructId,
+ builder: (context, controller) {
+ if (controller.isLoading) {
+ return const TextLoadingShimmer();
+ }
- if (controller.error != null) {
- if (controller.error is UnsubscribedException) {
- return ErrorIndicator(
- message: L10n.of(context).subscribeToUnlockDefinitions,
- style: style,
- onTap: () {
- MatrixState.pangeaController.subscriptionController
- .showPaywall(context);
- },
- );
- }
- return ErrorIndicator(
- message: L10n.of(context).errorFetchingDefinition,
- style: style,
- );
- }
-
- return Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Flexible(
- child: RichText(
- textAlign: leading == null ? TextAlign.center : TextAlign.start,
- text: TextSpan(
+ if (controller.error != null) {
+ if (controller.error is UnsubscribedException) {
+ return ErrorIndicator(
+ message: L10n.of(context).subscribeToUnlockDefinitions,
style: style,
- children: [
- if (leading != null) leading!,
- if (leading != null)
- const WidgetSpan(child: SizedBox(width: 6.0)),
- TextSpan(
- text: controller.lemmaInfo?.meaning,
+ onTap: () {
+ MatrixState.pangeaController.subscriptionController
+ .showPaywall(context);
+ },
+ );
+ }
+ return ErrorIndicator(
+ message: L10n.of(context).errorFetchingDefinition,
+ style: style,
+ );
+ }
+
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Flexible(
+ child: RichText(
+ textAlign: leading == null ? TextAlign.center : TextAlign.start,
+ text: TextSpan(
+ style: style,
+ children: [
+ if (leading != null) leading!,
+ if (leading != null)
+ const WidgetSpan(child: SizedBox(width: 6.0)),
+ TextSpan(
+ text: controller.lemmaInfo?.meaning,
+ ),
+ ],
),
- ],
+ ),
),
- ),
- ),
- ],
+ ],
+ );
+ },
);
}
}
diff --git a/lib/pangea/login/widgets/full_width_button.dart b/lib/pangea/login/widgets/full_width_button.dart
index 3ea6180f1..fe30be714 100644
--- a/lib/pangea/login/widgets/full_width_button.dart
+++ b/lib/pangea/login/widgets/full_width_button.dart
@@ -44,13 +44,14 @@ class FullWidthButtonState extends State {
onPressed: widget.onPressed,
borderRadius: BorderRadius.circular(36),
color: Theme.of(context).colorScheme.primary,
- child: Container(
- // internal padding
+ builder: (context, depressed, shadowColor) => Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
height: 40,
decoration: BoxDecoration(
color: widget.enabled
- ? Theme.of(context).colorScheme.primary
+ ? depressed
+ ? shadowColor
+ : Theme.of(context).colorScheme.primary
: Theme.of(context).disabledColor,
borderRadius: BorderRadius.circular(36),
),
diff --git a/lib/pangea/practice_activities/practice_target.dart b/lib/pangea/practice_activities/practice_target.dart
index 4b573a9bf..12353edbd 100644
--- a/lib/pangea/practice_activities/practice_target.dart
+++ b/lib/pangea/practice_activities/practice_target.dart
@@ -153,4 +153,13 @@ class PracticeTarget {
}
return null;
}
+
+ bool get hasAnyCorrectChoices {
+ for (final response in record.responses) {
+ if (response.isCorrect) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/lib/pangea/toolbar/message_practice/message_practice_mode_enum.dart b/lib/pangea/toolbar/message_practice/message_practice_mode_enum.dart
index be0375e31..e8d851186 100644
--- a/lib/pangea/toolbar/message_practice/message_practice_mode_enum.dart
+++ b/lib/pangea/toolbar/message_practice/message_practice_mode_enum.dart
@@ -4,6 +4,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
+import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
enum MessagePracticeMode {
@@ -70,4 +71,19 @@ enum MessagePracticeMode {
MessagePracticeMode.wordMeaning,
MessagePracticeMode.wordEmoji,
];
+
+ InstructionsEnum? get instruction {
+ switch (this) {
+ case MessagePracticeMode.listening:
+ return InstructionsEnum.chooseWordAudio;
+ case MessagePracticeMode.wordMeaning:
+ return InstructionsEnum.chooseLemmaMeaning;
+ case MessagePracticeMode.wordEmoji:
+ return InstructionsEnum.chooseEmoji;
+ case MessagePracticeMode.wordMorph:
+ return InstructionsEnum.chooseMorphs;
+ default:
+ return null;
+ }
+ }
}
diff --git a/lib/pangea/toolbar/message_practice/practice_match_card.dart b/lib/pangea/toolbar/message_practice/practice_match_card.dart
index 93928a188..952f5fefa 100644
--- a/lib/pangea/toolbar/message_practice/practice_match_card.dart
+++ b/lib/pangea/toolbar/message_practice/practice_match_card.dart
@@ -7,6 +7,7 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/common/widgets/choice_animation.dart';
+import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
@@ -30,21 +31,36 @@ class MatchActivityCard extends StatelessWidget {
ActivityTypeEnum get activityType => currentActivity.activityType;
Widget choiceDisplayContent(
+ BuildContext context,
String choice,
double? fontSize,
) {
switch (activityType) {
case ActivityTypeEnum.emoji:
case ActivityTypeEnum.wordMeaning:
- return Text(
- choice,
- style: TextStyle(fontSize: fontSize),
- textAlign: TextAlign.center,
+ return ShimmerBackground(
+ enabled: controller.selectedChoice == null &&
+ !currentActivity.practiceTarget.hasAnyCorrectChoices,
+ child: Padding(
+ padding: const EdgeInsets.all(8),
+ child: Text(
+ choice,
+ style: TextStyle(fontSize: fontSize),
+ textAlign: TextAlign.center,
+ ),
+ ),
);
case ActivityTypeEnum.wordFocusListening:
- return Icon(
- Icons.volume_up,
- size: fontSize,
+ return ShimmerBackground(
+ enabled: controller.selectedChoice == null &&
+ !currentActivity.practiceTarget.hasAnyCorrectChoices,
+ child: Padding(
+ padding: const EdgeInsets.all(8),
+ child: Icon(
+ Icons.volume_up,
+ size: fontSize,
+ ),
+ ),
);
default:
debugger(when: kDebugMode);
@@ -90,7 +106,8 @@ class MatchActivityCard extends StatelessWidget {
isSelected: controller.selectedChoice == cf,
isCorrect: wasCorrect,
constructForm: cf,
- content: choiceDisplayContent(cf.choiceContent, fontSize),
+ content:
+ choiceDisplayContent(context, cf.choiceContent, fontSize),
audioContent:
activityType == ActivityTypeEnum.wordFocusListening
? cf.choiceContent
diff --git a/lib/pangea/toolbar/message_practice/reading_assistance_input_bar.dart b/lib/pangea/toolbar/message_practice/reading_assistance_input_bar.dart
index 66e4bd350..782769ff3 100644
--- a/lib/pangea/toolbar/message_practice/reading_assistance_input_bar.dart
+++ b/lib/pangea/toolbar/message_practice/reading_assistance_input_bar.dart
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
+import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/practice_activity_card.dart';
@@ -62,6 +63,15 @@ class ReadingAssistanceInputBarState extends State {
),
],
),
+ if (widget.controller.practiceMode.instruction != null)
+ InstructionsInlineTooltip(
+ instructionsEnum: widget.controller.practiceMode.instruction!,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 4.0,
+ ),
+ animate: false,
+ ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Material(
diff --git a/lib/pangea/toolbar/message_practice/toolbar_button.dart b/lib/pangea/toolbar/message_practice/toolbar_button.dart
index 0e7206349..388d0a7ee 100644
--- a/lib/pangea/toolbar/message_practice/toolbar_button.dart
+++ b/lib/pangea/toolbar/message_practice/toolbar_button.dart
@@ -35,11 +35,11 @@ class ToolbarButton extends StatelessWidget {
playSound: true,
colorFactor:
Theme.of(context).brightness == Brightness.light ? 0.55 : 0.3,
- child: Container(
+ builder: (context, depressed, shadowColor) => Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
- color: color,
+ color: depressed ? shadowColor : color,
shape: BoxShape.circle,
),
child: Icon(
diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart
index 233e222ad..797ac99e8 100644
--- a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart
+++ b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart
@@ -345,12 +345,15 @@ class SelectModeButtonsState extends State {
playSound: enabled && mode != SelectMode.audio,
colorFactor:
theme.brightness == Brightness.light ? 0.55 : 0.3,
- child: AnimatedContainer(
+ builder: (context, depressed, shadowColor) =>
+ AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: buttonSize,
width: buttonSize,
decoration: BoxDecoration(
- color: theme.colorScheme.primaryContainer,
+ color: depressed
+ ? shadowColor
+ : theme.colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: _SelectModeButtonIcon(
@@ -577,12 +580,12 @@ class _MoreButton extends StatelessWidget {
onPressed: () => _showMenu(context),
playSound: true,
colorFactor: theme.brightness == Brightness.light ? 0.55 : 0.3,
- child: AnimatedContainer(
+ builder: (context, depressed, shadowColor) => AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
- color: theme.colorScheme.primaryContainer,
+ color: depressed ? shadowColor : theme.colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: const Icon(
diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart
index 184844e8d..79b2ab38f 100644
--- a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart
+++ b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart
@@ -6,7 +6,9 @@ import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
+import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
import 'package:fluffychat/pangea/common/utils/async_state.dart';
+import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/speech_to_text/speech_to_text_response_model.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart';
@@ -71,7 +73,7 @@ class _AudioLoader extends AsyncLoader<(PangeaAudioFile, File?)> {
}
}
-class SelectModeController {
+class SelectModeController with LemmaEmojiSetter {
final PangeaMessageEvent messageEvent;
final _TranscriptionLoader _transcriptLoader;
final _TranslationLoader _translationLoader;
@@ -86,10 +88,14 @@ class SelectModeController {
_sttTranslationLoader = _STTTranslationLoader(messageEvent);
ValueNotifier selectedMode = ValueNotifier(null);
+ ValueNotifier<(ConstructIdentifier, String)?> constructEmojiNotifier =
+ ValueNotifier<(ConstructIdentifier, String)?>(null);
+
final StreamController contentChangedStream = StreamController.broadcast();
void dispose() {
selectedMode.dispose();
+ constructEmojiNotifier.dispose();
_transcriptLoader.dispose();
_translationLoader.dispose();
_sttTranslationLoader.dispose();
@@ -185,6 +191,12 @@ class SelectModeController {
selectedMode.value = mode;
}
+ void setTokenEmoji(
+ ConstructIdentifier constructId,
+ String emoji,
+ ) =>
+ constructEmojiNotifier.value = (constructId, emoji);
+
Future fetchAudio() => _audioLoader.load();
Future fetchTranslation() => _translationLoader.load();
Future fetchTranscription() => _transcriptLoader.load();
diff --git a/lib/pangea/toolbar/reading_assistance/token_emoji_button.dart b/lib/pangea/toolbar/reading_assistance/token_emoji_button.dart
index 45bd36caf..9b1c31e98 100644
--- a/lib/pangea/toolbar/reading_assistance/token_emoji_button.dart
+++ b/lib/pangea/toolbar/reading_assistance/token_emoji_button.dart
@@ -1,19 +1,16 @@
import 'package:flutter/material.dart';
-import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.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';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
-import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
-import 'package:fluffychat/pangea/toolbar/reading_assistance/lemma_emoji_picker.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance/select_mode_buttons.dart';
import 'package:fluffychat/widgets/matrix.dart';
class TokenEmojiButton extends StatefulWidget {
final ValueNotifier selectModeNotifier;
- final ValueNotifier selectedTokenNotifier;
+ final ValueNotifier<(ConstructIdentifier, String)?> constructEmojiNotifier;
+ final VoidCallback onTap;
final PangeaToken? token;
final String? targetId;
@@ -22,7 +19,8 @@ class TokenEmojiButton extends StatefulWidget {
const TokenEmojiButton({
super.key,
required this.selectModeNotifier,
- required this.selectedTokenNotifier,
+ required this.constructEmojiNotifier,
+ required this.onTap,
this.token,
this.targetId,
this.enabled = true,
@@ -49,14 +47,14 @@ class TokenEmojiButtonState extends State
_initAnimation();
_prevMode = widget.selectModeNotifier.value;
widget.selectModeNotifier.addListener(_onUpdateSelectMode);
- widget.selectedTokenNotifier.addListener(_onSelectToken);
+ widget.constructEmojiNotifier.addListener(_onUpdateEmoji);
}
@override
void dispose() {
_controller?.dispose();
widget.selectModeNotifier.removeListener(_onUpdateSelectMode);
- widget.selectedTokenNotifier.removeListener(_onSelectToken);
+ widget.constructEmojiNotifier.removeListener(_onUpdateEmoji);
super.dispose();
}
@@ -87,68 +85,18 @@ class TokenEmojiButtonState extends State
_prevMode = mode;
}
- void _onSelectToken() {
- final selected = widget.selectedTokenNotifier.value;
- if (selected != null && selected == widget.token) {
- showTokenEmojiPopup();
+ void _onUpdateEmoji() {
+ final value = widget.constructEmojiNotifier.value;
+ if (value == null) return;
+
+ final constructId = value.$1;
+ final emoji = value.$2;
+
+ if (mounted && constructId == widget.token?.vocabConstructID) {
+ setState(() => _emoji = emoji);
}
}
- void showTokenEmojiPopup() {
- if (widget.targetId == null || widget.token == null) return;
- OverlayUtil.showPositionedCard(
- overlayKey: "overlay_emoji_selector",
- context: context,
- cardToShow: LemmaMeaningBuilder(
- langCode: MatrixState.pangeaController.userController.userL2Code!,
- constructId: widget.token!.vocabConstructID,
- builder: (context, controller) {
- return Material(
- type: MaterialType.transparency,
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surface,
- borderRadius: BorderRadius.circular(
- AppConfig.borderRadius,
- ),
- ),
- child: LemmaEmojiPicker(
- emojis: controller.lemmaInfo?.emoji ?? [],
- onSelect: (emoji) {
- _setTokenEmoji(emoji);
- MatrixState.pAnyState.closeOverlay("overlay_emoji_selector");
- },
- loading: controller.isLoading,
- ),
- ),
- );
- },
- ),
- transformTargetId: widget.targetId!,
- closePrevOverlay: false,
- addBorder: false,
- maxWidth: (40 * 5) + (4 * 5) + 16,
- maxHeight: 60,
- );
- }
-
- void _setTokenEmoji(String emoji) {
- setState(() => _emoji = emoji);
-
- if (widget.targetId == null || widget.token == null) return;
- setLemmaEmoji(
- widget.token!.vocabConstructID,
- emoji,
- widget.targetId,
- ).catchError((e, s) {
- ErrorHandler.logError(
- data: widget.token!.toJson(),
- e: e,
- s: s,
- );
- });
- }
-
@override
Widget build(BuildContext context) {
if (_sizeAnimation == null) {
@@ -183,7 +131,7 @@ class TokenEmojiButtonState extends State
child: child,
builder: (context, child) {
return InkWell(
- onTap: showTokenEmojiPopup,
+ onTap: widget.onTap,
borderRadius: BorderRadius.circular(99.0),
child: Container(
height: _sizeAnimation!.value,
diff --git a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart
index 00ba9ee27..4fe4c1c56 100644
--- a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart
+++ b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart
@@ -3,64 +3,107 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
+import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
-import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart';
-import 'package:fluffychat/pangea/toolbar/reading_assistance/lemma_emoji_picker.dart';
+import 'package:fluffychat/pangea/lemmas/lemma_highlight_emoji_row.dart';
+import 'package:fluffychat/widgets/matrix.dart';
-class LemmaReactionPicker extends StatelessWidget {
+class LemmaReactionPicker extends StatefulWidget {
final Event? event;
- final ConstructIdentifier construct;
+ final ConstructIdentifier constructId;
+ final Function(String)? onSetEmoji;
final String langCode;
const LemmaReactionPicker({
super.key,
- required this.construct,
+ required this.constructId,
+ required this.onSetEmoji,
required this.langCode,
this.event,
});
- Future setEmoji(
- String emoji,
- List emojis,
- ) async {
- if (event?.room.timeline == null) {
- throw Exception("Timeline is null in reaction picker");
- }
+ @override
+ State createState() => LemmaReactionPickerState();
+}
- final client = event!.room.client;
- final userSentEmojis = event!
+class LemmaReactionPickerState extends State
+ with LemmaEmojiSetter {
+ String? _selectedEmoji;
+
+ @override
+ void initState() {
+ super.initState();
+ _setInitialEmoji();
+ }
+
+ @override
+ void didUpdateWidget(covariant LemmaReactionPicker oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.constructId != widget.constructId) {
+ _setInitialEmoji();
+ }
+ }
+
+ void _setInitialEmoji() {
+ setState(
+ () {
+ _selectedEmoji = widget.constructId.userLemmaInfo.emojis?.firstOrNull;
+ },
+ );
+ }
+
+ Event? _sentReaction(String emoji) {
+ final userSentEmojis = widget.event!
.aggregatedEvents(
- event!.room.timeline!,
+ widget.event!.room.timeline!,
RelationshipTypes.reaction,
)
.where(
- (e) =>
- e.senderId == client.userID &&
- emojis.contains(e.content.tryGetMap('m.relates_to')?['key']),
+ (e) => e.senderId == Matrix.of(context).client.userID,
);
- final reactionEvent = userSentEmojis.firstWhereOrNull(
+ return userSentEmojis.firstWhereOrNull(
(e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji,
);
+ }
+
+ Future _setEmoji(String emoji) async {
+ setState(() => _selectedEmoji = emoji);
+ widget.onSetEmoji?.call(emoji);
+
+ await setLemmaEmoji(
+ widget.constructId,
+ emoji,
+ "emoji-choice-item-$emoji-${widget.constructId.lemma}",
+ );
+
+ if (mounted) {
+ showLemmaEmojiSnackbar(context, widget.constructId, emoji);
+ }
+ }
+
+ Future _sendOrRedactReaction(String emoji) async {
+ if (widget.event?.room.timeline == null) return;
try {
+ final reactionEvent = _sentReaction(emoji);
if (reactionEvent != null) {
await reactionEvent.redactEvent();
return;
}
- await Future.wait([
- ...userSentEmojis.map((e) => e.redactEvent()),
- event!.room.sendReaction(event!.eventId, emoji),
- ]);
+ await widget.event!.room.sendReaction(
+ widget.event!.eventId,
+ emoji,
+ );
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'emoji': emoji,
- 'eventId': event?.eventId,
+ 'eventId': widget.event?.eventId,
},
);
}
@@ -68,44 +111,28 @@ class LemmaReactionPicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return LemmaMeaningBuilder(
- langCode: langCode,
- constructId: construct,
- builder: (context, controller) {
- final sentReactions = {};
- if (event?.room.timeline != null) {
- sentReactions.addAll(
- event!
- .aggregatedEvents(
- event!.room.timeline!,
- RelationshipTypes.reaction,
- )
- .where(
- (event) =>
- event.senderId == event.room.client.userID &&
- event.type == 'm.reaction',
- )
- .map(
- (event) => event.content
- .tryGetMap('m.relates_to')
- ?.tryGet('key'),
- )
- .whereType(),
- );
- }
-
- return LemmaEmojiPicker(
- emojis: controller.lemmaInfo?.emoji ?? [],
- onSelect: event?.room.timeline != null
- ? (emoji) => setEmoji(
- emoji,
- controller.lemmaInfo?.emoji ?? [],
- )
- : null,
- disabled: (emoji) => sentReactions.contains(emoji),
- loading: controller.isLoading,
- );
- },
+ return LemmaHighlightEmojiRow(
+ cId: widget.constructId,
+ langCode: widget.langCode,
+ onEmojiSelected: (emoji) => emoji != _selectedEmoji
+ ? _setEmoji(emoji)
+ : _sendOrRedactReaction(emoji),
+ emoji: _selectedEmoji,
+ selectedEmojiBadge: widget.event != null &&
+ _selectedEmoji != null &&
+ _sentReaction(_selectedEmoji!) == null
+ ? CircleAvatar(
+ backgroundColor: Theme.of(context).colorScheme.onSurface,
+ radius: 8.0,
+ child: Center(
+ child: Icon(
+ Icons.send,
+ size: 12.0,
+ color: Theme.of(context).colorScheme.surface,
+ ),
+ ),
+ )
+ : null,
);
}
}
diff --git a/lib/pangea/toolbar/word_card/reading_assistance_content.dart b/lib/pangea/toolbar/word_card/reading_assistance_content.dart
index 71d2e0d97..83dca55fb 100644
--- a/lib/pangea/toolbar/word_card/reading_assistance_content.dart
+++ b/lib/pangea/toolbar/word_card/reading_assistance_content.dart
@@ -51,6 +51,10 @@ class ReadingAssistanceContent extends StatelessWidget {
onClose: () => overlayController.updateSelectedSpan(null),
langCode: overlayController.pangeaMessageEvent.messageDisplayLangCode,
onDismissNewWordOverlay: () => overlayController.setState(() {}),
+ setEmoji: (emoji) => overlayController.selectModeController.setTokenEmoji(
+ overlayController.selectedToken!.vocabConstructID,
+ emoji,
+ ),
onFlagTokenInfo: (LemmaInfoResponse lemmaInfo, String phonetics) {
if (selectedTokenIndex < 0) return;
final requestData = TokenInfoFeedbackRequestData(
diff --git a/lib/pangea/toolbar/word_card/word_card_switcher.dart b/lib/pangea/toolbar/word_card/word_card_switcher.dart
index 1e2ab854f..18551a02a 100644
--- a/lib/pangea/toolbar/word_card/word_card_switcher.dart
+++ b/lib/pangea/toolbar/word_card/word_card_switcher.dart
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/toolbar/layout/message_selection_positioner.dart';
-import 'package:fluffychat/pangea/toolbar/reading_assistance/select_mode_buttons.dart';
import 'package:fluffychat/pangea/toolbar/word_card/reading_assistance_content.dart';
class WordCardSwitcher extends StatelessWidget {
@@ -19,15 +18,13 @@ class WordCardSwitcher extends StatelessWidget {
? Alignment.bottomRight
: Alignment.bottomLeft,
duration: FluffyThemes.animationDuration,
- child: mode == SelectMode.emoji
- ? const SizedBox()
- : controller.widget.overlayController.selectedToken != null
- ? ReadingAssistanceContent(
- overlayController: controller.widget.overlayController,
- )
- : MessageReactionPicker(
- chatController: controller.widget.chatController,
- ),
+ child: controller.widget.overlayController.selectedToken != null
+ ? ReadingAssistanceContent(
+ overlayController: controller.widget.overlayController,
+ )
+ : MessageReactionPicker(
+ chatController: controller.widget.chatController,
+ ),
);
},
);
diff --git a/lib/pangea/toolbar/word_card/word_zoom_widget.dart b/lib/pangea/toolbar/word_card/word_zoom_widget.dart
index 3bb8b0bb9..caabccaa5 100644
--- a/lib/pangea/toolbar/word_card/word_zoom_widget.dart
+++ b/lib/pangea/toolbar/word_card/word_zoom_widget.dart
@@ -29,12 +29,14 @@ class WordZoomWidget extends StatelessWidget {
final VoidCallback? onDismissNewWordOverlay;
final Function(LemmaInfoResponse, String)? onFlagTokenInfo;
+ final Function(String)? setEmoji;
const WordZoomWidget({
super.key,
required this.token,
required this.construct,
required this.langCode,
+ this.setEmoji,
this.onClose,
this.wordIsNew = false,
this.event,
@@ -141,8 +143,9 @@ class WordZoomWidget extends StatelessWidget {
iconSize: 24.0,
),
LemmaReactionPicker(
- construct: construct,
+ constructId: construct,
langCode: langCode,
+ onSetEmoji: setEmoji,
event: event,
),
LemmaMeaningDisplay(