import and merge

master
gitea 4 weeks ago
parent 1ae9413c71
commit 25b005db3a

@ -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,20 +568,43 @@ 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(
onPressed: _isLoading ? null : _importDeck, children: [
icon: _isLoading Expanded(
? const SizedBox( child: OutlinedButton.icon(
width: 20, onPressed: _isLoading ? null : _mergeWithExistingDeck,
height: 20, icon: _isLoading
child: CircularProgressIndicator(strokeWidth: 2), ? const SizedBox(
) width: 20,
: const Icon(Icons.upload), height: 20,
label: Text(_isLoading ? 'Importing...' : 'Import Deck'), child: CircularProgressIndicator(strokeWidth: 2),
style: FilledButton.styleFrom( )
padding: const EdgeInsets.symmetric(vertical: 16), : 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,
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.add_circle_outline, size: 20),
label: Text(_isLoading ? 'Importing...' : 'Import and create deck'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

@ -73,6 +73,68 @@ class _FlaggedQuestionsScreenState extends State<FlaggedQuestionsScreen> {
showTopSnackBar(context, message: 'Question unflagged'); showTopSnackBar(context, message: 'Question unflagged');
} }
void _removeQuestion(int index) {
if (_deck == null || index < 0 || index >= _editors.length) return;
final questionId = _editors[index].originalId;
if (questionId == null) return;
final updatedQuestions =
_deck!.questions.where((q) => q.id != questionId).toList();
final updatedDeck = _deck!.copyWith(questions: updatedQuestions);
_deckStorage.saveDeckSync(updatedDeck);
_editors[index].dispose();
setState(() {
_deck = updatedDeck;
_editors.removeAt(index);
});
showTopSnackBar(context, message: 'Question removed from deck');
}
Future<void> _removeAllQuestions() async {
if (_deck == null || _editors.isEmpty) return;
final count = _editors.length;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Remove all flagged questions?'),
content: Text(
'Remove all $count flagged question${count == 1 ? '' : 's'} from the deck? This cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('Remove all'),
),
],
),
);
if (confirmed != true || !mounted) return;
final idsToRemove =
_editors.map((e) => e.originalId).whereType<String>().toSet();
final updatedQuestions =
_deck!.questions.where((q) => !idsToRemove.contains(q.id)).toList();
final updatedDeck = _deck!.copyWith(questions: updatedQuestions);
_deckStorage.saveDeckSync(updatedDeck);
for (final e in _editors) {
e.dispose();
}
setState(() {
_deck = updatedDeck;
_editors.clear();
});
showTopSnackBar(
context,
message: '$count question${count == 1 ? '' : 's'} removed from deck',
backgroundColor: Colors.green,
);
}
void _save() { void _save() {
if (_deck == null) return; if (_deck == null) return;
for (int i = 0; i < _editors.length; i++) { for (int i = 0; i < _editors.length; i++) {
@ -162,6 +224,11 @@ class _FlaggedQuestionsScreenState extends State<FlaggedQuestionsScreen> {
title: Text('Flagged questions (${_editors.length})'), title: Text('Flagged questions (${_editors.length})'),
actions: [ actions: [
if (hasEditors) ...[ if (hasEditors) ...[
IconButton(
onPressed: _removeAllQuestions,
icon: const Icon(Icons.delete_forever),
tooltip: 'Remove all from deck',
),
IconButton( IconButton(
onPressed: _save, onPressed: _save,
icon: const Icon(Icons.save), icon: const Icon(Icons.save),
@ -216,7 +283,7 @@ class _FlaggedQuestionsScreenState extends State<FlaggedQuestionsScreen> {
key: ValueKey('flagged_${_editors[index].originalId}_$index'), key: ValueKey('flagged_${_editors[index].originalId}_$index'),
editor: _editors[index], editor: _editors[index],
questionNumber: index + 1, questionNumber: index + 1,
onDelete: null, onDelete: () => _removeQuestion(index),
onUnflag: () => _unflag(index), onUnflag: () => _unflag(index),
onChanged: () => setState(() {}), onChanged: () => setState(() {}),
); );

Loading…
Cancel
Save

Powered by TurnKey Linux.