You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
11 KiB
302 lines
11 KiB
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<DeckImportScreen> createState() => _DeckImportScreenState();
|
|
}
|
|
|
|
class _DeckImportScreenState extends State<DeckImportScreen> {
|
|
final TextEditingController _jsonController = TextEditingController();
|
|
String? _errorMessage;
|
|
bool _isLoading = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_jsonController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Deck? _parseDeckFromJson(String jsonString) {
|
|
try {
|
|
final Map<String, dynamic> json = jsonDecode(jsonString);
|
|
|
|
// Parse config
|
|
final configJson = json['config'] as Map<String, dynamic>? ?? {};
|
|
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<dynamic>? ?? [];
|
|
final questions = questionsJson.map((qJson) {
|
|
final questionMap = qJson as Map<String, dynamic>;
|
|
return Question(
|
|
id: questionMap['id'] as String? ?? '',
|
|
prompt: questionMap['prompt'] as String? ?? '',
|
|
answers: (questionMap['answers'] as List<dynamic>?)
|
|
?.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'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|