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/flagged_questions_screen.dart

230 lines
7.2 KiB

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:practice_engine/practice_engine.dart';
import 'deck_edit_screen.dart';
import '../services/deck_storage.dart';
import '../utils/top_snackbar.dart';
/// Screen that shows flagged questions in the same editable card layout as deck edit,
/// with Save, Unflag per question, and export/copy as JSON.
class FlaggedQuestionsScreen extends StatefulWidget {
const FlaggedQuestionsScreen({super.key});
@override
State<FlaggedQuestionsScreen> createState() => _FlaggedQuestionsScreenState();
}
class _FlaggedQuestionsScreenState extends State<FlaggedQuestionsScreen> {
Deck? _deck;
final List<QuestionEditor> _editors = [];
final DeckStorage _deckStorage = DeckStorage();
String? _lastDeckId;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final deck = ModalRoute.of(context)?.settings.arguments as Deck?;
if (deck == null) return;
if (_deck?.id != deck.id || _lastDeckId != deck.id) {
_lastDeckId = deck.id;
_loadDeck(deck);
}
}
void _loadDeck(Deck deck) {
final fresh = _deckStorage.getDeckSync(deck.id) ?? deck;
setState(() {
_deck = fresh;
for (final e in _editors) {
e.dispose();
}
_editors.clear();
for (final q in fresh.flaggedQuestions) {
_editors.add(QuestionEditor.fromQuestion(q));
}
});
}
@override
void dispose() {
for (final e in _editors) {
e.dispose();
}
super.dispose();
}
void _unflag(int index) {
if (_deck == null || index < 0 || index >= _editors.length) return;
final questionId = _editors[index].originalId;
if (questionId == null) return;
final updatedQuestions = _deck!.questions.map((q) {
if (q.id == questionId) return q.copyWith(isFlagged: false);
return q;
}).toList();
final updatedDeck = _deck!.copyWith(questions: updatedQuestions);
_deckStorage.saveDeckSync(updatedDeck);
_editors[index].dispose();
setState(() {
_deck = updatedDeck;
_editors.removeAt(index);
});
showTopSnackBar(context, message: 'Question unflagged');
}
void _save() {
if (_deck == null) return;
for (int i = 0; i < _editors.length; i++) {
final editor = _editors[i];
if (!editor.isValid) {
showTopSnackBar(
context,
message: 'Question ${i + 1} is invalid. Fill in all fields.',
backgroundColor: Colors.red,
);
return;
}
}
final questionMap = Map<String, Question>.fromEntries(
_deck!.questions.map((q) => MapEntry(q.id, q)),
);
for (final editor in _editors) {
final id = editor.originalId;
if (id == null) continue;
final existing = questionMap[id];
if (existing == null) continue;
final updated = existing.copyWith(
prompt: editor.promptController.text.trim(),
answers: editor.answerControllers.map((c) => c.text.trim()).toList(),
correctAnswerIndices: editor.correctAnswerIndices.toList()..sort(),
);
questionMap[id] = updated;
}
final updatedQuestions = _deck!.questions.map((q) => questionMap[q.id] ?? q).toList();
final updatedDeck = _deck!.copyWith(questions: updatedQuestions);
_deckStorage.saveDeckSync(updatedDeck);
setState(() => _deck = updatedDeck);
showTopSnackBar(
context,
message: 'Changes saved',
backgroundColor: Colors.green,
);
}
void _copyJson() {
final questions = _editors.map((e) => e.toQuestion()).toList();
if (questions.isEmpty) {
showTopSnackBar(context, message: 'No questions to copy');
return;
}
final json = _questionsToJson(questions);
Clipboard.setData(ClipboardData(text: json));
showTopSnackBar(context, message: '${questions.length} question(s) copied as JSON');
}
void _exportJson() {
final questions = _editors.map((e) => e.toQuestion()).toList();
if (questions.isEmpty) {
showTopSnackBar(context, message: 'No questions to export');
return;
}
final json = _questionsToJson(questions);
Clipboard.setData(ClipboardData(text: json));
showTopSnackBar(
context,
message: 'JSON copied to clipboard. Paste into a file to save.',
);
}
String _questionsToJson(List<Question> questions) {
final list = questions.map((q) => {
'id': q.id,
'prompt': q.prompt,
'answers': q.answers,
'correctAnswerIndices': q.correctAnswerIndices,
}).toList();
return const JsonEncoder.withIndent(' ').convert(list);
}
@override
Widget build(BuildContext context) {
if (_deck == null) {
return Scaffold(
appBar: AppBar(title: const Text('Flagged questions')),
body: const Center(child: CircularProgressIndicator()),
);
}
final hasEditors = _editors.isNotEmpty;
return Scaffold(
appBar: AppBar(
title: Text('Flagged questions (${_editors.length})'),
actions: [
if (hasEditors) ...[
IconButton(
onPressed: _save,
icon: const Icon(Icons.save),
tooltip: 'Save',
),
IconButton(
onPressed: _copyJson,
icon: const Icon(Icons.copy),
tooltip: 'Copy as JSON',
),
IconButton(
onPressed: _exportJson,
icon: const Icon(Icons.file_download),
tooltip: 'Export as JSON',
),
],
],
),
body: !hasEditors
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.flag_outlined,
size: 64,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
),
const SizedBox(height: 16),
Text(
'No flagged questions',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Flag questions during an attempt to see them here.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
],
),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...List.generate(_editors.length, (index) {
return QuestionEditorCard(
key: ValueKey('flagged_${_editors[index].originalId}_$index'),
editor: _editors[index],
questionNumber: index + 1,
onDelete: null,
onUnflag: () => _unflag(index),
onChanged: () => setState(() {}),
);
}),
],
),
),
);
}
}

Powered by TurnKey Linux.