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(