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.
decky/lib/screens/deck_import_screen.dart

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'),
],
),
),
],
),
],
),
),
);
}
}

Powered by TurnKey Linux.