use the same widget for word card and vocab details emoji pickers
This commit is contained in:
parent
2897142b9d
commit
b795ba3c06
8 changed files with 439 additions and 383 deletions
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,41 @@
|
|||
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/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,126 +43,115 @@ 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}",
|
||||
);
|
||||
showLemmaEmojiSnackbar(context, widget.cId, emoji);
|
||||
} 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;
|
||||
|
|
@ -172,58 +161,59 @@ class EmojiChoiceItemState extends State<EmojiChoiceItem> {
|
|||
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(
|
||||
link: layerLink,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered
|
||||
? Theme.of(context).colorScheme.primary.withAlpha(50)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
border: widget.isDisplay
|
||||
? Border.all(
|
||||
color: AppConfig.goldLight,
|
||||
width: 4,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
widget.emoji,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
CompositedTransformTarget(
|
||||
link: layerLink,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: hovered
|
||||
? Theme.of(context).colorScheme.primary.withAlpha(50)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
border: widget.selected
|
||||
? Border.all(
|
||||
color: AppConfig.goldLight.withAlpha(200),
|
||||
width: 2,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
widget.emoji,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
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 (shimmer)
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ 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/common/utils/error_handler.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';
|
||||
|
|
@ -192,26 +191,11 @@ class SelectModeController with LemmaEmojiSetter {
|
|||
selectedMode.value = mode;
|
||||
}
|
||||
|
||||
Future<void> setTokenEmoji(
|
||||
void setTokenEmoji(
|
||||
ConstructIdentifier constructId,
|
||||
String emoji,
|
||||
String targetId,
|
||||
) async {
|
||||
constructEmojiNotifier.value = (constructId, emoji);
|
||||
try {
|
||||
await setLemmaEmoji(
|
||||
constructId,
|
||||
emoji,
|
||||
targetId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
data: constructId.toJson(),
|
||||
e: e,
|
||||
s: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
) =>
|
||||
constructEmojiNotifier.value = (constructId, emoji);
|
||||
|
||||
Future<void> fetchAudio() => _audioLoader.load();
|
||||
Future<void> fetchTranslation() => _translationLoader.load();
|
||||
|
|
|
|||
|
|
@ -3,68 +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 Future<void> Function(String)? setEmoji;
|
||||
final ConstructIdentifier constructId;
|
||||
final Function(String)? onSetEmoji;
|
||||
final String langCode;
|
||||
|
||||
const LemmaReactionPicker({
|
||||
super.key,
|
||||
required this.construct,
|
||||
required this.setEmoji,
|
||||
required this.constructId,
|
||||
required this.onSetEmoji,
|
||||
required this.langCode,
|
||||
this.event,
|
||||
});
|
||||
|
||||
Future<void> onSelect(
|
||||
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 {
|
||||
await setEmoji?.call(emoji);
|
||||
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -72,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) => onSelect(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ class ReadingAssistanceContent extends StatelessWidget {
|
|||
setEmoji: (emoji) => overlayController.selectModeController.setTokenEmoji(
|
||||
overlayController.selectedToken!.vocabConstructID,
|
||||
emoji,
|
||||
overlayController.tokenEmojiPopupKey(overlayController.selectedToken!),
|
||||
),
|
||||
onFlagTokenInfo: (LemmaInfoResponse lemmaInfo, String phonetics) {
|
||||
if (selectedTokenIndex < 0) return;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class WordZoomWidget extends StatelessWidget {
|
|||
|
||||
final VoidCallback? onDismissNewWordOverlay;
|
||||
final Function(LemmaInfoResponse, String)? onFlagTokenInfo;
|
||||
final Future<void> Function(String)? setEmoji;
|
||||
final Function(String)? setEmoji;
|
||||
|
||||
const WordZoomWidget({
|
||||
super.key,
|
||||
|
|
@ -143,10 +143,10 @@ class WordZoomWidget extends StatelessWidget {
|
|||
iconSize: 24.0,
|
||||
),
|
||||
LemmaReactionPicker(
|
||||
construct: construct,
|
||||
constructId: construct,
|
||||
langCode: langCode,
|
||||
onSetEmoji: setEmoji,
|
||||
event: event,
|
||||
setEmoji: setEmoji,
|
||||
),
|
||||
LemmaMeaningDisplay(
|
||||
langCode: langCode,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue