flagging
This commit is contained in:
@@ -7,6 +7,7 @@ import 'screens/deck_edit_screen.dart';
|
|||||||
import 'screens/deck_create_screen.dart';
|
import 'screens/deck_create_screen.dart';
|
||||||
import 'screens/attempt_screen.dart';
|
import 'screens/attempt_screen.dart';
|
||||||
import 'screens/attempt_result_screen.dart';
|
import 'screens/attempt_result_screen.dart';
|
||||||
|
import 'screens/flagged_questions_screen.dart';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
static const String deckList = '/';
|
static const String deckList = '/';
|
||||||
@@ -17,6 +18,7 @@ class Routes {
|
|||||||
static const String deckCreate = '/deck-create';
|
static const String deckCreate = '/deck-create';
|
||||||
static const String attempt = '/attempt';
|
static const String attempt = '/attempt';
|
||||||
static const String attemptResult = '/attempt-result';
|
static const String attemptResult = '/attempt-result';
|
||||||
|
static const String flaggedQuestions = '/flagged-questions';
|
||||||
|
|
||||||
static Map<String, WidgetBuilder> get routes {
|
static Map<String, WidgetBuilder> get routes {
|
||||||
return {
|
return {
|
||||||
@@ -28,6 +30,7 @@ class Routes {
|
|||||||
deckCreate: (context) => const DeckCreateScreen(),
|
deckCreate: (context) => const DeckCreateScreen(),
|
||||||
attempt: (context) => const AttemptScreen(),
|
attempt: (context) => const AttemptScreen(),
|
||||||
attemptResult: (context) => const AttemptResultScreen(),
|
attemptResult: (context) => const AttemptResultScreen(),
|
||||||
|
flaggedQuestions: (context) => const FlaggedQuestionsScreen(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -572,14 +572,44 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Attempt - ${_deck!.title}'),
|
title: Text('Attempt - ${_deck!.title}'),
|
||||||
actions: [
|
actions: [
|
||||||
// Show "Jump to First Unanswered" if there are unanswered questions
|
// Flag current question for review
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (_attempt!.questions.isEmpty) return const SizedBox.shrink();
|
||||||
|
final currentQuestionId = _attempt!.questions[_currentQuestionIndex].id;
|
||||||
|
final isFlagged = _deck!.questions
|
||||||
|
.where((q) => q.id == currentQuestionId)
|
||||||
|
.firstOrNull
|
||||||
|
?.isFlagged ?? false;
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_deck = DeckService.toggleQuestionFlag(
|
||||||
|
deck: _deck!,
|
||||||
|
questionId: currentQuestionId,
|
||||||
|
);
|
||||||
|
_deckStorage.saveDeckSync(_deck!);
|
||||||
|
});
|
||||||
|
showTopSnackBar(
|
||||||
|
context,
|
||||||
|
message: isFlagged ? 'Question unflagged' : 'Question flagged for review',
|
||||||
|
backgroundColor: isFlagged ? null : Colors.orange,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
isFlagged ? Icons.flag : Icons.outlined_flag,
|
||||||
|
color: isFlagged ? Colors.red : null,
|
||||||
|
),
|
||||||
|
tooltip: isFlagged ? 'Unflag question' : 'Flag for review',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (_hasUnansweredQuestions)
|
if (_hasUnansweredQuestions)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _jumpToFirstUnanswered,
|
onPressed: _jumpToFirstUnanswered,
|
||||||
icon: const Icon(Icons.skip_next),
|
icon: const Icon(Icons.skip_next),
|
||||||
tooltip: 'Jump to first unanswered question',
|
tooltip: 'Jump to first unanswered question',
|
||||||
),
|
),
|
||||||
// Only show "Continue Later" button if there's progress
|
|
||||||
if (_hasAnyProgress)
|
if (_hasAnyProgress)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _saveForLater,
|
onPressed: _saveForLater,
|
||||||
|
|||||||
@@ -143,6 +143,11 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Edit Deck'),
|
title: const Text('Edit Deck'),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: _addQuestion,
|
||||||
|
tooltip: 'Add Question',
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.save),
|
icon: const Icon(Icons.save),
|
||||||
onPressed: _save,
|
onPressed: _save,
|
||||||
@@ -177,20 +182,10 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Questions Section
|
// Questions Section
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
Text(
|
||||||
'Questions',
|
'Questions',
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: _addQuestion,
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
label: const Text('Add Question'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Questions List
|
// Questions List
|
||||||
@@ -200,6 +195,7 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
|
|||||||
editor: _questionEditors[index],
|
editor: _questionEditors[index],
|
||||||
questionNumber: index + 1,
|
questionNumber: index + 1,
|
||||||
onDelete: () => _removeQuestion(index),
|
onDelete: () => _removeQuestion(index),
|
||||||
|
onUnflag: null,
|
||||||
onChanged: () => setState(() {}),
|
onChanged: () => setState(() {}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -223,7 +219,7 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Tap "Add Question" to get started',
|
'Tap + in the app bar to add a question',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
@@ -244,14 +240,16 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
|
|||||||
class QuestionEditorCard extends StatefulWidget {
|
class QuestionEditorCard extends StatefulWidget {
|
||||||
final QuestionEditor editor;
|
final QuestionEditor editor;
|
||||||
final int questionNumber;
|
final int questionNumber;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback? onDelete;
|
||||||
|
final VoidCallback? onUnflag;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
|
|
||||||
const QuestionEditorCard({
|
const QuestionEditorCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.editor,
|
required this.editor,
|
||||||
required this.questionNumber,
|
required this.questionNumber,
|
||||||
required this.onDelete,
|
this.onDelete,
|
||||||
|
this.onUnflag,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -276,6 +274,16 @@ class _QuestionEditorCardState extends State<QuestionEditorCard> {
|
|||||||
'Question ${widget.questionNumber}',
|
'Question ${widget.questionNumber}',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (widget.onUnflag != null)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.flag, color: Colors.orange.shade700),
|
||||||
|
onPressed: widget.onUnflag,
|
||||||
|
tooltip: 'Unflag',
|
||||||
|
),
|
||||||
|
if (widget.onDelete != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||||||
onPressed: widget.onDelete,
|
onPressed: widget.onDelete,
|
||||||
@@ -283,6 +291,8 @@ class _QuestionEditorCardState extends State<QuestionEditorCard> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Question Prompt
|
// Question Prompt
|
||||||
TextField(
|
TextField(
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
|||||||
correctAnswerIndices: correctIndices ?? [0],
|
correctAnswerIndices: correctIndices ?? [0],
|
||||||
consecutiveCorrect: questionMap['consecutiveCorrect'] as int? ?? 0,
|
consecutiveCorrect: questionMap['consecutiveCorrect'] as int? ?? 0,
|
||||||
isKnown: questionMap['isKnown'] as bool? ?? false,
|
isKnown: questionMap['isKnown'] as bool? ?? false,
|
||||||
|
isFlagged: questionMap['isFlagged'] as bool? ?? false,
|
||||||
priorityPoints: questionMap['priorityPoints'] as int? ?? 0,
|
priorityPoints: questionMap['priorityPoints'] as int? ?? 0,
|
||||||
lastAttemptIndex: questionMap['lastAttemptIndex'] as int? ?? -1,
|
lastAttemptIndex: questionMap['lastAttemptIndex'] as int? ?? -1,
|
||||||
totalCorrectAttempts: questionMap['totalCorrectAttempts'] as int? ?? 0,
|
totalCorrectAttempts: questionMap['totalCorrectAttempts'] as int? ?? 0,
|
||||||
|
|||||||
@@ -627,6 +627,26 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
Routes.flaggedQuestions,
|
||||||
|
arguments: _deck,
|
||||||
|
).then((_) => _refreshDeck());
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.flag,
|
||||||
|
color: _deck!.flaggedCount > 0 ? Colors.orange : null,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
'Flagged questions (${_deck!.flaggedCount})',
|
||||||
|
),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:practice_engine/practice_engine.dart';
|
||||||
|
import 'deck_edit_screen.dart';
|
||||||
|
import '../services/deck_storage.dart';
|
||||||
|
import '../utils/top_snackbar.dart';
|
||||||
|
|
||||||
|
/// Screen that shows flagged questions in the same editable card layout as deck edit,
|
||||||
|
/// with Save, Unflag per question, and export/copy as JSON.
|
||||||
|
class FlaggedQuestionsScreen extends StatefulWidget {
|
||||||
|
const FlaggedQuestionsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FlaggedQuestionsScreen> createState() => _FlaggedQuestionsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlaggedQuestionsScreenState extends State<FlaggedQuestionsScreen> {
|
||||||
|
Deck? _deck;
|
||||||
|
final List<QuestionEditor> _editors = [];
|
||||||
|
final DeckStorage _deckStorage = DeckStorage();
|
||||||
|
String? _lastDeckId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final deck = ModalRoute.of(context)?.settings.arguments as Deck?;
|
||||||
|
if (deck == null) return;
|
||||||
|
if (_deck?.id != deck.id || _lastDeckId != deck.id) {
|
||||||
|
_lastDeckId = deck.id;
|
||||||
|
_loadDeck(deck);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadDeck(Deck deck) {
|
||||||
|
final fresh = _deckStorage.getDeckSync(deck.id) ?? deck;
|
||||||
|
setState(() {
|
||||||
|
_deck = fresh;
|
||||||
|
for (final e in _editors) {
|
||||||
|
e.dispose();
|
||||||
|
}
|
||||||
|
_editors.clear();
|
||||||
|
for (final q in fresh.flaggedQuestions) {
|
||||||
|
_editors.add(QuestionEditor.fromQuestion(q));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (final e in _editors) {
|
||||||
|
e.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unflag(int index) {
|
||||||
|
if (_deck == null || index < 0 || index >= _editors.length) return;
|
||||||
|
final questionId = _editors[index].originalId;
|
||||||
|
if (questionId == null) return;
|
||||||
|
final updatedQuestions = _deck!.questions.map((q) {
|
||||||
|
if (q.id == questionId) return q.copyWith(isFlagged: false);
|
||||||
|
return q;
|
||||||
|
}).toList();
|
||||||
|
final updatedDeck = _deck!.copyWith(questions: updatedQuestions);
|
||||||
|
_deckStorage.saveDeckSync(updatedDeck);
|
||||||
|
_editors[index].dispose();
|
||||||
|
setState(() {
|
||||||
|
_deck = updatedDeck;
|
||||||
|
_editors.removeAt(index);
|
||||||
|
});
|
||||||
|
showTopSnackBar(context, message: 'Question unflagged');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _save() {
|
||||||
|
if (_deck == null) return;
|
||||||
|
for (int i = 0; i < _editors.length; i++) {
|
||||||
|
final editor = _editors[i];
|
||||||
|
if (!editor.isValid) {
|
||||||
|
showTopSnackBar(
|
||||||
|
context,
|
||||||
|
message: 'Question ${i + 1} is invalid. Fill in all fields.',
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final questionMap = Map<String, Question>.fromEntries(
|
||||||
|
_deck!.questions.map((q) => MapEntry(q.id, q)),
|
||||||
|
);
|
||||||
|
for (final editor in _editors) {
|
||||||
|
final id = editor.originalId;
|
||||||
|
if (id == null) continue;
|
||||||
|
final existing = questionMap[id];
|
||||||
|
if (existing == null) continue;
|
||||||
|
final updated = existing.copyWith(
|
||||||
|
prompt: editor.promptController.text.trim(),
|
||||||
|
answers: editor.answerControllers.map((c) => c.text.trim()).toList(),
|
||||||
|
correctAnswerIndices: editor.correctAnswerIndices.toList()..sort(),
|
||||||
|
);
|
||||||
|
questionMap[id] = updated;
|
||||||
|
}
|
||||||
|
final updatedQuestions = _deck!.questions.map((q) => questionMap[q.id] ?? q).toList();
|
||||||
|
final updatedDeck = _deck!.copyWith(questions: updatedQuestions);
|
||||||
|
_deckStorage.saveDeckSync(updatedDeck);
|
||||||
|
setState(() => _deck = updatedDeck);
|
||||||
|
showTopSnackBar(
|
||||||
|
context,
|
||||||
|
message: 'Changes saved',
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _copyJson() {
|
||||||
|
final questions = _editors.map((e) => e.toQuestion()).toList();
|
||||||
|
if (questions.isEmpty) {
|
||||||
|
showTopSnackBar(context, message: 'No questions to copy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final json = _questionsToJson(questions);
|
||||||
|
Clipboard.setData(ClipboardData(text: json));
|
||||||
|
showTopSnackBar(context, message: '${questions.length} question(s) copied as JSON');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _exportJson() {
|
||||||
|
final questions = _editors.map((e) => e.toQuestion()).toList();
|
||||||
|
if (questions.isEmpty) {
|
||||||
|
showTopSnackBar(context, message: 'No questions to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final json = _questionsToJson(questions);
|
||||||
|
Clipboard.setData(ClipboardData(text: json));
|
||||||
|
showTopSnackBar(
|
||||||
|
context,
|
||||||
|
message: 'JSON copied to clipboard. Paste into a file to save.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _questionsToJson(List<Question> questions) {
|
||||||
|
final list = questions.map((q) => {
|
||||||
|
'id': q.id,
|
||||||
|
'prompt': q.prompt,
|
||||||
|
'answers': q.answers,
|
||||||
|
'correctAnswerIndices': q.correctAnswerIndices,
|
||||||
|
}).toList();
|
||||||
|
return const JsonEncoder.withIndent(' ').convert(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_deck == null) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Flagged questions')),
|
||||||
|
body: const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasEditors = _editors.isNotEmpty;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Flagged questions (${_editors.length})'),
|
||||||
|
actions: [
|
||||||
|
if (hasEditors) ...[
|
||||||
|
IconButton(
|
||||||
|
onPressed: _save,
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
tooltip: 'Save',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _copyJson,
|
||||||
|
icon: const Icon(Icons.copy),
|
||||||
|
tooltip: 'Copy as JSON',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _exportJson,
|
||||||
|
icon: const Icon(Icons.file_download),
|
||||||
|
tooltip: 'Export as JSON',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: !hasEditors
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.flag_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'No flagged questions',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Flag questions during an attempt to see them here.',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
...List.generate(_editors.length, (index) {
|
||||||
|
return QuestionEditorCard(
|
||||||
|
key: ValueKey('flagged_${_editors[index].originalId}_$index'),
|
||||||
|
editor: _editors[index],
|
||||||
|
questionNumber: index + 1,
|
||||||
|
onDelete: null,
|
||||||
|
onUnflag: () => _unflag(index),
|
||||||
|
onChanged: () => setState(() {}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ class DeckStorage {
|
|||||||
'correctAnswerIndices': q.correctAnswerIndices,
|
'correctAnswerIndices': q.correctAnswerIndices,
|
||||||
'consecutiveCorrect': q.consecutiveCorrect,
|
'consecutiveCorrect': q.consecutiveCorrect,
|
||||||
'isKnown': q.isKnown,
|
'isKnown': q.isKnown,
|
||||||
|
'isFlagged': q.isFlagged,
|
||||||
'priorityPoints': q.priorityPoints,
|
'priorityPoints': q.priorityPoints,
|
||||||
'lastAttemptIndex': q.lastAttemptIndex,
|
'lastAttemptIndex': q.lastAttemptIndex,
|
||||||
'totalCorrectAttempts': q.totalCorrectAttempts,
|
'totalCorrectAttempts': q.totalCorrectAttempts,
|
||||||
@@ -137,6 +138,7 @@ class DeckStorage {
|
|||||||
correctAnswerIndices: correctIndices ?? [0],
|
correctAnswerIndices: correctIndices ?? [0],
|
||||||
consecutiveCorrect: questionMap['consecutiveCorrect'] as int? ?? 0,
|
consecutiveCorrect: questionMap['consecutiveCorrect'] as int? ?? 0,
|
||||||
isKnown: questionMap['isKnown'] as bool? ?? false,
|
isKnown: questionMap['isKnown'] as bool? ?? false,
|
||||||
|
isFlagged: questionMap['isFlagged'] as bool? ?? false,
|
||||||
priorityPoints: questionMap['priorityPoints'] as int? ?? 0,
|
priorityPoints: questionMap['priorityPoints'] as int? ?? 0,
|
||||||
lastAttemptIndex: questionMap['lastAttemptIndex'] as int? ?? -1,
|
lastAttemptIndex: questionMap['lastAttemptIndex'] as int? ?? -1,
|
||||||
totalCorrectAttempts: questionMap['totalCorrectAttempts'] as int? ?? 0,
|
totalCorrectAttempts: questionMap['totalCorrectAttempts'] as int? ?? 0,
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ class DeckService {
|
|||||||
return deck.copyWith(questions: updatedQuestions);
|
return deck.copyWith(questions: updatedQuestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggles the flagged state of a question.
|
||||||
|
static Deck toggleQuestionFlag({
|
||||||
|
required Deck deck,
|
||||||
|
required String questionId,
|
||||||
|
}) {
|
||||||
|
final updatedQuestions = deck.questions.map((q) {
|
||||||
|
if (q.id == questionId) {
|
||||||
|
return q.copyWith(isFlagged: !q.isFlagged);
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}).toList();
|
||||||
|
return deck.copyWith(questions: updatedQuestions);
|
||||||
|
}
|
||||||
|
|
||||||
/// Marks a question as needs practice (manual override).
|
/// Marks a question as needs practice (manual override).
|
||||||
///
|
///
|
||||||
/// Sets isKnown to false and streak to 0, but keeps priority unchanged.
|
/// Sets isKnown to false and streak to 0, but keeps priority unchanged.
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ class Deck {
|
|||||||
/// Number of questions marked as known.
|
/// Number of questions marked as known.
|
||||||
int get knownCount => questions.where((q) => q.isKnown).length;
|
int get knownCount => questions.where((q) => q.isKnown).length;
|
||||||
|
|
||||||
|
/// Number of questions the user has flagged.
|
||||||
|
int get flaggedCount => questions.where((q) => q.isFlagged).length;
|
||||||
|
|
||||||
|
/// Questions that the user has flagged.
|
||||||
|
List<Question> get flaggedQuestions => questions.where((q) => q.isFlagged).toList();
|
||||||
|
|
||||||
/// Practice percentage: (known / total) * 100
|
/// Practice percentage: (known / total) * 100
|
||||||
double get practicePercentage {
|
double get practicePercentage {
|
||||||
if (questions.isEmpty) return 0.0;
|
if (questions.isEmpty) return 0.0;
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ class Question {
|
|||||||
/// Whether this question is considered "known".
|
/// Whether this question is considered "known".
|
||||||
final bool isKnown;
|
final bool isKnown;
|
||||||
|
|
||||||
|
/// Whether the user has flagged this question for review.
|
||||||
|
final bool isFlagged;
|
||||||
|
|
||||||
/// Priority points (higher = more likely to be selected).
|
/// Priority points (higher = more likely to be selected).
|
||||||
final int priorityPoints;
|
final int priorityPoints;
|
||||||
|
|
||||||
@@ -44,6 +47,7 @@ class Question {
|
|||||||
@Deprecated('Use correctAnswerIndices instead') int? correctAnswerIndex,
|
@Deprecated('Use correctAnswerIndices instead') int? correctAnswerIndex,
|
||||||
this.consecutiveCorrect = 0,
|
this.consecutiveCorrect = 0,
|
||||||
this.isKnown = false,
|
this.isKnown = false,
|
||||||
|
this.isFlagged = false,
|
||||||
this.priorityPoints = 0,
|
this.priorityPoints = 0,
|
||||||
this.lastAttemptIndex = -1,
|
this.lastAttemptIndex = -1,
|
||||||
this.totalCorrectAttempts = 0,
|
this.totalCorrectAttempts = 0,
|
||||||
@@ -61,6 +65,7 @@ class Question {
|
|||||||
@Deprecated('Use correctAnswerIndices instead') int? correctAnswerIndex,
|
@Deprecated('Use correctAnswerIndices instead') int? correctAnswerIndex,
|
||||||
int? consecutiveCorrect,
|
int? consecutiveCorrect,
|
||||||
bool? isKnown,
|
bool? isKnown,
|
||||||
|
bool? isFlagged,
|
||||||
int? priorityPoints,
|
int? priorityPoints,
|
||||||
int? lastAttemptIndex,
|
int? lastAttemptIndex,
|
||||||
int? totalCorrectAttempts,
|
int? totalCorrectAttempts,
|
||||||
@@ -74,6 +79,7 @@ class Question {
|
|||||||
correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
|
correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
|
||||||
consecutiveCorrect: consecutiveCorrect ?? this.consecutiveCorrect,
|
consecutiveCorrect: consecutiveCorrect ?? this.consecutiveCorrect,
|
||||||
isKnown: isKnown ?? this.isKnown,
|
isKnown: isKnown ?? this.isKnown,
|
||||||
|
isFlagged: isFlagged ?? this.isFlagged,
|
||||||
priorityPoints: priorityPoints ?? this.priorityPoints,
|
priorityPoints: priorityPoints ?? this.priorityPoints,
|
||||||
lastAttemptIndex: lastAttemptIndex ?? this.lastAttemptIndex,
|
lastAttemptIndex: lastAttemptIndex ?? this.lastAttemptIndex,
|
||||||
totalCorrectAttempts: totalCorrectAttempts ?? this.totalCorrectAttempts,
|
totalCorrectAttempts: totalCorrectAttempts ?? this.totalCorrectAttempts,
|
||||||
@@ -117,6 +123,7 @@ class Question {
|
|||||||
correctAnswerIndices.toString() == other.correctAnswerIndices.toString() &&
|
correctAnswerIndices.toString() == other.correctAnswerIndices.toString() &&
|
||||||
consecutiveCorrect == other.consecutiveCorrect &&
|
consecutiveCorrect == other.consecutiveCorrect &&
|
||||||
isKnown == other.isKnown &&
|
isKnown == other.isKnown &&
|
||||||
|
isFlagged == other.isFlagged &&
|
||||||
priorityPoints == other.priorityPoints &&
|
priorityPoints == other.priorityPoints &&
|
||||||
lastAttemptIndex == other.lastAttemptIndex &&
|
lastAttemptIndex == other.lastAttemptIndex &&
|
||||||
totalCorrectAttempts == other.totalCorrectAttempts &&
|
totalCorrectAttempts == other.totalCorrectAttempts &&
|
||||||
@@ -130,6 +137,7 @@ class Question {
|
|||||||
correctAnswerIndices.hashCode ^
|
correctAnswerIndices.hashCode ^
|
||||||
consecutiveCorrect.hashCode ^
|
consecutiveCorrect.hashCode ^
|
||||||
isKnown.hashCode ^
|
isKnown.hashCode ^
|
||||||
|
isFlagged.hashCode ^
|
||||||
priorityPoints.hashCode ^
|
priorityPoints.hashCode ^
|
||||||
lastAttemptIndex.hashCode ^
|
lastAttemptIndex.hashCode ^
|
||||||
totalCorrectAttempts.hashCode ^
|
totalCorrectAttempts.hashCode ^
|
||||||
|
|||||||
Reference in New Issue
Block a user