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.
decky/lib/screens/attempt_screen.dart

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

Powered by TurnKey Linux.