|
|
|
@ -118,7 +118,91 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
|
|
|
final deckStorage = DeckStorage();
|
|
|
|
final deckStorage = DeckStorage();
|
|
|
|
deckStorage.saveDeckSync(deck);
|
|
|
|
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<Deck>(
|
|
|
|
|
|
|
|
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);
|
|
|
|
Navigator.pop(context);
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
setState(() {
|
|
|
|
setState(() {
|
|
|
|
@ -391,12 +475,12 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
Text(
|
|
|
|
'Import Deck from JSON',
|
|
|
|
'Import from JSON',
|
|
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
Text(
|
|
|
|
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,
|
|
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
@ -484,8 +568,28 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
|
|
|
|
|
|
|
|
|
|
|
if (_errorMessage != null) const SizedBox(height: 16),
|
|
|
|
if (_errorMessage != null) const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
|
|
// Import Button
|
|
|
|
// Import and Merge Buttons
|
|
|
|
FilledButton.icon(
|
|
|
|
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,
|
|
|
|
onPressed: _isLoading ? null : _importDeck,
|
|
|
|
icon: _isLoading
|
|
|
|
icon: _isLoading
|
|
|
|
? const SizedBox(
|
|
|
|
? const SizedBox(
|
|
|
|
@ -493,12 +597,15 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
|
|
|
height: 20,
|
|
|
|
height: 20,
|
|
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
: const Icon(Icons.upload),
|
|
|
|
: const Icon(Icons.add_circle_outline, size: 20),
|
|
|
|
label: Text(_isLoading ? 'Importing...' : 'Import Deck'),
|
|
|
|
label: Text(_isLoading ? 'Importing...' : 'Import and create deck'),
|
|
|
|
style: FilledButton.styleFrom(
|
|
|
|
style: FilledButton.styleFrom(
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
|
|
|