Merge pull request #4861 from pangeachat/4858-play-test-1216
4858 play test 1216
This commit is contained in:
commit
69c64c1e45
22 changed files with 608 additions and 471 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -601,7 +601,7 @@ class Message extends StatelessWidget {
|
|||
color: color,
|
||||
visible:
|
||||
isButton && !noBubble,
|
||||
child:
|
||||
builder: (context, _, __) =>
|
||||
// Pangea#
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<VocabDetailsEmojiSelector> createState() =>
|
||||
VocabDetailsEmojiSelectorState();
|
||||
}
|
||||
|
||||
class VocabDetailsEmojiSelectorState extends State<VocabDetailsEmojiSelector>
|
||||
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<void> _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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T extends StatefulWidget> on State<T> {
|
||||
mixin LemmaEmojiSetter {
|
||||
Future<void> setLemmaEmoji(
|
||||
ConstructIdentifier constructId,
|
||||
String emoji,
|
||||
|
|
@ -26,11 +26,13 @@ mixin LemmaEmojiSetter<T extends StatefulWidget> on State<T> {
|
|||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PressableButton>
|
|||
|
||||
@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<PressableButton>
|
|||
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<PressableButton>
|
|||
? 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<PressableButton>
|
|||
],
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: widget.borderRadius,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
44
lib/pangea/common/widgets/shimmer_background.dart
Normal file
44
lib/pangea/common/widgets/shimmer_background.dart
Normal file
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<LemmaHighlightEmojiRow> createState() => LemmaHighlightEmojiRowState();
|
||||
}
|
||||
|
||||
class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow>
|
||||
with LemmaEmojiSetter {
|
||||
bool _showShimmer = true;
|
||||
String? _selectedEmoji;
|
||||
|
||||
class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow> {
|
||||
late StreamSubscription<AnalyticsStreamUpdate> _analyticsSubscription;
|
||||
Timer? _shimmerTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -43,157 +42,145 @@ class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow>
|
|||
_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<void> _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<EmojiChoiceItem> createState() => EmojiChoiceItemState();
|
||||
}
|
||||
|
||||
class EmojiChoiceItemState extends State<EmojiChoiceItem> {
|
||||
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<EmojiChoiceItem> {
|
|||
),
|
||||
),
|
||||
),
|
||||
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!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,13 +44,14 @@ class FullWidthButtonState extends State<FullWidthButton> {
|
|||
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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -153,4 +153,13 @@ class PracticeTarget {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get hasAnyCorrectChoices {
|
||||
for (final response in record.responses) {
|
||||
if (response.isCorrect) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ReadingAssistanceInputBar> {
|
|||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -345,12 +345,15 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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<SelectMode?> selectedMode = ValueNotifier<SelectMode?>(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<void> fetchAudio() => _audioLoader.load();
|
||||
Future<void> fetchTranslation() => _translationLoader.load();
|
||||
Future<void> fetchTranscription() => _transcriptLoader.load();
|
||||
|
|
|
|||
|
|
@ -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<SelectMode?> selectModeNotifier;
|
||||
final ValueNotifier<PangeaToken?> 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<TokenEmojiButton>
|
|||
_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<TokenEmojiButton>
|
|||
_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<TokenEmojiButton>
|
|||
child: child,
|
||||
builder: (context, child) {
|
||||
return InkWell(
|
||||
onTap: showTokenEmojiPopup,
|
||||
onTap: widget.onTap,
|
||||
borderRadius: BorderRadius.circular(99.0),
|
||||
child: Container(
|
||||
height: _sizeAnimation!.value,
|
||||
|
|
|
|||
|
|
@ -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<void> setEmoji(
|
||||
String emoji,
|
||||
List<String> emojis,
|
||||
) async {
|
||||
if (event?.room.timeline == null) {
|
||||
throw Exception("Timeline is null in reaction picker");
|
||||
}
|
||||
@override
|
||||
State<LemmaReactionPicker> createState() => LemmaReactionPickerState();
|
||||
}
|
||||
|
||||
final client = event!.room.client;
|
||||
final userSentEmojis = event!
|
||||
class LemmaReactionPickerState extends State<LemmaReactionPicker>
|
||||
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<void> _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<void> _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 = <String>{};
|
||||
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<String, Object?>('m.relates_to')
|
||||
?.tryGet<String>('key'),
|
||||
)
|
||||
.whereType<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue