refactor: remove tokens and detections from IGC text data model (#2528)
* refactor: remove tokens and detections from IGC text data model * generated * refactor: initial work to remove tokens from span_details and IT responses * refactor: add xp field to construct use class, rewrite function for turning choreo record into construct uses * refactor: add translation assistance construct use type * refactor: move analytics feedback to popup above messages * generated --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>pull/1817/head
parent
0027ce8536
commit
b25676a58d
@ -1,219 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../repo/similarity_repo.dart';
|
||||
|
||||
class AlternativeTranslator {
|
||||
final Choreographer choreographer;
|
||||
bool showAlternativeTranslations = false;
|
||||
bool loadingAlternativeTranslations = false;
|
||||
bool showTranslationFeedback = false;
|
||||
String? userTranslation;
|
||||
FeedbackKey? translationFeedbackKey;
|
||||
List<String> translations = [];
|
||||
SimilartyResponseModel? similarityResponse;
|
||||
|
||||
AlternativeTranslator(this.choreographer);
|
||||
|
||||
void clear() {
|
||||
userTranslation = null;
|
||||
showAlternativeTranslations = false;
|
||||
loadingAlternativeTranslations = false;
|
||||
showTranslationFeedback = false;
|
||||
translationFeedbackKey = null;
|
||||
translations = [];
|
||||
similarityResponse = null;
|
||||
}
|
||||
|
||||
double get _percentCorrectChoices {
|
||||
final totalSteps = choreographer.choreoRecord.itSteps.length;
|
||||
if (totalSteps == 0) return 0.0;
|
||||
final int correctFirstAttempts = choreographer.itController.completedITSteps
|
||||
.where(
|
||||
(step) => !step.continuances.any(
|
||||
(c) =>
|
||||
c.level != ChoreoConstants.levelThresholdForGreen &&
|
||||
c.wasClicked,
|
||||
),
|
||||
)
|
||||
.length;
|
||||
final double percentage = (correctFirstAttempts / totalSteps) * 100;
|
||||
return percentage;
|
||||
}
|
||||
|
||||
int get starRating {
|
||||
final double percent = _percentCorrectChoices;
|
||||
if (percent == 100) return 5;
|
||||
if (percent >= 80) return 4;
|
||||
if (percent >= 60) return 3;
|
||||
if (percent >= 40) return 2;
|
||||
if (percent > 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
Future<void> setTranslationFeedback() async {
|
||||
try {
|
||||
choreographer.startLoading();
|
||||
translationFeedbackKey = FeedbackKey.loadingPleaseWait;
|
||||
showTranslationFeedback = true;
|
||||
userTranslation = choreographer.currentText;
|
||||
|
||||
final double percentCorrect = _percentCorrectChoices;
|
||||
|
||||
// Set feedback based on percentage
|
||||
if (percentCorrect == 100) {
|
||||
translationFeedbackKey = FeedbackKey.allCorrect;
|
||||
} else if (percentCorrect >= 80) {
|
||||
translationFeedbackKey = FeedbackKey.newWayAllGood;
|
||||
} else {
|
||||
translationFeedbackKey = FeedbackKey.othersAreBetter;
|
||||
}
|
||||
} catch (err, stack) {
|
||||
if (err is! http.Response) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: stack,
|
||||
data: {
|
||||
"sourceText": choreographer.itController.sourceText,
|
||||
"currentText": choreographer.currentText,
|
||||
"userL1": choreographer.l1LangCode,
|
||||
"userL2": choreographer.l2LangCode,
|
||||
"goldRouteTranslation":
|
||||
choreographer.itController.goldRouteTracker.fullTranslation,
|
||||
},
|
||||
);
|
||||
}
|
||||
choreographer.errorService.setError(
|
||||
ChoreoError(type: ChoreoErrorType.unknown, raw: err),
|
||||
);
|
||||
} finally {
|
||||
choreographer.stopLoading();
|
||||
}
|
||||
}
|
||||
|
||||
List<OneConstructUse> get _itStepConstructs {
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: choreographer.roomId,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
final List<OneConstructUse> constructs = [];
|
||||
for (final step in choreographer.choreoRecord.itSteps) {
|
||||
for (final continuance in step.continuances) {
|
||||
final ConstructUseTypeEnum useType = continuance.wasClicked &&
|
||||
continuance.level == ChoreoConstants.levelThresholdForGreen
|
||||
? ConstructUseTypeEnum.corIt
|
||||
: continuance.wasClicked
|
||||
? ConstructUseTypeEnum.incIt
|
||||
: ConstructUseTypeEnum.ignIt;
|
||||
|
||||
final tokens = continuance.tokens.where((t) => t.lemma.saveVocab);
|
||||
constructs.addAll(
|
||||
tokens.map(
|
||||
(token) => OneConstructUse(
|
||||
useType: useType,
|
||||
lemma: token.lemma.text,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: metadata,
|
||||
category: token.pos,
|
||||
form: token.text.content,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (final token in tokens) {
|
||||
constructs.add(
|
||||
OneConstructUse(
|
||||
useType: useType,
|
||||
lemma: token.pos,
|
||||
form: token.text.content,
|
||||
category: "POS",
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
for (final entry in token.morph.entries) {
|
||||
constructs.add(
|
||||
OneConstructUse(
|
||||
useType: useType,
|
||||
lemma: entry.value,
|
||||
form: token.text.content,
|
||||
category: entry.key,
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return constructs;
|
||||
}
|
||||
|
||||
int countNewConstructs(ConstructTypeEnum type) {
|
||||
final vocabUses = _itStepConstructs.where((c) => c.constructType == type);
|
||||
final Map<ConstructIdentifier, int> constructPoints = {};
|
||||
for (final use in vocabUses) {
|
||||
constructPoints[use.identifier] ??= 0;
|
||||
constructPoints[use.identifier] =
|
||||
constructPoints[use.identifier]! + use.pointValue;
|
||||
}
|
||||
|
||||
final constructListModel =
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel;
|
||||
|
||||
int newConstructCount = 0;
|
||||
for (final entry in constructPoints.entries) {
|
||||
final construct = constructListModel.getConstructUses(entry.key);
|
||||
if (construct?.points == entry.value) {
|
||||
newConstructCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return newConstructCount;
|
||||
}
|
||||
|
||||
String getDefaultFeedback(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
switch (translationFeedbackKey) {
|
||||
case FeedbackKey.allCorrect:
|
||||
return l10n.perfectTranslation;
|
||||
case FeedbackKey.newWayAllGood:
|
||||
return l10n.greatJobTranslation;
|
||||
case FeedbackKey.othersAreBetter:
|
||||
if (_percentCorrectChoices >= 60) {
|
||||
return l10n.goodJobTranslation;
|
||||
}
|
||||
if (_percentCorrectChoices >= 40) {
|
||||
return l10n.makingProgress;
|
||||
}
|
||||
return l10n.keepPracticing;
|
||||
case FeedbackKey.loadingPleaseWait:
|
||||
return l10n.letMeThink;
|
||||
case FeedbackKey.allDone:
|
||||
return l10n.allDone;
|
||||
default:
|
||||
return l10n.loadingPleaseWait;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FeedbackKey {
|
||||
allCorrect,
|
||||
newWayAllGood,
|
||||
othersAreBetter,
|
||||
loadingPleaseWait,
|
||||
allDone,
|
||||
}
|
||||
|
||||
extension FeedbackKeyExtension on FeedbackKey {}
|
||||
@ -0,0 +1,46 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
|
||||
|
||||
class TokensRepo {
|
||||
static Future<TokensResponseModel> get(
|
||||
String? accessToken, {
|
||||
required TokensRequestModel request,
|
||||
}) async {
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.tokenize,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final TokensResponseModel response = TokensResponseModel.fromJson(
|
||||
jsonDecode(
|
||||
utf8.decode(res.bodyBytes).toString(),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.tokens.isEmpty) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception(
|
||||
"empty tokens in tokenize response return",
|
||||
),
|
||||
data: {
|
||||
"accessToken": accessToken,
|
||||
"request": request.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,315 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class MessageAnalyticsFeedback extends StatefulWidget {
|
||||
final String overlayId;
|
||||
final int newGrammarConstructs;
|
||||
final int newVocabConstructs;
|
||||
|
||||
const MessageAnalyticsFeedback({
|
||||
required this.overlayId,
|
||||
required this.newGrammarConstructs,
|
||||
required this.newVocabConstructs,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MessageAnalyticsFeedback> createState() =>
|
||||
MessageAnalyticsFeedbackState();
|
||||
}
|
||||
|
||||
class MessageAnalyticsFeedbackState extends State<MessageAnalyticsFeedback>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _vocabController;
|
||||
late AnimationController _grammarController;
|
||||
late AnimationController _bubbleController;
|
||||
|
||||
late Animation<double> _vocabOpacity;
|
||||
late Animation<double> _grammarOpacity;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _opacityAnimation;
|
||||
|
||||
static const counterDelay = Duration(milliseconds: 400);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_grammarController = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_grammarOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _grammarController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_vocabController = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_vocabOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_bubbleController = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_opacityAnimation = Tween<double>(begin: 0.0, end: 0.9).animate(
|
||||
CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _bubbleController.forward();
|
||||
|
||||
Future.delayed(counterDelay, () {
|
||||
if (mounted) {
|
||||
_vocabController.forward();
|
||||
_grammarController.forward();
|
||||
}
|
||||
});
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 4000), () {
|
||||
if (!mounted) return;
|
||||
_bubbleController.reverse().then((_) {
|
||||
MatrixState.pAnyState.closeOverlay(widget.overlayId);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_vocabController.dispose();
|
||||
_grammarController.dispose();
|
||||
_bubbleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showAnalyticsDialog(ConstructTypeEnum? type) {
|
||||
showDialog<AnalyticsPopupWrapper>(
|
||||
context: context,
|
||||
builder: (context) => AnalyticsPopupWrapper(
|
||||
view: type ?? ConstructTypeEnum.vocab,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.newVocabConstructs <= 0 && widget.newGrammarConstructs <= 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => _showAnalyticsDialog(null),
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
alignment: Alignment.bottomRight,
|
||||
child: AnimatedBuilder(
|
||||
animation: _bubbleController,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest
|
||||
.withAlpha((_opacityAnimation.value * 255).round()),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
bottomLeft: Radius.circular(16.0),
|
||||
bottomRight: Radius.circular(4.0),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.newVocabConstructs > 0)
|
||||
NewConstructsBadge(
|
||||
controller: _vocabController,
|
||||
opacityAnimation: _vocabOpacity,
|
||||
newConstructs: widget.newVocabConstructs,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
tooltip: L10n.of(context).newVocab,
|
||||
onTap: () => _showAnalyticsDialog(
|
||||
ConstructTypeEnum.vocab,
|
||||
),
|
||||
),
|
||||
if (widget.newGrammarConstructs > 0)
|
||||
NewConstructsBadge(
|
||||
controller: _grammarController,
|
||||
opacityAnimation: _grammarOpacity,
|
||||
newConstructs: widget.newGrammarConstructs,
|
||||
type: ConstructTypeEnum.morph,
|
||||
tooltip: L10n.of(context).newGrammar,
|
||||
onTap: () => _showAnalyticsDialog(
|
||||
ConstructTypeEnum.morph,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NewConstructsBadge extends StatelessWidget {
|
||||
final AnimationController controller;
|
||||
final Animation<double> opacityAnimation;
|
||||
|
||||
final int newConstructs;
|
||||
final ConstructTypeEnum type;
|
||||
final String tooltip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const NewConstructsBadge({
|
||||
required this.controller,
|
||||
required this.opacityAnimation,
|
||||
required this.newConstructs,
|
||||
required this.type,
|
||||
required this.tooltip,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: opacityAnimation.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.toys_and_games,
|
||||
color: ProgressIndicatorEnum.morphsUsed.color(context),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
AnimatedCounter(
|
||||
key: ValueKey("$type-counter"),
|
||||
endValue: newConstructs,
|
||||
startAnimation: opacityAnimation.value > 0.9,
|
||||
style: TextStyle(
|
||||
color: ProgressIndicatorEnum.morphsUsed.color(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCounter extends StatefulWidget {
|
||||
final int endValue;
|
||||
final TextStyle? style;
|
||||
final bool startAnimation;
|
||||
|
||||
const AnimatedCounter({
|
||||
super.key,
|
||||
required this.endValue,
|
||||
this.style,
|
||||
this.startAnimation = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedCounter> createState() => _AnimatedCounterState();
|
||||
}
|
||||
|
||||
class _AnimatedCounterState extends State<AnimatedCounter>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<int> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
_animation = IntTween(
|
||||
begin: 0,
|
||||
end: widget.endValue,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.startAnimation) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _controller.forward();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AnimatedCounter oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!oldWidget.startAnimation && widget.startAnimation && !_hasAnimated) {
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
bool get _hasAnimated => _controller.isCompleted || _controller.isAnimating;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Text(
|
||||
"+ ${_animation.value}",
|
||||
style: widget.style,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,379 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_stars.dart';
|
||||
import '../../bot/utils/bot_style.dart';
|
||||
import '../../common/utils/error_handler.dart';
|
||||
import '../controllers/it_controller.dart';
|
||||
|
||||
class TranslationFeedback extends StatefulWidget {
|
||||
final int vocabCount;
|
||||
final int grammarCount;
|
||||
final String feedbackText;
|
||||
final ITController controller;
|
||||
|
||||
const TranslationFeedback({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.vocabCount,
|
||||
required this.grammarCount,
|
||||
required this.feedbackText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TranslationFeedback> createState() => _TranslationFeedbackState();
|
||||
}
|
||||
|
||||
class _TranslationFeedbackState extends State<TranslationFeedback>
|
||||
with TickerProviderStateMixin {
|
||||
late final int starRating;
|
||||
late final int vocabCount;
|
||||
late final int grammarCount;
|
||||
|
||||
// Animation controllers for each component
|
||||
late AnimationController _starsController;
|
||||
late AnimationController _vocabController;
|
||||
late AnimationController _grammarController;
|
||||
|
||||
// Animations for opacity and scale
|
||||
late Animation<double> _starsOpacity;
|
||||
late Animation<double> _starsScale;
|
||||
late Animation<double> _vocabOpacity;
|
||||
late Animation<double> _grammarOpacity;
|
||||
|
||||
// Constants for animation timing
|
||||
static const vocabDelay = Duration(milliseconds: 800);
|
||||
static const grammarDelay = Duration(milliseconds: 1400);
|
||||
|
||||
// Duration for each individual animation
|
||||
static const elementAnimDuration = Duration(milliseconds: 800);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
vocabCount = widget.vocabCount;
|
||||
grammarCount = widget.grammarCount;
|
||||
|
||||
final altTranslator = widget.controller.choreographer.altTranslator;
|
||||
starRating = altTranslator.starRating;
|
||||
|
||||
// Initialize animation controllers
|
||||
_starsController = AnimationController(
|
||||
vsync: this,
|
||||
duration: elementAnimDuration,
|
||||
);
|
||||
|
||||
_vocabController = AnimationController(
|
||||
vsync: this,
|
||||
duration: elementAnimDuration,
|
||||
);
|
||||
|
||||
_grammarController = AnimationController(
|
||||
vsync: this,
|
||||
duration: elementAnimDuration,
|
||||
);
|
||||
|
||||
// Define animations
|
||||
_starsOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _starsController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_starsScale = Tween<double>(begin: 0.5, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _starsController, curve: Curves.elasticOut),
|
||||
);
|
||||
|
||||
_vocabOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_grammarOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _grammarController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
// Start animations with appropriate delays
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
// Start stars animation immediately
|
||||
_starsController.forward();
|
||||
|
||||
// Start vocab animation after delay if there's vocab to show
|
||||
if (vocabCount > 0) {
|
||||
Future.delayed(vocabDelay, () {
|
||||
if (mounted) _vocabController.forward();
|
||||
});
|
||||
}
|
||||
|
||||
// Start grammar animation after delay if there's grammar to show
|
||||
if (grammarCount > 0) {
|
||||
Future.delayed(grammarDelay, () {
|
||||
if (mounted) _grammarController.forward();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_starsController.dispose();
|
||||
_vocabController.dispose();
|
||||
_grammarController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Animated stars
|
||||
AnimatedBuilder(
|
||||
animation: _starsController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _starsOpacity.value,
|
||||
child: Transform.scale(
|
||||
scale: _starsScale.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: FillingStars(rating: starRating),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (vocabCount > 0 || grammarCount > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (vocabCount > 0)
|
||||
AnimatedBuilder(
|
||||
animation: _vocabController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _vocabOpacity.value,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.dictionary,
|
||||
color: ProgressIndicatorEnum.wordsUsed
|
||||
.color(context),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
AnimatedCounter(
|
||||
key: const ValueKey("vocabCounter"),
|
||||
endValue: vocabCount,
|
||||
// Only start counter animation when opacity animation is complete
|
||||
startAnimation: _vocabOpacity.value > 0.9,
|
||||
style: TextStyle(
|
||||
color: ProgressIndicatorEnum.wordsUsed
|
||||
.color(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (grammarCount > 0)
|
||||
AnimatedBuilder(
|
||||
animation: _grammarController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _grammarOpacity.value,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.toys_and_games,
|
||||
color: ProgressIndicatorEnum.morphsUsed
|
||||
.color(context),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
AnimatedCounter(
|
||||
key: const ValueKey("grammarCounter"),
|
||||
endValue: grammarCount,
|
||||
// Only start counter animation when opacity animation is complete
|
||||
startAnimation: _grammarOpacity.value > 0.9,
|
||||
style: TextStyle(
|
||||
color: ProgressIndicatorEnum.morphsUsed
|
||||
.color(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
AnimatedBuilder(
|
||||
animation: _starsController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _starsOpacity.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
widget.feedbackText,
|
||||
textAlign: TextAlign.center,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
);
|
||||
} catch (err, stack) {
|
||||
debugPrint("Error in TranslationFeedback: $err");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: stack,
|
||||
data: {},
|
||||
);
|
||||
// Fallback to a simple message if anything goes wrong
|
||||
return Center(child: Text(L10n.of(context).niceJob));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCounter extends StatefulWidget {
|
||||
final int endValue;
|
||||
final TextStyle? style;
|
||||
final Duration duration;
|
||||
final String prefix;
|
||||
final bool startAnimation;
|
||||
|
||||
const AnimatedCounter({
|
||||
super.key,
|
||||
required this.endValue,
|
||||
this.style,
|
||||
this.duration = const Duration(milliseconds: 1500),
|
||||
this.prefix = "+ ",
|
||||
this.startAnimation = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedCounter> createState() => _AnimatedCounterState();
|
||||
}
|
||||
|
||||
class _AnimatedCounterState extends State<AnimatedCounter>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<int> _animation;
|
||||
bool _hasAnimated = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
);
|
||||
|
||||
_animation = IntTween(
|
||||
begin: 0,
|
||||
end: widget.endValue,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
|
||||
// Only start animation if startAnimation is true
|
||||
if (widget.startAnimation) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_controller.forward();
|
||||
_hasAnimated = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AnimatedCounter oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// Start animation when startAnimation changes to true
|
||||
if (!oldWidget.startAnimation && widget.startAnimation && !_hasAnimated) {
|
||||
_controller.forward();
|
||||
_hasAnimated = true;
|
||||
}
|
||||
|
||||
if (oldWidget.endValue != widget.endValue) {
|
||||
if (_hasAnimated) {
|
||||
_animation = IntTween(
|
||||
begin: _animation.value,
|
||||
end: widget.endValue,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
|
||||
_controller.forward(from: 0.0);
|
||||
} else if (widget.startAnimation) {
|
||||
_animation = IntTween(
|
||||
begin: 0,
|
||||
end: widget.endValue,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
|
||||
_controller.forward();
|
||||
_hasAnimated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Text(
|
||||
"${widget.prefix}${_animation.value}",
|
||||
style: widget.style,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue