Emoji-assignment (#2218)
* feat(lemma_emoji_row): vocab assignment and some reading assistance tweaks * generated --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
aab980ec4b
commit
87f60857e9
25 changed files with 570 additions and 666 deletions
|
|
@ -4671,7 +4671,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"chooseLemmaMeaningInstructionsBody": "Match the meanings below with the underlined words in the message.",
|
||||
"chooseLemmaMeaningInstructionsBody": "Match the meanings to the words in the message!",
|
||||
"doubleClickToEdit": "Double-click to edit.",
|
||||
"removeFeature": "Remove {feature}",
|
||||
"@removeFeature": {
|
||||
|
|
@ -4808,7 +4808,7 @@
|
|||
"joinByCode": "Join by code",
|
||||
"createASpace": "Create a space",
|
||||
"chooseWordAudioInstructionsBody": "Listen to the full message then match the word audios to the right blanks!",
|
||||
"chooseMorphsInstructionsBody": "Match the grammar tags with the words in the message. Click and hold an option for a hint!",
|
||||
"chooseMorphsInstructionsBody": "Click the puzzle pieces for grammar questions!",
|
||||
"inviteAndLaunch": "Launch and invite",
|
||||
"createOwnChat": "Create your own chat",
|
||||
"pleaseEnterInt": "Please enter a number",
|
||||
|
|
@ -4830,5 +4830,6 @@
|
|||
"referFriends": "Refer friends",
|
||||
"referFriendDialogTitle": "Invite a friend to your conversation",
|
||||
"referFriendDialogDesc": "Do you have a friend who is excited to learn a new language with you? Then copy and send this invitation link to join and start chatting with you today.",
|
||||
"youUnlocked": "You've unlocked"
|
||||
"youUnlocked": "You've unlocked",
|
||||
"selectForGrammar": "Select a grammar icon for activities and details."
|
||||
}
|
||||
|
|
@ -78,9 +78,13 @@ class VocabDetailsView extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
LemmaEmojiRow(
|
||||
isSelected: false,
|
||||
shouldShowEmojis: true,
|
||||
cId: constructId,
|
||||
onTap: () => {},
|
||||
removeCallback: null,
|
||||
onTapOverride: null,
|
||||
emojiSetCallback: () {
|
||||
debugPrint('Emoji set callback');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class VocabAnalyticsListTileState extends State<VocabAnalyticsListTile> {
|
|||
height: (maxWidth - padding * 2) * 0.6,
|
||||
child: Opacity(
|
||||
opacity:
|
||||
widget.constructUse.id.userSetEmoji.isEmpty ? 0.2 : 1,
|
||||
widget.constructUse.id.userSetEmoji.isEmpty ? 0.5 : 1,
|
||||
child: widget.constructUse.id.userSetEmoji.isNotEmpty
|
||||
? Text(
|
||||
widget.constructUse.id.userSetEmoji.first,
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class VocabAnalyticsListViewState extends State<VocabAnalyticsListView> {
|
|||
padding: const EdgeInsets.all(8.0),
|
||||
child: Badge(
|
||||
label: Text(count.toString()),
|
||||
child: constructLevelCategory.icon(24),
|
||||
child: constructLevelCategory.icon(40),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -125,50 +125,54 @@ class VocabAnalyticsListViewState extends State<VocabAnalyticsListView> {
|
|||
curve: Curves.easeInOut,
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: _isSearching ? 8.0 : 24.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 225.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child: _isSearching
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
key: const ValueKey('search'),
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 12.0,
|
||||
child: Container(
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 225.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child: _isSearching
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
key: const ValueKey('search'),
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: _toggleSearching,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
key: const ValueKey('filters'),
|
||||
children: filters,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: _toggleSearching,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
key: const ValueKey('filters'),
|
||||
children: filters,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
|
@ -65,20 +66,24 @@ class PangeaAnyState {
|
|||
Overlay.of(context).insert(entry);
|
||||
}
|
||||
|
||||
void closeOverlay() {
|
||||
if (entries.isNotEmpty) {
|
||||
void closeOverlay([String? overlayKey]) {
|
||||
final entry = overlayKey != null
|
||||
? entries.firstWhereOrNull((element) => element.key == overlayKey)
|
||||
: entries.lastOrNull;
|
||||
|
||||
if (entry != null) {
|
||||
try {
|
||||
entries.last.entry.remove();
|
||||
entry.entry.remove();
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {
|
||||
"overlay": entries.last,
|
||||
"overlay": entry,
|
||||
},
|
||||
);
|
||||
}
|
||||
entries.removeLast();
|
||||
entries.remove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,4 +129,16 @@ class LayerLinkAndKey {
|
|||
"link": link.toString(),
|
||||
"transformTargetId": transformTargetId,
|
||||
};
|
||||
|
||||
@override
|
||||
operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LayerLinkAndKey &&
|
||||
runtimeType == other.runtimeType &&
|
||||
key == other.key &&
|
||||
link == other.link &&
|
||||
transformTargetId == other.transformTargetId;
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ link.hashCode ^ transformTargetId.hashCode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -489,6 +489,9 @@ class PangeaToken {
|
|||
Future<void> setEmoji(List<String> emojis) =>
|
||||
vocabConstructID.setUserLemmaInfo(UserSetLemmaInfo(emojis: emojis));
|
||||
|
||||
Future<void> setMeaning(String meaning) =>
|
||||
vocabConstructID.setUserLemmaInfo(UserSetLemmaInfo(meaning: meaning));
|
||||
|
||||
/// [getEmoji] gets the emoji for the lemma
|
||||
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
|
||||
List<String> getEmoji() => vocabConstructID.userSetEmoji;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:country_picker/country_picker.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instruction_settings.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
|
@ -129,6 +133,13 @@ class SettingsLearningController extends State<SettingsLearning> {
|
|||
},
|
||||
waitForDataInSync: true,
|
||||
),
|
||||
onError: (e, s) {
|
||||
debugPrint("Error resetting instruction tooltips: $e");
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e, s: s, data: {"resetInstructionTooltips": true});
|
||||
return null;
|
||||
},
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +1,270 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/app_emojis.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/user_set_lemma_info.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class LemmaEmojiRow extends StatelessWidget {
|
||||
class LemmaEmojiRow extends StatefulWidget {
|
||||
final ConstructIdentifier cId;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onTapOverride;
|
||||
final bool isSelected;
|
||||
final bool shouldShowEmojis;
|
||||
|
||||
/// if a setState is defined then we're in a context where
|
||||
/// we allow removing an emoji
|
||||
/// later we'll probably want to allow this everywhere
|
||||
final void Function()? removeCallback;
|
||||
final void Function()? emojiSetCallback;
|
||||
|
||||
const LemmaEmojiRow({
|
||||
required this.cId,
|
||||
required this.onTap,
|
||||
required this.removeCallback,
|
||||
this.isSelected = false,
|
||||
super.key,
|
||||
required this.cId,
|
||||
required this.onTapOverride,
|
||||
required this.isSelected,
|
||||
required this.shouldShowEmojis,
|
||||
this.emojiSetCallback,
|
||||
});
|
||||
|
||||
List<String> get emojis => cId.userSetEmoji;
|
||||
@override
|
||||
LemmaEmojiRowState createState() => LemmaEmojiRowState();
|
||||
}
|
||||
|
||||
Future<void> onEmojiTap(String toRemove) async {
|
||||
await cId.setUserLemmaInfo(
|
||||
UserSetLemmaInfo(
|
||||
emojis: emojis.where((e) => e != toRemove).toList(),
|
||||
),
|
||||
);
|
||||
removeCallback!();
|
||||
class LemmaEmojiRowState extends State<LemmaEmojiRow> {
|
||||
String? displayEmoji;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
displayEmoji = widget.cId.userSetEmoji.firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
didUpdateWidget(LemmaEmojiRow oldWidget) {
|
||||
if (oldWidget.isSelected != widget.isSelected ||
|
||||
widget.cId.userSetEmoji != oldWidget.cId.userSetEmoji) {
|
||||
setState(() => displayEmoji = widget.cId.userSetEmoji.firstOrNull);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
MatrixState.pAnyState.disposeByWidgetKey(widget.cId.string);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void openEmojiSetOverlay() async {
|
||||
List<String> emojiChoices = [];
|
||||
try {
|
||||
final info = await widget.cId.getLemmaInfo();
|
||||
emojiChoices = info.emoji;
|
||||
} catch (e, s) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
emojiChoices
|
||||
.add(AppEmojis.emojis[Random().nextInt(AppEmojis.emojis.length)]);
|
||||
}
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
}
|
||||
|
||||
try {
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: EmojiEditOverlay(
|
||||
cId: widget.cId,
|
||||
onSelectEmoji: setEmoji,
|
||||
emojis: emojiChoices,
|
||||
),
|
||||
transformTargetId: widget.cId.string,
|
||||
backDropToDismiss: true,
|
||||
blurBackground: false,
|
||||
borderColor: Theme.of(context).colorScheme.primary,
|
||||
closePrevOverlay: false,
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setEmoji(String emoji) async {
|
||||
try {
|
||||
displayEmoji = emoji;
|
||||
|
||||
widget.cId
|
||||
.setUserLemmaInfo(
|
||||
UserSetLemmaInfo(
|
||||
emojis: [emoji],
|
||||
),
|
||||
)
|
||||
.catchError((e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
});
|
||||
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
|
||||
widget.emojiSetCallback?.call();
|
||||
|
||||
setState(() {});
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
for (var i = 0; i < maxEmojisPerLemma; i++)
|
||||
Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
alignment: Alignment.center,
|
||||
child: i < emojis.length
|
||||
? GestureDetector(
|
||||
onTap: removeCallback == null
|
||||
? null
|
||||
: () => onEmojiTap(emojis[i]),
|
||||
child: Text(
|
||||
emojis[i],
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
)
|
||||
: WordZoomActivityButton(
|
||||
icon: Icon(
|
||||
Icons.add_reaction_outlined,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
isSelected: isSelected,
|
||||
onPressed: onTap,
|
||||
opacity: isSelected ? 1 : 0.4,
|
||||
tooltip: MessageMode.wordEmoji.title(context),
|
||||
return CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey(
|
||||
widget.cId.string,
|
||||
)
|
||||
.link,
|
||||
child: Container(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey(
|
||||
widget.cId.string,
|
||||
)
|
||||
.key,
|
||||
height: 50,
|
||||
width: 50,
|
||||
alignment: Alignment.center,
|
||||
child: displayEmoji != null && widget.shouldShowEmojis
|
||||
? InkWell(
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withAlpha(50),
|
||||
onTap: widget.onTapOverride ?? openEmojiSetOverlay,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
displayEmoji!,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: WordZoomActivityButton(
|
||||
icon: Icon(
|
||||
Icons.add_reaction_outlined,
|
||||
color: widget.isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
isSelected: widget.isSelected,
|
||||
onPressed: widget.onTapOverride ?? openEmojiSetOverlay,
|
||||
opacity: widget.isSelected ? 1 : 0.4,
|
||||
tooltip: MessageMode.wordEmoji.title(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiEditOverlay extends StatelessWidget {
|
||||
final Function(String) onSelectEmoji;
|
||||
final ConstructIdentifier cId;
|
||||
final List<String> emojis;
|
||||
|
||||
const EmojiEditOverlay({
|
||||
super.key,
|
||||
required this.onSelectEmoji,
|
||||
required this.cId,
|
||||
required this.emojis,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
height: 70,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha(50),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: emojis
|
||||
.map(
|
||||
(emoji) => EmojiChoiceItem(
|
||||
emoji: emoji,
|
||||
onSelectEmoji: onSelectEmoji,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiChoiceItem extends StatefulWidget {
|
||||
final String emoji;
|
||||
final Function(String) onSelectEmoji;
|
||||
|
||||
const EmojiChoiceItem({
|
||||
super.key,
|
||||
required this.emoji,
|
||||
required this.onSelectEmoji,
|
||||
});
|
||||
|
||||
@override
|
||||
EmojiChoiceItemState createState() => EmojiChoiceItemState();
|
||||
}
|
||||
|
||||
class EmojiChoiceItemState extends State<EmojiChoiceItem> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint('Selected emoji: ${widget.emoji}');
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
widget.onSelectEmoji(widget.emoji);
|
||||
},
|
||||
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),
|
||||
),
|
||||
child: Text(
|
||||
widget.emoji,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import 'package:fluffychat/pangea/common/network/urls.dart';
|
|||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class LemmaInfoRepo {
|
||||
|
|
@ -69,33 +67,34 @@ class LemmaInfoRepo {
|
|||
/// Get lemma info, prefering user set data over fetched data
|
||||
static Future<LemmaInfoResponse> get(LemmaInfoRequest request) async {
|
||||
try {
|
||||
return await _fetch(request);
|
||||
// if the user has either emojis or meaning in the past, use those first
|
||||
final UserSetLemmaInfo? userSetLemmaInfo = request.cId.userLemmaInfo;
|
||||
// final UserSetLemmaInfo? userSetLemmaInfo = request.cId.userLemmaInfo;
|
||||
|
||||
final List<String> emojis = userSetLemmaInfo?.emojis ?? [];
|
||||
String? meaning = userSetLemmaInfo?.meaning;
|
||||
// final List<String> emojis = userSetLemmaInfo?.emojis ?? [];
|
||||
// String? meaning = userSetLemmaInfo?.meaning;
|
||||
|
||||
// if the user has not set these, fetch from the server
|
||||
if (emojis.length < maxEmojisPerLemma || meaning == null) {
|
||||
final LemmaInfoResponse fetched = await _fetch(request);
|
||||
// if (emojis.length < maxEmojisPerLemma || meaning == null) {
|
||||
// final LemmaInfoResponse fetched = await _fetch(request);
|
||||
|
||||
while (emojis.length < maxEmojisPerLemma && fetched.emoji.isNotEmpty) {
|
||||
final maybeToAdd = fetched.emoji.removeAt(0);
|
||||
if (!emojis.contains(maybeToAdd)) {
|
||||
emojis.add(maybeToAdd);
|
||||
}
|
||||
}
|
||||
meaning ??= fetched.meaning;
|
||||
} else {
|
||||
// debugPrint(
|
||||
// 'using user set data for ${request.lemma} ${userSetLemmaInfo?.toJson()}',
|
||||
// );
|
||||
}
|
||||
// while (emojis.length < maxEmojisPerLemma && fetched.emoji.isNotEmpty) {
|
||||
// final maybeToAdd = fetched.emoji.removeAt(0);
|
||||
// if (!emojis.contains(maybeToAdd)) {
|
||||
// emojis.add(maybeToAdd);
|
||||
// }
|
||||
// }
|
||||
// meaning ??= fetched.meaning;
|
||||
// } else {
|
||||
// // debugPrint(
|
||||
// // 'using user set data for ${request.lemma} ${userSetLemmaInfo?.toJson()}',
|
||||
// // );
|
||||
// }
|
||||
|
||||
return LemmaInfoResponse(
|
||||
emoji: emojis,
|
||||
meaning: meaning,
|
||||
);
|
||||
// return LemmaInfoResponse(
|
||||
// emoji: emojis,
|
||||
// meaning: meaning,
|
||||
// );
|
||||
} catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, data: request.toJson());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:fluffychat/pangea/events/models/content_feedback.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
|
||||
class LemmaInfoResponse implements JsonSerializable {
|
||||
final List<String> emoji;
|
||||
|
|
@ -15,11 +14,7 @@ class LemmaInfoResponse implements JsonSerializable {
|
|||
factory LemmaInfoResponse.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaInfoResponse(
|
||||
// NOTE: This is a workaround for the fact that the server sometimes sends more than 3 emojis
|
||||
emoji: (json['emoji'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList()
|
||||
.take(maxEmojisPerLemma)
|
||||
.toList(),
|
||||
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
meaning: json['meaning'] as String,
|
||||
expireAt: json['expireAt'] == null
|
||||
? null
|
||||
|
|
|
|||
47
lib/pangea/message_token_text/dotted_border_painter.dart
Normal file
47
lib/pangea/message_token_text/dotted_border_painter.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class DottedBorderPainter extends CustomPainter {
|
||||
final Color color;
|
||||
final double strokeWidth;
|
||||
final double dashWidth;
|
||||
final double dashSpace;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
DottedBorderPainter({
|
||||
required this.color,
|
||||
this.strokeWidth = 2.0,
|
||||
this.dashWidth = 4.0,
|
||||
this.dashSpace = 4.0,
|
||||
required this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final path = Path()
|
||||
..addRRect(
|
||||
borderRadius.toRRect(Rect.fromLTWH(0, 0, size.width, size.height)));
|
||||
|
||||
final dashPath = Path();
|
||||
final pathMetrics = path.computeMetrics();
|
||||
for (final pathMetric in pathMetrics) {
|
||||
double distance = 0.0;
|
||||
while (distance < pathMetric.length) {
|
||||
final segment = pathMetric.extractPath(distance, distance + dashWidth);
|
||||
dashPath.addPath(segment, Offset.zero);
|
||||
distance += dashWidth + dashSpace;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawPath(dashPath, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/dotted_border_painter.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
|
||||
|
|
@ -181,7 +182,8 @@ class MessageTokenButtonState extends State<MessageTokenButton>
|
|||
return DragTarget<ConstructForm>(
|
||||
builder: (BuildContext context, accepted, rejected) {
|
||||
final double colorAlpha = 0.3 +
|
||||
(widget.overlayController?.selectedChoice != null ? 0.3 : 0.0);
|
||||
(widget.overlayController?.selectedChoice != null ? 0.4 : 0.0) +
|
||||
(accepted.isNotEmpty || _isHovered ? 0.3 : 0.0);
|
||||
|
||||
return InkWell(
|
||||
onHover: (isHovered) => setState(() => _isHovered = isHovered),
|
||||
|
|
@ -192,28 +194,29 @@ class MessageTokenButtonState extends State<MessageTokenButton>
|
|||
)
|
||||
: null,
|
||||
borderRadius: borderRadius,
|
||||
child: Container(
|
||||
height: height,
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
width:
|
||||
MessageMode.wordMeaning == widget.overlayController?.toolbarMode
|
||||
? widget.width
|
||||
: min(widget.width, height),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
child: CustomPaint(
|
||||
painter: DottedBorderPainter(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha((colorAlpha * 255).toInt()),
|
||||
borderRadius: borderRadius,
|
||||
border: accepted.isNotEmpty ||
|
||||
(widget.overlayController?.selectedChoice != null &&
|
||||
_isHovered)
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Container(
|
||||
height: height,
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
width: MessageMode.wordMeaning ==
|
||||
widget.overlayController?.toolbarMode
|
||||
? widget.width
|
||||
: min(widget.width, height),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha((max(0, colorAlpha - 0.7) * 255).toInt()),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,27 @@ class MorphIcon extends StatelessWidget {
|
|||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
final content = CustomizedSvg(
|
||||
svgUrl: getMorphSvgLink(
|
||||
morphFeature: morphFeature.name,
|
||||
morphTag: morphTag,
|
||||
context: context,
|
||||
),
|
||||
colorReplacements: theme.brightness == Brightness.dark
|
||||
? {
|
||||
"white": theme.cardColor.hexValue.toString(),
|
||||
"black": "white",
|
||||
}
|
||||
: {},
|
||||
errorIcon: Icon(morphFeature.fallbackIcon),
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
);
|
||||
|
||||
if (!showTooltip) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
message: morphTag == null
|
||||
? morphFeature.getDisplayCopy(context)
|
||||
|
|
@ -35,22 +56,7 @@ class MorphIcon extends StatelessWidget {
|
|||
context: context,
|
||||
),
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
child: CustomizedSvg(
|
||||
svgUrl: getMorphSvgLink(
|
||||
morphFeature: morphFeature.name,
|
||||
morphTag: morphTag,
|
||||
context: context,
|
||||
),
|
||||
colorReplacements: theme.brightness == Brightness.dark
|
||||
? {
|
||||
"white": theme.cardColor.hexValue.toString(),
|
||||
"black": "white",
|
||||
}
|
||||
: {},
|
||||
errorIcon: Icon(morphFeature.fallbackIcon),
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
|
||||
|
|
@ -217,6 +218,29 @@ class MessageAnalyticsEntry {
|
|||
|
||||
bool isTokenInHiddenWordActivity(PangeaToken token) =>
|
||||
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.isNotEmpty ?? false;
|
||||
|
||||
Future<List<LemmaInfoResponse>> getLemmaInfoForActivityTokens() async {
|
||||
// make a list of unique tokens in emoji and wordMeaning activities
|
||||
final List<PangeaToken> uniqueTokens = [];
|
||||
for (final t in _activityQueue[ActivityTypeEnum.emoji] ?? []) {
|
||||
if (!uniqueTokens.contains(t.tokens.first)) {
|
||||
uniqueTokens.add(t.tokens.first);
|
||||
}
|
||||
}
|
||||
for (final t in _activityQueue[ActivityTypeEnum.wordMeaning] ?? []) {
|
||||
if (!uniqueTokens.contains(t.tokens.first)) {
|
||||
uniqueTokens.add(t.tokens.first);
|
||||
}
|
||||
}
|
||||
|
||||
// get the lemma info for each token
|
||||
final List<Future<LemmaInfoResponse>> lemmaInfoFutures = [];
|
||||
for (final t in uniqueTokens) {
|
||||
lemmaInfoFutures.add(t.vocabConstructID.getLemmaInfo());
|
||||
}
|
||||
|
||||
return Future.wait(lemmaInfoFutures);
|
||||
}
|
||||
}
|
||||
|
||||
/// computes TokenWithXP for given a pangeaMessageEvent and caches the result, according to the full text of the message
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ extension MessageModeExtension on MessageMode {
|
|||
case MessageMode.noneSelected:
|
||||
return InstructionsEnum.readingAssistanceOverview;
|
||||
case MessageMode.messageTranslation:
|
||||
return InstructionsEnum.completeActivitiesToUnlock;
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.practiceActivity:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import 'package:fluffychat/pangea/morphs/morph_repo.dart';
|
|||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_morph_choice_item.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morph_focus_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
// this widget will handle the content of the input bar when mode == MessageMode.wordMorph
|
||||
|
|
@ -133,7 +133,7 @@ class MessageMorphInputBarContentState
|
|||
? ConstructUseTypeEnum.corM
|
||||
: ConstructUseTypeEnum.incM,
|
||||
lemma: choice,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: overlay.pangeaMessageEvent!.room.id,
|
||||
timeStamp: DateTime.now(),
|
||||
|
|
@ -181,6 +181,7 @@ class MessageMorphInputBarContentState
|
|||
morphFeature: morph!,
|
||||
morphTag: null,
|
||||
size: const Size(30, 30),
|
||||
showTooltip: false,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).whatIsTheMorphTag(
|
||||
|
|
@ -227,8 +228,11 @@ class MessageMorphInputBarContentState
|
|||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text("Select a grammar icon for activities and details."),
|
||||
return Center(
|
||||
child: Text(
|
||||
L10n.of(context).selectForGrammar,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ class MessageMorphChoiceItemState extends State<MessageMorphChoiceItem> {
|
|||
),
|
||||
morphTag: widget.cId.lemma,
|
||||
size: const Size(40, 40),
|
||||
showTooltip: false,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
|
||||
class MessageModeLockedCard extends StatelessWidget {
|
||||
|
|
@ -21,13 +19,13 @@ class MessageModeLockedCard extends StatelessWidget {
|
|||
size: 40,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
if (!InstructionsEnum.completeActivitiesToUnlock.isToggledOff) ...[
|
||||
const SizedBox(height: 8),
|
||||
const InstructionsInlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.completeActivitiesToUnlock,
|
||||
bold: true,
|
||||
),
|
||||
],
|
||||
// if (!InstructionsEnum.completeActivitiesToUnlock.isToggledOff) ...[
|
||||
// const SizedBox(height: 8),
|
||||
// const InstructionsInlineTooltip(
|
||||
// instructionsEnum: InstructionsEnum.completeActivitiesToUnlock,
|
||||
// bold: true,
|
||||
// ),
|
||||
// ],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,6 +377,20 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
if (isCorrect) {
|
||||
messageAnalyticsEntry?.onActivityComplete(activityType, token);
|
||||
|
||||
if (activityType == ActivityTypeEnum.emoji &&
|
||||
!token.vocabConstructID.userSetEmoji.contains(choice.form)) {
|
||||
final allEmojis = token.vocabConstructID.userSetEmoji + [choice.form];
|
||||
token.setEmoji(allEmojis).then((_) async {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
if (activityType == ActivityTypeEnum.wordMeaning) {
|
||||
token.setMeaning(choice.form).then((_) async {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
feedbackStates.removeWhere((e) => e.form == choice);
|
||||
|
|
|
|||
|
|
@ -53,31 +53,52 @@ class ToolbarButtonRow extends StatelessWidget {
|
|||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
ToolbarButton(
|
||||
mode: MessageMode.wordMorph,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
// wrapping these with a container to prevent the buttons from
|
||||
// moving around when they press and depress
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.wordMorph,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
ToolbarButton(
|
||||
mode: MessageMode.wordMeaning,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.wordMeaning,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
ToolbarButton(
|
||||
mode: MessageMode.listening,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.listening,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
ToolbarButton(
|
||||
mode: MessageMode.wordEmoji,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.wordEmoji,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -168,19 +168,20 @@ class LemmaWidgetState extends State<LemmaWidget> {
|
|||
|
||||
return Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _toggleEditMode(true),
|
||||
onDoubleTap: () => _toggleEditMode(true),
|
||||
child: Text(
|
||||
widget.token.lemma.text,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// Tooltip(
|
||||
// triggerMode: TooltipTriggerMode.tap,
|
||||
// message: L10n.of(context).doubleClickToEdit,
|
||||
// child: GestureDetector(
|
||||
// onLongPress: () => _toggleEditMode(true),
|
||||
// onDoubleTap: () => _toggleEditMode(true),
|
||||
// child:
|
||||
Text(
|
||||
widget.token.lemma.text,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// ),
|
||||
// ),
|
||||
if (widget.token.lemma.text.toLowerCase() ==
|
||||
widget.token.text.content.toLowerCase())
|
||||
WordAudioButton(
|
||||
|
|
|
|||
|
|
@ -1,355 +0,0 @@
|
|||
// stateful widget that displays morphological label and a shimmer effect while the text is loading
|
||||
// takes a token and morphological feature as input
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart';
|
||||
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_feature_display.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_tag_display.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class MorphFocusWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
final String morphFeature;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
final VoidCallback onEditDone;
|
||||
|
||||
const MorphFocusWidget({
|
||||
required this.token,
|
||||
required this.morphFeature,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overlayController,
|
||||
required this.onEditDone,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
MorphFocusWidgetState createState() => MorphFocusWidgetState();
|
||||
}
|
||||
|
||||
class MorphFocusWidgetState extends State<MorphFocusWidget> {
|
||||
bool editMode = false;
|
||||
|
||||
/// the morphological tag that the user has selected in edit mode
|
||||
String selectedMorphTag = "";
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
void resetMorphTag() => setState(
|
||||
() => selectedMorphTag =
|
||||
widget.token.getMorphTag(widget.morphFeature) ?? "X",
|
||||
);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MorphFocusWidget oldWidget) {
|
||||
if (widget.token != oldWidget.token ||
|
||||
widget.morphFeature != oldWidget.morphFeature) {
|
||||
resetMorphTag();
|
||||
setState(() => editMode = false);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
resetMorphTag();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void enterEditMode() {
|
||||
setState(() {
|
||||
editMode = true;
|
||||
});
|
||||
}
|
||||
|
||||
PangeaMessageEvent get pm => widget.pangeaMessageEvent;
|
||||
|
||||
/// confirm the changes made by the user
|
||||
/// this will send a new message to the server
|
||||
/// with the new morphological tag
|
||||
Future<void> saveChanges(
|
||||
PangeaToken Function(PangeaToken token) changeCallback,
|
||||
) async {
|
||||
try {
|
||||
// NOTE: it is not clear how this would work if the user was not editing the originalSent tokens
|
||||
// this case would only happen in immersion mode which is disabled until further notice
|
||||
// this flow assumes that the user is editing the originalSent tokens
|
||||
// if not, we'll get an error and we'll cross that bridge
|
||||
|
||||
// make a copy of the original tokens
|
||||
final existingTokens = pm.originalSent!.tokens!
|
||||
.map((token) => PangeaToken.fromJson(token.toJson()))
|
||||
.toList();
|
||||
|
||||
// change the morphological tag in the selected token
|
||||
final tokenIndex = existingTokens
|
||||
.indexWhere((token) => token.text.offset == widget.token.text.offset);
|
||||
if (tokenIndex == -1) {
|
||||
throw Exception("Token not found in message");
|
||||
}
|
||||
existingTokens[tokenIndex] = changeCallback(existingTokens[tokenIndex]);
|
||||
|
||||
// send a new message as an edit to original message to the server
|
||||
// including the new tokens
|
||||
// marking the message as a morphological edit will allow use to filter
|
||||
// from some processing and potentially find the data for LLM fine-tuning
|
||||
await pm.room.pangeaSendTextEvent(
|
||||
pm.messageDisplayText,
|
||||
editEventId: pm.eventId,
|
||||
originalSent: pm.originalSent?.content,
|
||||
originalWritten: pm.originalWritten?.content,
|
||||
tokensSent: PangeaMessageTokens(
|
||||
tokens: existingTokens,
|
||||
detections: pm.originalSent?.detections,
|
||||
),
|
||||
tokensWritten: pm.originalWritten?.tokens != null
|
||||
? PangeaMessageTokens(
|
||||
tokens: pm.originalWritten!.tokens!,
|
||||
detections: pm.originalWritten?.detections,
|
||||
)
|
||||
: null,
|
||||
choreo: pm.originalSent?.choreo,
|
||||
messageTag: ModelKey.messageTagMorphEdit,
|
||||
);
|
||||
|
||||
setState(() => editMode = false);
|
||||
widget.onEditDone();
|
||||
} catch (e) {
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
data: {
|
||||
"selectedMorphTag": selectedMorphTag,
|
||||
"morphFeature": widget.morphFeature,
|
||||
"token": widget.token.toJson(),
|
||||
"pangeaMessageEvent": widget.pangeaMessageEvent.event.content,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConstructIdentifier get id {
|
||||
return ConstructIdentifier(
|
||||
lemma: selectedMorphTag,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: widget.morphFeature,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!editMode) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
MorphFeatureDisplay(
|
||||
morphFeature: widget.morphFeature,
|
||||
),
|
||||
if (widget.token.getMorphTag(widget.morphFeature) != null) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: enterEditMode,
|
||||
onDoubleTap: enterEditMode,
|
||||
child: MorphTagDisplay(
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(
|
||||
widget.morphFeature,
|
||||
),
|
||||
morphTag:
|
||||
widget.token.getMorphTag(widget.morphFeature) ??
|
||||
L10n.of(context).nan,
|
||||
textColor: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? id.constructUses.lemmaCategory.darkColor(context)
|
||||
: id.constructUses.lemmaCategory.color(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
ConstructXpWidget(
|
||||
id: id,
|
||||
onTap: () => showDialog<AnalyticsPopupWrapper>(
|
||||
context: context,
|
||||
builder: (context) => AnalyticsPopupWrapper(
|
||||
constructZoom: id,
|
||||
view: ConstructTypeEnum.morph,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MorphMeaningWidget(
|
||||
feature: widget.morphFeature,
|
||||
tag: widget.token.getMorphTag(widget.morphFeature)!,
|
||||
),
|
||||
] else
|
||||
Text(L10n.of(context).nan),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).chooseCorrectLabel}",
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: MorphsRepo.get(),
|
||||
builder: (context, snapshot) {
|
||||
final allMorphTagsForEdit =
|
||||
snapshot.data?.getDisplayTags(widget.morphFeature) ??
|
||||
defaultMorphMapping.getDisplayTags(widget.morphFeature);
|
||||
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
children: allMorphTagsForEdit.map((tag) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
border: Border.all(
|
||||
color: selectedMorphTag == tag
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 7,
|
||||
),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
selectedMorphTag == tag
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha(50)
|
||||
: Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => selectedMorphTag = tag);
|
||||
},
|
||||
child: Text(
|
||||
getGrammarCopy(
|
||||
category: widget.morphFeature,
|
||||
lemma: tag,
|
||||
context: context,
|
||||
) ??
|
||||
tag,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
editMode = false;
|
||||
});
|
||||
},
|
||||
child: Text(L10n.of(context).cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
),
|
||||
onPressed: selectedMorphTag ==
|
||||
widget.token.morph[widget.morphFeature]
|
||||
? null
|
||||
: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => saveChanges(
|
||||
(token) {
|
||||
token.morph[widget.morphFeature] =
|
||||
selectedMorphTag;
|
||||
if (widget.morphFeature.toLowerCase() ==
|
||||
'pos') {
|
||||
token.pos = selectedMorphTag;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context).saveChanges),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
|
||||
class MorphologicalListItem extends StatelessWidget {
|
||||
final MorphFeaturesEnum morphFeature;
|
||||
final PangeaToken token;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const MorphologicalListItem({
|
||||
required this.morphFeature,
|
||||
required this.token,
|
||||
required this.overlayController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
bool get shouldDoActivity =>
|
||||
overlayController.messageAnalyticsEntry?.hasActivity(
|
||||
ActivityTypeEnum.morphId,
|
||||
token,
|
||||
morphFeature,
|
||||
) ==
|
||||
true;
|
||||
|
||||
bool get isSelected => overlayController.toolbarMode == MessageMode.wordMorph;
|
||||
|
||||
String get morphTag => token.getMorphTag(morphFeature.name) ?? "X";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: WordZoomActivityButton(
|
||||
icon: shouldDoActivity
|
||||
? const Icon(Symbols.toys_and_games)
|
||||
: MorphIcon(
|
||||
morphFeature: morphFeature,
|
||||
morphTag: token.getMorphTag(morphFeature.name),
|
||||
size: const Size(24, 24),
|
||||
),
|
||||
isSelected: isSelected,
|
||||
// onPressed: shouldDoActivity
|
||||
// ? () => overlayController.updateToolbarMode(MessageMode.wordMorph)
|
||||
// : () => (feature) => showDialog<AnalyticsPopupWrapper>(
|
||||
// context: context,
|
||||
// builder: (context) => AnalyticsPopupWrapper(
|
||||
// constructZoom: token.morphIdByFeature(feature),
|
||||
// view: ConstructTypeEnum.vocab,
|
||||
// ),
|
||||
// ),
|
||||
onPressed: () =>
|
||||
overlayController.onMorphActivitySelect(token, morphFeature),
|
||||
tooltip: shouldDoActivity
|
||||
? morphFeature.getDisplayCopy(context)
|
||||
: getGrammarCopy(
|
||||
category: morphFeature.name,
|
||||
lemma: morphTag,
|
||||
context: context,
|
||||
),
|
||||
opacity: isSelected
|
||||
? 1
|
||||
: shouldDoActivity
|
||||
? 0.4
|
||||
: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
|
||||
enum WordZoomSelection {
|
||||
meaning,
|
||||
emoji,
|
||||
lemma,
|
||||
morph,
|
||||
}
|
||||
|
||||
extension WordZoomSelectionUtils on WordZoomSelection {
|
||||
ActivityTypeEnum get activityType {
|
||||
switch (this) {
|
||||
case WordZoomSelection.meaning:
|
||||
return ActivityTypeEnum.wordMeaning;
|
||||
case WordZoomSelection.emoji:
|
||||
return ActivityTypeEnum.emoji;
|
||||
case WordZoomSelection.lemma:
|
||||
return ActivityTypeEnum.lemmaId;
|
||||
case WordZoomSelection.morph:
|
||||
return ActivityTypeEnum.morphId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_widget.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_item.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class WordZoomWidget extends StatelessWidget {
|
||||
|
|
@ -114,12 +114,21 @@ class WordZoomWidget extends StatelessWidget {
|
|||
alignment: Alignment.center,
|
||||
child: LemmaEmojiRow(
|
||||
cId: _selectedToken.vocabConstructID,
|
||||
onTap: () => overlayController.updateToolbarMode(
|
||||
onTapOverride: () =>
|
||||
overlayController.updateToolbarMode(
|
||||
MessageMode.wordEmoji,
|
||||
),
|
||||
isSelected: overlayController.toolbarMode ==
|
||||
MessageMode.wordEmoji,
|
||||
removeCallback: () => overlayController.setState(() {}),
|
||||
emojiSetCallback: () =>
|
||||
overlayController.setState(() {}),
|
||||
shouldShowEmojis: overlayController
|
||||
.messageAnalyticsEntry
|
||||
?.hasActivity(
|
||||
MessageMode.wordEmoji.associatedActivityType!,
|
||||
_selectedToken,
|
||||
) ==
|
||||
false,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue