chore(reading_assistance): more tweaks based on feedback
This commit is contained in:
parent
42f56c1c54
commit
ac5356acd7
30 changed files with 692 additions and 587 deletions
|
|
@ -4814,7 +4814,7 @@
|
|||
"pleaseEnterInt": "Please enter a number",
|
||||
"home": "Home",
|
||||
"join": "Join",
|
||||
"readingAssistanceOverviewBody": "Click the buttons below for mini-games on visualizing vocab, practice listening, meaning, and grammar concepts. Click any word for details.",
|
||||
"readingAssistanceOverviewBody": "Click the buttons below for mini-games on matching emojis, audios, word meanings, and grammar concepts. Or click on any word for details.",
|
||||
"learnByTexting": "Learn by texting",
|
||||
"levelSummaryTrigger": "View summary",
|
||||
"levelSummaryPopupTitle": "Level {level} Summary",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
abstract class AppConfig {
|
||||
// #Pangea
|
||||
|
|
@ -28,7 +26,7 @@ abstract class AppConfig {
|
|||
static const double toolbarMinWidth = 350.0;
|
||||
static const double defaultHeaderHeight = 56.0;
|
||||
static const double readingAssistanceInputBarHeight = 170;
|
||||
static const double toolbarButtonsHeight = 100.0;
|
||||
static const double toolbarButtonsHeight = 50.0;
|
||||
static const double toolbarSpacing = 8.0;
|
||||
static const double toolbarIconSize = 24.0;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,22 +4,9 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
|
@ -64,6 +51,18 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'
|
|||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import '../../utils/account_bundles.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import 'send_file_dialog.dart';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
|
|
@ -15,6 +12,7 @@ import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
|||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
|
||||
import 'package:fluffychat/pangea/user/client_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MorphAnalyticsListView extends StatelessWidget {
|
||||
final void Function(ConstructIdentifier) onConstructZoom;
|
||||
|
|
@ -88,9 +86,7 @@ class MorphFeatureBox extends StatelessWidget {
|
|||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppConfig.primaryColorLight
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class MorphMeaningWidget extends StatefulWidget {
|
||||
final String feature;
|
||||
|
|
@ -33,11 +31,24 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
|
|||
late TextEditingController _controller;
|
||||
static const int maxCharacters = 140;
|
||||
String? _cachedResponse;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MorphMeaningWidget oldWidget) {
|
||||
if (oldWidget.tag != widget.tag || oldWidget.feature != widget.feature) {
|
||||
_cachedResponse = null;
|
||||
_isLoading = true;
|
||||
_loadMorphMeaning();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
_loadMorphMeaning();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -46,6 +57,19 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadMorphMeaning() async {
|
||||
try {
|
||||
final response = await _morphMeaning();
|
||||
_setMeaningText(response);
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _morphMeaning() async {
|
||||
if (_cachedResponse != null) {
|
||||
return _cachedResponse!;
|
||||
|
|
@ -87,62 +111,56 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String>(
|
||||
future: _morphMeaning(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
_setMeaningText(snapshot.data!);
|
||||
}
|
||||
if (_isLoading) {
|
||||
return const TextLoadingShimmer();
|
||||
}
|
||||
|
||||
if (_editMode) {
|
||||
return MorphEditView(
|
||||
morphFeature: widget.feature,
|
||||
morphTag: widget.tag,
|
||||
meaning: snapshot.data ?? "",
|
||||
controller: _controller,
|
||||
toggleEditMode: _toggleEditMode,
|
||||
editMorphMeaning: editMorphMeaning,
|
||||
);
|
||||
}
|
||||
if (_error != null) {
|
||||
debugger(when: kDebugMode);
|
||||
return Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
textAlign: TextAlign.center,
|
||||
style: widget.style,
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const TextLoadingShimmer();
|
||||
}
|
||||
if (_editMode) {
|
||||
return MorphEditView(
|
||||
morphFeature: widget.feature,
|
||||
morphTag: widget.tag,
|
||||
meaning: _cachedResponse ?? "",
|
||||
controller: _controller,
|
||||
toggleEditMode: _toggleEditMode,
|
||||
editMorphMeaning: editMorphMeaning,
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError || snapshot.data == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
textAlign: TextAlign.center,
|
||||
style: widget.style,
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _toggleEditMode(true),
|
||||
onDoubleTap: () => _toggleEditMode(true),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: widget.style,
|
||||
children: [
|
||||
if (widget.leading != null) widget.leading!,
|
||||
if (widget.leading != null) const TextSpan(text: ' '),
|
||||
TextSpan(text: snapshot.data!),
|
||||
],
|
||||
),
|
||||
),
|
||||
return Row(
|
||||
mainAxisAlignment: widget.leading != null
|
||||
? MainAxisAlignment.start
|
||||
: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: L10n.of(context).doubleClickToEdit,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _toggleEditMode(true),
|
||||
onDoubleTap: () => _toggleEditMode(true),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: widget.style,
|
||||
children: [
|
||||
if (widget.leading != null) widget.leading!,
|
||||
if (widget.leading != null) const TextSpan(text: ' '),
|
||||
TextSpan(text: _cachedResponse!),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
|
|
@ -23,6 +17,10 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
|||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// A minimized version of AnalyticsController that get the logged in user's analytics
|
||||
class GetAnalyticsController extends BaseController {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.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/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
|
||||
class ChatListItemSubtitle extends StatelessWidget {
|
||||
|
|
@ -91,7 +90,6 @@ class ChatListItemSubtitle extends StatelessWidget {
|
|||
final analyticsEntry = tokens != null
|
||||
? MessageAnalyticsController.get(
|
||||
tokens,
|
||||
pangeaMessageEvent,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../bot/widgets/bot_face_svg.dart';
|
||||
|
||||
class CardHeader extends StatelessWidget {
|
||||
|
|
@ -49,9 +48,6 @@ class CardHeader extends StatelessWidget {
|
|||
if (onClose != null) onClose!();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
},
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? AppConfig.primaryColorLight
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
|
|
@ -23,6 +20,8 @@ import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
|
|||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../common/constants/model_keys.dart';
|
||||
import '../../lemmas/lemma.dart';
|
||||
|
||||
|
|
@ -640,9 +639,9 @@ class PangeaToken {
|
|||
);
|
||||
}
|
||||
|
||||
/// [0,infinity) - a lower number means higher priority
|
||||
/// [0,infinity) - a higher number means higher priority
|
||||
int activityPriorityScore(ActivityTypeEnum a, String? morphFeature) {
|
||||
return daysSinceLastUseByType(a, morphFeature) *
|
||||
(vocabConstructID.isContentWord ? 1 : 2);
|
||||
(vocabConstructID.isContentWord ? 10 : 9);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.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/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_entry.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TokenPosition {
|
||||
/// Start index of the full substring in the message
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class InstructionsInlineTooltip extends StatefulWidget {
|
||||
final InstructionsEnum instructionsEnum;
|
||||
|
|
@ -29,7 +27,6 @@ class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
|
|||
|
||||
@override
|
||||
void didUpdateWidget(covariant InstructionsInlineTooltip oldWidget) {
|
||||
debugPrint("InstructionsInlineTooltip didUpdateWidget");
|
||||
if (oldWidget.instructionsEnum != widget.instructionsEnum) {
|
||||
setToggled();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
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';
|
||||
|
|
@ -13,6 +10,8 @@ import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.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';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LemmaEmojiRow extends StatefulWidget {
|
||||
final ConstructIdentifier cId;
|
||||
|
|
@ -89,6 +88,7 @@ class LemmaEmojiRowState extends State<LemmaEmojiRow> {
|
|||
blurBackground: false,
|
||||
borderColor: Theme.of(context).colorScheme.primary,
|
||||
closePrevOverlay: false,
|
||||
offset: const Offset(0, 60),
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -109,6 +109,11 @@ class LemmaEmojiRowState extends State<LemmaEmojiRow> {
|
|||
.catchError((e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
}).then((_) {
|
||||
if (mounted) {
|
||||
widget.emojiSetCallback?.call();
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
|
|
@ -124,46 +129,49 @@ class LemmaEmojiRowState extends State<LemmaEmojiRow> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey(
|
||||
widget.cId.string,
|
||||
)
|
||||
.link,
|
||||
child: Container(
|
||||
key: MatrixState.pAnyState
|
||||
return Material(
|
||||
child: CompositedTransformTarget(
|
||||
link: 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,
|
||||
),
|
||||
),
|
||||
.link,
|
||||
child: Container(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey(
|
||||
widget.cId.string,
|
||||
)
|
||||
: WordZoomActivityButton(
|
||||
icon: Icon(
|
||||
Icons.add_reaction_outlined,
|
||||
color: widget.isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
.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),
|
||||
),
|
||||
isSelected: widget.isSelected,
|
||||
onPressed: widget.onTapOverride ?? openEmojiSetOverlay,
|
||||
opacity: widget.isSelected ? 1 : 0.4,
|
||||
tooltip: MessageMode.wordEmoji.title(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -184,6 +192,7 @@ class EmojiEditOverlay extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
height: 70,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
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/reading_assistance_input_row/morph_selection.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
const double tokenButtonHeight = 40.0;
|
||||
const double tokenButtonDefaultFontSize = 10;
|
||||
|
|
@ -155,23 +154,26 @@ class MessageTokenButtonState extends State<MessageTokenButton>
|
|||
}
|
||||
return InkWell(
|
||||
onHover: (isHovered) => setState(() => _isHovered = isHovered),
|
||||
onTap: () => widget.overlayController!
|
||||
.onMorphActivitySelect(widget.token, activity!.morphFeature!),
|
||||
onTap: () => widget.overlayController!.onMorphActivitySelect(
|
||||
MorphSelection(widget.token, activity!.morphFeature!),
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
child: Container(
|
||||
height: height,
|
||||
width: min(widget.width, height),
|
||||
alignment: Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: (widget.overlayController?.selectedToken == widget.token &&
|
||||
widget.overlayController?.selectedMorph ==
|
||||
opacity: (widget.overlayController?.selectedMorph?.token ==
|
||||
widget.token &&
|
||||
widget.overlayController?.selectedMorph?.morph ==
|
||||
activity?.morphFeature) ||
|
||||
_isHovered
|
||||
? 1.0
|
||||
: 0.5,
|
||||
: 0.4,
|
||||
child: Icon(
|
||||
Symbols.toys_and_games,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: min(24, widget.width),
|
||||
),
|
||||
// MorphIcon(morphFeature: activity!.morphFeature!, morphTag: null),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,301 +1,76 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
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';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_entry.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
class MessageAnalyticsEntry {
|
||||
final DateTime createdAt = DateTime.now();
|
||||
|
||||
late final List<PangeaToken> _tokens;
|
||||
|
||||
final Map<ActivityTypeEnum, List<TargetTokensAndActivityType>>
|
||||
_activityQueue = {};
|
||||
|
||||
final int _maxQueueLength = 5;
|
||||
|
||||
MessageAnalyticsEntry({
|
||||
required List<PangeaToken> tokens,
|
||||
required bool includeHiddenWordActivities,
|
||||
required PangeaMessageEvent pangeaMessageEvent,
|
||||
}) {
|
||||
_tokens = tokens;
|
||||
initialize();
|
||||
}
|
||||
|
||||
void _pushQueue(TargetTokensAndActivityType entry) {
|
||||
if (_activityQueue.containsKey(entry.activityType)) {
|
||||
_activityQueue[entry.activityType]!.insert(0, entry);
|
||||
} else {
|
||||
_activityQueue[entry.activityType] = [entry];
|
||||
}
|
||||
|
||||
// just in case we make a mistake and the queue gets too long
|
||||
if (_activityQueue[entry.activityType]!.length > _maxQueueLength) {
|
||||
debugger(when: kDebugMode);
|
||||
_activityQueue[entry.activityType]!.removeRange(
|
||||
_maxQueueLength,
|
||||
_activityQueue.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TargetTokensAndActivityType? nextActivity(ActivityTypeEnum a) =>
|
||||
_activityQueue[a]?.firstOrNull;
|
||||
|
||||
bool get hasHiddenWordActivity =>
|
||||
activities(ActivityTypeEnum.hiddenWordListening).isNotEmpty;
|
||||
|
||||
bool get hasMessageMeaningActivity =>
|
||||
activities(ActivityTypeEnum.messageMeaning).isNotEmpty;
|
||||
|
||||
int get numActivities => _activityQueue.length;
|
||||
|
||||
List<TargetTokensAndActivityType> activities(ActivityTypeEnum a) =>
|
||||
_activityQueue[a] ?? [];
|
||||
|
||||
// /// If there are more than 4 tokens that can be heard, we don't want to do word focus listening
|
||||
// /// Otherwise, we don't have enough distractors
|
||||
// bool get canDoWordFocusListening =>
|
||||
// _tokens.where((t) => t.canBeHeard).length > 4;
|
||||
|
||||
/// On initialization, we pick which tokens to do activities on and what types of activities to do
|
||||
void initialize() {
|
||||
final eligibleTokens = _tokens.where((t) => t.lemma.saveVocab);
|
||||
|
||||
// EMOJI
|
||||
// sort the tokens by the preference of them for an emoji activity
|
||||
// order from least to most recent
|
||||
// words that have never been used are counted as 1000 days
|
||||
// we preference content words over function words by multiplying the days since last use by 2
|
||||
// NOTE: for now, we put it at the end if it has no uses and basically just give them the answer
|
||||
// later on, we may introduce an emoji activity that is easier than the current matching one
|
||||
// i.e. we show them 3 good emojis and 1 bad one and ask them to pick the bad one
|
||||
_activityQueue[ActivityTypeEnum.emoji] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.emoji,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => a.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.emoji, null)
|
||||
.compareTo(
|
||||
b.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.emoji, null),
|
||||
),
|
||||
)
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// WORD MEANING
|
||||
// make word meaning activities
|
||||
// same as emojis for now
|
||||
_activityQueue[ActivityTypeEnum.wordMeaning] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => a.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null)
|
||||
.compareTo(
|
||||
b.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null),
|
||||
),
|
||||
)
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// WORD FOCUS LISTENING
|
||||
// make word focus listening activities
|
||||
// same as emojis for now
|
||||
_activityQueue[ActivityTypeEnum.wordFocusListening] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.wordFocusListening,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => a.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordFocusListening, null)
|
||||
.compareTo(
|
||||
b.tokens.first.activityPriorityScore(
|
||||
ActivityTypeEnum.wordFocusListening,
|
||||
null,
|
||||
),
|
||||
),
|
||||
)
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// GRAMMAR
|
||||
// build a list of TargetTokensAndActivityType for all tokens and all features in the message
|
||||
// limits to _maxQueueLength activities and only one per token
|
||||
final List<TargetTokensAndActivityType> candidates = eligibleTokens.expand(
|
||||
(t) {
|
||||
return t.morphsBasicallyEligibleForPracticeByPriority.map(
|
||||
(m) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.morphId,
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(m.category),
|
||||
),
|
||||
);
|
||||
},
|
||||
).sorted(
|
||||
(a, b) => a.tokens.first
|
||||
.activityPriorityScore(
|
||||
ActivityTypeEnum.morphId,
|
||||
a.morphFeature!.name,
|
||||
)
|
||||
.compareTo(
|
||||
b.tokens.first.activityPriorityScore(
|
||||
ActivityTypeEnum.morphId,
|
||||
b.morphFeature!.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
//pick from the top 5, only including one per token
|
||||
_activityQueue[ActivityTypeEnum.morphId] = [];
|
||||
for (final candidate in candidates) {
|
||||
if (_activityQueue[ActivityTypeEnum.morphId]!.length >= _maxQueueLength) {
|
||||
break;
|
||||
}
|
||||
if (_activityQueue[ActivityTypeEnum.morphId]?.any(
|
||||
(entry) => entry.tokens.contains(candidate.tokens.first),
|
||||
) ==
|
||||
false) {
|
||||
_activityQueue[ActivityTypeEnum.morphId]?.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool hasActivity(
|
||||
ActivityTypeEnum a,
|
||||
PangeaToken t, [
|
||||
MorphFeaturesEnum? morph,
|
||||
]) =>
|
||||
_activityQueue[a]?.any(
|
||||
(entry) =>
|
||||
entry.tokens.contains(t) &&
|
||||
(morph == null || entry.morphFeature == morph),
|
||||
) ==
|
||||
true;
|
||||
|
||||
/// Add a message meaning activity to the front of the queue
|
||||
/// And limits to _maxQueueLength activities
|
||||
void addMessageMeaningActivity() {
|
||||
final entry = TargetTokensAndActivityType(
|
||||
tokens: _tokens,
|
||||
activityType: ActivityTypeEnum.messageMeaning,
|
||||
);
|
||||
_pushQueue(entry);
|
||||
}
|
||||
|
||||
void onActivityComplete(ActivityTypeEnum a, PangeaToken? token) {
|
||||
_activityQueue[a]
|
||||
?.removeWhere((entry) => token == null || entry.tokens.contains(token));
|
||||
}
|
||||
|
||||
void exitPracticeFlow() => _activityQueue.clear();
|
||||
|
||||
void revealAllTokens() =>
|
||||
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.clear();
|
||||
|
||||
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
|
||||
/// listens for analytics updates and updates the cache accordingly
|
||||
class MessageAnalyticsController {
|
||||
static final Map<String, MessageAnalyticsEntry> _cache = {};
|
||||
static final GetStorage _storage = GetStorage('message_analytics_cache');
|
||||
static final Map<String, MessageAnalyticsEntry> _memoryCache = {};
|
||||
static const int _maxMemoryCacheSize = 50;
|
||||
|
||||
void dispose() {
|
||||
_cache.clear();
|
||||
_storage.erase();
|
||||
_memoryCache.clear();
|
||||
}
|
||||
|
||||
static void save(MessageAnalyticsEntry entry) {
|
||||
final key = _key(entry.tokens);
|
||||
_storage.write(key, entry.toJson());
|
||||
_memoryCache[key] = entry;
|
||||
}
|
||||
|
||||
// if over 300, remove oldest 5 entries by createdAt
|
||||
static void clean() {
|
||||
if (_cache.length > 300) {
|
||||
final sortedEntries = _cache.entries.toList()
|
||||
final Iterable<String> keys = _storage.getKeys();
|
||||
if (keys.length > 300) {
|
||||
final entries = keys
|
||||
.map((key) {
|
||||
final entry = MessageAnalyticsEntry.fromJson(_storage.read(key));
|
||||
return MapEntry(key, entry);
|
||||
})
|
||||
.cast<MapEntry<String, MessageAnalyticsEntry>>()
|
||||
.toList()
|
||||
..sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
|
||||
for (var i = 0; i < 5; i++) {
|
||||
_cache.remove(sortedEntries[i].key);
|
||||
_storage.remove(entries[i].key);
|
||||
}
|
||||
}
|
||||
if (_memoryCache.length > _maxMemoryCacheSize) {
|
||||
_memoryCache.remove(_memoryCache.keys.first);
|
||||
}
|
||||
}
|
||||
|
||||
static String _key(List<PangeaToken> tokens) =>
|
||||
PangeaToken.reconstructText(tokens);
|
||||
tokens.map((t) => t.text.content).join(' ');
|
||||
|
||||
static MessageAnalyticsEntry? get(
|
||||
List<PangeaToken> tokens,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) {
|
||||
final String key = _key(tokens);
|
||||
final entry = _cache[key];
|
||||
|
||||
// if cache is older than 1 day, then remove and recompute
|
||||
if (entry != null &&
|
||||
DateTime.now().difference(entry.createdAt).inDays > 1) {
|
||||
_cache.remove(key);
|
||||
if (_memoryCache.containsKey(key)) {
|
||||
return _memoryCache[key];
|
||||
}
|
||||
|
||||
if (entry != null) {
|
||||
return entry;
|
||||
final entryJson = _storage.read(key);
|
||||
if (entryJson != null) {
|
||||
final entry = MessageAnalyticsEntry.fromJson(entryJson);
|
||||
if (DateTime.now().difference(entry.createdAt).inDays > 1) {
|
||||
debugPrint('removing old entry ${entry.createdAt}');
|
||||
_storage.remove(key);
|
||||
} else {
|
||||
_memoryCache[key] = entry;
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
final bool includeHiddenWordActivities = !pangeaMessageEvent.ownMessage &&
|
||||
pangeaMessageEvent.messageDisplayRepresentation?.tokens != null &&
|
||||
pangeaMessageEvent.messageDisplayLangIsL2 &&
|
||||
!pangeaMessageEvent.event.isRichMessage;
|
||||
|
||||
_cache[key] = MessageAnalyticsEntry(
|
||||
final newEntry = MessageAnalyticsEntry(
|
||||
tokens: tokens,
|
||||
includeHiddenWordActivities: includeHiddenWordActivities,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
);
|
||||
|
||||
_storage.write(key, newEntry.toJson());
|
||||
_memoryCache[key] = newEntry;
|
||||
|
||||
clean();
|
||||
|
||||
return _cache[key];
|
||||
return newEntry;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
284
lib/pangea/practice_activities/message_analytics_entry.dart
Normal file
284
lib/pangea/practice_activities/message_analytics_entry.dart
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.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/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class MessageAnalyticsEntry {
|
||||
final DateTime createdAt = DateTime.now();
|
||||
|
||||
late final List<PangeaToken> _tokens;
|
||||
|
||||
final Map<ActivityTypeEnum, List<TargetTokensAndActivityType>>
|
||||
_activityQueue = {};
|
||||
|
||||
final int _maxQueueLength = 5;
|
||||
|
||||
MessageAnalyticsEntry({
|
||||
required List<PangeaToken> tokens,
|
||||
}) {
|
||||
_tokens = tokens;
|
||||
initialize();
|
||||
}
|
||||
|
||||
List<PangeaToken> get tokens => _tokens;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'tokens': _tokens.map((t) => t.toJson()).toList(),
|
||||
'activityQueue': _activityQueue.map(
|
||||
(key, value) => MapEntry(
|
||||
key.toString(),
|
||||
value.map((e) => e.toJson()).toList(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
static MessageAnalyticsEntry fromJson(Map<String, dynamic> json) {
|
||||
return MessageAnalyticsEntry(
|
||||
tokens:
|
||||
(json['tokens'] as List).map((t) => PangeaToken.fromJson(t)).toList(),
|
||||
).._activityQueue.addAll(
|
||||
(json['activityQueue'] as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(
|
||||
ActivityTypeEnum.values.firstWhere((e) => e.toString() == key),
|
||||
(value as List)
|
||||
.map((e) => TargetTokensAndActivityType.fromJson(e))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _pushQueue(TargetTokensAndActivityType entry) {
|
||||
if (_activityQueue.containsKey(entry.activityType)) {
|
||||
_activityQueue[entry.activityType]!.insert(0, entry);
|
||||
} else {
|
||||
_activityQueue[entry.activityType] = [entry];
|
||||
}
|
||||
|
||||
// just in case we make a mistake and the queue gets too long
|
||||
if (_activityQueue[entry.activityType]!.length > _maxQueueLength) {
|
||||
debugger(when: kDebugMode);
|
||||
_activityQueue[entry.activityType]!.removeRange(
|
||||
_maxQueueLength,
|
||||
_activityQueue.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TargetTokensAndActivityType? nextActivity(ActivityTypeEnum a) =>
|
||||
_activityQueue[a]?.firstOrNull;
|
||||
|
||||
bool get hasHiddenWordActivity =>
|
||||
activities(ActivityTypeEnum.hiddenWordListening).isNotEmpty;
|
||||
|
||||
bool get hasMessageMeaningActivity =>
|
||||
activities(ActivityTypeEnum.messageMeaning).isNotEmpty;
|
||||
|
||||
int get numActivities => _activityQueue.length;
|
||||
|
||||
List<TargetTokensAndActivityType> activities(ActivityTypeEnum a) =>
|
||||
_activityQueue[a] ?? [];
|
||||
|
||||
// /// If there are more than 4 tokens that can be heard, we don't want to do word focus listening
|
||||
// /// Otherwise, we don't have enough distractors
|
||||
// bool get canDoWordFocusListening =>
|
||||
// _tokens.where((t) => t.canBeHeard).length > 4;
|
||||
|
||||
/// On initialization, we pick which tokens to do activities on and what types of activities to do
|
||||
void initialize() {
|
||||
final eligibleTokens = _tokens.where((t) => t.lemma.saveVocab);
|
||||
|
||||
// EMOJI
|
||||
// sort the tokens by the preference of them for an emoji activity
|
||||
// order from least to most recent
|
||||
// words that have never been used are counted as 1000 days
|
||||
// we preference content words over function words by multiplying the days since last use by 2
|
||||
// NOTE: for now, we put it at the end if it has no uses and basically just give them the answer
|
||||
// later on, we may introduce an emoji activity that is easier than the current matching one
|
||||
// i.e. we show them 3 good emojis and 1 bad one and ask them to pick the bad one
|
||||
_activityQueue[ActivityTypeEnum.emoji] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.emoji,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => b.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.emoji, null)
|
||||
.compareTo(
|
||||
a.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.emoji, null),
|
||||
),
|
||||
);
|
||||
debugPrint(
|
||||
'emoji activity priority score: ${_activityQueue[ActivityTypeEnum.emoji]!.map(
|
||||
(e) => e.tokens.first.activityPriorityScore(ActivityTypeEnum.emoji, null),
|
||||
)}');
|
||||
|
||||
_activityQueue[ActivityTypeEnum.emoji] =
|
||||
_activityQueue[ActivityTypeEnum.emoji]!
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// WORD MEANING
|
||||
// make word meaning activities
|
||||
// same as emojis for now
|
||||
_activityQueue[ActivityTypeEnum.wordMeaning] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => b.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null)
|
||||
.compareTo(
|
||||
a.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null),
|
||||
),
|
||||
)
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// WORD FOCUS LISTENING
|
||||
// make word focus listening activities
|
||||
// same as emojis for now
|
||||
_activityQueue[ActivityTypeEnum.wordFocusListening] = eligibleTokens
|
||||
.map(
|
||||
(t) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.wordFocusListening,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => b.tokens.first
|
||||
.activityPriorityScore(ActivityTypeEnum.wordFocusListening, null)
|
||||
.compareTo(
|
||||
a.tokens.first.activityPriorityScore(
|
||||
ActivityTypeEnum.wordFocusListening,
|
||||
null,
|
||||
),
|
||||
),
|
||||
)
|
||||
.take(_maxQueueLength)
|
||||
.shuffled()
|
||||
.toList();
|
||||
|
||||
// GRAMMAR
|
||||
// build a list of TargetTokensAndActivityType for all tokens and all features in the message
|
||||
// limits to _maxQueueLength activities and only one per token
|
||||
final List<TargetTokensAndActivityType> candidates = eligibleTokens.expand(
|
||||
(t) {
|
||||
return t.morphsBasicallyEligibleForPracticeByPriority.map(
|
||||
(m) => TargetTokensAndActivityType(
|
||||
tokens: [t],
|
||||
activityType: ActivityTypeEnum.morphId,
|
||||
morphFeature: MorphFeaturesEnumExtension.fromString(m.category),
|
||||
),
|
||||
);
|
||||
},
|
||||
).sorted(
|
||||
(a, b) => b.tokens.first
|
||||
.activityPriorityScore(
|
||||
ActivityTypeEnum.morphId,
|
||||
b.morphFeature!.name,
|
||||
)
|
||||
.compareTo(
|
||||
a.tokens.first.activityPriorityScore(
|
||||
ActivityTypeEnum.morphId,
|
||||
a.morphFeature!.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
//pick from the top 5, only including one per token
|
||||
_activityQueue[ActivityTypeEnum.morphId] = [];
|
||||
for (final candidate in candidates) {
|
||||
if (_activityQueue[ActivityTypeEnum.morphId]!.length >= _maxQueueLength) {
|
||||
break;
|
||||
}
|
||||
if (_activityQueue[ActivityTypeEnum.morphId]?.any(
|
||||
(entry) => entry.tokens.contains(candidate.tokens.first),
|
||||
) ==
|
||||
false) {
|
||||
_activityQueue[ActivityTypeEnum.morphId]?.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
MessageAnalyticsController.save(this);
|
||||
}
|
||||
|
||||
bool hasActivity(
|
||||
ActivityTypeEnum a,
|
||||
PangeaToken t, [
|
||||
MorphFeaturesEnum? morph,
|
||||
]) =>
|
||||
_activityQueue[a]?.any(
|
||||
(entry) =>
|
||||
entry.tokens.contains(t) &&
|
||||
(morph == null || entry.morphFeature == morph),
|
||||
) ==
|
||||
true;
|
||||
|
||||
/// Add a message meaning activity to the front of the queue
|
||||
/// And limits to _maxQueueLength activities
|
||||
void addMessageMeaningActivity() {
|
||||
final entry = TargetTokensAndActivityType(
|
||||
tokens: _tokens,
|
||||
activityType: ActivityTypeEnum.messageMeaning,
|
||||
);
|
||||
_pushQueue(entry);
|
||||
}
|
||||
|
||||
void onActivityComplete(ActivityTypeEnum a, PangeaToken? token) {
|
||||
_activityQueue[a]
|
||||
?.removeWhere((entry) => token == null || entry.tokens.contains(token));
|
||||
MessageAnalyticsController.save(this);
|
||||
}
|
||||
|
||||
void exitPracticeFlow() {
|
||||
_activityQueue.clear();
|
||||
MessageAnalyticsController.save(this);
|
||||
}
|
||||
|
||||
void revealAllTokens() {
|
||||
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.clear();
|
||||
MessageAnalyticsController.save(this);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Picks which tokens to do activities on and what types of activities to do
|
||||
/// Caches result so that we don't have to recompute it
|
||||
|
|
@ -46,4 +45,23 @@ class TargetTokensAndActivityType {
|
|||
@override
|
||||
int get hashCode =>
|
||||
tokens.hashCode ^ activityType.hashCode ^ morphFeature.hashCode;
|
||||
|
||||
static TargetTokensAndActivityType fromJson(Map<String, dynamic> json) {
|
||||
return TargetTokensAndActivityType(
|
||||
tokens:
|
||||
(json['tokens'] as List).map((e) => PangeaToken.fromJson(e)).toList(),
|
||||
activityType: ActivityTypeEnum.values[json['activityType']],
|
||||
morphFeature: json['morphFeature'] == null
|
||||
? null
|
||||
: MorphFeaturesEnum.values[json['morphFeature']],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tokens': tokens.map((e) => e.toJson()).toList(),
|
||||
'activityType': activityType.index,
|
||||
'morphFeature': morphFeature?.index,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
|
|
@ -11,6 +8,8 @@ import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
|||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_match_activity_item.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageMatchActivity extends StatelessWidget {
|
||||
final MessageOverlayController overlayController;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/match_feedback_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageMatchActivityItem extends StatefulWidget {
|
||||
const MessageMatchActivityItem({
|
||||
|
|
@ -88,14 +86,14 @@ class MessageMatchActivityItemState extends State<MessageMatchActivityItem> {
|
|||
}
|
||||
|
||||
if (isSelected) {
|
||||
return AppConfig.primaryColor;
|
||||
return Theme.of(context).colorScheme.primaryContainer;
|
||||
}
|
||||
|
||||
if (_isHovered) {
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
return Theme.of(context).colorScheme.primaryContainer;
|
||||
}
|
||||
|
||||
return Colors.transparent;
|
||||
return Theme.of(context).colorScheme.surface;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -147,12 +145,6 @@ class MessageMatchActivityItemState extends State<MessageMatchActivityItem> {
|
|||
onDragStarted: () {
|
||||
widget.overlayController.onChoiceSelect(widget.constructForm, true);
|
||||
},
|
||||
// onDragCompleted: () {
|
||||
// debugger(when: kDebugMode);
|
||||
// },
|
||||
// onDragEnd: (details) {
|
||||
// // debugger(when: kDebugMode);
|
||||
// },
|
||||
child: InkWell(
|
||||
onHover: (isHovered) => setState(() => _isHovered = isHovered),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
|
|
@ -23,6 +18,9 @@ import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_m
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morph_focus_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
// this widget will handle the content of the input bar when mode == MessageMode.wordMorph
|
||||
|
||||
|
|
@ -63,8 +61,8 @@ class MessageMorphInputBarContentState
|
|||
// }
|
||||
|
||||
MessageOverlayController get overlay => widget.overlayController;
|
||||
PangeaToken? get token => overlay.selectedToken;
|
||||
MorphFeaturesEnum? get morph => overlay.selectedMorph;
|
||||
PangeaToken? get token => overlay.selectedMorph?.token;
|
||||
MorphFeaturesEnum? get morph => overlay.selectedMorph?.morph;
|
||||
|
||||
// void init() async {
|
||||
// initialized = false;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.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:flutter/material.dart';
|
||||
|
||||
class MessageMorphChoiceItem extends StatefulWidget {
|
||||
const MessageMorphChoiceItem({
|
||||
|
|
@ -47,10 +46,13 @@ class MessageMorphChoiceItemState extends State<MessageMorphChoiceItem> {
|
|||
: AppConfig.warning.withAlpha((0.4 * 255).toInt());
|
||||
}
|
||||
if (widget.isSelected) {
|
||||
return AppConfig.primaryColor.withAlpha((0.4 * 255).toInt());
|
||||
return Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha((0.4 * 255).toInt());
|
||||
}
|
||||
return _isHovered
|
||||
? AppConfig.primaryColor.withAlpha((0.2 * 255).toInt())
|
||||
? Theme.of(context).colorScheme.primary.withAlpha((0.2 * 255).toInt())
|
||||
: Colors.transparent;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
|
||||
class MorphSelection {
|
||||
PangeaToken token;
|
||||
MorphFeaturesEnum morph;
|
||||
|
||||
MorphSelection(
|
||||
this.token,
|
||||
this.morph,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is MorphSelection &&
|
||||
other.token == token &&
|
||||
other.morph == morph;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => token.hashCode ^ morph.hashCode;
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
|
|
@ -17,6 +14,10 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart'
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double minContentHeight = 120;
|
||||
|
||||
class ReadingAssistanceInputBar extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
|
@ -56,6 +57,7 @@ class ReadingAssistanceInputBar extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget barContent(BuildContext context) {
|
||||
Widget? content;
|
||||
switch (overlayController.toolbarMode) {
|
||||
// message meaning will not use the input bar (for now at least)
|
||||
// maybe we move some choices there later
|
||||
|
|
@ -64,7 +66,7 @@ class ReadingAssistanceInputBar extends StatelessWidget {
|
|||
case MessageMode.wordZoom:
|
||||
case MessageMode.noneSelected:
|
||||
//TODO: show all emojis for the lemmas and allow sending normal reactions
|
||||
return const SizedBox.shrink();
|
||||
break;
|
||||
// return MessageEmojiChoice(
|
||||
// controller: controller,
|
||||
// overlayController: overlayController,
|
||||
|
|
@ -72,27 +74,38 @@ class ReadingAssistanceInputBar extends StatelessWidget {
|
|||
|
||||
case MessageMode.messageTranslation:
|
||||
if (overlayController.isTranslationUnlocked) {
|
||||
return MessageTranslationCard(
|
||||
content = MessageTranslationCard(
|
||||
messageEvent: overlayController.pangeaMessageEvent!,
|
||||
);
|
||||
} else {
|
||||
return MessageModeLockedCard(controller: overlayController);
|
||||
content = MessageModeLockedCard(controller: overlayController);
|
||||
}
|
||||
|
||||
case MessageMode.wordEmoji:
|
||||
case MessageMode.messageMeaning:
|
||||
case MessageMode.wordMeaning:
|
||||
case MessageMode.listening:
|
||||
return MessageMatchActivity(
|
||||
content = MessageMatchActivity(
|
||||
overlayController: overlayController,
|
||||
);
|
||||
|
||||
case MessageMode.wordMorph:
|
||||
return MessageMorphInputBarContent(
|
||||
content = MessageMorphInputBarContent(
|
||||
overlayController: overlayController,
|
||||
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
|
||||
);
|
||||
}
|
||||
|
||||
if (content == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: minContentHeight,
|
||||
),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class MessageAudioCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
|
|
@ -86,8 +84,9 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
height: 40,
|
||||
child: _isLoading
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
? const TextLoadingShimmer(width: 200)
|
||||
: audioFile != null
|
||||
? AudioPlayerWidget(
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
|
|
@ -25,15 +19,20 @@ import 'package:fluffychat/pangea/events/models/pangea_token_text_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/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/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_entry.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/match_feedback_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Controls data at the top level of the toolbar (mainly token / toolbar mode selection)
|
||||
class MessageSelectionOverlay extends StatefulWidget {
|
||||
|
|
@ -72,7 +71,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
Map<ConstructIdentifier, LemmaInfoResponse>? messageLemmaInfos;
|
||||
|
||||
MorphFeaturesEnum? selectedMorph;
|
||||
MorphSelection? selectedMorph;
|
||||
ConstructForm? selectedChoice;
|
||||
PangeaTokenText? _selectedSpan;
|
||||
|
||||
|
|
@ -108,7 +107,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
Future<void> initializeTokensAndMode() async {
|
||||
try {
|
||||
debugPrint("what");
|
||||
RepresentationEvent? repEvent =
|
||||
pangeaMessageEvent?.messageDisplayRepresentation;
|
||||
repEvent ??= await _fetchNewRepEvent();
|
||||
|
|
@ -323,12 +321,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
void onMorphActivitySelect(PangeaToken token, MorphFeaturesEnum morph) {
|
||||
void onMorphActivitySelect(MorphSelection newMorph) {
|
||||
if (toolbarMode != MessageMode.wordMorph) {
|
||||
updateToolbarMode(MessageMode.wordMorph);
|
||||
}
|
||||
selectedMorph = morph;
|
||||
_updateSelectedSpan(token.text, true);
|
||||
selectedMorph = newMorph;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// only used for word meaning, emoji, and word focus listening atm
|
||||
|
|
@ -338,6 +336,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
) async {
|
||||
final ActivityTypeEnum activityType = toolbarMode.associatedActivityType!;
|
||||
|
||||
//TODO - account for some emojis being the same for multiple words
|
||||
final bool isCorrect = token.vocabConstructID == choice.cId;
|
||||
|
||||
final ConstructUseTypeEnum? useType =
|
||||
|
|
@ -457,7 +456,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
pangeaMessageEvent?.messageDisplayRepresentation?.tokens != null
|
||||
? MessageAnalyticsController.get(
|
||||
pangeaMessageEvent!.messageDisplayRepresentation!.tokens!,
|
||||
pangeaMessageEvent!,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
|
@ -23,6 +19,8 @@ import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart';
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Controls positioning of the message overlay.
|
||||
class MessageSelectionPositioner extends StatefulWidget {
|
||||
|
|
@ -552,9 +550,9 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
_headerHeight,
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 200.0,
|
||||
maxWidth: 400.0,
|
||||
maxWidth: _toolbarMaxWidth,
|
||||
),
|
||||
child: InstructionsInlineTooltip(
|
||||
instructionsEnum:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
|
|
@ -10,10 +6,13 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
|||
import 'package:fluffychat/pangea/events/utils/message_text_util.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_analytics_entry.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
/// Question - does this need to be stateful or does this work?
|
||||
/// Need to test.
|
||||
|
|
@ -56,7 +55,6 @@ class MessageTokenText extends StatelessWidget {
|
|||
MessageAnalyticsEntry? get messageAnalyticsEntry => _tokens != null
|
||||
? MessageAnalyticsController.get(
|
||||
_tokens!,
|
||||
_pangeaMessageEvent,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
@ -184,7 +182,7 @@ class MessageTextWidget extends StatelessWidget {
|
|||
return textPainter.width;
|
||||
}
|
||||
|
||||
Color backgroundColor(TokenPosition tokenPosition) {
|
||||
Color backgroundColor(BuildContext context, TokenPosition tokenPosition) {
|
||||
final hideTokenHighlights = messageAnalyticsEntry != null &&
|
||||
(messageAnalyticsEntry!.hasHiddenWordActivity ||
|
||||
messageAnalyticsEntry!.hasMessageMeaningActivity);
|
||||
|
|
@ -193,7 +191,7 @@ class MessageTextWidget extends StatelessWidget {
|
|||
|
||||
if (!hideTokenHighlights) {
|
||||
if (tokenPosition.selected) {
|
||||
backgroundColor = AppConfig.primaryColor;
|
||||
backgroundColor = Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
// else if (tokenPosition.isHighlighted) {
|
||||
// backgroundColor = AppConfig.success.withAlpha(80);
|
||||
|
|
@ -378,7 +376,7 @@ class MessageTextWidget extends StatelessWidget {
|
|||
: 0,
|
||||
width: tokenWidth,
|
||||
child: Container(
|
||||
color: backgroundColor(tokenPosition),
|
||||
color: backgroundColor(context, tokenPosition),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.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/toolbar_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class ToolbarButtonRow extends StatelessWidget {
|
||||
final MessageOverlayController overlayController;
|
||||
|
|
@ -35,71 +33,65 @@ class ToolbarButtonRow extends StatelessWidget {
|
|||
|
||||
return SizedBox(
|
||||
height: AppConfig.toolbarButtonsHeight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ToolbarButton(
|
||||
mode: MessageMode.messageTranslation,
|
||||
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,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.wordMorph,
|
||||
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,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: 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.wordEmoji,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.wordMorph,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.messageTranslation,
|
||||
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,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: buttonSize + 4,
|
||||
height: buttonSize + 4,
|
||||
alignment: Alignment.center,
|
||||
child: ToolbarButton(
|
||||
mode: MessageMode.wordEmoji,
|
||||
overlayController: overlayController,
|
||||
onPressed: overlayController.updateToolbarMode,
|
||||
buttonSize: buttonSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ class MorphFocusWidgetState extends State<MorphFocusWidget> {
|
|||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
MorphFeatureDisplay(
|
||||
morphFeature: widget.morphFeature,
|
||||
|
|
@ -214,6 +215,7 @@ class MorphFocusWidgetState extends State<MorphFocusWidget> {
|
|||
MorphMeaningWidget(
|
||||
feature: widget.morphFeature,
|
||||
tag: widget.token.getMorphTag(widget.morphFeature)!,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
] else
|
||||
Text(L10n.of(context).nan),
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
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/reading_assistance_input_row/morph_selection.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class MorphologicalListItem extends StatelessWidget {
|
||||
final MorphFeaturesEnum morphFeature;
|
||||
|
|
@ -31,7 +30,9 @@ class MorphologicalListItem extends StatelessWidget {
|
|||
) ==
|
||||
true;
|
||||
|
||||
bool get isSelected => overlayController.toolbarMode == MessageMode.wordMorph;
|
||||
bool get isSelected =>
|
||||
overlayController.toolbarMode == MessageMode.wordMorph &&
|
||||
overlayController.selectedMorph?.morph == morphFeature;
|
||||
|
||||
String get morphTag => token.getMorphTag(morphFeature.name) ?? "X";
|
||||
|
||||
|
|
@ -58,8 +59,8 @@ class MorphologicalListItem extends StatelessWidget {
|
|||
// view: ConstructTypeEnum.vocab,
|
||||
// ),
|
||||
// ),
|
||||
onPressed: () =>
|
||||
overlayController.onMorphActivitySelect(token, morphFeature),
|
||||
onPressed: () => overlayController
|
||||
.onMorphActivitySelect(MorphSelection(token, morphFeature)),
|
||||
tooltip: shouldDoActivity
|
||||
? morphFeature.getDisplayCopy(context)
|
||||
: getGrammarCopy(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
|
|
@ -11,6 +9,7 @@ import 'package:fluffychat/pangea/learning_settings/constants/language_constants
|
|||
import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
|
|
@ -19,6 +18,7 @@ import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_widget.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WordZoomWidget extends StatelessWidget {
|
||||
final PangeaToken token;
|
||||
|
|
@ -38,6 +38,13 @@ class WordZoomWidget extends StatelessWidget {
|
|||
|
||||
void onEditDone() => overlayController.initializeTokensAndMode();
|
||||
|
||||
bool get hasEmojiActivity =>
|
||||
overlayController.messageAnalyticsEntry?.hasActivity(
|
||||
ActivityTypeEnum.emoji,
|
||||
_selectedToken,
|
||||
) ==
|
||||
true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
|
|
@ -64,6 +71,7 @@ class WordZoomWidget extends StatelessWidget {
|
|||
constraints: const BoxConstraints(
|
||||
minHeight: 40,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
|
@ -114,21 +122,16 @@ class WordZoomWidget extends StatelessWidget {
|
|||
alignment: Alignment.center,
|
||||
child: LemmaEmojiRow(
|
||||
cId: _selectedToken.vocabConstructID,
|
||||
onTapOverride: () =>
|
||||
overlayController.updateToolbarMode(
|
||||
MessageMode.wordEmoji,
|
||||
),
|
||||
onTapOverride: hasEmojiActivity
|
||||
? () => overlayController.updateToolbarMode(
|
||||
MessageMode.wordEmoji,
|
||||
)
|
||||
: null,
|
||||
isSelected: overlayController.toolbarMode ==
|
||||
MessageMode.wordEmoji,
|
||||
emojiSetCallback: () =>
|
||||
overlayController.setState(() {}),
|
||||
shouldShowEmojis: overlayController
|
||||
.messageAnalyticsEntry
|
||||
?.hasActivity(
|
||||
MessageMode.wordEmoji.associatedActivityType!,
|
||||
_selectedToken,
|
||||
) ==
|
||||
false,
|
||||
shouldShowEmojis: !hasEmojiActivity,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue