chore(reading_assistance): several fixes and an enhancement to gain points animation

This commit is contained in:
wcjord 2025-04-01 14:13:16 -04:00
parent 2ea2338a1b
commit 6e7ae5c044
13 changed files with 285 additions and 230 deletions

View file

@ -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;
}
}

View file

@ -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<PointsGainedAnimation>
final Color? loseColor = Colors.red;
late AnimationController _controller;
late Animation<Offset> _offsetAnimation;
late Animation<double> _offsetAnimation;
late Animation<double> _fadeAnimation;
final List<Animation<double>> _swayAnimation = [];
final List<Offset> _particleTrajectories = [];
final List<Offset> _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<Offset>(
begin: const Offset(0.0, 0),
end: const Offset(0.0, -3),
_offsetAnimation = Tween<double>(
begin: 0.0,
end: 3.0,
).animate(
CurvedAnimation(
parent: _controller,
@ -59,7 +63,7 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
curve: Curves.easeIn,
),
);
@ -67,17 +71,18 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
}
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<PointsGainedAnimation>
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,
);
},
);
}),
),
),
),

View file

@ -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;

View file

@ -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<ChoiceAnimationWidget>
@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<ChoiceAnimationWidget>
super.dispose();
}
Animation<double> get _animation => widget.isCorrect
Animation<double> get _animation => widget.isCorrect == true
? TweenSequence<double>([
TweenSequenceItem<double>(
tween: Tween<double>(begin: 1.0, end: 1.2),
@ -81,7 +81,7 @@ class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
@override
Widget build(BuildContext context) {
return widget.isCorrect
return widget.isCorrect == true
? AnimatedBuilder(
animation: _animation,
builder: (context, child) {

View file

@ -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<CustomizedSvg> createState() => _CustomizedSvgState();
}
class _CustomizedSvgState extends State<CustomizedSvg> {
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<void> _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<String?> _getModifiedSvg() async {
final svgContent = await _fetchSvg();
if (svgContent == null) {
return null;
}
return _modifySVG(svgContent);
}
Future<String?> _fetchSvg() async {
final cachedSvgEntry = _svgStorage.read(svgUrl);
final cachedSvgEntry = CustomizedSvg._svgStorage.read(widget.svgUrl);
if (cachedSvgEntry != null && cachedSvgEntry is Map<String, dynamic>) {
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<String?> _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<String, dynamic> &&
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<String?>(
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();
}
},
);
}
}

View file

@ -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<PangeaToken> targetTokens;
@ -138,7 +136,6 @@ class PracticeActivityModel {
choice.choiceContent,
) ||
isComplete) {
debugger(when: kDebugMode);
return;
}

View file

@ -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<PangeaToken> _tokens;
final String langCode;
final Map<ActivityTypeEnum, List<PracticeTarget>> _activityQueue = {};
final int _maxQueueLength = 5;
PracticeSelection({
required List<PangeaToken> tokens,
required this.langCode,
String? userL1,
String? userL2,
}) {
@ -37,10 +38,14 @@ class PracticeSelection {
List<PangeaToken> get tokens => _tokens;
bool get eligibleForPractice =>
_tokens.any((t) => t.lemma.saveVocab) && langCode == _userL2;
String get messageText => PangeaToken.reconstructText(tokens);
Map<String, dynamic> 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<String, dynamic> 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<PracticeTarget> buildActivity(ActivityTypeEnum activityType) {
if (!eligibleForPractice) {
return [];
}
final List<PangeaToken> 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<PracticeTarget> 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<PracticeTarget> 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<PracticeTarget> 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),

View file

@ -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<PangeaToken> tokens,
) {
final String key = _key(tokens);
@ -65,6 +64,7 @@ class PracticeSelectionRepo {
}
final newEntry = PracticeSelection(
langCode: messageLanguage,
tokens: tokens,
);

View file

@ -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),

View file

@ -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({

View file

@ -417,6 +417,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
PracticeSelection? get practiceSelection =>
pangeaMessageEvent?.messageDisplayRepresentation?.tokens != null
? PracticeSelectionRepo.get(
pangeaMessageEvent!.messageDisplayLangCode,
pangeaMessageEvent!.messageDisplayRepresentation!.tokens!,
)
: null;

View file

@ -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;

View file

@ -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<LemmaWidget> {
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),
),
],
),
],
],
),
],
),
),
);
}