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() {
@ -366,15 +192,15 @@ class _TestPageState extends State<TestPage> {
const SizedBox(height: 32),
Column(
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _playGame(context),
icon: const Icon(Icons.games),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _playGame(context),
icon: const Icon(Icons.games),
label: const Text('Play Interactive Game'),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
textStyle: const TextStyle(fontSize: 18),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
textStyle: const TextStyle(fontSize: 18),
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
),
@ -414,6 +240,159 @@ class _TestPageState extends State<TestPage> {
);
}
void _playGame(BuildContext context) {
// Navigate to the game page
context.go('/game/${widget.testId}');
}
void _startTest() {
setState(() {
_isTestStarted = true;
_currentQuestionIndex = 0;
_userAnswers.clear();
});
}
void _selectAnswer(String answer) {
setState(() {
_userAnswers[_currentQuestionIndex] = answer;
});
}
void _nextQuestion() {
if (_currentQuestionIndex < _test!.questions.length - 1) {
setState(() {
_currentQuestionIndex++;
});
} else {
_finishTest();
}
}
void _previousQuestion() {
if (_currentQuestionIndex > 0) {
setState(() {
_currentQuestionIndex--;
});
}
}
Future<void> _finishTest() async {
if (_test == null) return;
final startTime = DateTime.now();
final endTime = DateTime.now();
final timeTaken = endTime.difference(startTime).inSeconds;
// Calculate results
int correctAnswers = 0;
int incorrectAnswers = 0;
for (int i = 0; i < _test!.questions.length; i++) {
final question = _test!.questions[i];
final userAnswer = _userAnswers[i];
if (question is SimpleTestQuestionBody) {
if (userAnswer == question.answer) {
correctAnswers++;
} else {
incorrectAnswers++;
}
} else {
// For unsupported question types, count as incorrect
incorrectAnswers++;
}
}
final result = TestResult(
testId: widget.testId,
correctAnswers: correctAnswers,
incorrectAnswers: incorrectAnswers,
totalQuestions: _test!.questions.length,
timeTaken: timeTaken,
completedAt: endTime,
);
// Submit results to backend
try {
final appScope = ScopeProvider.of<AppScopeContainer>(
context,
listen: false,
);
final userScope = appScope?.userScopeHolder.scope;
if (userScope != null) {
// Create a simplified statistics DTO for submission
final statistics = TestStatisticsDto(
testId: int.tryParse(widget.testId) ?? 0,
words: AllWordsStatisticsDto.empty(),
attempts: 1,
);
await userScope.testsModule.testManager.submitTestStatistics(
widget.testId,
statistics,
);
}
} catch (e, s) {
log(
'Error submitting test statistics',
error: e,
stackTrace: s,
name: 'TestPage',
);
}
setState(() {
_isTestCompleted = true;
_testResult = result;
});
}
void _retakeTest() {
setState(() {
_isTestStarted = false;
_isTestCompleted = false;
_currentQuestionIndex = 0;
_userAnswers.clear();
_testResult = null;
});
}
void _goBack() {
context.pop();
}
void _handleBack() {
if (_isTestStarted && !_isTestCompleted) {
_showExitConfirmation();
} else {
context.pop();
}
}
void _showExitConfirmation() {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Exit Test'),
content: const Text('Are you sure you want to exit? Your progress will be lost.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
context.pop();
},
child: const Text('Exit'),
),
],
),
);
}
Widget _buildTestQuestion() {
final test = _test!;
final question = test.questions[_currentQuestionIndex];
@ -475,7 +454,7 @@ class _TestPageState extends State<TestPage> {
question.text!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
@ -513,7 +492,7 @@ class _TestPageState extends State<TestPage> {
),
borderRadius: BorderRadius.circular(8),
color: isSelected
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)
: null,
),
child: Row(
@ -630,7 +609,7 @@ class _TestPageState extends State<TestPage> {
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getScoreColor(score).withOpacity(0.1),
color: _getScoreColor(score).withValues(alpha: 0.1),
border: Border.all(
color: _getScoreColor(score),
width: 4,
@ -686,10 +665,10 @@ class _TestPageState extends State<TestPage> {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _getScoreColor(score).withOpacity(0.1),
color: _getScoreColor(score).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _getScoreColor(score).withOpacity(0.3),
color: _getScoreColor(score).withValues(alpha: 0.3),
),
),
child: Row(
@ -774,158 +753,24 @@ class _TestPageState extends State<TestPage> {
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 _startTest() {
setState(() {
_isTestStarted = true;
_currentQuestionIndex = 0;
_userAnswers.clear();
});
}
void _playGame(BuildContext context) {
// Navigate to the game page
context.go('/game/${widget.testId}');
}
void _selectAnswer(String answer) {
setState(() {
_userAnswers[_currentQuestionIndex] = answer;
});
}
void _nextQuestion() {
if (_currentQuestionIndex < _test!.questions.length - 1) {
setState(() {
_currentQuestionIndex++;
});
} else {
_finishTest();
}
}
void _previousQuestion() {
if (_currentQuestionIndex > 0) {
setState(() {
_currentQuestionIndex--;
});
}
}
Future<void> _finishTest() async {
if (_test == null) return;
final startTime = DateTime.now();
final endTime = DateTime.now();
final timeTaken = endTime.difference(startTime).inSeconds;
// Calculate results
int correctAnswers = 0;
int incorrectAnswers = 0;
for (int i = 0; i < _test!.questions.length; i++) {
final question = _test!.questions[i];
final userAnswer = _userAnswers[i];
if (question is SimpleTestQuestionBody) {
if (userAnswer == question.answer) {
correctAnswers++;
} else {
incorrectAnswers++;
}
} else {
// For unsupported question types, count as incorrect
incorrectAnswers++;
}
}
final result = TestResult(
testId: widget.testId,
correctAnswers: correctAnswers,
incorrectAnswers: incorrectAnswers,
totalQuestions: _test!.questions.length,
timeTaken: timeTaken,
completedAt: endTime,
);
// Submit results to backend
try {
final appScope = ScopeProvider.of<AppScopeContainer>(
context,
listen: false,
);
final userScope = appScope?.userScopeHolder.scope;
if (userScope != null) {
// Create a simplified statistics DTO for submission
final statistics = TestStatisticsDto(
testId: int.tryParse(widget.testId) ?? 0,
words: AllWordsStatisticsDto.empty(),
attempts: 1,
);
await userScope.testsModule.testManager.submitTestStatistics(
widget.testId,
statistics,
);
}
} catch (e, s) {
log(
'Error submitting test statistics',
error: e,
stackTrace: s,
name: 'TestPage',
);
}
setState(() {
_isTestCompleted = true;
_testResult = result;
});
}
void _retakeTest() {
setState(() {
_isTestStarted = false;
_isTestCompleted = false;
_currentQuestionIndex = 0;
_userAnswers.clear();
_testResult = null;
});
}
void _goBack() {
context.pop();
}
void _handleBack() {
if (_isTestStarted && !_isTestCompleted) {
_showExitConfirmation();
} else {
context.pop();
}
}
void _showExitConfirmation() {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Exit Test'),
content: const Text('Are you sure you want to exit? Your progress will be lost.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
context.pop();
},
child: const Text('Exit'),
),
],
),
);
}
}
/// 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;
}