You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
269 lines
8.0 KiB
269 lines
8.0 KiB
import 'package:flutter/material.dart';
|
|
import 'package:practice_engine/practice_engine.dart';
|
|
import '../routes.dart';
|
|
import '../widgets/question_card.dart';
|
|
import '../widgets/answer_option.dart';
|
|
|
|
class AttemptScreen extends StatefulWidget {
|
|
const AttemptScreen({super.key});
|
|
|
|
@override
|
|
State<AttemptScreen> createState() => _AttemptScreenState();
|
|
}
|
|
|
|
class _AttemptScreenState extends State<AttemptScreen> {
|
|
Deck? _deck;
|
|
Attempt? _attempt;
|
|
AttemptService? _attemptService;
|
|
int _currentQuestionIndex = 0;
|
|
int? _selectedAnswerIndex;
|
|
final Map<String, int> _answers = {};
|
|
final Map<String, bool> _manualOverrides = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_attemptService = AttemptService();
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (_deck == null) {
|
|
// Get deck from route arguments
|
|
final args = ModalRoute.of(context)?.settings.arguments;
|
|
_deck = args is Deck ? args : _createSampleDeck();
|
|
_attempt = _attemptService!.createAttempt(deck: _deck!);
|
|
}
|
|
}
|
|
|
|
Deck _createSampleDeck() {
|
|
const config = DeckConfig();
|
|
final questions = List.generate(10, (i) {
|
|
return Question(
|
|
id: 'q$i',
|
|
prompt: 'Sample Question $i?',
|
|
answers: ['A', 'B', 'C', 'D'],
|
|
correctAnswerIndex: i % 4,
|
|
);
|
|
});
|
|
return Deck(
|
|
id: 'sample',
|
|
title: 'Sample',
|
|
description: 'Sample',
|
|
questions: questions,
|
|
config: config,
|
|
);
|
|
}
|
|
|
|
Question get _currentQuestion => _attempt!.questions[_currentQuestionIndex];
|
|
bool get _isLastQuestion => _currentQuestionIndex == _attempt!.questions.length - 1;
|
|
bool get _hasAnswer => _selectedAnswerIndex != null;
|
|
|
|
void _selectAnswer(int index) {
|
|
setState(() {
|
|
_selectedAnswerIndex = index;
|
|
});
|
|
|
|
if (_deck != null && _deck!.config.immediateFeedbackEnabled) {
|
|
// Show feedback immediately
|
|
}
|
|
}
|
|
|
|
void _submitAnswer() {
|
|
if (_selectedAnswerIndex == null) return;
|
|
|
|
_answers[_currentQuestion.id] = _selectedAnswerIndex!;
|
|
|
|
if (_isLastQuestion) {
|
|
_completeAttempt();
|
|
} else {
|
|
setState(() {
|
|
_currentQuestionIndex++;
|
|
_selectedAnswerIndex = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _markAsKnown() {
|
|
if (_deck == null) return;
|
|
setState(() {
|
|
_manualOverrides[_currentQuestion.id] = false; // Not needs practice
|
|
_deck = DeckService.markQuestionAsKnown(
|
|
deck: _deck!,
|
|
questionId: _currentQuestion.id,
|
|
);
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Question marked as Known'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _markAsNeedsPractice() {
|
|
if (_deck == null) return;
|
|
setState(() {
|
|
_manualOverrides[_currentQuestion.id] = true;
|
|
_deck = DeckService.markQuestionAsNeedsPractice(
|
|
deck: _deck!,
|
|
questionId: _currentQuestion.id,
|
|
);
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Question marked as Needs Practice'),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _completeAttempt() {
|
|
if (_deck == null || _attempt == null || _attemptService == null) return;
|
|
|
|
final result = _attemptService!.processAttempt(
|
|
deck: _deck!,
|
|
attempt: _attempt!,
|
|
answers: _answers,
|
|
manualOverrides: _manualOverrides,
|
|
endTime: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
|
|
Navigator.pushReplacementNamed(
|
|
context,
|
|
Routes.attemptResult,
|
|
arguments: {
|
|
'deck': result.updatedDeck,
|
|
'result': result.result,
|
|
'attempt': _attempt,
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_deck == null || _attempt == null) {
|
|
return const Scaffold(
|
|
body: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('Attempt - ${_deck!.title}'),
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// Progress Indicator
|
|
LinearProgressIndicator(
|
|
value: (_currentQuestionIndex + 1) / _attempt!.questions.length,
|
|
minHeight: 4,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Question ${_currentQuestionIndex + 1} of ${_attempt!.questions.length}',
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
if (_currentQuestion.isKnown)
|
|
Chip(
|
|
label: const Text('Known'),
|
|
avatar: const Icon(Icons.check_circle, size: 18),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Question Card
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
QuestionCard(question: _currentQuestion),
|
|
const SizedBox(height: 24),
|
|
|
|
// Answer Options
|
|
...List.generate(
|
|
_currentQuestion.answers.length,
|
|
(index) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: AnswerOption(
|
|
text: _currentQuestion.answers[index],
|
|
isSelected: _selectedAnswerIndex == index,
|
|
onTap: () => _selectAnswer(index),
|
|
isCorrect: _deck != null && _deck!.config.immediateFeedbackEnabled &&
|
|
_selectedAnswerIndex == index
|
|
? index == _currentQuestion.correctAnswerIndex
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Manual Override Buttons
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
'Manual Override',
|
|
style: Theme.of(context).textTheme.titleSmall,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: _markAsKnown,
|
|
icon: const Icon(Icons.check_circle),
|
|
label: const Text('Mark as Known'),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: _markAsNeedsPractice,
|
|
icon: const Icon(Icons.school),
|
|
label: const Text('Needs Practice'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Submit/Next Button
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: FilledButton(
|
|
onPressed: _hasAnswer ? _submitAnswer : null,
|
|
style: FilledButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
),
|
|
child: Text(_isLastQuestion ? 'Complete Attempt' : 'Next Question'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|