diff --git a/lib/screens/deck_import_screen.dart b/lib/screens/deck_import_screen.dart index e457a4e..b79bf29 100644 --- a/lib/screens/deck_import_screen.dart +++ b/lib/screens/deck_import_screen.dart @@ -118,7 +118,91 @@ class _DeckImportScreenState extends State { final deckStorage = DeckStorage(); deckStorage.saveDeckSync(deck); - // Navigate back to deck list + showTopSnackBar( + context, + message: 'Deck created successfully', + backgroundColor: Colors.green, + ); + Navigator.pop(context); + } catch (e) { + setState(() { + _errorMessage = e.toString(); + _isLoading = false; + }); + } + } + + void _mergeWithExistingDeck() async { + setState(() { + _errorMessage = null; + _isLoading = true; + }); + + try { + if (_jsonController.text.trim().isEmpty) { + throw FormatException('Please enter JSON data'); + } + + final parsedDeck = _parseDeckFromJson(_jsonController.text.trim()); + if (parsedDeck.questions.isEmpty) { + throw FormatException('JSON deck must contain at least one question to merge'); + } + + final deckStorage = DeckStorage(); + final existingDecks = deckStorage.getAllDecksSync(); + if (existingDecks.isEmpty) { + throw FormatException('No existing decks to merge into. Create or import a deck first.'); + } + + setState(() => _isLoading = false); + + final selectedDeck = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Merge with existing deck'), + content: SizedBox( + width: double.maxFinite, + child: ListView.builder( + shrinkWrap: true, + itemCount: existingDecks.length, + itemBuilder: (context, index) { + final deck = existingDecks[index]; + return ListTile( + title: Text(deck.title), + subtitle: Text('${deck.questions.length} questions'), + onTap: () => Navigator.pop(context, deck), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ], + ), + ); + + if (selectedDeck == null || !mounted) return; + + final timestamp = DateTime.now().millisecondsSinceEpoch; + final mergedQuestions = [ + ...selectedDeck.questions, + ...parsedDeck.questions.asMap().entries.map((e) { + final q = e.value; + final i = e.key; + return q.copyWith(id: '${q.id}_merged_${timestamp}_$i'); + }), + ]; + final mergedDeck = selectedDeck.copyWith(questions: mergedQuestions); + deckStorage.saveDeckSync(mergedDeck); + + showTopSnackBar( + context, + message: 'Added ${parsedDeck.questions.length} question(s) to "${selectedDeck.title}"', + backgroundColor: Colors.green, + ); Navigator.pop(context); } catch (e) { setState(() { @@ -391,12 +475,12 @@ class _DeckImportScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Import Deck from JSON', + 'Import from JSON', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( - 'Paste your deck JSON below. The format should include id, title, description, config, and questions.', + 'Paste your deck JSON below. Create a new deck or merge questions into an existing one.', style: Theme.of(context).textTheme.bodyMedium, ), ], @@ -484,20 +568,43 @@ class _DeckImportScreenState extends State { if (_errorMessage != null) const SizedBox(height: 16), - // Import Button - FilledButton.icon( - onPressed: _isLoading ? null : _importDeck, - icon: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.upload), - label: Text(_isLoading ? 'Importing...' : 'Import Deck'), - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - ), + // Import and Merge Buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _isLoading ? null : _mergeWithExistingDeck, + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.merge_type, size: 20), + label: Text(_isLoading ? '...' : 'Merge with existing deck'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton.icon( + onPressed: _isLoading ? null : _importDeck, + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.add_circle_outline, size: 20), + label: Text(_isLoading ? 'Importing...' : 'Import and create deck'), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ], ), const SizedBox(height: 16), diff --git a/lib/screens/flagged_questions_screen.dart b/lib/screens/flagged_questions_screen.dart index 4dec977..91f5940 100644 --- a/lib/screens/flagged_questions_screen.dart +++ b/lib/screens/flagged_questions_screen.dart @@ -73,6 +73,68 @@ class _FlaggedQuestionsScreenState extends State { showTopSnackBar(context, message: 'Question unflagged'); } + void _removeQuestion(int index) { + if (_deck == null || index < 0 || index >= _editors.length) return; + final questionId = _editors[index].originalId; + if (questionId == null) return; + final updatedQuestions = + _deck!.questions.where((q) => q.id != questionId).toList(); + final updatedDeck = _deck!.copyWith(questions: updatedQuestions); + _deckStorage.saveDeckSync(updatedDeck); + _editors[index].dispose(); + setState(() { + _deck = updatedDeck; + _editors.removeAt(index); + }); + showTopSnackBar(context, message: 'Question removed from deck'); + } + + Future _removeAllQuestions() async { + if (_deck == null || _editors.isEmpty) return; + final count = _editors.length; + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Remove all flagged questions?'), + content: Text( + 'Remove all $count flagged question${count == 1 ? '' : 's'} from the deck? This cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + ), + child: const Text('Remove all'), + ), + ], + ), + ); + if (confirmed != true || !mounted) return; + final idsToRemove = + _editors.map((e) => e.originalId).whereType().toSet(); + final updatedQuestions = + _deck!.questions.where((q) => !idsToRemove.contains(q.id)).toList(); + final updatedDeck = _deck!.copyWith(questions: updatedQuestions); + _deckStorage.saveDeckSync(updatedDeck); + for (final e in _editors) { + e.dispose(); + } + setState(() { + _deck = updatedDeck; + _editors.clear(); + }); + showTopSnackBar( + context, + message: '$count question${count == 1 ? '' : 's'} removed from deck', + backgroundColor: Colors.green, + ); + } + void _save() { if (_deck == null) return; for (int i = 0; i < _editors.length; i++) { @@ -162,6 +224,11 @@ class _FlaggedQuestionsScreenState extends State { title: Text('Flagged questions (${_editors.length})'), actions: [ if (hasEditors) ...[ + IconButton( + onPressed: _removeAllQuestions, + icon: const Icon(Icons.delete_forever), + tooltip: 'Remove all from deck', + ), IconButton( onPressed: _save, icon: const Icon(Icons.save), @@ -216,7 +283,7 @@ class _FlaggedQuestionsScreenState extends State { key: ValueKey('flagged_${_editors[index].originalId}_$index'), editor: _editors[index], questionNumber: index + 1, - onDelete: null, + onDelete: () => _removeQuestion(index), onUnflag: () => _unflag(index), onChanged: () => setState(() {}), );