fix: simplify word zoom card, make all activity buttons stateless (#1330)
* fix: simplify word zoom card, make all activity buttons stateless * feat: make it visually clearer which activity type is selected * fix: give message to user when no token is selected in word zoom card * feat: don't highlight selected token or speak selected token if message has hidden word activity * feat: added error widgets to word zoom card * feat: added x-out badge to word zoom activity buttons, created word zoom activity button widget * fix: sort morph activity buttons to always have POS as first option, then having unlocked buttons before locked buttons, then alphabetically
This commit is contained in:
parent
f626d5fe7e
commit
09942aa47e
19 changed files with 439 additions and 409 deletions
|
|
@ -4657,5 +4657,6 @@
|
|||
"pleaseSelectALanguage": "Please select a language",
|
||||
"myBaseLanguage": "My base language",
|
||||
"publicProfileTitle": "Allow my profile to be found in search",
|
||||
"publicProfileDesc": "By enabling this option, I confirm that I am of legal age in my country of residence"
|
||||
"publicProfileDesc": "By enabling this option, I confirm that I am of legal age in my country of residence",
|
||||
"clickWordsInstructions": "Click on individual words for more activities."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,6 +213,10 @@ IconData getIconForMorphFeature(String feature) {
|
|||
return Symbols.abc;
|
||||
case 'pos':
|
||||
return Symbols.toys_and_games;
|
||||
case 'polarity':
|
||||
return Icons.swap_vert;
|
||||
case 'definite':
|
||||
return Icons.check_circle_outline;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
return Icons.help_outline;
|
||||
|
|
|
|||
|
|
@ -165,26 +165,38 @@ class ActivityRecordResponse {
|
|||
PracticeActivityModel practiceActivity,
|
||||
ConstructUseMetaData metadata,
|
||||
) {
|
||||
if (practiceActivity.activityType == ActivityTypeEnum.emoji) {
|
||||
if (practiceActivity.targetTokens != null &&
|
||||
practiceActivity.targetTokens!.isNotEmpty) {
|
||||
final token = practiceActivity.targetTokens!.first;
|
||||
return [
|
||||
OneConstructUse(
|
||||
lemma: token.lemma.text,
|
||||
form: token.text.content,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
useType: useType(practiceActivity.activityType),
|
||||
metadata: metadata,
|
||||
category: token.pos,
|
||||
),
|
||||
];
|
||||
}
|
||||
if (practiceActivity.targetTokens == null ||
|
||||
practiceActivity.targetTokens!.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (practiceActivity.targetTokens == null) {
|
||||
return [];
|
||||
if (practiceActivity.activityType == ActivityTypeEnum.emoji) {
|
||||
final token = practiceActivity.targetTokens!.first;
|
||||
return [
|
||||
OneConstructUse(
|
||||
lemma: token.lemma.text,
|
||||
form: token.text.content,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
useType: useType(practiceActivity.activityType),
|
||||
metadata: metadata,
|
||||
category: token.pos,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (practiceActivity.activityType == ActivityTypeEnum.morphId) {
|
||||
return practiceActivity.tgtConstructs
|
||||
.map(
|
||||
(token) => OneConstructUse(
|
||||
lemma: token.lemma,
|
||||
form: practiceActivity.targetTokens!.first.text.content,
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
useType: useType(practiceActivity.activityType),
|
||||
metadata: metadata,
|
||||
category: token.category,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final uses = practiceActivity.targetTokens!
|
||||
|
|
|
|||
|
|
@ -134,7 +134,9 @@ class LemmaDictionaryRepo {
|
|||
|
||||
/// From the cache, get a random set of cached definitions that are not for a specific lemma
|
||||
static List<String> getDistractorDefinitions(
|
||||
LemmaDefinitionRequest req, int count) {
|
||||
LemmaDefinitionRequest req,
|
||||
int count,
|
||||
) {
|
||||
_clearExpiredEntries();
|
||||
|
||||
final List<String> definitions = [];
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import 'package:fluffychat/pangea/repo/practice/morph_activity_generator.dart';
|
|||
import 'package:fluffychat/pangea/repo/practice/word_meaning_activity_generator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -119,6 +120,7 @@ class PracticeGenerationController {
|
|||
Future<MessageActivityResponse> _routePracticeActivity({
|
||||
required String accessToken,
|
||||
required MessageActivityRequest req,
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
// some activities we'll get from the server and others we'll generate locally
|
||||
switch (req.targetType) {
|
||||
|
|
@ -129,7 +131,7 @@ class PracticeGenerationController {
|
|||
case ActivityTypeEnum.morphId:
|
||||
return _morph.get(req);
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return _wordMeaning.get(req);
|
||||
return _wordMeaning.get(req, context);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return _fetchFromServer(
|
||||
|
|
@ -142,6 +144,7 @@ class PracticeGenerationController {
|
|||
Future<PracticeActivityModelResponse> getPracticeActivity(
|
||||
MessageActivityRequest req,
|
||||
PangeaMessageEvent event,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final int cacheKey = req.hashCode;
|
||||
|
||||
|
|
@ -154,6 +157,7 @@ class PracticeGenerationController {
|
|||
final MessageActivityResponse res = await _routePracticeActivity(
|
||||
accessToken: _pangeaController.userController.accessToken,
|
||||
req: req,
|
||||
context: context,
|
||||
);
|
||||
|
||||
// TODO resolve some wierdness here whereby the activity can be null but then... it's not
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import 'package:fluffychat/pangea/models/practice_activities.dart/message_activi
|
|||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class WordMeaningActivityGenerator {
|
||||
Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final ConstructIdentifier lemmaId = ConstructIdentifier(
|
||||
lemma: req.targetTokens[0].lemma.text,
|
||||
|
|
@ -29,6 +32,11 @@ class WordMeaningActivityGenerator {
|
|||
final choices =
|
||||
LemmaDictionaryRepo.getDistractorDefinitions(lemmaDefReq, 3);
|
||||
|
||||
if (!choices.contains(res.definition)) {
|
||||
choices.add(res.definition);
|
||||
choices.shuffle();
|
||||
}
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
tgtConstructs: [lemmaId],
|
||||
|
|
@ -36,7 +44,7 @@ class WordMeaningActivityGenerator {
|
|||
langCode: req.userL2,
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
content: ActivityContent(
|
||||
question: "?",
|
||||
question: "${L10n.of(context).definition}?",
|
||||
choices: choices,
|
||||
answers: [res.definition],
|
||||
spanDisplayDetails: null,
|
||||
|
|
|
|||
|
|
@ -202,7 +202,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
)
|
||||
: const CardErrorWidget(
|
||||
error: "Null audio file in message_audio_card",
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -110,11 +110,13 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
void _updateSelectedSpan(PangeaTokenText selectedSpan) {
|
||||
_selectedSpan = selectedSpan;
|
||||
|
||||
widget.chatController.choreographer.tts.tryToSpeak(
|
||||
selectedSpan.content,
|
||||
context,
|
||||
widget._pangeaMessageEvent?.eventId,
|
||||
);
|
||||
if (!(messageAnalyticsEntry?.hasHiddenWordActivity ?? false)) {
|
||||
widget.chatController.choreographer.tts.tryToSpeak(
|
||||
selectedSpan.content,
|
||||
context,
|
||||
widget._pangeaMessageEvent?.eventId,
|
||||
);
|
||||
}
|
||||
|
||||
// if a token is selected, then the toolbar should be in wordZoom mode
|
||||
if (toolbarMode != MessageMode.wordZoom) {
|
||||
|
|
|
|||
|
|
@ -67,13 +67,16 @@ class MessageTokenText extends StatelessWidget {
|
|||
final hideContent =
|
||||
messageAnalyticsEntry?.isTokenInHiddenWordActivity(token) ?? false;
|
||||
|
||||
final hasHiddenContent =
|
||||
messageAnalyticsEntry?.hasHiddenWordActivity ?? false;
|
||||
|
||||
if (globalIndex < startIndex) {
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
start: globalIndex,
|
||||
end: startIndex,
|
||||
hideContent: false,
|
||||
highlight: _isSelected?.call(token) ?? false,
|
||||
highlight: (_isSelected?.call(token) ?? false) && !hasHiddenContent,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -84,7 +87,9 @@ class MessageTokenText extends StatelessWidget {
|
|||
end: endIndex,
|
||||
token: token,
|
||||
hideContent: hideContent,
|
||||
highlight: (_isSelected?.call(token) ?? false) && !hideContent,
|
||||
highlight: (_isSelected?.call(token) ?? false) &&
|
||||
!hideContent &&
|
||||
!hasHiddenContent,
|
||||
),
|
||||
);
|
||||
globalIndex = endIndex;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_ca
|
|||
import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
||||
|
||||
const double minCardHeight = 70;
|
||||
|
|
@ -73,11 +74,11 @@ class MessageToolbar extends StatelessWidget {
|
|||
messageEvent: pangeaMessageEvent,
|
||||
);
|
||||
case MessageMode.noneSelected:
|
||||
return const SizedBox();
|
||||
return Text(L10n.of(context).clickWordsInstructions);
|
||||
case MessageMode.practiceActivity:
|
||||
case MessageMode.wordZoom:
|
||||
if (overlayController.selectedToken == null) {
|
||||
return const SizedBox();
|
||||
return Text(L10n.of(context).clickWordsInstructions);
|
||||
}
|
||||
return WordZoomWidget(
|
||||
token: overlayController.selectedToken!,
|
||||
|
|
|
|||
|
|
@ -9,14 +9,16 @@ class CardErrorWidget extends StatelessWidget {
|
|||
final Object error;
|
||||
final Choreographer? choreographer;
|
||||
final int? offset;
|
||||
final double? maxWidth;
|
||||
final double maxWidth;
|
||||
final double padding;
|
||||
|
||||
const CardErrorWidget({
|
||||
super.key,
|
||||
required this.error,
|
||||
this.choreographer,
|
||||
this.offset,
|
||||
this.maxWidth,
|
||||
this.maxWidth = 275,
|
||||
this.padding = 8,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -24,8 +26,8 @@ class CardErrorWidget extends StatelessWidget {
|
|||
final ErrorCopy errorCopy = ErrorCopy(context, error);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(maxWidth: 275),
|
||||
padding: EdgeInsets.all(padding),
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
|
|
@ -36,7 +38,7 @@ class CardErrorWidget extends StatelessWidget {
|
|||
cursorOffset: offset,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
const SizedBox(height: 6.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
|
|
|
|||
|
|
@ -1,36 +1,21 @@
|
|||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmojiPracticeButton extends StatefulWidget {
|
||||
class EmojiPracticeButton extends StatelessWidget {
|
||||
final PangeaToken token;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final String? emoji;
|
||||
final Function(String) setEmoji;
|
||||
final bool isSelected;
|
||||
|
||||
const EmojiPracticeButton({
|
||||
required this.token,
|
||||
required this.onPressed,
|
||||
this.emoji,
|
||||
required this.setEmoji,
|
||||
this.isSelected = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
EmojiPracticeButtonState createState() => EmojiPracticeButtonState();
|
||||
}
|
||||
|
||||
class EmojiPracticeButtonState extends State<EmojiPracticeButton> {
|
||||
@override
|
||||
void didUpdateWidget(covariant EmojiPracticeButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.token != oldWidget.token) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
bool get _shouldDoActivity => widget.token.shouldDoActivity(
|
||||
bool get _shouldDoActivity => token.shouldDoActivity(
|
||||
a: ActivityTypeEnum.emoji,
|
||||
feature: null,
|
||||
tag: null,
|
||||
|
|
@ -38,26 +23,16 @@ class EmojiPracticeButtonState extends State<EmojiPracticeButton> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final emoji = widget.token.getEmoji();
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: _shouldDoActivity || emoji != null
|
||||
? Opacity(
|
||||
opacity: _shouldDoActivity ? 0.5 : 1,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
widget.onPressed();
|
||||
if (widget.emoji == null && emoji != null) {
|
||||
widget.setEmoji(emoji);
|
||||
}
|
||||
},
|
||||
icon: emoji == null
|
||||
? const Icon(Icons.add_reaction_outlined)
|
||||
: Text(emoji),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
final emoji = token.getEmoji();
|
||||
return _shouldDoActivity || emoji != null
|
||||
? WordZoomActivityButton(
|
||||
icon: emoji == null
|
||||
? const Icon(Icons.add_reaction_outlined)
|
||||
: Text(emoji),
|
||||
isSelected: isSelected,
|
||||
onPressed: onPressed,
|
||||
opacity: _shouldDoActivity ? 0.5 : 1,
|
||||
)
|
||||
: const SizedBox(width: 40);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,9 +147,21 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
|
||||
// If the selected choice is correct, send the record and get the next activity
|
||||
if (widget.currentActivity.content.isCorrect(value, index)) {
|
||||
MatrixState.pangeaController.getAnalytics.analyticsStream.stream.first
|
||||
.then((_) {
|
||||
widget.practiceCardController.onActivityFinish(correctAnswer: value);
|
||||
// If the activity is an emoji activity, set the emoji value
|
||||
if (widget.currentActivity.activityType == ActivityTypeEnum.emoji) {
|
||||
if (widget.currentActivity.targetTokens?.length != 1) {
|
||||
debugger(when: kDebugMode);
|
||||
} else {
|
||||
widget.currentActivity.targetTokens!.first.setEmoji(value);
|
||||
}
|
||||
}
|
||||
|
||||
// The next entry in the analytics stream should be from the above putAnalytics.setState.
|
||||
// So we can wait for the stream to update before calling onActivityFinish.
|
||||
final streamFuture = MatrixState
|
||||
.pangeaController.getAnalytics.analyticsStream.stream.first;
|
||||
streamFuture.then((_) {
|
||||
widget.practiceCardController.onActivityFinish();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
|
|||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -65,6 +66,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
PracticeGenerationController();
|
||||
|
||||
PangeaController get pangeaController => MatrixState.pangeaController;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -96,6 +98,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
Future<void> _fetchActivity({
|
||||
ActivityQualityFeedback? activityFeedback,
|
||||
}) async {
|
||||
_error = null;
|
||||
if (!mounted ||
|
||||
!pangeaController.languageController.languagesSet ||
|
||||
widget.overlayController.messageAnalyticsEntry == null) {
|
||||
|
|
@ -134,6 +137,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
},
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_updateFetchingActivity(false);
|
||||
}
|
||||
|
|
@ -152,14 +156,18 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
final existingActivity =
|
||||
widget.pangeaMessageEvent.practiceActivities.firstWhereOrNull(
|
||||
(activity) {
|
||||
final sameActivity = activity.practiceActivity.targetTokens != null &&
|
||||
activity.practiceActivity.activityType == type &&
|
||||
activity.practiceActivity.targetTokens!
|
||||
.map((t) => t.vocabConstructID.string)
|
||||
.toSet()
|
||||
.containsAll(
|
||||
tokens.map((t) => t.vocabConstructID.string).toSet(),
|
||||
);
|
||||
final sameActivity =
|
||||
activity.practiceActivity.content.choices.toSet().containsAll(
|
||||
activity.practiceActivity.content.answers.toSet(),
|
||||
) &&
|
||||
activity.practiceActivity.targetTokens != null &&
|
||||
activity.practiceActivity.activityType == type &&
|
||||
activity.practiceActivity.targetTokens!
|
||||
.map((t) => t.vocabConstructID.string)
|
||||
.toSet()
|
||||
.containsAll(
|
||||
tokens.map((t) => t.vocabConstructID.string).toSet(),
|
||||
);
|
||||
if (type != ActivityTypeEnum.morphId || sameActivity == false) {
|
||||
return sameActivity;
|
||||
}
|
||||
|
|
@ -198,6 +206,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
await practiceGenerationController.getPracticeActivity(
|
||||
req,
|
||||
widget.pangeaMessageEvent,
|
||||
context,
|
||||
);
|
||||
|
||||
if (activityResponse.activity == null) return null;
|
||||
|
|
@ -213,12 +222,14 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
final Duration _savorTheJoyDuration = const Duration(seconds: 1);
|
||||
|
||||
Future<void> _savorTheJoy() async {
|
||||
try {
|
||||
debugger(when: savoringTheJoy && kDebugMode);
|
||||
|
||||
if (mounted) setState(() => savoringTheJoy = true);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
await Future.delayed(_savorTheJoyDuration);
|
||||
if (mounted) setState(() => savoringTheJoy = false);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -238,23 +249,23 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
/// Saves the completion record and sends it to the server.
|
||||
/// Fetches a new activity if there are any left to complete.
|
||||
/// Exits the practice flow if there are no more activities.
|
||||
void onActivityFinish({String? correctAnswer}) async {
|
||||
void onActivityFinish() async {
|
||||
try {
|
||||
if (currentCompletionRecord == null || currentActivity == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
widget.wordDetailsController?.onActivityFinish(
|
||||
savorTheJoyDuration: _savorTheJoyDuration,
|
||||
);
|
||||
|
||||
widget.overlayController.onActivityFinish();
|
||||
pangeaController.activityRecordController.completeActivity(
|
||||
widget.pangeaMessageEvent.eventId,
|
||||
);
|
||||
|
||||
await _savorTheJoy();
|
||||
widget.wordDetailsController?.onActivityFinish(
|
||||
activityType: currentActivity!.activityType,
|
||||
correctAnswer: correctAnswer,
|
||||
);
|
||||
} catch (e, s) {
|
||||
_onError();
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -301,9 +312,20 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_error != null) {
|
||||
return CardErrorWidget(
|
||||
error: _error!,
|
||||
maxWidth: 500,
|
||||
);
|
||||
}
|
||||
|
||||
if (!fetchingActivity && currentActivity == null) {
|
||||
debugPrint("don't think we should be here");
|
||||
debugger(when: kDebugMode);
|
||||
return CardErrorWidget(
|
||||
error: _error!,
|
||||
maxWidth: 500,
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WordZoomActivityButton extends StatelessWidget {
|
||||
final Widget icon;
|
||||
final bool isSelected;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final String? tooltip;
|
||||
final double? opacity;
|
||||
|
||||
const WordZoomActivityButton({
|
||||
required this.icon,
|
||||
required this.isSelected,
|
||||
required this.onPressed,
|
||||
this.tooltip,
|
||||
this.opacity,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget buttonContent = IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: isSelected
|
||||
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.25)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
|
||||
if (opacity != null) {
|
||||
buttonContent = Opacity(
|
||||
opacity: opacity!,
|
||||
child: buttonContent,
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltip != null) {
|
||||
buttonContent = Tooltip(
|
||||
message: tooltip!,
|
||||
child: buttonContent,
|
||||
);
|
||||
}
|
||||
|
||||
return Badge(
|
||||
offset: kIsWeb ? null : const Offset(-1, 1),
|
||||
isLabelVisible: isSelected,
|
||||
label: SizedBox(
|
||||
height: 10,
|
||||
width: 10,
|
||||
child: IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(Icons.close, size: 10),
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
),
|
||||
),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: buttonContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +1,57 @@
|
|||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ContextualTranslationWidget extends StatefulWidget {
|
||||
class ContextualTranslationWidget extends StatelessWidget {
|
||||
final PangeaToken token;
|
||||
final String fullText;
|
||||
final String langCode;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final String? definition;
|
||||
final Function(String) setDefinition;
|
||||
|
||||
const ContextualTranslationWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.fullText,
|
||||
required this.langCode,
|
||||
required this.onPressed,
|
||||
required this.setDefinition,
|
||||
this.definition,
|
||||
});
|
||||
|
||||
@override
|
||||
ContextualTranslationWidgetState createState() =>
|
||||
ContextualTranslationWidgetState();
|
||||
}
|
||||
|
||||
class ContextualTranslationWidgetState
|
||||
extends State<ContextualTranslationWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.definition == null) {
|
||||
_fetchDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ContextualTranslationWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.token != widget.token && widget.definition == null) {
|
||||
_fetchDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchDefinition() async {
|
||||
// final FullTextTranslationResponseModel response =
|
||||
// await FullTextTranslationRepo.translate(
|
||||
// accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
// request: FullTextTranslationRequestModel(
|
||||
// text: widget.fullText,
|
||||
// tgtLang:
|
||||
// MatrixState.pangeaController.languageController.userL1?.langCode ??
|
||||
// LanguageKeys.defaultLanguage,
|
||||
// userL2:
|
||||
// MatrixState.pangeaController.languageController.userL2?.langCode ??
|
||||
// LanguageKeys.defaultLanguage,
|
||||
// userL1:
|
||||
// MatrixState.pangeaController.languageController.userL1?.langCode ??
|
||||
// LanguageKeys.defaultLanguage,
|
||||
// offset: widget.token.text.offset,
|
||||
// length: widget.token.text.length,
|
||||
// deepL: false,
|
||||
// ),
|
||||
// );
|
||||
// widget.setDefinition(response.bestTranslation);
|
||||
Future<String> _fetchDefinition() async {
|
||||
final LemmaDefinitionRequest lemmaDefReq = LemmaDefinitionRequest(
|
||||
lemma: widget.token.lemma.text,
|
||||
partOfSpeech: widget.token.pos,
|
||||
lemma: token.lemma.text,
|
||||
partOfSpeech: token.pos,
|
||||
|
||||
/// This assumes that the user's L2 is the language of the lemma
|
||||
lemmaLang: widget.langCode,
|
||||
lemmaLang: langCode,
|
||||
userL1:
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
final res = await LemmaDictionaryRepo.get(lemmaDefReq);
|
||||
|
||||
widget.setDefinition(res.definition);
|
||||
return res.definition;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
// child: IconButton(
|
||||
// iconSize: 30,
|
||||
// onPressed: widget.onPressed,
|
||||
// icon: const Icon(Symbols.dictionary),
|
||||
// ),
|
||||
child: Text(widget.definition ?? "..."),
|
||||
),
|
||||
return FutureBuilder(
|
||||
future: _fetchDefinition(),
|
||||
builder: (context, snapshot) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: snapshot.connectionState != ConnectionState.done
|
||||
? const CircularProgressIndicator()
|
||||
: snapshot.hasError
|
||||
? CardErrorWidget(
|
||||
error: L10n.of(context).oopsSomethingWentWrong,
|
||||
padding: 0,
|
||||
maxWidth: 500,
|
||||
)
|
||||
: Text(snapshot.data ?? "..."),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,25 @@
|
|||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LemmaWidget extends StatelessWidget {
|
||||
final PangeaToken token;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final String? lemma;
|
||||
final Function(String) setLemma;
|
||||
final bool isSelected;
|
||||
|
||||
const LemmaWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.onPressed,
|
||||
this.lemma,
|
||||
required this.setLemma,
|
||||
this.isSelected = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
onPressed();
|
||||
if (lemma == null) {
|
||||
setLemma(token.lemma.text);
|
||||
}
|
||||
},
|
||||
icon: Text(token.xpEmoji),
|
||||
),
|
||||
return WordZoomActivityButton(
|
||||
icon: Text(token.xpEmoji),
|
||||
isSelected: isSelected,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:fluffychat/pangea/constants/morph_categories_and_labels.dart';
|
||||
import 'package:fluffychat/pangea/enum/analytics/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/practice/morph_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ActivityMorph {
|
||||
|
|
@ -22,63 +20,49 @@ class MorphologicalListWidget extends StatelessWidget {
|
|||
final PangeaToken token;
|
||||
final String? selectedMorphFeature;
|
||||
final Function(String?) setMorphFeature;
|
||||
final int completedActivities;
|
||||
|
||||
const MorphologicalListWidget({
|
||||
super.key,
|
||||
required this.selectedMorphFeature,
|
||||
required this.token,
|
||||
required this.setMorphFeature,
|
||||
required this.completedActivities,
|
||||
});
|
||||
|
||||
List<ActivityMorph> get _visibleMorphs {
|
||||
// we always start with the part of speech
|
||||
final visibleMorphs = [
|
||||
ActivityMorph(
|
||||
morphFeature: "pos",
|
||||
morphTag: token.pos,
|
||||
revealed: !token.shouldDoPosActivity,
|
||||
// revealed: !shouldDoActivity || !canGenerateDistractors,
|
||||
),
|
||||
];
|
||||
|
||||
// each pos has a defined set of morphological features to display and do activities for
|
||||
final List<String> seq = MorphActivityGenerator().getSequence(
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode,
|
||||
token.pos,
|
||||
);
|
||||
|
||||
for (final String feature in seq) {
|
||||
// don't add any more if the last one is not revealed yet
|
||||
if (!visibleMorphs.last.revealed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// check that the feature is in token.morph
|
||||
if (!token.morph.containsKey(feature)) {
|
||||
ErrorHandler.logError(
|
||||
m: "Morphological feature suggested for pos but not found in token",
|
||||
data: {
|
||||
"feature": feature,
|
||||
"token": token,
|
||||
"lang_code": MatrixState
|
||||
.pangeaController.languageController.userL2?.langCode,
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
visibleMorphs.add(
|
||||
ActivityMorph(
|
||||
morphFeature: feature,
|
||||
morphTag: token.morph[feature],
|
||||
revealed: !token.shouldDoMorphActivity(feature),
|
||||
),
|
||||
final activityMorphs = token.morph.entries.map((entry) {
|
||||
return ActivityMorph(
|
||||
morphFeature: entry.key,
|
||||
morphTag: entry.value,
|
||||
revealed: !token.shouldDoMorphActivity(entry.key),
|
||||
);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return visibleMorphs;
|
||||
activityMorphs.sort((a, b) {
|
||||
if (a.morphFeature.toLowerCase() == "pos") {
|
||||
return -1;
|
||||
} else if (b.morphFeature.toLowerCase() == "pos") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.revealed && !b.revealed) {
|
||||
return -1;
|
||||
} else if (!a.revealed && b.revealed) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.morphFeature.compareTo(b.morphFeature);
|
||||
});
|
||||
|
||||
final lastRevealedIndex =
|
||||
activityMorphs.lastIndexWhere((morph) => morph.revealed);
|
||||
|
||||
if (lastRevealedIndex == -1) {
|
||||
return activityMorphs;
|
||||
} else if (lastRevealedIndex >= (activityMorphs.length - 1)) {
|
||||
return activityMorphs;
|
||||
} else {
|
||||
return activityMorphs.take(lastRevealedIndex + 2).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -119,23 +103,15 @@ class MorphologicalActivityButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: getMorphologicalCategoryCopy(
|
||||
morphCategory,
|
||||
context,
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: isSelected ? 1 : 0.5,
|
||||
child: IconButton(
|
||||
onPressed: () => onPressed(morphCategory),
|
||||
icon: Icon(icon),
|
||||
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return WordZoomActivityButton(
|
||||
icon: Icon(icon),
|
||||
isSelected: isSelected,
|
||||
onPressed: () => onPressed(morphCategory),
|
||||
tooltip: getMorphologicalCategoryCopy(
|
||||
morphCategory,
|
||||
context,
|
||||
),
|
||||
opacity: (isSelected || !isUnlocked) ? 1 : 0.5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,23 @@ import 'package:flutter/material.dart';
|
|||
enum WordZoomSelection {
|
||||
translation,
|
||||
emoji,
|
||||
lemma,
|
||||
morph,
|
||||
}
|
||||
|
||||
extension on WordZoomSelection {
|
||||
ActivityTypeEnum get activityType {
|
||||
switch (this) {
|
||||
case WordZoomSelection.translation:
|
||||
return ActivityTypeEnum.wordMeaning;
|
||||
case WordZoomSelection.emoji:
|
||||
return ActivityTypeEnum.emoji;
|
||||
case WordZoomSelection.lemma:
|
||||
return ActivityTypeEnum.lemmaId;
|
||||
case WordZoomSelection.morph:
|
||||
return ActivityTypeEnum.morphId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WordZoomWidget extends StatefulWidget {
|
||||
|
|
@ -38,48 +55,29 @@ class WordZoomWidget extends StatefulWidget {
|
|||
}
|
||||
|
||||
class WordZoomWidgetState extends State<WordZoomWidget> {
|
||||
ActivityTypeEnum _activityType = ActivityTypeEnum.wordMeaning;
|
||||
/// The currently selected word zoom activity type.
|
||||
/// If an activity should be shown for this type, shows that activity.
|
||||
/// If not, shows the info related to that activity type.
|
||||
/// Defaults to the lemma translation.
|
||||
WordZoomSelection _selectionType = WordZoomSelection.translation;
|
||||
|
||||
// morphological activities
|
||||
/// If doing a morphological activity, this is the selected morph feature.
|
||||
String? _selectedMorphFeature;
|
||||
|
||||
/// used to trigger a rebuild of the morph activity
|
||||
/// button when a morph activity is completed
|
||||
int completedMorphActivities = 0;
|
||||
/// If true, the activity will be shown regardless of shouldDoActivity.
|
||||
/// Used to show the practice activity card's savor the joy animation.
|
||||
/// (Analytics sending triggers the point gain animation, do also
|
||||
/// causes shouldDoActivity to be false. This is a workaround.)
|
||||
bool _forceShowActivity = false;
|
||||
|
||||
// defintion activities
|
||||
String? _definition;
|
||||
|
||||
// lemma activities
|
||||
String? _lemma;
|
||||
|
||||
// emoji activities
|
||||
String? _emoji;
|
||||
|
||||
// whether activity type can be generated
|
||||
Map<ActivityTypeEnum, bool> canGenerateActivity = {
|
||||
ActivityTypeEnum.morphId: true,
|
||||
ActivityTypeEnum.wordMeaning: true,
|
||||
ActivityTypeEnum.lemmaId: false,
|
||||
ActivityTypeEnum.emoji: true,
|
||||
};
|
||||
|
||||
Future<void> _initCanGenerateActivity() async {
|
||||
widget.token.canGenerateDistractors(ActivityTypeEnum.lemmaId).then((value) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
canGenerateActivity[ActivityTypeEnum.lemmaId] = value;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// if learner should do translation activity then setActivityType to wordMeaning
|
||||
}
|
||||
// The function to determine if lemma distractors can be generated
|
||||
// is computationally expensive, so we only do it once
|
||||
bool canGenerateLemmaActivity = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initCanGenerateActivity();
|
||||
_setCanGenerateLemmaActivity();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -87,88 +85,81 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
|
|||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.token != oldWidget.token) {
|
||||
_clean();
|
||||
_initCanGenerateActivity();
|
||||
_setCanGenerateLemmaActivity();
|
||||
}
|
||||
}
|
||||
|
||||
void _clean() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_activityType = ActivityTypeEnum.wordMeaning;
|
||||
_selectionType = WordZoomSelection.translation;
|
||||
_selectedMorphFeature = null;
|
||||
_definition = null;
|
||||
_lemma = null;
|
||||
_emoji = null;
|
||||
_forceShowActivity = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _setSelectedMorphFeature(String? feature) {
|
||||
_selectedMorphFeature = _selectedMorphFeature == feature ? null : feature;
|
||||
_setActivityType(
|
||||
_selectedMorphFeature == null
|
||||
? ActivityTypeEnum.wordMeaning
|
||||
: ActivityTypeEnum.morphId,
|
||||
);
|
||||
void _setCanGenerateLemmaActivity() {
|
||||
widget.token.canGenerateDistractors(ActivityTypeEnum.lemmaId).then((value) {
|
||||
if (mounted) setState(() => canGenerateLemmaActivity = value);
|
||||
});
|
||||
}
|
||||
|
||||
void _setActivityType(ActivityTypeEnum activityType) {
|
||||
if (mounted) setState(() => _activityType = activityType);
|
||||
}
|
||||
|
||||
void _setDefinition(String definition) {
|
||||
if (mounted) setState(() => _definition = definition);
|
||||
}
|
||||
|
||||
void _setLemma(String lemma) {
|
||||
if (mounted) setState(() => _lemma = lemma);
|
||||
}
|
||||
|
||||
void _setEmoji(String emoji) {
|
||||
if (mounted) setState(() => _emoji = emoji);
|
||||
}
|
||||
|
||||
void onActivityFinish({
|
||||
required ActivityTypeEnum activityType,
|
||||
String? correctAnswer,
|
||||
}) {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.morphId:
|
||||
if (mounted) setState(() => completedMorphActivities++);
|
||||
break;
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
if (correctAnswer == null) return;
|
||||
_setDefinition(correctAnswer);
|
||||
break;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
if (correctAnswer == null) return;
|
||||
_setLemma(correctAnswer);
|
||||
break;
|
||||
case ActivityTypeEnum.emoji:
|
||||
if (correctAnswer == null) return;
|
||||
widget.token
|
||||
.setEmoji(correctAnswer)
|
||||
.then((_) => _setEmoji(correctAnswer));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
void _setSelectionType(WordZoomSelection type, {String? feature}) {
|
||||
WordZoomSelection newSelectedType = type;
|
||||
String? newSelectedFeature;
|
||||
if (type != WordZoomSelection.morph) {
|
||||
// if setting selectionType to non-morph activity, either set it if it's not
|
||||
// already selected, or reset to it the default type
|
||||
newSelectedType =
|
||||
_selectionType == type ? WordZoomSelection.translation : type;
|
||||
} else {
|
||||
// otherwise (because there could be multiple different morph features), check
|
||||
// if the feature is already selected, and if so, reset to the default type.
|
||||
// if not, set the selectionType and feature
|
||||
newSelectedFeature = _selectedMorphFeature == feature ? null : feature;
|
||||
newSelectedType = newSelectedFeature == null
|
||||
? WordZoomSelection.translation
|
||||
: WordZoomSelection.morph;
|
||||
}
|
||||
|
||||
_selectionType = newSelectedType;
|
||||
_selectedMorphFeature = newSelectedFeature;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _setForceShowActivity(bool showActivity) {
|
||||
if (mounted) setState(() => _forceShowActivity = showActivity);
|
||||
}
|
||||
|
||||
/// This function should be called before overlayController.onActivityFinish to
|
||||
/// prevent shouldDoActivity being set to false before _forceShowActivity is set to true.
|
||||
/// This keep the completed actvity visible to the user for a short time.
|
||||
void onActivityFinish({
|
||||
Duration savorTheJoyDuration = const Duration(seconds: 1),
|
||||
}) {
|
||||
_setForceShowActivity(true);
|
||||
Future.delayed(savorTheJoyDuration, () {
|
||||
_setForceShowActivity(false);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _wordZoomCenterWidget {
|
||||
if (widget.token.shouldDoActivity(
|
||||
a: _activityType,
|
||||
final showActivity = widget.token.shouldDoActivity(
|
||||
a: _selectionType.activityType,
|
||||
feature: _selectedMorphFeature,
|
||||
tag: _selectedMorphFeature == null
|
||||
? null
|
||||
: widget.token.morph[_selectedMorphFeature],
|
||||
) &&
|
||||
canGenerateActivity[_activityType]!) {
|
||||
PracticeActivityCard(
|
||||
(_selectionType != WordZoomSelection.lemma || canGenerateLemmaActivity);
|
||||
|
||||
if (showActivity || _forceShowActivity) {
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: widget.messageEvent,
|
||||
targetTokensAndActivityType: TargetTokensAndActivityType(
|
||||
tokens: [widget.token],
|
||||
activityType: _activityType,
|
||||
activityType: _selectionType.activityType,
|
||||
),
|
||||
overlayController: widget.overlayController,
|
||||
morphFeature: _selectedMorphFeature,
|
||||
|
|
@ -176,27 +167,26 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
if (_activityType == ActivityTypeEnum.wordMeaning) {
|
||||
if (_selectionType == WordZoomSelection.translation) {
|
||||
return ContextualTranslationWidget(
|
||||
token: widget.token,
|
||||
fullText: widget.messageEvent.messageDisplayText,
|
||||
langCode: widget.messageEvent.messageDisplayLangCode,
|
||||
onPressed: () => _setActivityType(ActivityTypeEnum.wordMeaning),
|
||||
definition: _definition,
|
||||
setDefinition: _setDefinition,
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [_activityAnswer],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [_activityAnswer],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _activityAnswer {
|
||||
switch (_activityType) {
|
||||
case ActivityTypeEnum.morphId:
|
||||
switch (_selectionType) {
|
||||
case WordZoomSelection.morph:
|
||||
if (_selectedMorphFeature == null) {
|
||||
return const Text("There should be a selected morph feature");
|
||||
}
|
||||
|
|
@ -207,75 +197,77 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
|
|||
context: context,
|
||||
);
|
||||
return Text(copy ?? morphTag);
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return _definition != null
|
||||
? Text(_definition!)
|
||||
: const Text("definition is null");
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return _lemma != null ? Text(_lemma!) : const Text("lemma is null");
|
||||
case ActivityTypeEnum.emoji:
|
||||
return _emoji != null ? Text(_emoji!) : const Text("emoji is null");
|
||||
default:
|
||||
case WordZoomSelection.lemma:
|
||||
return Text(widget.token.lemma.text);
|
||||
case WordZoomSelection.emoji:
|
||||
return widget.token.getEmoji() != null
|
||||
? Text(widget.token.getEmoji()!)
|
||||
: const Text("emoji is null");
|
||||
case WordZoomSelection.translation:
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: IntrinsicWidth(
|
||||
child: ConstrainedBox(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: AppConfig.toolbarMinHeight),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: AppConfig.toolbarMinWidth),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EmojiPracticeButton(
|
||||
emoji: _emoji,
|
||||
token: widget.token,
|
||||
onPressed: () => _setActivityType(
|
||||
_activityType == ActivityTypeEnum.emoji
|
||||
? ActivityTypeEnum.wordMeaning
|
||||
: ActivityTypeEnum.emoji,
|
||||
),
|
||||
setEmoji: _setEmoji,
|
||||
),
|
||||
WordTextWithAudioButton(
|
||||
text: widget.token.text.content,
|
||||
ttsController: widget.tts,
|
||||
eventID: widget.messageEvent.eventId,
|
||||
),
|
||||
LemmaWidget(
|
||||
token: widget.token,
|
||||
onPressed: () => _setActivityType(
|
||||
_activityType == ActivityTypeEnum.lemmaId
|
||||
? ActivityTypeEnum.wordMeaning
|
||||
: ActivityTypeEnum.lemmaId,
|
||||
),
|
||||
lemma: _lemma,
|
||||
setLemma: _setLemma,
|
||||
),
|
||||
],
|
||||
),
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: AppConfig.toolbarMinHeight,
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: IntrinsicWidth(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: AppConfig.toolbarMinHeight,
|
||||
),
|
||||
_wordZoomCenterWidget,
|
||||
MorphologicalListWidget(
|
||||
token: widget.token,
|
||||
setMorphFeature: _setSelectedMorphFeature,
|
||||
selectedMorphFeature: _selectedMorphFeature,
|
||||
completedActivities: completedMorphActivities,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: AppConfig.toolbarMinWidth,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EmojiPracticeButton(
|
||||
token: widget.token,
|
||||
onPressed: () =>
|
||||
_setSelectionType(WordZoomSelection.emoji),
|
||||
isSelected: _selectionType == WordZoomSelection.emoji,
|
||||
),
|
||||
WordTextWithAudioButton(
|
||||
text: widget.token.text.content,
|
||||
ttsController: widget.tts,
|
||||
eventID: widget.messageEvent.eventId,
|
||||
),
|
||||
LemmaWidget(
|
||||
token: widget.token,
|
||||
onPressed: () =>
|
||||
_setSelectionType(WordZoomSelection.lemma),
|
||||
isSelected: _selectionType == WordZoomSelection.lemma,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_wordZoomCenterWidget,
|
||||
MorphologicalListWidget(
|
||||
token: widget.token,
|
||||
setMorphFeature: (feature) => _setSelectionType(
|
||||
WordZoomSelection.morph,
|
||||
feature: feature,
|
||||
),
|
||||
selectedMorphFeature: _selectedMorphFeature,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue