chore(reading_assistance): several fixes and an enhancement to gain points animation
This commit is contained in:
parent
2ea2338a1b
commit
6e7ae5c044
13 changed files with 285 additions and 230 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -417,6 +417,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
PracticeSelection? get practiceSelection =>
|
||||
pangeaMessageEvent?.messageDisplayRepresentation?.tokens != null
|
||||
? PracticeSelectionRepo.get(
|
||||
pangeaMessageEvent!.messageDisplayLangCode,
|
||||
pangeaMessageEvent!.messageDisplayRepresentation!.tokens!,
|
||||
)
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue