parent
6b8cf10e3c
commit
10a4837ee0
@ -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(() {}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue