This commit is contained in:
Dmitry 2025-12-03 01:35:25 +03:00
parent aeab3e9603
commit f2f42aa9e0

View file

@ -26,185 +26,11 @@ class _TestPageState extends State<TestPage> {
TestDto? _test;
bool _isLoading = true;
String? _errorMessage;
@override
void initState() {
super.initState();
_loadTest();
}
Future<void> _loadTest() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final appScope = ScopeProvider.of<AppScopeContainer>(
context,
listen: false,
);
final userScope = appScope?.userScopeHolder.scope;
if (userScope == null) {
throw Exception('Scope not available');
}
log('Loading test: ${widget.testId}', name: 'TestPage');
final test = await userScope.testsModule.testManager.loadTest(widget.testId);
if (mounted) {
setState(() {
_test = test;
_isLoading = false;
});
}
} catch (e, s) {
log(
'Error loading test',
error: e,
stackTrace: s,
name: 'TestPage',
);
if (mounted) {
setState(() {
_isLoading = false;
_errorMessage = 'Failed to load test: ${e.toString()}';
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_test?.name ?? 'Test'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop(),
),
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return const LoadingView(message: 'Loading test...');
}
if (_errorMessage != null) {
return ErrorView(
title: 'Failed to load test',
message: _errorMessage!,
onRetry: _loadTest,
);
}
if (_test == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text('Test not found'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadTest,
child: const Text('Retry'),
),
],
),
);
}
return _buildTestIntro();
}
Widget _buildTestIntro() {
final test = _test!;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
test.name,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Test Information',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
_buildInfoRow(Icons.quiz, 'Questions', '${test.questions.length}'),
const SizedBox(height: 8),
_buildInfoRow(Icons.timer, 'Estimated Time', '${test.questions.length * 2} minutes'),
const SizedBox(height: 8),
_buildInfoRow(Icons.help_outline, 'Type', 'Interactive Game'),
],
),
),
),
const SizedBox(height: 24),
Text(
'Instructions',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
const Text(
'• Answer each question as it appears\n'
'• Get instant feedback on your answers\n'
'• Complete all questions to see your results\n'
'• Your progress and statistics will be tracked',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _playGame(context),
icon: const Icon(Icons.games),
label: const Text('Start Test'),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
textStyle: const TextStyle(fontSize: 18),
),
),
),
],
),
);
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Row(
children: [
Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Text(
'$label: ',
style: const TextStyle(fontWeight: FontWeight.w500),
),
Text(value),
],
);
}
void _playGame(BuildContext context) {
// Navigate to the game page
context.go('/game/${widget.testId}');
}
}
bool _isTestStarted = false;
bool _isTestCompleted = false;
int _currentQuestionIndex = 0;
final Map<int, String> _userAnswers = {};
TestResult? _testResult;
@override
void initState() {
@ -414,365 +240,9 @@ class _TestPageState extends State<TestPage> {
);
}
Widget _buildTestQuestion() {
final test = _test!;
final question = test.questions[_currentQuestionIndex];
return Column(
children: [
// Progress bar
Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Question ${_currentQuestionIndex + 1} of ${test.questions.length}'),
Text('${((_currentQuestionIndex + 1) / test.questions.length * 100).round()}%'),
],
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: (_currentQuestionIndex + 1) / test.questions.length,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
),
],
),
),
// Question content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Question ${_currentQuestionIndex + 1}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Text(
question.word,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 18,
height: 1.4,
),
),
if (question is SimpleTestQuestionBody && question.text != null) ...[
const SizedBox(height: 8),
Text(
question.text!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
],
],
),
),
),
const SizedBox(height: 16),
// Answer options
Text(
'Select your answer:',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
if (question is SimpleTestQuestionBody) ...[
...question.buttons.map((button) {
final isSelected = _userAnswers[_currentQuestionIndex] == (button.text ?? '');
return Container(
margin: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: () => _selectAnswer(button.text ?? ''),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
color: isSelected
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
: null,
),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.transparent,
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
width: 2,
),
),
child: isSelected
? Builder(
builder: (context) {
final colorScheme =
Theme.of(context).colorScheme;
return Icon(
Icons.check,
color: colorScheme.onPrimary,
size: 16,
);
},
)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
button.text ?? '',
style: TextStyle(
fontSize: 16,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
),
],
),
),
),
);
}),
] else ...[
// Fallback for other question types
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'This question type is not yet supported.',
style: TextStyle(fontSize: 16),
),
),
],
],
),
),
),
// Navigation buttons
Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
if (_currentQuestionIndex > 0)
Expanded(
child: OutlinedButton.icon(
onPressed: _previousQuestion,
icon: const Icon(Icons.arrow_back),
label: const Text('Previous'),
),
),
if (_currentQuestionIndex > 0) const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: _nextQuestion,
icon: Icon(_currentQuestionIndex == test.questions.length - 1
? Icons.check
: Icons.arrow_forward),
label: Text(_currentQuestionIndex == test.questions.length - 1
? 'Finish'
: 'Next'),
),
),
],
),
),
],
);
}
Widget _buildTestResults() {
final result = _testResult!;
final score = (result.correctAnswers / result.totalQuestions * 100).round();
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const SizedBox(height: 32),
// Score circle
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getScoreColor(score).withOpacity(0.1),
border: Border.all(
color: _getScoreColor(score),
width: 4,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$score%',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: _getScoreColor(score),
),
),
Text(
'Score',
style: TextStyle(
fontSize: 14,
color: _getScoreColor(score),
),
),
],
),
),
const SizedBox(height: 32),
// Results summary
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
'Test Results',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
_buildResultRow('Correct Answers', '${result.correctAnswers}/${result.totalQuestions}'),
_buildResultRow('Incorrect Answers', '${result.incorrectAnswers}/${result.totalQuestions}'),
_buildResultRow('Time Taken', '${result.timeTaken} seconds'),
_buildResultRow('Date', result.completedAt.toString().split(' ')[0]),
],
),
),
),
const SizedBox(height: 24),
// Performance message
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _getScoreColor(score).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _getScoreColor(score).withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
_getScoreIcon(score),
color: _getScoreColor(score),
size: 32,
),
const SizedBox(width: 16),
Expanded(
child: Text(
_getScoreMessage(score),
style: TextStyle(
fontSize: 16,
color: _getScoreColor(score),
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const SizedBox(height: 32),
// Action buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _retakeTest,
icon: const Icon(Icons.refresh),
label: const Text('Retake Test'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: _goBack,
icon: const Icon(Icons.home),
label: const Text('Back to Packs'),
),
),
],
),
],
),
);
}
Widget _buildResultRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
);
}
Color _getScoreColor(int score) {
if (score >= 80) return Colors.green;
if (score >= 60) return Colors.orange;
return Colors.red;
}
IconData _getScoreIcon(int score) {
if (score >= 80) return Icons.celebration;
if (score >= 60) return Icons.thumb_up;
return Icons.thumb_down;
}
String _getScoreMessage(int score) {
if (score >= 80) return 'Excellent work! You have a great understanding of this topic.';
if (score >= 60) return 'Good job! You have a decent understanding, but there\'s room for improvement.';
return 'Keep studying! Review the material and try again.';
void _playGame(BuildContext context) {
// Navigate to the game page
context.go('/game/${widget.testId}');
}
void _startTest() {
@ -783,11 +253,6 @@ class _TestPageState extends State<TestPage> {
});
}
void _playGame(BuildContext context) {
// Navigate to the game page
context.go('/game/${widget.testId}');
}
void _selectAnswer(String answer) {
setState(() {
_userAnswers[_currentQuestionIndex] = answer;
@ -927,5 +392,385 @@ class _TestPageState extends State<TestPage> {
),
);
}
Widget _buildTestQuestion() {
final test = _test!;
final question = test.questions[_currentQuestionIndex];
return Column(
children: [
// Progress bar
Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Question ${_currentQuestionIndex + 1} of ${test.questions.length}'),
Text('${((_currentQuestionIndex + 1) / test.questions.length * 100).round()}%'),
],
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: (_currentQuestionIndex + 1) / test.questions.length,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
),
],
),
),
// Question content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Question ${_currentQuestionIndex + 1}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Text(
question.word,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 18,
height: 1.4,
),
),
if (question is SimpleTestQuestionBody && question.text != null) ...[
const SizedBox(height: 8),
Text(
question.text!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
],
),
),
),
const SizedBox(height: 16),
// Answer options
Text(
'Select your answer:',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
if (question is SimpleTestQuestionBody) ...[
...question.buttons.map((button) {
final isSelected = _userAnswers[_currentQuestionIndex] == (button.text ?? '');
return Container(
margin: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: () => _selectAnswer(button.text ?? ''),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
color: isSelected
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)
: null,
),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.transparent,
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
width: 2,
),
),
child: isSelected
? Builder(
builder: (context) {
final colorScheme =
Theme.of(context).colorScheme;
return Icon(
Icons.check,
color: colorScheme.onPrimary,
size: 16,
);
},
)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
button.text ?? '',
style: TextStyle(
fontSize: 16,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
),
],
),
),
),
);
}),
] else ...[
// Fallback for other question types
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'This question type is not yet supported.',
style: TextStyle(fontSize: 16),
),
),
],
],
),
),
),
// Navigation buttons
Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
if (_currentQuestionIndex > 0)
Expanded(
child: OutlinedButton.icon(
onPressed: _previousQuestion,
icon: const Icon(Icons.arrow_back),
label: const Text('Previous'),
),
),
if (_currentQuestionIndex > 0) const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: _nextQuestion,
icon: Icon(_currentQuestionIndex == test.questions.length - 1
? Icons.check
: Icons.arrow_forward),
label: Text(_currentQuestionIndex == test.questions.length - 1
? 'Finish'
: 'Next'),
),
),
],
),
),
],
);
}
Widget _buildTestResults() {
final result = _testResult!;
final score = (result.correctAnswers / result.totalQuestions * 100).round();
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const SizedBox(height: 32),
// Score circle
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getScoreColor(score).withValues(alpha: 0.1),
border: Border.all(
color: _getScoreColor(score),
width: 4,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$score%',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: _getScoreColor(score),
),
),
Text(
'Score',
style: TextStyle(
fontSize: 14,
color: _getScoreColor(score),
),
),
],
),
),
const SizedBox(height: 32),
// Results summary
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
'Test Results',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
_buildResultRow('Correct Answers', '${result.correctAnswers}/${result.totalQuestions}'),
_buildResultRow('Incorrect Answers', '${result.incorrectAnswers}/${result.totalQuestions}'),
_buildResultRow('Time Taken', '${result.timeTaken} seconds'),
_buildResultRow('Date', result.completedAt.toString().split(' ')[0]),
],
),
),
),
const SizedBox(height: 24),
// Performance message
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _getScoreColor(score).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _getScoreColor(score).withValues(alpha: 0.3),
),
),
child: Row(
children: [
Icon(
_getScoreIcon(score),
color: _getScoreColor(score),
size: 32,
),
const SizedBox(width: 16),
Expanded(
child: Text(
_getScoreMessage(score),
style: TextStyle(
fontSize: 16,
color: _getScoreColor(score),
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const SizedBox(height: 32),
// Action buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _retakeTest,
icon: const Icon(Icons.refresh),
label: const Text('Retake Test'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: _goBack,
icon: const Icon(Icons.home),
label: const Text('Back to Packs'),
),
),
],
),
],
),
);
}
Widget _buildResultRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
);
}
Color _getScoreColor(int score) {
if (score >= 80) return Colors.green;
if (score >= 60) return Colors.orange;
return Colors.red;
}
IconData _getScoreIcon(int score) {
if (score >= 80) return Icons.celebration;
if (score >= 60) return Icons.thumb_up;
return Icons.thumb_down;
}
String _getScoreMessage(int score) {
if (score >= 80) return 'Excellent work! You have a great understanding of this topic.';
if (score >= 60) return 'Good job! You have a decent understanding, but there\'s room for improvement.';
return 'Keep studying! Review the material and try again.';
}
}
/// Result of a completed test
class TestResult {
const TestResult({
required this.testId,
required this.correctAnswers,
required this.incorrectAnswers,
required this.totalQuestions,
required this.timeTaken,
required this.completedAt,
});
final String testId;
final int correctAnswers;
final int incorrectAnswers;
final int totalQuestions;
final int timeTaken;
final DateTime completedAt;
}