You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fluffychat/lib/pangea/choreographer/widgets/translation_finished_flow.dart

380 lines
12 KiB
Dart

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,
);
},
);
}
}