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 createState() => _FlaggedQuestionsScreenState(); } class _FlaggedQuestionsScreenState extends State { Deck? _deck; final List _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.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 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(() {}), ); }), ], ), ), ); } }