import 'package:flutter/material.dart'; import '../utils/top_snackbar.dart'; import 'package:practice_engine/practice_engine.dart'; import '../services/deck_storage.dart'; class DeckEditScreen extends StatefulWidget { const DeckEditScreen({super.key}); @override State createState() => _DeckEditScreenState(); } class _DeckEditScreenState extends State { Deck? _deck; late TextEditingController _titleController; late TextEditingController _descriptionController; final List _questionEditors = []; final DeckStorage _deckStorage = DeckStorage(); final ScrollController _scrollController = ScrollController(); int? _focusNewQuestionIndex; @override void initState() { super.initState(); } @override void didChangeDependencies() { super.didChangeDependencies(); if (_deck == null) { final args = ModalRoute.of(context)?.settings.arguments; if (args is Deck) { _initializeFromDeck(args); } } } void _initializeFromDeck(Deck deck) { setState(() { _deck = deck; _titleController = TextEditingController(text: deck.title); _descriptionController = TextEditingController(text: deck.description); _questionEditors.clear(); for (final question in deck.questions) { _questionEditors.add(QuestionEditor.fromQuestion(question)); } }); } @override void dispose() { _scrollController.dispose(); _titleController.dispose(); _descriptionController.dispose(); for (final editor in _questionEditors) { editor.dispose(); } super.dispose(); } void _addQuestion() { setState(() { _questionEditors.add(QuestionEditor.empty()); _focusNewQuestionIndex = _questionEditors.length - 1; }); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final pos = _scrollController.position; if (pos.maxScrollExtent > pos.pixels) { _scrollController.animateTo( pos.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } Future.microtask(() { if (mounted) setState(() => _focusNewQuestionIndex = null); }); }); } void _removeQuestion(int index) { setState(() { _questionEditors.removeAt(index); }); } void _save() { if (_deck == null) return; final title = _titleController.text.trim(); if (title.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Deck title cannot be empty'), backgroundColor: Colors.red, ), ); return; } // Validate all questions final questions = []; for (int i = 0; i < _questionEditors.length; i++) { final editor = _questionEditors[i]; if (!editor.isValid) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Question ${i + 1} is invalid. Please fill in all fields.'), backgroundColor: Colors.red, ), ); return; } questions.add(editor.toQuestion()); } if (questions.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Deck must have at least one question'), backgroundColor: Colors.red, ), ); return; } // Preserve attempt history and other deck data when editing final updatedDeck = _deck!.copyWith( title: title, description: _descriptionController.text.trim(), questions: questions, // Preserve attempt history, config, and current attempt index attemptHistory: _deck!.attemptHistory, config: _deck!.config, currentAttemptIndex: _deck!.currentAttemptIndex, ); _deckStorage.saveDeckSync(updatedDeck); showTopSnackBar( context, message: 'Deck saved successfully', backgroundColor: Colors.green, duration: const Duration(seconds: 1), ); Navigator.pop(context, updatedDeck); } @override Widget build(BuildContext context) { if (_deck == null) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } return Scaffold( appBar: AppBar( title: const Text('Edit Deck'), actions: [ IconButton( icon: const Icon(Icons.add), onPressed: _addQuestion, tooltip: 'Add Question', ), IconButton( icon: const Icon(Icons.save), onPressed: _save, tooltip: 'Save Deck', ), ], ), body: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Deck Title TextField( controller: _titleController, decoration: const InputDecoration( labelText: 'Deck Title', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), // Deck Description TextField( controller: _descriptionController, decoration: const InputDecoration( labelText: 'Description (optional)', border: OutlineInputBorder(), ), maxLines: 3, ), const SizedBox(height: 24), // Questions Section Text( 'Questions', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), // Questions List ...List.generate(_questionEditors.length, (index) { return QuestionEditorCard( key: ValueKey('question_$index'), editor: _questionEditors[index], questionNumber: index + 1, onDelete: () => _removeQuestion(index), onUnflag: null, onChanged: () => setState(() {}), requestFocusOnPrompt: _focusNewQuestionIndex == index, ); }), if (_questionEditors.isEmpty) Card( child: Padding( padding: const EdgeInsets.all(32), child: Center( child: Column( children: [ Icon( Icons.quiz_outlined, size: 48, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), ), const SizedBox(height: 16), Text( 'No questions yet', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 8), Text( 'Tap + in the app bar to add a question', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ), ), ), ), ], ), ), ); } } class QuestionEditorCard extends StatefulWidget { final QuestionEditor editor; final int questionNumber; final VoidCallback? onDelete; final VoidCallback? onUnflag; final VoidCallback onChanged; final bool requestFocusOnPrompt; const QuestionEditorCard({ super.key, required this.editor, required this.questionNumber, this.onDelete, this.onUnflag, required this.onChanged, this.requestFocusOnPrompt = false, }); @override State createState() => _QuestionEditorCardState(); } class _QuestionEditorCardState extends State { final FocusNode _promptFocusNode = FocusNode(); @override void dispose() { _promptFocusNode.dispose(); super.dispose(); } @override void didUpdateWidget(QuestionEditorCard oldWidget) { super.didUpdateWidget(oldWidget); if (widget.requestFocusOnPrompt && !oldWidget.requestFocusOnPrompt) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _promptFocusNode.requestFocus(); }); } } @override Widget build(BuildContext context) { if (widget.requestFocusOnPrompt) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _promptFocusNode.requestFocus(); }); } return Card( margin: const EdgeInsets.only(bottom: 16), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Question ${widget.questionNumber}', 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( icon: const Icon(Icons.delete, color: Colors.red), onPressed: widget.onDelete, tooltip: 'Delete Question', ), ], ), ], ), const SizedBox(height: 12), // Question Prompt TextField( controller: widget.editor.promptController, focusNode: _promptFocusNode, decoration: const InputDecoration( labelText: 'Question', border: OutlineInputBorder(), ), maxLines: 2, ), const SizedBox(height: 16), // Answers Text( 'Answers', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), ...List.generate(widget.editor.answerControllers.length, (index) { final isCorrect = widget.editor.correctAnswerIndices.contains(index); return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Checkbox( value: isCorrect, onChanged: (value) { setState(() { if (value == true) { widget.editor.correctAnswerIndices.add(index); } else { widget.editor.correctAnswerIndices.remove(index); // Ensure at least one answer is marked as correct if (widget.editor.correctAnswerIndices.isEmpty && widget.editor.answerControllers.length > 0) { widget.editor.correctAnswerIndices.add(0); } } }); widget.onChanged(); }, ), Expanded( child: TextField( controller: widget.editor.answerControllers[index], decoration: InputDecoration( labelText: 'Answer ${index + 1}', border: const OutlineInputBorder(), suffixIcon: isCorrect ? const Icon(Icons.check_circle, color: Colors.green) : null, ), ), ), ], ), ); }), // Add/Remove Answer buttons Row( children: [ TextButton.icon( onPressed: () { setState(() { widget.editor.answerControllers.add(TextEditingController()); }); widget.onChanged(); }, icon: const Icon(Icons.add), label: const Text('Add Answer'), ), if (widget.editor.answerControllers.length > 2) TextButton.icon( onPressed: () { setState(() { final lastIndex = widget.editor.answerControllers.length - 1; // Remove the index from correct answers if it was marked widget.editor.correctAnswerIndices.remove(lastIndex); // Adjust indices if needed widget.editor.correctAnswerIndices = widget.editor.correctAnswerIndices .where((idx) => idx < lastIndex) .toSet(); // Ensure at least one answer is marked as correct if (widget.editor.correctAnswerIndices.isEmpty && widget.editor.answerControllers.length > 1) { widget.editor.correctAnswerIndices.add(0); } widget.editor.answerControllers.removeLast().dispose(); }); widget.onChanged(); }, icon: const Icon(Icons.remove), label: const Text('Remove Answer'), ), ], ), ], ), ), ); } } class QuestionEditor { final TextEditingController promptController; final List answerControllers; Set correctAnswerIndices; final String? originalId; QuestionEditor({ required this.promptController, required this.answerControllers, Set? correctAnswerIndices, this.originalId, }) : correctAnswerIndices = correctAnswerIndices ?? {0}; factory QuestionEditor.fromQuestion(Question question) { return QuestionEditor( promptController: TextEditingController(text: question.prompt), answerControllers: question.answers .map((answer) => TextEditingController(text: answer)) .toList(), correctAnswerIndices: question.correctIndices.toSet(), originalId: question.id, ); } factory QuestionEditor.empty() { return QuestionEditor( promptController: TextEditingController(), answerControllers: [ TextEditingController(), TextEditingController(), ], correctAnswerIndices: {0}, ); } bool get isValid { if (promptController.text.trim().isEmpty) return false; if (answerControllers.length < 2) return false; for (final controller in answerControllers) { if (controller.text.trim().isEmpty) return false; } if (correctAnswerIndices.isEmpty) return false; if (correctAnswerIndices.any((idx) => idx < 0 || idx >= answerControllers.length)) { return false; } return true; } Question toQuestion() { return Question( id: originalId ?? DateTime.now().millisecondsSinceEpoch.toString(), prompt: promptController.text.trim(), answers: answerControllers.map((c) => c.text.trim()).toList(), correctAnswerIndices: correctAnswerIndices.toList()..sort(), ); } void dispose() { promptController.dispose(); for (final controller in answerControllers) { controller.dispose(); } } }