From 6e7ae5c0441f194b641670f88cd2179e69c6283e Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:13:16 -0400 Subject: [PATCH] chore(reading_assistance): several fixes and an enhancement to gain points animation --- .../construct_use_type_enum.dart | 14 +- .../analytics_misc/gain_points_animation.dart | 88 +++++------ .../utils/get_chat_list_item_subtitle.dart | 10 +- .../widgets/choice_animation.dart | 10 +- lib/pangea/common/widgets/customized_svg.dart | 138 ++++++++++++------ .../practice_activity_model.dart | 11 +- .../practice_selection.dart | 116 ++++++++------- .../practice_selection_repo.dart | 8 +- .../practice_match_card.dart | 7 +- .../practice_match_item.dart | 5 +- .../widgets/message_selection_overlay.dart | 1 + .../toolbar/widgets/message_token_text.dart | 7 +- .../widgets/word_zoom/lemma_widget.dart | 100 ++++++------- 13 files changed, 285 insertions(+), 230 deletions(-) diff --git a/lib/pangea/analytics_misc/construct_use_type_enum.dart b/lib/pangea/analytics_misc/construct_use_type_enum.dart index f26479195..d75a32aab 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -1,10 +1,8 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/pangea/analytics_downloads/analytics_summary_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; enum ConstructUseTypeEnum { /// produced in chat by user, igc was run, and we've judged it to be a correct use @@ -227,18 +225,16 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.ga: case ConstructUseTypeEnum.incMM: - return -1; - case ConstructUseTypeEnum.incIt: case ConstructUseTypeEnum.incIGC: - case ConstructUseTypeEnum.incL: case ConstructUseTypeEnum.incM: - return -2; + return -1; case ConstructUseTypeEnum.incPA: case ConstructUseTypeEnum.incWL: case ConstructUseTypeEnum.incHWL: - return -3; + case ConstructUseTypeEnum.incL: + return -2; } } diff --git a/lib/pangea/analytics_misc/gain_points_animation.dart b/lib/pangea/analytics_misc/gain_points_animation.dart index fb2f53040..3f34949b3 100644 --- a/lib/pangea/analytics_misc/gain_points_animation.dart +++ b/lib/pangea/analytics_misc/gain_points_animation.dart @@ -1,10 +1,9 @@ import 'dart:math'; -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'; class PointsGainedAnimation extends StatefulWidget { final int points; @@ -26,26 +25,31 @@ class PointsGainedAnimationState extends State final Color? loseColor = Colors.red; late AnimationController _controller; - late Animation _offsetAnimation; + late Animation _offsetAnimation; late Animation _fadeAnimation; final List> _swayAnimation = []; - final List _particleTrajectories = []; + final List _initialVelocities = []; final Random _random = Random(); + static const double _particleSpeed = 50; // Base speed for particles. + static const double gravity = 15; // Gravity constant for the animation. + static const int duration = + 2000; // Duration of the animation in milliseconds. + @override void initState() { super.initState(); if (widget.points == 0) return; _controller = AnimationController( - duration: const Duration(milliseconds: 2000), + duration: const Duration(milliseconds: duration), vsync: this, ); - _offsetAnimation = Tween( - begin: const Offset(0.0, 0), - end: const Offset(0.0, -3), + _offsetAnimation = Tween( + begin: 0.0, + end: 3.0, ).animate( CurvedAnimation( parent: _controller, @@ -59,7 +63,7 @@ class PointsGainedAnimationState extends State ).animate( CurvedAnimation( parent: _controller, - curve: Curves.easeOut, + curve: Curves.easeIn, ), ); @@ -67,17 +71,18 @@ class PointsGainedAnimationState extends State } void initParticleTrajectories() { - _particleTrajectories.clear(); + _initialVelocities.clear(); for (int i = 0; i < widget.points.abs(); i++) { - final angle = _random.nextDouble() * (pi / 2) + - pi / 4; // Random angle in the V-shaped range. - const baseSpeed = 20; // Initial base speed. - const exponentialFactor = 30; // Factor for exponential growth. - final speedMultiplier = _random.nextDouble(); // Random speed multiplier. - final speed = baseSpeed * - pow(exponentialFactor, speedMultiplier); // Exponential speed. - _particleTrajectories - .add(Offset(speed * cos(angle), -speed * sin(angle))); + final angle = + (i - widget.points.abs() / 2) / widget.points.abs() * (pi / 3) + + (_random.nextDouble() - 0.5) * pi / 6 + + pi / 2; + final speedMultiplier = + 0.75 + _random.nextDouble() / 4; // Random speed multiplier. + final speed = _particleSpeed * + speedMultiplier * + (widget.points > 0 ? 2 : 1); // Exponential speed. + _initialVelocities.add(Offset(speed * cos(angle), -speed * sin(angle))); } } @@ -144,30 +149,27 @@ class PointsGainedAnimationState extends State return Material( type: MaterialType.transparency, - child: SlideTransition( - position: _offsetAnimation, - child: FadeTransition( - opacity: _fadeAnimation, - child: IgnorePointer( - ignoring: _controller.isAnimating, - child: Stack( - children: List.generate(widget.points.abs(), (index) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - final progress = _controller.value; - final trajectory = _particleTrajectories[index]; - return Transform.translate( - offset: Offset( - trajectory.dx * pow(progress, 2), - trajectory.dy * pow(progress, 2), - ), - child: plusWidget, - ); - }, - ); - }), - ), + child: FadeTransition( + opacity: _fadeAnimation, + child: IgnorePointer( + ignoring: _controller.isAnimating, + child: Stack( + children: List.generate(widget.points.abs(), (index) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + final progress = _offsetAnimation.value; + final trajectory = _initialVelocities[index]; + return Transform.translate( + offset: Offset( + trajectory.dx * progress, + trajectory.dy * progress + gravity * pow(progress, 2), + ), + child: plusWidget, + ); + }, + ); + }), ), ), ), diff --git a/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart b/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart index 9acfb4266..167916e7d 100644 --- a/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/chat_list/utils/get_chat_list_item_subtitle.dart @@ -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/practice_selection_repo.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 { @@ -90,6 +89,7 @@ class ChatListItemSubtitle extends StatelessWidget { final analyticsEntry = tokens != null ? PracticeSelectionRepo.get( + messageEventAndTokens.event.messageDisplayLangCode, tokens, ) : null; diff --git a/lib/pangea/choreographer/widgets/choice_animation.dart b/lib/pangea/choreographer/widgets/choice_animation.dart index 6c8256d2b..a557568d1 100644 --- a/lib/pangea/choreographer/widgets/choice_animation.dart +++ b/lib/pangea/choreographer/widgets/choice_animation.dart @@ -6,7 +6,7 @@ const int choiceArrayAnimationDuration = 500; class ChoiceAnimationWidget extends StatefulWidget { final bool isSelected; - final bool isCorrect; + final bool? isCorrect; final Widget child; const ChoiceAnimationWidget({ @@ -41,8 +41,8 @@ class ChoiceAnimationWidgetState extends State @override void didUpdateWidget(ChoiceAnimationWidget oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.isSelected && - (!oldWidget.isSelected || widget.isCorrect != oldWidget.isCorrect)) { + if ((widget.isSelected && widget.isSelected != oldWidget.isSelected) || + widget.isCorrect != oldWidget.isCorrect) { _controller.forward().then((_) => _controller.reset()); } } @@ -53,7 +53,7 @@ class ChoiceAnimationWidgetState extends State super.dispose(); } - Animation get _animation => widget.isCorrect + Animation get _animation => widget.isCorrect == true ? TweenSequence([ TweenSequenceItem( tween: Tween(begin: 1.0, end: 1.2), @@ -81,7 +81,7 @@ class ChoiceAnimationWidgetState extends State @override Widget build(BuildContext context) { - return widget.isCorrect + return widget.isCorrect == true ? AnimatedBuilder( animation: _animation, builder: (context, child) { diff --git a/lib/pangea/common/widgets/customized_svg.dart b/lib/pangea/common/widgets/customized_svg.dart index 0f6d945d5..fe2109c0b 100644 --- a/lib/pangea/common/widgets/customized_svg.dart +++ b/lib/pangea/common/widgets/customized_svg.dart @@ -1,12 +1,10 @@ +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_svg/flutter_svg.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart' as http; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; - -class CustomizedSvg extends StatelessWidget { +class CustomizedSvg extends StatefulWidget { /// URL of the SVG file final String svgUrl; @@ -27,6 +25,7 @@ class CustomizedSvg extends StatelessWidget { final double? height; static final GetStorage _svgStorage = GetStorage('svg_cache'); + const CustomizedSvg({ super.key, required this.svgUrl, @@ -36,8 +35,68 @@ class CustomizedSvg extends StatelessWidget { this.height = 24, }); + @override + State createState() => _CustomizedSvgState(); +} + +class _CustomizedSvgState extends State { + String? _svgContent; + bool _isLoading = true; + bool _hasError = false; + bool _showProgressIndicator = false; + + @override + void initState() { + super.initState(); + _startLoadingTimer(); + _loadSvg(); + } + + void _startLoadingTimer() { + Future.delayed(const Duration(seconds: 1), () { + if (_isLoading) { + setState(() { + _showProgressIndicator = true; + }); + } + }); + } + + Future _loadSvg() async { + try { + final cached = _getSvgFromCache(); + if (cached != null) { + setState(() { + _svgContent = cached; + _isLoading = false; + }); + return; + } + + final modifiedSvg = await _getModifiedSvg(); + setState(() { + _svgContent = modifiedSvg; + _isLoading = false; + _hasError = modifiedSvg == null; + }); + } catch (_) { + setState(() { + _isLoading = false; + _hasError = true; + }); + } + } + + Future _getModifiedSvg() async { + final svgContent = await _fetchSvg(); + if (svgContent == null) { + return null; + } + return _modifySVG(svgContent); + } + Future _fetchSvg() async { - final cachedSvgEntry = _svgStorage.read(svgUrl); + final cachedSvgEntry = CustomizedSvg._svgStorage.read(widget.svgUrl); if (cachedSvgEntry != null && cachedSvgEntry is Map) { final cachedSvg = cachedSvgEntry['svg'] as String?; final timestamp = cachedSvgEntry['timestamp'] as int?; @@ -54,24 +113,24 @@ class CustomizedSvg extends StatelessWidget { } } - final response = await http.get(Uri.parse(svgUrl)); + final response = await http.get(Uri.parse(widget.svgUrl)); if (response.statusCode != 200) { final e = Exception('Failed to load SVG: ${response.statusCode}'); ErrorHandler.logError( e: e, data: { - "svgUrl": svgUrl, + "svgUrl": widget.svgUrl, }, ); - await _svgStorage.write( - svgUrl, + await CustomizedSvg._svgStorage.write( + widget.svgUrl, {'timestamp': DateTime.now().millisecondsSinceEpoch}, ); throw e; } final String svgContent = response.body; - await _svgStorage.write(svgUrl, { + await CustomizedSvg._svgStorage.write(widget.svgUrl, { 'svg': svgContent, 'timestamp': DateTime.now().millisecondsSinceEpoch, }); @@ -79,26 +138,16 @@ class CustomizedSvg extends StatelessWidget { return svgContent; } - Future _getModifiedSvg() async { - final svgContent = await _fetchSvg(); - final String? modifiedSvg = svgContent; - if (modifiedSvg == null) { - return null; - } - - return _modifySVG(modifiedSvg); - } - String _modifySVG(String svgContent) { String modifiedSvg = svgContent.replaceAll("fill=\"none\"", ''); - for (final entry in colorReplacements.entries) { + for (final entry in widget.colorReplacements.entries) { modifiedSvg = modifiedSvg.replaceAll(entry.key, entry.value); } return modifiedSvg; } String? _getSvgFromCache() { - final cachedSvgEntry = _svgStorage.read(svgUrl); + final cachedSvgEntry = CustomizedSvg._svgStorage.read(widget.svgUrl); if (cachedSvgEntry != null && cachedSvgEntry is Map && cachedSvgEntry['svg'] is String) { @@ -109,33 +158,30 @@ class CustomizedSvg extends StatelessWidget { @override Widget build(BuildContext context) { - final cached = _getSvgFromCache(); - if (cached != null) { + if (_isLoading) { + if (_showProgressIndicator) { + return SizedBox( + width: widget.width, + height: widget.height, + child: const Center( + child: CircularProgressIndicator(), + ), + ); + } else { + return SizedBox( + width: widget.width, + height: widget.height, + ); + } + } else if (_hasError || _svgContent == null) { + return widget.errorIcon; + } else { return SvgPicture.string( - cached, - width: width, - height: height, + _svgContent!, + width: widget.width, + height: widget.height, ); } - - return FutureBuilder( - future: _getModifiedSvg(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } else if (snapshot.hasError || snapshot.data == null) { - return errorIcon; - } else if (snapshot.hasData) { - return SvgPicture.string( - snapshot.data!, - width: width, - height: height, - ); - } else { - return const SizedBox.shrink(); - } - }, - ); } } diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index 936b2dfa2..6d7a2645b 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -1,12 +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:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; @@ -23,6 +17,10 @@ import 'package:fluffychat/pangea/practice_activities/practice_record.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; import 'package:fluffychat/pangea/practice_activities/relevant_span_display_details.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'; +import 'package:sentry_flutter/sentry_flutter.dart'; class PracticeActivityModel { List targetTokens; @@ -138,7 +136,6 @@ class PracticeActivityModel { choice.choiceContent, ) || isComplete) { - debugger(when: kDebugMode); return; } diff --git a/lib/pangea/practice_activities/practice_selection.dart b/lib/pangea/practice_activities/practice_selection.dart index f1fa1da0a..5eefdc8b6 100644 --- a/lib/pangea/practice_activities/practice_selection.dart +++ b/lib/pangea/practice_activities/practice_selection.dart @@ -1,9 +1,6 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; - import 'package:collection/collection.dart'; - import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; @@ -12,6 +9,7 @@ import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; class PracticeSelection { late String _userL2; @@ -19,12 +17,15 @@ class PracticeSelection { late final List _tokens; + final String langCode; + final Map> _activityQueue = {}; final int _maxQueueLength = 5; PracticeSelection({ required List tokens, + required this.langCode, String? userL1, String? userL2, }) { @@ -37,10 +38,14 @@ class PracticeSelection { List get tokens => _tokens; + bool get eligibleForPractice => + _tokens.any((t) => t.lemma.saveVocab) && langCode == _userL2; + String get messageText => PangeaToken.reconstructText(tokens); Map toJson() => { 'createdAt': createdAt.toIso8601String(), + 'lang_code': langCode, 'tokens': _tokens.map((t) => t.toJson()).toList(), 'activityQueue': _activityQueue.map( (key, value) => MapEntry( @@ -52,6 +57,7 @@ class PracticeSelection { static PracticeSelection fromJson(Map json) { return PracticeSelection( + langCode: json['lang_code'] as String, tokens: (json['tokens'] as List).map((t) => PangeaToken.fromJson(t)).toList(), ).._activityQueue.addAll( @@ -82,7 +88,10 @@ class PracticeSelection { } PracticeTarget? nextActivity(ActivityTypeEnum a) => - _activityQueue[a]?.firstOrNull; + MatrixState.pangeaController.languageController.userL2?.langCode == + _userL2 + ? _activityQueue[a]?.firstOrNull + : null; bool get hasHiddenWordActivity => activities(ActivityTypeEnum.hiddenWordListening).isNotEmpty; @@ -100,57 +109,32 @@ class PracticeSelection { // bool get canDoWordFocusListening => // _tokens.where((t) => t.canBeHeard).length > 4; - PracticeTarget buildActivity(ActivityTypeEnum activityType) { + List buildActivity(ActivityTypeEnum activityType) { + if (!eligibleForPractice) { + return []; + } + final List tokens = _tokens.where((t) => t.lemma.saveVocab).sorted( (a, b) => b.activityPriorityScore(activityType, null).compareTo( a.activityPriorityScore(activityType, null), ), ); - // debugPrint('emoji activity priority score: ${tokens.map( - // (e) => e.activityPriorityScore(activityType, null), - // )}'); - return PracticeTarget( - activityType: activityType, - tokens: tokens.take(_maxQueueLength).shuffled().toList(), - userL2: _userL2, - ); + return [ + PracticeTarget( + activityType: activityType, + tokens: tokens.take(_maxQueueLength).shuffled().toList(), + userL2: _userL2, + ), + ]; } - /// On initialization, we pick which tokens to do activities on and what types of activities to do - void initialize() { + List buildMorphActivity() { 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] = [ - buildActivity(ActivityTypeEnum.emoji), - ]; - - // WORD MEANING - // make word meaning activities - // same as emojis for now - _activityQueue[ActivityTypeEnum.wordMeaning] = [ - buildActivity(ActivityTypeEnum.wordMeaning), - ]; - - // WORD FOCUS LISTENING - // make word focus listening activities - // same as emojis for now - _activityQueue[ActivityTypeEnum.wordFocusListening] = [ - buildActivity(ActivityTypeEnum.wordFocusListening), - ]; - - // GRAMMAR - // build a list of TargetTokensAndActivityType for all tokens and all features in the message - // limits to _maxQueueLength activities and only one per token + if (!eligibleForPractice) { + return []; + } final List candidates = eligibleTokens.expand( (t) { return t.morphsBasicallyEligibleForPracticeByPriority.map( @@ -176,18 +160,50 @@ class PracticeSelection { ), ); //pick from the top 5, only including one per token - _activityQueue[ActivityTypeEnum.morphId] = []; + final List finalSelection = []; for (final candidate in candidates) { - if (_activityQueue[ActivityTypeEnum.morphId]!.length >= _maxQueueLength) { + if (finalSelection.length >= _maxQueueLength) { break; } - if (_activityQueue[ActivityTypeEnum.morphId]?.any( + if (finalSelection.any( (entry) => entry.tokens.contains(candidate.tokens.first), ) == false) { - _activityQueue[ActivityTypeEnum.morphId]?.add(candidate); + finalSelection.add(candidate); } } + return finalSelection; + } + + /// On initialization, we pick which tokens to do activities on and what types of activities to do + void initialize() { + // 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] = + buildActivity(ActivityTypeEnum.emoji); + + // WORD MEANING + // make word meaning activities + // same as emojis for now + _activityQueue[ActivityTypeEnum.wordMeaning] = + buildActivity(ActivityTypeEnum.wordMeaning); + + // WORD FOCUS LISTENING + // make word focus listening activities + // same as emojis for now + _activityQueue[ActivityTypeEnum.wordFocusListening] = + buildActivity(ActivityTypeEnum.wordFocusListening); + + // GRAMMAR + // build a list of TargetTokensAndActivityType for all tokens and all features in the message + // limits to _maxQueueLength activities and only one per token + _activityQueue[ActivityTypeEnum.morphId] = buildMorphActivity(); PracticeSelectionRepo.save(this); } @@ -200,7 +216,7 @@ class PracticeSelection { if (a == ActivityTypeEnum.morphId && (t == null || morph == null)) { return null; } - return _activityQueue[a]?.firstWhereOrNull( + return activities(a).firstWhereOrNull( (entry) => (t == null || entry.tokens.contains(t)) && (morph == null || entry.morphFeature == morph), diff --git a/lib/pangea/practice_activities/practice_selection_repo.dart b/lib/pangea/practice_activities/practice_selection_repo.dart index 88eb5e4d6..35bb2a0fb 100644 --- a/lib/pangea/practice_activities/practice_selection_repo.dart +++ b/lib/pangea/practice_activities/practice_selection_repo.dart @@ -1,9 +1,7 @@ -import 'package:flutter/material.dart'; - -import 'package:get_storage/get_storage.dart'; - import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection.dart'; +import 'package:flutter/material.dart'; +import 'package:get_storage/get_storage.dart'; class PracticeSelectionRepo { static final GetStorage _storage = GetStorage('practice_selection_cache'); @@ -45,6 +43,7 @@ class PracticeSelectionRepo { tokens.map((t) => t.text.content).join(' '); static PracticeSelection? get( + String messageLanguage, List tokens, ) { final String key = _key(tokens); @@ -65,6 +64,7 @@ class PracticeSelectionRepo { } final newEntry = PracticeSelection( + langCode: messageLanguage, tokens: tokens, ); diff --git a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart index 22467a7cd..625ae8d7e 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart @@ -1,8 +1,5 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; @@ -12,6 +9,8 @@ import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/practice_match_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 MatchActivityCard extends StatelessWidget { final PracticeActivityModel currentActivity; @@ -85,7 +84,7 @@ class MatchActivityCard extends StatelessWidget { (PracticeChoice cf) { return ChoiceAnimationWidget( isSelected: overlayController.selectedChoice == cf, - isCorrect: currentActivity.wasCorrectMatch(cf) ?? false, + isCorrect: currentActivity.wasCorrectMatch(cf), child: PracticeMatchItem( isSelected: overlayController.selectedChoice == cf, isCorrect: currentActivity.wasCorrectMatch(cf), diff --git a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart index 7cd1f0651..773e4fc45 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart @@ -1,13 +1,12 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/practice_activities/practice_choice.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; class PracticeMatchItem extends StatefulWidget { const PracticeMatchItem({ diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 3c70104ea..0b9d048e8 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -417,6 +417,7 @@ class MessageOverlayController extends State PracticeSelection? get practiceSelection => pangeaMessageEvent?.messageDisplayRepresentation?.tokens != null ? PracticeSelectionRepo.get( + pangeaMessageEvent!.messageDisplayLangCode, pangeaMessageEvent!.messageDisplayRepresentation!.tokens!, ) : null; diff --git a/lib/pangea/toolbar/widgets/message_token_text.dart b/lib/pangea/toolbar/widgets/message_token_text.dart index 0be41310b..72ec9cc50 100644 --- a/lib/pangea/toolbar/widgets/message_token_text.dart +++ b/lib/pangea/toolbar/widgets/message_token_text.dart @@ -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'; @@ -15,6 +11,8 @@ 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,6 +54,7 @@ class MessageTokenText extends StatelessWidget { PracticeSelection? get messageAnalyticsEntry => _tokens != null ? PracticeSelectionRepo.get( + _pangeaMessageEvent.messageDisplayLangCode, _tokens!, ) : null; diff --git a/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart index 908d8bc3e..77a9d91bf 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart @@ -1,7 +1,3 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; @@ -13,6 +9,8 @@ import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class LemmaWidget extends StatefulWidget { final PangeaToken token; @@ -112,56 +110,58 @@ class LemmaWidgetState extends State { Widget build(BuildContext context) { if (_editMode) { _controller.text = widget.token.lemma.text; - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - spacing: 10.0, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsLemma}", - textAlign: TextAlign.center, - style: const TextStyle(fontStyle: FontStyle.italic), - ), - TextField( - minLines: 1, - maxLines: 3, - controller: _controller, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () => _toggleEditMode(false), - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + return Material( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsLemma}", + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + TextField( + minLines: 1, + maxLines: 3, + controller: _controller, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => _toggleEditMode(false), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 10), ), - padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text(L10n.of(context).cancel), ), - child: Text(L10n.of(context).cancel), - ), - const SizedBox(width: 10), - ElevatedButton( - onPressed: () { - _controller.text != widget.token.lemma.text - ? showFutureLoadingDialog( - context: context, - future: () async => _editLemma(), - ) - : null; - }, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () { + _controller.text != widget.token.lemma.text + ? showFutureLoadingDialog( + context: context, + future: () async => _editLemma(), + ) + : null; + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 10), ), - padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text(L10n.of(context).saveChanges), ), - child: Text(L10n.of(context).saveChanges), - ), - ], - ), - ], + ], + ), + ], + ), ), ); }