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.
722 lines
25 KiB
722 lines
25 KiB
import 'dart:convert';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:practice_engine/practice_engine.dart';
|
|
import '../data/default_deck.dart';
|
|
import '../services/deck_storage.dart';
|
|
import '../utils/top_snackbar.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,
|
|
includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false,
|
|
shuffleAnswerOrder: configJson['shuffleAnswerOrder'] as bool? ?? true,
|
|
excludeFlaggedQuestions: configJson['excludeFlaggedQuestions'] as bool? ?? false,
|
|
);
|
|
|
|
// Parse questions
|
|
final questionsJson = json['questions'] as List<dynamic>? ?? [];
|
|
final questions = questionsJson.map((qJson) {
|
|
final questionMap = qJson as Map<String, dynamic>;
|
|
|
|
// Support both old format (correctAnswerIndex) and new format (correctAnswerIndices)
|
|
List<int>? correctIndices;
|
|
if (questionMap['correctAnswerIndices'] != null) {
|
|
final indicesJson = questionMap['correctAnswerIndices'] as List<dynamic>?;
|
|
correctIndices = indicesJson?.map((e) => e as int).toList();
|
|
} else if (questionMap['correctAnswerIndex'] != null) {
|
|
// Backward compatibility: convert single index to list
|
|
final singleIndex = questionMap['correctAnswerIndex'] as int?;
|
|
if (singleIndex != null) {
|
|
correctIndices = [singleIndex];
|
|
}
|
|
}
|
|
|
|
return Question(
|
|
id: questionMap['id'] as String? ?? '',
|
|
prompt: questionMap['prompt'] as String? ?? '',
|
|
answers: (questionMap['answers'] as List<dynamic>?)
|
|
?.map((e) => e.toString())
|
|
.toList() ??
|
|
[],
|
|
correctAnswerIndices: correctIndices ?? [0],
|
|
consecutiveCorrect: questionMap['consecutiveCorrect'] as int? ?? 0,
|
|
isKnown: questionMap['isKnown'] as bool? ?? false,
|
|
isFlagged: questionMap['isFlagged'] 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');
|
|
}
|
|
|
|
// Save deck to storage
|
|
final deckStorage = DeckStorage();
|
|
deckStorage.saveDeckSync(deck);
|
|
|
|
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);
|
|
} catch (e) {
|
|
setState(() {
|
|
_errorMessage = e.toString();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _useDefaultDeck() async {
|
|
final deckStorage = DeckStorage();
|
|
|
|
// Always show dialog to ask if user wants mock data
|
|
final includeMockData = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => _MockDataDialog(),
|
|
);
|
|
|
|
if (includeMockData == null) {
|
|
// User cancelled
|
|
return;
|
|
}
|
|
|
|
// Get the appropriate deck version
|
|
final defaultDeck = includeMockData
|
|
? DefaultDeck.deckWithMockData
|
|
: DefaultDeck.deck;
|
|
|
|
final deckExists = deckStorage.hasDeckSync(DefaultDeck.deck.id);
|
|
|
|
if (deckExists) {
|
|
// Show dialog with options: Replace, Add Copy, or Cancel
|
|
final action = await showDialog<String>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Deck Already Exists'),
|
|
content: const Text(
|
|
'The General Knowledge Quiz already exists. What would you like to do?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, 'cancel'),
|
|
child: const Text('Cancel'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, 'copy'),
|
|
child: const Text('Add Copy'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(context, 'replace'),
|
|
child: const Text('Replace'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (action == null || action == 'cancel') {
|
|
return;
|
|
} else if (action == 'replace') {
|
|
deckStorage.saveDeckSync(defaultDeck);
|
|
|
|
showTopSnackBar(
|
|
context,
|
|
message: 'Default deck replaced successfully',
|
|
backgroundColor: Colors.green,
|
|
);
|
|
} else if (action == 'copy') {
|
|
// Find the next copy number
|
|
final allDecks = deckStorage.getAllDecksSync();
|
|
final baseTitle = defaultDeck.title;
|
|
final copyPattern = RegExp(r'^(.+?)\s*\(Copy\s+(\d+)\)$');
|
|
|
|
int maxCopyNumber = 0;
|
|
for (final deck in allDecks) {
|
|
if (deck.title == baseTitle) {
|
|
// Original deck exists
|
|
continue;
|
|
}
|
|
final match = copyPattern.firstMatch(deck.title);
|
|
if (match != null) {
|
|
final titlePart = match.group(1)?.trim();
|
|
if (titlePart == baseTitle) {
|
|
final copyNum = int.tryParse(match.group(2) ?? '0') ?? 0;
|
|
if (copyNum > maxCopyNumber) {
|
|
maxCopyNumber = copyNum;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final nextCopyNumber = maxCopyNumber + 1;
|
|
final copiedDeck = defaultDeck.copyWith(
|
|
id: '${defaultDeck.id}_${DateTime.now().millisecondsSinceEpoch}',
|
|
title: '$baseTitle (Copy $nextCopyNumber)',
|
|
);
|
|
deckStorage.saveDeckSync(copiedDeck);
|
|
|
|
showTopSnackBar(
|
|
context,
|
|
message: 'Default deck copied successfully',
|
|
backgroundColor: Colors.green,
|
|
);
|
|
}
|
|
} else {
|
|
// Save default deck to storage
|
|
deckStorage.saveDeckSync(defaultDeck);
|
|
|
|
showTopSnackBar(
|
|
context,
|
|
message: 'Default deck added successfully',
|
|
backgroundColor: Colors.green,
|
|
);
|
|
}
|
|
|
|
// Navigate back to deck list
|
|
Navigator.pop(context);
|
|
}
|
|
|
|
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,
|
|
'includeKnownInAttempts': false,
|
|
'shuffleAnswerOrder': true,
|
|
'excludeFlaggedQuestions': false,
|
|
},
|
|
'questions': List.generate(20, (i) {
|
|
return {
|
|
'id': 'q$i',
|
|
'prompt': 'Sample Question $i?',
|
|
'answers': ['Answer A', 'Answer B', 'Answer C', 'Answer D'],
|
|
'correctAnswerIndices': [i % 4],
|
|
'isKnown': i < 5,
|
|
};
|
|
}),
|
|
};
|
|
|
|
_jsonController.text = const JsonEncoder.withIndent(' ').convert(sampleJson);
|
|
}
|
|
|
|
void _copyFieldToClipboard() {
|
|
final text = _jsonController.text.trim();
|
|
if (text.isEmpty) {
|
|
showTopSnackBar(context, message: 'Nothing to copy');
|
|
return;
|
|
}
|
|
Clipboard.setData(ClipboardData(text: text));
|
|
showTopSnackBar(context, message: 'JSON copied to clipboard');
|
|
}
|
|
|
|
Future<void> _pickJsonFile() async {
|
|
final result = await FilePicker.platform.pickFiles(
|
|
type: FileType.custom,
|
|
allowedExtensions: ['json'],
|
|
withData: true,
|
|
);
|
|
if (result == null || result.files.isEmpty || !mounted) return;
|
|
final file = result.files.single;
|
|
if (file.bytes == null) {
|
|
if (mounted) {
|
|
showTopSnackBar(context, message: 'Could not read file content');
|
|
}
|
|
return;
|
|
}
|
|
final text = utf8.decode(file.bytes!);
|
|
setState(() {
|
|
_errorMessage = null;
|
|
_jsonController.text = text;
|
|
});
|
|
if (mounted) {
|
|
showTopSnackBar(context, message: 'File loaded');
|
|
}
|
|
}
|
|
|
|
@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: [
|
|
// Default Deck Button
|
|
Card(
|
|
color: Theme.of(context).colorScheme.primaryContainer,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.quiz,
|
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Start with Default Quiz',
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'20 general knowledge questions ready to practice',
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
FilledButton.icon(
|
|
onPressed: _useDefaultDeck,
|
|
icon: const Icon(Icons.play_arrow),
|
|
label: const Text('Use Default Quiz'),
|
|
style: FilledButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Divider
|
|
Row(
|
|
children: [
|
|
Expanded(child: Divider(color: Theme.of(context).colorScheme.outline)),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Text(
|
|
'OR',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
),
|
|
Expanded(child: Divider(color: Theme.of(context).colorScheme.outline)),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Import from JSON',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Paste your deck JSON below. Create a new deck or merge questions into an existing one.',
|
|
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,
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextButton.icon(
|
|
onPressed: _pickJsonFile,
|
|
icon: const Icon(Icons.folder_open),
|
|
label: const Text('Select file'),
|
|
),
|
|
TextButton.icon(
|
|
onPressed: _loadSampleDeck,
|
|
icon: const Icon(Icons.description),
|
|
label: const Text('Load Sample'),
|
|
),
|
|
IconButton(
|
|
onPressed: _copyFieldToClipboard,
|
|
icon: const Icon(Icons.copy),
|
|
tooltip: 'Copy field contents',
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
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 and Merge Buttons
|
|
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,
|
|
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),
|
|
|
|
// 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 (deprecated, use correctAnswerIndices)'),
|
|
const Text('• correctAnswerIndices: array of numbers (for multiple correct answers)'),
|
|
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'),
|
|
const Text('• includeKnownInAttempts: boolean'),
|
|
const Text('• shuffleAnswerOrder: boolean'),
|
|
const Text('• excludeFlaggedQuestions: boolean'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MockDataDialog extends StatefulWidget {
|
|
@override
|
|
State<_MockDataDialog> createState() => _MockDataDialogState();
|
|
}
|
|
|
|
class _MockDataDialogState extends State<_MockDataDialog> {
|
|
bool _includeMockData = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Add Default Deck'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Would you like to include mock attempt history and progress data?',
|
|
style: TextStyle(fontSize: 14),
|
|
),
|
|
const SizedBox(height: 16),
|
|
CheckboxListTile(
|
|
value: _includeMockData,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_includeMockData = value ?? false;
|
|
});
|
|
},
|
|
title: const Text('Include mock data'),
|
|
subtitle: const Text(
|
|
'Adds 4 sample attempts showing progress over time',
|
|
style: TextStyle(fontSize: 12),
|
|
),
|
|
contentPadding: EdgeInsets.zero,
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, null),
|
|
child: const Text('Cancel'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(context, _includeMockData),
|
|
child: const Text('Add Deck'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|