diff --git a/lib/pangea/analytics_details_popup/morph_meaning_widget.dart b/lib/pangea/analytics_details_popup/morph_meaning_widget.dart index fd34f5c9d..a1716846e 100644 --- a/lib/pangea/analytics_details_popup/morph_meaning_widget.dart +++ b/lib/pangea/analytics_details_popup/morph_meaning_widget.dart @@ -1,7 +1,5 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; import 'package:fluffychat/pangea/languages/language_constants.dart'; @@ -11,17 +9,20 @@ import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart'; import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; class MorphMeaningWidget extends StatefulWidget { final MorphFeaturesEnum feature; final String tag; final TextStyle? style; + final bool blankErrorFeedback; const MorphMeaningWidget({ super.key, required this.feature, required this.tag, this.style, + this.blankErrorFeedback = false, }); @override @@ -91,12 +92,13 @@ class MorphMeaningWidgetState extends State { ); if (result.isError) { - return L10n.of(context).meaningNotFound; + return widget.blankErrorFeedback ? '' : L10n.of(context).meaningNotFound; } final morph = result.result!.getFeatureByCode(widget.feature.name); final data = morph?.getTagByCode(widget.tag); - return data?.l1Description ?? L10n.of(context).meaningNotFound; + return data?.l1Description ?? + (widget.blankErrorFeedback ? '' : L10n.of(context).meaningNotFound); } void _toggleEditMode(bool value) => setState(() => _editMode = value); diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 1b8b2c0e2..f3c5a8e49 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -505,4 +505,5 @@ class AnalyticsPracticeState extends State @override Widget build(BuildContext context) => AnalyticsPracticeView(this); + final request = activityTarget.value; } diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index 15cefb1a9..cf48314f9 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -267,6 +267,7 @@ class AnalyticsPracticeSessionRepo { choreo: choreo, stepIndex: i, eventID: event.eventId, + event: event, ), ), ); diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 44275cf4d..86acb81d1 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -74,8 +75,7 @@ class AnalyticsPracticeView extends StatelessWidget { ), body: Padding( padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 24.0, + horizontal: 8.0, ), child: MaxWidthBody( withScrolling: false, @@ -123,25 +123,36 @@ class _AnalyticsActivityView extends StatelessWidget { ), Expanded( child: Column( - spacing: 16.0, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Expanded( - flex: 1, - child: ValueListenableBuilder( - valueListenable: controller.activityTarget, - builder: (context, target, __) => target != null - ? Column( + ValueListenableBuilder( + valueListenable: controller.activityTarget, + builder: (context, target, __) => target != null + ? Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + top: 16.0, + ), + child: Column( spacing: 12.0, children: [ Text( target.promptText(context), textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith( - fontWeight: FontWeight.bold, - ), + style: FluffyThemes.isColumnMode(context) + ? Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.bold, + ) + : Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + ), ), if (controller.widget.type == ConstructTypeEnum.vocab) @@ -153,22 +164,56 @@ class _AnalyticsActivityView extends StatelessWidget { style: const TextStyle(fontSize: 14.0), ), ], - ) - : const SizedBox(), - ), + ), + ) + : const SizedBox.shrink(), ), - Expanded( - flex: 2, - child: Center( + Flexible( + fit: FlexFit.loose, + child: SingleChildScrollView( child: _AnalyticsPracticeCenterContent( controller: controller, ), ), ), Expanded( - flex: 6, child: _ActivityChoicesWidget(controller), ), + //reserve space for grammar category morph meaning to avoid shifting, but only in those questions + AnimatedBuilder( + animation: Listenable.merge([ + controller.activityState, + controller.selectedMorphChoice, + ]), + builder: (context, _) { + final activityState = controller.activityState.value; + final selectedChoice = controller.selectedMorphChoice.value; + + final isGrammarCategory = activityState + is AsyncLoaded && + activityState.value.activityType == + ActivityTypeEnum.grammarCategory; + + if (!isGrammarCategory) { + return const SizedBox.shrink(); + } + + return ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 80, + ), + child: selectedChoice == null + ? const SizedBox.shrink() + : SingleChildScrollView( + child: MorphMeaningWidget( + feature: selectedChoice.feature, + tag: selectedChoice.tag, + blankErrorFeedback: true, + ), + ), + ); + }, + ), ], ), ), @@ -193,8 +238,23 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget { ActivityTypeEnum.grammarError => ValueListenableBuilder( valueListenable: controller.activityState, builder: (context, state, __) => switch (state) { - AsyncLoaded(value: final activity) => _ErrorBlankWidget( - activity: activity as GrammarErrorPracticeActivityModel, + AsyncLoaded( + value: final GrammarErrorPracticeActivityModel activity + ) => + Column( + mainAxisSize: MainAxisSize.min, + children: [ + _ErrorBlankWidget( + activity: activity, + ), + const SizedBox(height: 12), + _GrammarErrorTranslationButton( + key: ValueKey( + '${activity.eventID}_${activity.errorOffset}_${activity.errorLength}', + ), + controller: controller, + ), + ], ), _ => const SizedBox(), }, @@ -349,11 +409,10 @@ class _ActivityChoicesWidget extends StatelessWidget { return Column( children: [ - Container( - constraints: const BoxConstraints(maxHeight: 400.0), + Expanded( child: Column( spacing: 4.0, - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: choices .map( (choice) => _ChoiceCard( @@ -372,20 +431,6 @@ class _ActivityChoicesWidget extends StatelessWidget { .toList(), ), ), - if (value.activityType == ActivityTypeEnum.grammarCategory) - ValueListenableBuilder( - valueListenable: controller.selectedMorphChoice, - builder: (context, selectedChoice, __) { - if (selectedChoice == null) { - return const SizedBox.shrink(); - } - - return MorphMeaningWidget( - feature: selectedChoice.feature, - tag: selectedChoice.tag, - ); - }, - ), ], ); }, @@ -470,6 +515,7 @@ class _ChoiceCard extends StatelessWidget { tag: choiceText, onPressed: onPressed, isCorrect: isCorrect, + height: cardHeight, enabled: enabled, ); @@ -504,3 +550,144 @@ class _ChoiceCard extends StatelessWidget { } } } + +class _GrammarErrorTranslationButton extends StatefulWidget { + final AnalyticsPracticeState controller; + + const _GrammarErrorTranslationButton({ + super.key, + required this.controller, + }); + + @override + State<_GrammarErrorTranslationButton> createState() => + _GrammarErrorTranslationButtonState(); +} + +class _GrammarErrorTranslationButtonState + extends State<_GrammarErrorTranslationButton> { + Future? _translationFuture; + bool _showTranslation = false; + + void _toggleTranslation() { + if (_showTranslation) { + setState(() { + _showTranslation = false; + _translationFuture = null; + }); + } else { + setState(() { + _showTranslation = true; + _translationFuture = widget.controller.requestTranslation(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Center( + child: GestureDetector( + onTap: _toggleTranslation, + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8.0, + children: [ + if (_showTranslation) + Flexible( + child: FutureBuilder( + future: _translationFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.white.withAlpha(180), + ThemeData.dark().colorScheme.primary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ); + } + + if (snapshot.hasError) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.white.withAlpha(180), + ThemeData.dark().colorScheme.primary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + L10n.of(context).oopsSomethingWentWrong, + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: AppConfig.fontSizeFactor * + AppConfig.messageFontSize, + ), + ), + ); + } + + if (snapshot.hasData) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.white.withAlpha(180), + ThemeData.dark().colorScheme.primary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + snapshot.data!, + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: AppConfig.fontSizeFactor * + AppConfig.messageFontSize, + ), + textAlign: TextAlign.center, + ), + ); + } + + return const SizedBox(); + }, + ), + ), + if (!_showTranslation) + ElevatedButton( + onPressed: _toggleTranslation, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(8), + ), + child: const Icon( + Icons.lightbulb_outline, + size: 20, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index 47eebe6db..23454688c 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -1,13 +1,12 @@ -import 'package:flutter/material.dart'; - -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.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/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; // includes feedback text and the bad activity model class ActivityQualityFeedback { @@ -45,11 +44,13 @@ class GrammarErrorRequestInfo { final ChoreoRecordModel choreo; final int stepIndex; final String eventID; + final PangeaMessageEvent? event; const GrammarErrorRequestInfo({ required this.choreo, required this.stepIndex, required this.eventID, + this.event, }); Map toJson() {