import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:practice_engine/practice_engine.dart'; import '../data/default_deck.dart'; import '../services/deck_storage.dart'; class DeckImportScreen extends StatefulWidget { const DeckImportScreen({super.key}); @override State createState() => _DeckImportScreenState(); } class _DeckImportScreenState extends State { final TextEditingController _jsonController = TextEditingController(); String? _errorMessage; bool _isLoading = false; @override void dispose() { _jsonController.dispose(); super.dispose(); } Deck? _parseDeckFromJson(String jsonString) { try { final Map json = jsonDecode(jsonString); // Parse config final configJson = json['config'] as Map? ?? {}; final config = DeckConfig( requiredConsecutiveCorrect: configJson['requiredConsecutiveCorrect'] as int? ?? 3, defaultAttemptSize: configJson['defaultAttemptSize'] as int? ?? 10, priorityIncreaseOnIncorrect: configJson['priorityIncreaseOnIncorrect'] as int? ?? 5, priorityDecreaseOnCorrect: configJson['priorityDecreaseOnCorrect'] as int? ?? 2, immediateFeedbackEnabled: configJson['immediateFeedbackEnabled'] as bool? ?? true, includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false, ); // Parse questions final questionsJson = json['questions'] as List? ?? []; final questions = questionsJson.map((qJson) { final questionMap = qJson as Map; // Support both old format (correctAnswerIndex) and new format (correctAnswerIndices) List? correctIndices; if (questionMap['correctAnswerIndices'] != null) { final indicesJson = questionMap['correctAnswerIndices'] as List?; correctIndices = indicesJson?.map((e) => e as int).toList(); } else if (questionMap['correctAnswerIndex'] != null) { // Backward compatibility: convert single index to list final singleIndex = questionMap['correctAnswerIndex'] as int?; if (singleIndex != null) { correctIndices = [singleIndex]; } } return Question( id: questionMap['id'] as String? ?? '', prompt: questionMap['prompt'] as String? ?? '', answers: (questionMap['answers'] as List?) ?.map((e) => e.toString()) .toList() ?? [], correctAnswerIndices: correctIndices ?? [0], consecutiveCorrect: questionMap['consecutiveCorrect'] as int? ?? 0, isKnown: questionMap['isKnown'] as bool? ?? false, priorityPoints: questionMap['priorityPoints'] as int? ?? 0, lastAttemptIndex: questionMap['lastAttemptIndex'] as int? ?? -1, totalCorrectAttempts: questionMap['totalCorrectAttempts'] as int? ?? 0, totalAttempts: questionMap['totalAttempts'] as int? ?? 0, ); }).toList(); // Create deck return Deck( id: json['id'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString(), title: json['title'] as String? ?? 'Imported Deck', description: json['description'] as String? ?? '', questions: questions, config: config, currentAttemptIndex: json['currentAttemptIndex'] as int? ?? 0, ); } catch (e) { throw FormatException('Invalid JSON format: $e'); } } void _importDeck() { setState(() { _errorMessage = null; _isLoading = true; }); try { if (_jsonController.text.trim().isEmpty) { throw FormatException('Please enter JSON data'); } final deck = _parseDeckFromJson(_jsonController.text.trim()); if (deck == null) { throw FormatException('Failed to parse deck'); } if (deck.questions.isEmpty) { throw FormatException('Deck must contain at least one question'); } // Save deck to storage final deckStorage = DeckStorage(); deckStorage.saveDeck(deck); // Navigate back to deck list Navigator.pop(context); } catch (e) { setState(() { _errorMessage = e.toString(); _isLoading = false; }); } } void _useDefaultDeck() { final defaultDeck = DefaultDeck.deck; final deckStorage = DeckStorage(); // Check if default deck already exists if (deckStorage.hasDeck(defaultDeck.id)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Default deck already exists'), backgroundColor: Colors.orange, ), ); return; } // Save default deck to storage deckStorage.saveDeck(defaultDeck); // Navigate back to deck list Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Default deck added successfully'), backgroundColor: Colors.green, ), ); } void _loadSampleDeck() { final sampleJson = { 'id': 'sample-deck', 'title': 'Sample Practice Deck', 'description': 'This is a sample deck for practicing. It contains various questions to help you learn.', 'config': { 'requiredConsecutiveCorrect': 3, 'defaultAttemptSize': 10, 'priorityIncreaseOnIncorrect': 5, 'priorityDecreaseOnCorrect': 2, 'immediateFeedbackEnabled': true, 'includeKnownInAttempts': false, }, 'questions': List.generate(20, (i) { return { 'id': 'q$i', 'prompt': 'Sample Question $i?', 'answers': ['Answer A', 'Answer B', 'Answer C', 'Answer D'], 'correctAnswerIndices': [i % 4], 'isKnown': i < 5, }; }), }; _jsonController.text = const JsonEncoder.withIndent(' ').convert(sampleJson); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Import Deck'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Default Deck Button Card( color: Theme.of(context).colorScheme.primaryContainer, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon( Icons.quiz, color: Theme.of(context).colorScheme.onPrimaryContainer, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Start with Default Quiz', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.onPrimaryContainer, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '20 general knowledge questions ready to practice', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.8), ), ), ], ), ), ], ), const SizedBox(height: 16), FilledButton.icon( onPressed: _useDefaultDeck, icon: const Icon(Icons.play_arrow), label: const Text('Use Default Quiz'), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Theme.of(context).colorScheme.onPrimary, ), ), ], ), ), ), const SizedBox(height: 24), // Divider Row( children: [ Expanded(child: Divider(color: Theme.of(context).colorScheme.outline)), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'OR', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ), Expanded(child: Divider(color: Theme.of(context).colorScheme.outline)), ], ), const SizedBox(height: 24), Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Import Deck 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.', style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), ), const SizedBox(height: 16), // JSON Input Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Deck JSON', style: Theme.of(context).textTheme.titleMedium, ), TextButton.icon( onPressed: _loadSampleDeck, icon: const Icon(Icons.description), label: const Text('Load Sample'), ), ], ), const SizedBox(height: 8), TextField( controller: _jsonController, maxLines: 15, decoration: InputDecoration( hintText: 'Paste JSON here...', border: const OutlineInputBorder(), errorText: _errorMessage, ), style: const TextStyle( fontFamily: 'monospace', fontSize: 12, ), ), ], ), ), ), const SizedBox(height: 24), // Error Message if (_errorMessage != null) Card( color: Colors.red.shade50, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon(Icons.error, color: Colors.red.shade700), const SizedBox(width: 12), Expanded( child: Text( _errorMessage!, style: TextStyle(color: Colors.red.shade700), ), ), ], ), ), ), 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), ), ), const SizedBox(height: 16), // JSON Format Help ExpansionTile( title: const Text('JSON Format Help'), children: [ Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Required fields:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text('• id: string'), const Text('• title: string'), const Text('• description: string'), const Text('• questions: array of question objects'), const SizedBox(height: 16), const Text( 'Question object format:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text('• id: string (required)'), const Text('• prompt: string (required)'), const Text('• answers: array of strings (required)'), const Text('• correctAnswerIndex: number (deprecated, use correctAnswerIndices)'), const Text('• correctAnswerIndices: array of numbers (for multiple correct answers)'), const Text('• isKnown: boolean (optional)'), const Text('• consecutiveCorrect: number (optional)'), const Text('• priorityPoints: number (optional)'), const SizedBox(height: 16), const Text( 'Config object (optional):', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text('• requiredConsecutiveCorrect: number'), const Text('• defaultAttemptSize: number'), const Text('• priorityIncreaseOnIncorrect: number'), const Text('• priorityDecreaseOnCorrect: number'), const Text('• immediateFeedbackEnabled: boolean'), const Text('• includeKnownInAttempts: boolean'), ], ), ), ], ), ], ), ), ); } }