|
|
|
|
@ -1,4 +1,5 @@
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import '../utils/top_snackbar.dart';
|
|
|
|
|
import 'dart:math' as math;
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
import 'dart:ui';
|
|
|
|
|
@ -77,6 +78,8 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|
|
|
|
Set<int> _selectedAnswerIndices = {}; // For multiple answer questions
|
|
|
|
|
Map<String, dynamic> _answers = {}; // Can store int or List<int>
|
|
|
|
|
Map<String, bool> _manualOverrides = {};
|
|
|
|
|
/// Display order of answer indices per question (shuffled for new attempts).
|
|
|
|
|
Map<String, List<int>> _answerOrderPerQuestion = {};
|
|
|
|
|
final DeckStorage _deckStorage = DeckStorage();
|
|
|
|
|
late PageController _pageController;
|
|
|
|
|
Timer? _timer;
|
|
|
|
|
@ -117,10 +120,13 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|
|
|
|
_currentQuestionIndex = incomplete.currentQuestionIndex;
|
|
|
|
|
_answers = Map<String, dynamic>.from(incomplete.answers);
|
|
|
|
|
_manualOverrides = Map<String, bool>.from(incomplete.manualOverrides);
|
|
|
|
|
|
|
|
|
|
// Resumed attempt: use identity order so saved answers (original indices) match
|
|
|
|
|
_answerOrderPerQuestion = {
|
|
|
|
|
for (final q in _attempt!.questions)
|
|
|
|
|
q.id: List.generate(q.answers.length, (i) => i),
|
|
|
|
|
};
|
|
|
|
|
// Restore selected answer for current question
|
|
|
|
|
_loadQuestionState();
|
|
|
|
|
|
|
|
|
|
// Initialize PageController to the current question index
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (_pageController.hasClients) {
|
|
|
|
|
@ -132,6 +138,16 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|
|
|
|
deck: _deck!,
|
|
|
|
|
includeKnown: includeKnown,
|
|
|
|
|
);
|
|
|
|
|
// New attempt: randomize answer order per question when deck config enables it
|
|
|
|
|
_answerOrderPerQuestion = {};
|
|
|
|
|
final shuffleAnswers = _deck!.config.shuffleAnswerOrder;
|
|
|
|
|
for (final q in _attempt!.questions) {
|
|
|
|
|
final order = List.generate(q.answers.length, (i) => i);
|
|
|
|
|
if (shuffleAnswers) {
|
|
|
|
|
order.shuffle(math.Random());
|
|
|
|
|
}
|
|
|
|
|
_answerOrderPerQuestion[q.id] = order;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize timer if time limit is set
|
|
|
|
|
@ -451,12 +467,11 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|
|
|
|
final updatedDeck = _deck!.copyWith(incompleteAttempt: incompleteAttempt);
|
|
|
|
|
_deckStorage.saveDeckSync(updatedDeck);
|
|
|
|
|
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
const SnackBar(
|
|
|
|
|
content: Text('Attempt saved. You can continue later.'),
|
|
|
|
|
backgroundColor: Colors.blue,
|
|
|
|
|
duration: Duration(seconds: 2),
|
|
|
|
|
),
|
|
|
|
|
showTopSnackBar(
|
|
|
|
|
context,
|
|
|
|
|
message: 'Attempt saved. You can continue later.',
|
|
|
|
|
backgroundColor: Colors.blue,
|
|
|
|
|
duration: const Duration(seconds: 2),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
@ -709,58 +724,62 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|
|
|
|
QuestionCard(question: question),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
|
|
|
// Answer Options
|
|
|
|
|
...List.generate(
|
|
|
|
|
question.answers.length,
|
|
|
|
|
(answerIndex) => Padding(
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
|
|
|
child: AnswerOption(
|
|
|
|
|
text: question.answers[answerIndex],
|
|
|
|
|
isSelected: isMultipleCorrect
|
|
|
|
|
? selectedIndices.contains(answerIndex)
|
|
|
|
|
: selectedIndex == answerIndex,
|
|
|
|
|
onTap: () {
|
|
|
|
|
// Update selection for this question
|
|
|
|
|
setState(() {
|
|
|
|
|
if (isMultipleCorrect) {
|
|
|
|
|
if (selectedIndices.contains(answerIndex)) {
|
|
|
|
|
selectedIndices.remove(answerIndex);
|
|
|
|
|
} else {
|
|
|
|
|
selectedIndices.add(answerIndex);
|
|
|
|
|
}
|
|
|
|
|
_answers[question.id] = selectedIndices.toList()..sort();
|
|
|
|
|
} else {
|
|
|
|
|
// Toggle: if already selected, deselect; otherwise select
|
|
|
|
|
if (selectedIndex == answerIndex) {
|
|
|
|
|
selectedIndex = null;
|
|
|
|
|
_answers.remove(question.id);
|
|
|
|
|
} else {
|
|
|
|
|
selectedIndex = answerIndex;
|
|
|
|
|
_answers[question.id] = selectedIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Update current question state if viewing it
|
|
|
|
|
if (index == _currentQuestionIndex) {
|
|
|
|
|
if (isMultipleCorrect) {
|
|
|
|
|
_selectedAnswerIndices = selectedIndices;
|
|
|
|
|
_selectedAnswerIndex = null;
|
|
|
|
|
} else {
|
|
|
|
|
_selectedAnswerIndex = selectedIndex;
|
|
|
|
|
_selectedAnswerIndices.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
isCorrect: _deck != null && _deck!.config.immediateFeedbackEnabled &&
|
|
|
|
|
(isMultipleCorrect
|
|
|
|
|
? selectedIndices.contains(answerIndex)
|
|
|
|
|
: selectedIndex == answerIndex)
|
|
|
|
|
? question.isCorrectAnswer(answerIndex)
|
|
|
|
|
: null,
|
|
|
|
|
isMultipleChoice: isMultipleCorrect,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// Answer Options (displayed in shuffled order; we store original indices)
|
|
|
|
|
...() {
|
|
|
|
|
final order = _answerOrderPerQuestion[question.id] ??
|
|
|
|
|
List.generate(question.answers.length, (i) => i);
|
|
|
|
|
return List.generate(
|
|
|
|
|
question.answers.length,
|
|
|
|
|
(displayIndex) {
|
|
|
|
|
final originalIndex = order[displayIndex];
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
|
|
|
child: AnswerOption(
|
|
|
|
|
text: question.answers[originalIndex],
|
|
|
|
|
isSelected: isMultipleCorrect
|
|
|
|
|
? selectedIndices.contains(originalIndex)
|
|
|
|
|
: selectedIndex == originalIndex,
|
|
|
|
|
onTap: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
if (isMultipleCorrect) {
|
|
|
|
|
if (selectedIndices.contains(originalIndex)) {
|
|
|
|
|
selectedIndices.remove(originalIndex);
|
|
|
|
|
} else {
|
|
|
|
|
selectedIndices.add(originalIndex);
|
|
|
|
|
}
|
|
|
|
|
_answers[question.id] = selectedIndices.toList()..sort();
|
|
|
|
|
} else {
|
|
|
|
|
if (selectedIndex == originalIndex) {
|
|
|
|
|
selectedIndex = null;
|
|
|
|
|
_answers.remove(question.id);
|
|
|
|
|
} else {
|
|
|
|
|
selectedIndex = originalIndex;
|
|
|
|
|
_answers[question.id] = selectedIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (index == _currentQuestionIndex) {
|
|
|
|
|
if (isMultipleCorrect) {
|
|
|
|
|
_selectedAnswerIndices = selectedIndices;
|
|
|
|
|
_selectedAnswerIndex = null;
|
|
|
|
|
} else {
|
|
|
|
|
_selectedAnswerIndex = selectedIndex;
|
|
|
|
|
_selectedAnswerIndices.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
isCorrect: _deck != null && _deck!.config.immediateFeedbackEnabled &&
|
|
|
|
|
(isMultipleCorrect
|
|
|
|
|
? selectedIndices.contains(originalIndex)
|
|
|
|
|
: selectedIndex == originalIndex)
|
|
|
|
|
? question.isCorrectAnswer(originalIndex)
|
|
|
|
|
: null,
|
|
|
|
|
isMultipleChoice: isMultipleCorrect,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}(),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
|
|
|
@ -788,11 +807,10 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|
|
|
|
questionId: question.id,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
const SnackBar(
|
|
|
|
|
content: Text('Question marked as Known'),
|
|
|
|
|
backgroundColor: Colors.green,
|
|
|
|
|
),
|
|
|
|
|
showTopSnackBar(
|
|
|
|
|
context,
|
|
|
|
|
message: 'Question marked as Known',
|
|
|
|
|
backgroundColor: Colors.green,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
icon: const Icon(Icons.check_circle),
|
|
|
|
|
@ -810,10 +828,9 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|
|
|
|
questionId: question.id,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
const SnackBar(
|
|
|
|
|
content: Text('Question marked as Needs Practice'),
|
|
|
|
|
),
|
|
|
|
|
showTopSnackBar(
|
|
|
|
|
context,
|
|
|
|
|
message: 'Question marked as Needs Practice',
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
icon: const Icon(Icons.school),
|
|
|
|
|
|