import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:practice_engine/practice_engine.dart'; import '../routes.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, ); // Parse questions final questionsJson = json['questions'] as List? ?? []; final questions = questionsJson.map((qJson) { final questionMap = qJson as Map; return Question( id: questionMap['id'] as String? ?? '', prompt: questionMap['prompt'] as String? ?? '', answers: (questionMap['answers'] as List?) ?.map((e) => e.toString()) .toList() ?? [], correctAnswerIndex: questionMap['correctAnswerIndex'] as int? ?? 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'); } // Navigate to deck overview with the imported deck Navigator.pushReplacementNamed( context, Routes.deckOverview, arguments: deck, ); } catch (e) { setState(() { _errorMessage = e.toString(); _isLoading = false; }); } } 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, }, 'questions': List.generate(20, (i) { return { 'id': 'q$i', 'prompt': 'Sample Question $i?', 'answers': ['Answer A', 'Answer B', 'Answer C', 'Answer D'], 'correctAnswerIndex': 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: [ 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 (required)'), 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'), ], ), ), ], ), ], ), ), ); } }