merge decks
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../utils/top_snackbar.dart';
|
||||
import 'package:practice_engine/practice_engine.dart';
|
||||
import '../services/deck_storage.dart';
|
||||
@@ -149,6 +152,27 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
|
||||
Navigator.pop(context, updatedDeck);
|
||||
}
|
||||
|
||||
void _copyQuestionsJson() {
|
||||
if (_questionEditors.isEmpty) {
|
||||
showTopSnackBar(context, message: 'No questions to copy');
|
||||
return;
|
||||
}
|
||||
final questions = _questionEditors.map((e) => e.toQuestion()).toList();
|
||||
final list = questions.map((q) => {
|
||||
'id': q.id,
|
||||
'prompt': q.prompt,
|
||||
'answers': q.answers,
|
||||
'correctAnswerIndices': q.correctAnswerIndices,
|
||||
}).toList();
|
||||
final jsonString = const JsonEncoder.withIndent(' ').convert(list);
|
||||
Clipboard.setData(ClipboardData(text: jsonString));
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: '${questions.length} question(s) copied as JSON',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_deck == null) {
|
||||
@@ -161,6 +185,11 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
|
||||
appBar: AppBar(
|
||||
title: const Text('Edit Deck'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: _copyQuestionsJson,
|
||||
tooltip: 'Copy questions as JSON',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: _addQuestion,
|
||||
|
||||
@@ -586,7 +586,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.merge_type, size: 20),
|
||||
label: Text(_isLoading ? '...' : 'Merge with existing deck'),
|
||||
label: Text(_isLoading ? '...' : 'Merge with deck'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
|
||||
@@ -102,6 +102,68 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _mergeDeck(Deck sourceDeck) async {
|
||||
final otherDecks = _decks.where((d) => d.id != sourceDeck.id).toList();
|
||||
if (otherDecks.isEmpty) {
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'No other deck to merge with',
|
||||
backgroundColor: Colors.orange,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final targetDeck = await showDialog<Deck>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Merge with deck'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: otherDecks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final deck = otherDecks[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 (targetDeck == null || !mounted) return;
|
||||
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final mergedQuestions = [
|
||||
...targetDeck.questions,
|
||||
...sourceDeck.questions.asMap().entries.map((e) {
|
||||
final q = e.value;
|
||||
final i = e.key;
|
||||
return q.copyWith(id: '${q.id}_merged_${timestamp}_$i');
|
||||
}),
|
||||
];
|
||||
final mergedDeck = targetDeck.copyWith(questions: mergedQuestions);
|
||||
_deckStorage.saveDeckSync(mergedDeck);
|
||||
_loadDecks();
|
||||
if (mounted) {
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Merged ${sourceDeck.questions.length} question(s) into "${targetDeck.title}"',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _cloneDeck(Deck deck) {
|
||||
// Create a copy of the deck with a new ID and reset progress
|
||||
final clonedDeck = deck.copyWith(
|
||||
@@ -452,6 +514,21 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.merge_type, color: Colors.teal),
|
||||
SizedBox(width: 8),
|
||||
Text('Merge'),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() => _mergeDeck(deck),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: const Row(
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user