mock data and persistence

master
gitea 2 months ago
parent 09cc51c059
commit 9f06568eab

@ -142,5 +142,330 @@ class DefaultDeck {
config: config, config: config,
); );
} }
}
/// Default deck with mock attempt history and progress data
static Deck get deckWithMockData {
const config = DeckConfig(
requiredConsecutiveCorrect: 3,
defaultAttemptSize: 10,
priorityIncreaseOnIncorrect: 5,
priorityDecreaseOnCorrect: 2,
immediateFeedbackEnabled: true,
);
final now = DateTime.now();
// Create mock attempt history (4 previous attempts)
final attemptHistory = [
// First attempt - 6/10 correct (60%)
AttemptHistoryEntry(
timestamp: now.subtract(const Duration(days: 7)).millisecondsSinceEpoch,
totalQuestions: 10,
correctAnswers: 6,
incorrectAnswers: 4,
skippedAnswers: 0,
timeSpentSeconds: 245,
),
// Second attempt - 7/10 correct (70%)
AttemptHistoryEntry(
timestamp: now.subtract(const Duration(days: 5)).millisecondsSinceEpoch,
totalQuestions: 10,
correctAnswers: 7,
incorrectAnswers: 3,
skippedAnswers: 0,
timeSpentSeconds: 198,
),
// Third attempt - 8/10 correct (80%)
AttemptHistoryEntry(
timestamp: now.subtract(const Duration(days: 3)).millisecondsSinceEpoch,
totalQuestions: 10,
correctAnswers: 8,
incorrectAnswers: 2,
skippedAnswers: 0,
timeSpentSeconds: 187,
),
// Fourth attempt - 9/10 correct (90%)
AttemptHistoryEntry(
timestamp: now.subtract(const Duration(days: 1)).millisecondsSinceEpoch,
totalQuestions: 10,
correctAnswers: 9,
incorrectAnswers: 1,
skippedAnswers: 0,
timeSpentSeconds: 165,
),
];
final questions = [
// Question 1 - Known (answered correctly 3+ times)
Question(
id: 'gk_1',
prompt: 'What is the capital city of Australia?',
answers: ['Sydney', 'Melbourne', 'Canberra', 'Perth'],
correctAnswerIndices: [2],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 2 - Known
Question(
id: 'gk_2',
prompt: 'Which planet is known as the Red Planet?',
answers: ['Venus', 'Mars', 'Jupiter', 'Saturn'],
correctAnswerIndices: [1],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 3 - Known
Question(
id: 'gk_3',
prompt: 'What is the largest ocean on Earth?',
answers: ['Atlantic Ocean', 'Indian Ocean', 'Arctic Ocean', 'Pacific Ocean'],
correctAnswerIndices: [3],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 4 - Known
Question(
id: 'gk_4',
prompt: 'Who wrote the novel "1984"?',
answers: ['George Orwell', 'Aldous Huxley', 'Ray Bradbury', 'J.D. Salinger'],
correctAnswerIndices: [0],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 5 - Known
Question(
id: 'gk_5',
prompt: 'What is the chemical symbol for gold?',
answers: ['Go', 'Gd', 'Au', 'Ag'],
correctAnswerIndices: [2],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 6 - Progress (2 consecutive correct, not yet known)
Question(
id: 'gk_6',
prompt: 'In which year did World War II end?',
answers: ['1943', '1944', '1945', '1946'],
correctAnswerIndices: [2],
consecutiveCorrect: 2,
isKnown: false,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 3,
totalAttempts: 4,
),
// Question 7 - Progress (1 consecutive correct)
Question(
id: 'gk_7',
prompt: 'What is the smallest prime number?',
answers: ['0', '1', '2', '3'],
correctAnswerIndices: [2],
consecutiveCorrect: 1,
isKnown: false,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 2,
totalAttempts: 4,
),
// Question 8 - Some mistakes (has priority points from incorrect answers)
Question(
id: 'gk_8',
prompt: 'Which gas makes up approximately 78% of Earth\'s atmosphere?',
answers: ['Oxygen', 'Carbon Dioxide', 'Nitrogen', 'Argon'],
correctAnswerIndices: [2],
consecutiveCorrect: 0,
isKnown: false,
priorityPoints: 5,
lastAttemptIndex: 2,
totalCorrectAttempts: 2,
totalAttempts: 4,
),
// Question 9 - Known
Question(
id: 'gk_9',
prompt: 'What is the longest river in the world?',
answers: ['Amazon River', 'Nile River', 'Yangtze River', 'Mississippi River'],
correctAnswerIndices: [1],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 10 - Known
Question(
id: 'gk_10',
prompt: 'Who painted the Mona Lisa?',
answers: ['Vincent van Gogh', 'Pablo Picasso', 'Leonardo da Vinci', 'Michelangelo'],
correctAnswerIndices: [2],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 11 - Progress
Question(
id: 'gk_11',
prompt: 'What is the speed of light in a vacuum (approximately)?',
answers: ['300,000 km/s', '150,000 km/s', '450,000 km/s', '600,000 km/s'],
correctAnswerIndices: [0],
consecutiveCorrect: 2,
isKnown: false,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 3,
totalAttempts: 4,
),
// Question 12 - Known
Question(
id: 'gk_12',
prompt: 'Which country is home to the kangaroo?',
answers: ['New Zealand', 'Australia', 'South Africa', 'Brazil'],
correctAnswerIndices: [1],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 13 - Some mistakes
Question(
id: 'gk_13',
prompt: 'What is the hardest natural substance on Earth?',
answers: ['Gold', 'Diamond', 'Platinum', 'Titanium'],
correctAnswerIndices: [1],
consecutiveCorrect: 0,
isKnown: false,
priorityPoints: 3,
lastAttemptIndex: 1,
totalCorrectAttempts: 1,
totalAttempts: 4,
),
// Question 14 - Progress
Question(
id: 'gk_14',
prompt: 'How many continents are there on Earth?',
answers: ['5', '6', '7', '8'],
correctAnswerIndices: [2],
consecutiveCorrect: 1,
isKnown: false,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 2,
totalAttempts: 4,
),
// Question 15 - Known
Question(
id: 'gk_15',
prompt: 'What is the largest mammal in the world?',
answers: ['African Elephant', 'Blue Whale', 'Giraffe', 'Polar Bear'],
correctAnswerIndices: [1],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 16 - Known
Question(
id: 'gk_16',
prompt: 'In which city is the Eiffel Tower located?',
answers: ['London', 'Berlin', 'Paris', 'Rome'],
correctAnswerIndices: [2],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 17 - Known
Question(
id: 'gk_17',
prompt: 'What is the square root of 64?',
answers: ['6', '7', '8', '9'],
correctAnswerIndices: [2],
consecutiveCorrect: 3,
isKnown: true,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 4,
totalAttempts: 4,
),
// Question 18 - Progress
Question(
id: 'gk_18',
prompt: 'Which element has the atomic number 1?',
answers: ['Helium', 'Hydrogen', 'Lithium', 'Carbon'],
correctAnswerIndices: [1],
consecutiveCorrect: 2,
isKnown: false,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 3,
totalAttempts: 4,
),
// Question 19 - Some mistakes
Question(
id: 'gk_19',
prompt: 'What is the largest desert in the world?',
answers: ['Gobi Desert', 'Sahara Desert', 'Antarctic Desert', 'Arabian Desert'],
correctAnswerIndices: [2],
consecutiveCorrect: 0,
isKnown: false,
priorityPoints: 5,
lastAttemptIndex: 0,
totalCorrectAttempts: 1,
totalAttempts: 4,
),
// Question 20 - Progress
Question(
id: 'gk_20',
prompt: 'Who invented the telephone?',
answers: ['Thomas Edison', 'Alexander Graham Bell', 'Nikola Tesla', 'Guglielmo Marconi'],
correctAnswerIndices: [1],
consecutiveCorrect: 1,
isKnown: false,
priorityPoints: 0,
lastAttemptIndex: 3,
totalCorrectAttempts: 2,
totalAttempts: 4,
),
];
return Deck(
id: 'default-general-knowledge',
title: 'General Knowledge Quiz',
description: 'A collection of 20 general knowledge questions covering science, geography, history, and more. Perfect for testing your knowledge!',
questions: questions,
config: config,
currentAttemptIndex: 4, // 4 attempts have been made
attemptHistory: attemptHistory,
);
}
}

@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'routes.dart'; import 'routes.dart';
import 'theme.dart'; import 'theme.dart';
import 'services/deck_storage.dart';
void main() { void main() {
// Initialize storage early
DeckStorage().initialize();
runApp(const DeckyApp()); runApp(const DeckyApp());
} }

@ -73,7 +73,7 @@ class _AttemptResultScreenState extends State<AttemptResultScreen> {
void _done() { void _done() {
if (_deck == null) return; if (_deck == null) return;
// Save the updated deck to storage // Save the updated deck to storage
_deckStorage.saveDeck(_deck!); _deckStorage.saveDeckSync(_deck!);
// Navigate back to deck list // Navigate back to deck list
Navigator.pushNamedAndRemoveUntil( Navigator.pushNamedAndRemoveUntil(
context, context,

@ -449,7 +449,7 @@ class _AttemptScreenState extends State<AttemptScreen> {
// Update deck with incomplete attempt // Update deck with incomplete attempt
final updatedDeck = _deck!.copyWith(incompleteAttempt: incompleteAttempt); final updatedDeck = _deck!.copyWith(incompleteAttempt: incompleteAttempt);
_deckStorage.saveDeck(updatedDeck); _deckStorage.saveDeckSync(updatedDeck);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
@ -485,7 +485,7 @@ class _AttemptScreenState extends State<AttemptScreen> {
); );
// Save the updated deck to storage // Save the updated deck to storage
_deckStorage.saveDeck(updatedDeckWithHistory); _deckStorage.saveDeckSync(updatedDeckWithHistory);
Navigator.pushReplacementNamed( Navigator.pushReplacementNamed(
context, context,

@ -225,7 +225,7 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
if (updatedDeck != null && updatedDeck is Deck && mounted) { if (updatedDeck != null && updatedDeck is Deck && mounted) {
// Reload the deck from storage to get the latest state // Reload the deck from storage to get the latest state
final deckStorage = DeckStorage(); final deckStorage = DeckStorage();
final refreshedDeck = deckStorage.getDeck(updatedDeck.id); final refreshedDeck = deckStorage.getDeckSync(updatedDeck.id);
if (refreshedDeck != null) { if (refreshedDeck != null) {
setState(() { setState(() {
_deck = refreshedDeck; _deck = refreshedDeck;
@ -272,7 +272,7 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
// Save the reset deck to storage // Save the reset deck to storage
final deckStorage = DeckStorage(); final deckStorage = DeckStorage();
deckStorage.saveDeck(resetDeck); deckStorage.saveDeckSync(resetDeck);
// Show success message // Show success message
if (mounted) { if (mounted) {

@ -96,7 +96,7 @@ class _DeckCreateScreenState extends State<DeckCreateScreen> {
attemptHistory: [], attemptHistory: [],
); );
_deckStorage.saveDeck(newDeck); _deckStorage.saveDeckSync(newDeck);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(

@ -118,7 +118,7 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
currentAttemptIndex: _deck!.currentAttemptIndex, currentAttemptIndex: _deck!.currentAttemptIndex,
); );
_deckStorage.saveDeck(updatedDeck); _deckStorage.saveDeckSync(updatedDeck);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(

@ -109,7 +109,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
// Save deck to storage // Save deck to storage
final deckStorage = DeckStorage(); final deckStorage = DeckStorage();
deckStorage.saveDeck(deck); deckStorage.saveDeckSync(deck);
// Navigate back to deck list // Navigate back to deck list
Navigator.pop(context); Navigator.pop(context);
@ -121,12 +121,11 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
} }
} }
void _useDefaultDeck() { void _useDefaultDeck() async {
final defaultDeck = DefaultDeck.deck;
final deckStorage = DeckStorage(); final deckStorage = DeckStorage();
// Check if default deck already exists // Check if default deck already exists
if (deckStorage.hasDeck(defaultDeck.id)) { if (deckStorage.hasDeckSync(DefaultDeck.deck.id)) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Default deck already exists'), content: Text('Default deck already exists'),
@ -136,8 +135,24 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
return; return;
} }
// 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;
// Save default deck to storage // Save default deck to storage
deckStorage.saveDeck(defaultDeck); deckStorage.saveDeckSync(defaultDeck);
// Navigate back to deck list // Navigate back to deck list
Navigator.pop(context); Navigator.pop(context);
@ -420,3 +435,55 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
} }
} }
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'),
),
],
);
}
}

@ -22,8 +22,17 @@ class _DeckListScreenState extends State<DeckListScreen> {
} }
void _loadDecks() { void _loadDecks() {
// Use sync version for immediate UI update, will work once storage is initialized
setState(() { setState(() {
_decks = _deckStorage.getAllDecks(); _decks = _deckStorage.getAllDecksSync();
});
// Also trigger async load to ensure we have latest data
_deckStorage.getAllDecks().then((decks) {
if (mounted) {
setState(() {
_decks = decks;
});
}
}); });
} }
@ -61,7 +70,7 @@ class _DeckListScreenState extends State<DeckListScreen> {
); );
if (confirmed == true) { if (confirmed == true) {
_deckStorage.deleteDeck(deck.id); _deckStorage.deleteDeckSync(deck.id);
_loadDecks(); _loadDecks();
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -112,7 +121,7 @@ class _DeckListScreenState extends State<DeckListScreen> {
}).toList(), }).toList(),
); );
_deckStorage.saveDeck(clonedDeck); _deckStorage.saveDeckSync(clonedDeck);
_loadDecks(); _loadDecks();
if (mounted) { if (mounted) {
@ -181,16 +190,36 @@ class _DeckListScreenState extends State<DeckListScreen> {
); );
} }
void _useDefaultDeck() { void _useDefaultDeck() async {
final defaultDeck = DefaultDeck.deck;
// Check if default deck already exists // Check if default deck already exists
if (_deckStorage.hasDeck(defaultDeck.id)) { final baseDeck = DefaultDeck.deck;
final deckExists = _deckStorage.hasDeckSync(baseDeck.id);
// Show dialog to ask if user wants mock data (only if deck doesn't exist)
bool? includeMockData = false;
if (!deckExists) {
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;
if (deckExists) {
// If it exists, create a copy with a new ID // If it exists, create a copy with a new ID
final clonedDeck = defaultDeck.copyWith( final clonedDeck = defaultDeck.copyWith(
id: '${defaultDeck.id}_${DateTime.now().millisecondsSinceEpoch}', id: '${defaultDeck.id}_${DateTime.now().millisecondsSinceEpoch}',
); );
_deckStorage.saveDeck(clonedDeck); _deckStorage.saveDeckSync(clonedDeck);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -202,7 +231,7 @@ class _DeckListScreenState extends State<DeckListScreen> {
} }
} else { } else {
// Save default deck to storage // Save default deck to storage
_deckStorage.saveDeck(defaultDeck); _deckStorage.saveDeckSync(defaultDeck);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -421,3 +450,55 @@ class _StatChip extends StatelessWidget {
} }
} }
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'),
),
],
);
}
}

@ -24,7 +24,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
// ALWAYS load the latest version from storage to ensure we have the most up-to-date deck // ALWAYS load the latest version from storage to ensure we have the most up-to-date deck
// This is critical for getting the latest incomplete attempt state // This is critical for getting the latest incomplete attempt state
// Don't rely on route arguments - they might be stale // Don't rely on route arguments - they might be stale
final storedDeck = _deckStorage.getDeck(args.id); final storedDeck = _deckStorage.getDeckSync(args.id);
final deckToUse = storedDeck ?? args; final deckToUse = storedDeck ?? args;
// Always update to get the latest state from storage // Always update to get the latest state from storage
@ -37,7 +37,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
void _saveDeck() { void _saveDeck() {
if (_deck != null) { if (_deck != null) {
_deckStorage.saveDeck(_deck!); _deckStorage.saveDeckSync(_deck!);
} }
} }
@ -50,7 +50,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
// Force reload from storage before checking for incomplete attempts to ensure we have latest state // Force reload from storage before checking for incomplete attempts to ensure we have latest state
Deck? deckToCheck = _deck; Deck? deckToCheck = _deck;
if (_deck != null) { if (_deck != null) {
final freshDeck = _deckStorage.getDeck(_deck!.id); final freshDeck = _deckStorage.getDeckSync(_deck!.id);
if (freshDeck != null) { if (freshDeck != null) {
setState(() { setState(() {
_deck = freshDeck; _deck = freshDeck;
@ -117,8 +117,8 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
final updatedDeck = deckToCheck.copyWith(clearIncompleteAttempt: true); final updatedDeck = deckToCheck.copyWith(clearIncompleteAttempt: true);
// Save to storage multiple times to ensure it's persisted // Save to storage multiple times to ensure it's persisted
_deckStorage.saveDeck(updatedDeck); _deckStorage.saveDeckSync(updatedDeck);
_deckStorage.saveDeck(updatedDeck); // Save twice to be sure _deckStorage.saveDeckSync(updatedDeck); // Save twice to be sure
// Update local state immediately with cleared deck // Update local state immediately with cleared deck
setState(() { setState(() {
@ -128,13 +128,13 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
// Force reload from storage multiple times to verify it's cleared // Force reload from storage multiple times to verify it's cleared
Deck? finalClearedDeck = updatedDeck; Deck? finalClearedDeck = updatedDeck;
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
final verifiedDeck = _deckStorage.getDeck(updatedDeck.id); final verifiedDeck = _deckStorage.getDeckSync(updatedDeck.id);
if (verifiedDeck != null) { if (verifiedDeck != null) {
// Ensure incomplete attempt is null even if storage had it // Ensure incomplete attempt is null even if storage had it
if (verifiedDeck.incompleteAttempt != null) { if (verifiedDeck.incompleteAttempt != null) {
final clearedDeck = verifiedDeck.copyWith(clearIncompleteAttempt: true); final clearedDeck = verifiedDeck.copyWith(clearIncompleteAttempt: true);
_deckStorage.saveDeck(clearedDeck); _deckStorage.saveDeckSync(clearedDeck);
setState(() { setState(() {
_deck = clearedDeck; _deck = clearedDeck;
}); });
@ -211,7 +211,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
// Also ensure storage has the cleared state // Also ensure storage has the cleared state
final deckToUse = deckToCheck.copyWith(clearIncompleteAttempt: true); final deckToUse = deckToCheck.copyWith(clearIncompleteAttempt: true);
// Ensure storage also has the cleared state // Ensure storage also has the cleared state
_deckStorage.saveDeck(deckToUse); _deckStorage.saveDeckSync(deckToUse);
Navigator.pushNamed( Navigator.pushNamed(
context, context,
Routes.attempt, Routes.attempt,
@ -226,7 +226,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
void _refreshDeck() { void _refreshDeck() {
if (_deck != null) { if (_deck != null) {
// Reload the deck from storage to get the latest state (including incomplete attempts) // Reload the deck from storage to get the latest state (including incomplete attempts)
final refreshedDeck = _deckStorage.getDeck(_deck!.id); final refreshedDeck = _deckStorage.getDeckSync(_deck!.id);
if (refreshedDeck != null && mounted) { if (refreshedDeck != null && mounted) {
setState(() { setState(() {
_deck = refreshedDeck; _deck = refreshedDeck;

@ -1,61 +1,353 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:practice_engine/practice_engine.dart'; import 'package:practice_engine/practice_engine.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../data/default_deck.dart'; import '../data/default_deck.dart';
/// Service for managing deck storage and retrieval. /// Service for managing deck storage and retrieval.
/// Currently uses in-memory storage. Can be extended to use persistent storage. /// Uses persistent storage via shared_preferences.
class DeckStorage { class DeckStorage {
static final DeckStorage _instance = DeckStorage._internal(); static final DeckStorage _instance = DeckStorage._internal();
factory DeckStorage() => _instance; factory DeckStorage() => _instance;
DeckStorage._internal();
final Map<String, Deck> _decks = {}; static const String _decksKey = 'decks';
static const String _deckIdsKey = 'deck_ids';
bool _initialized = false; bool _initialized = false;
SharedPreferences? _prefs;
final Future<void> _initFuture;
DeckStorage._internal() : _initFuture = SharedPreferences.getInstance().then((prefs) {
_instance._prefs = prefs;
_instance._initialized = true;
});
/// Initialize storage with default deck if empty /// Initialize storage with default deck if empty
void initialize() { Future<void> initialize() async {
if (_initialized) return; if (_initialized) return;
_initialized = true; await _initFuture;
// Load existing decks
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
// Add default deck if no decks exist // Add default deck if no decks exist
if (_decks.isEmpty) { if (deckIds.isEmpty) {
final defaultDeck = DefaultDeck.deck; final defaultDeck = DefaultDeck.deck;
_decks[defaultDeck.id] = defaultDeck; await saveDeck(defaultDeck);
} }
} }
/// Ensure storage is initialized
Future<void> _ensureInitialized() async {
if (!_initialized) {
await _initFuture;
await initialize();
}
}
/// Convert Deck to JSON Map
Map<String, dynamic> _deckToJson(Deck deck) {
return {
'id': deck.id,
'title': deck.title,
'description': deck.description,
'currentAttemptIndex': deck.currentAttemptIndex,
'config': {
'requiredConsecutiveCorrect': deck.config.requiredConsecutiveCorrect,
'defaultAttemptSize': deck.config.defaultAttemptSize,
'priorityIncreaseOnIncorrect': deck.config.priorityIncreaseOnIncorrect,
'priorityDecreaseOnCorrect': deck.config.priorityDecreaseOnCorrect,
'immediateFeedbackEnabled': deck.config.immediateFeedbackEnabled,
'includeKnownInAttempts': deck.config.includeKnownInAttempts,
'timeLimitSeconds': deck.config.timeLimitSeconds,
},
'questions': deck.questions.map((q) => {
'id': q.id,
'prompt': q.prompt,
'answers': q.answers,
'correctAnswerIndices': q.correctAnswerIndices,
'consecutiveCorrect': q.consecutiveCorrect,
'isKnown': q.isKnown,
'priorityPoints': q.priorityPoints,
'lastAttemptIndex': q.lastAttemptIndex,
'totalCorrectAttempts': q.totalCorrectAttempts,
'totalAttempts': q.totalAttempts,
}).toList(),
'attemptHistory': deck.attemptHistory.map((entry) => {
'timestamp': entry.timestamp,
'totalQuestions': entry.totalQuestions,
'correctAnswers': entry.correctAnswers,
'incorrectAnswers': entry.incorrectAnswers,
'skippedAnswers': entry.skippedAnswers,
'timeSpentSeconds': entry.timeSpentSeconds,
}).toList(),
'incompleteAttempt': deck.incompleteAttempt != null ? {
'attemptId': deck.incompleteAttempt!.attemptId,
'questionIds': deck.incompleteAttempt!.questionIds,
'startTime': deck.incompleteAttempt!.startTime,
'currentQuestionIndex': deck.incompleteAttempt!.currentQuestionIndex,
'answers': deck.incompleteAttempt!.answers,
'manualOverrides': deck.incompleteAttempt!.manualOverrides,
'pausedAt': deck.incompleteAttempt!.pausedAt,
'remainingSeconds': deck.incompleteAttempt!.remainingSeconds,
} : null,
};
}
/// Convert JSON Map to Deck
Deck _jsonToDeck(Map<String, dynamic> json) {
// 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,
timeLimitSeconds: configJson['timeLimitSeconds'] as int?,
);
// Parse questions
final questionsJson = json['questions'] as List<dynamic>? ?? [];
final questions = questionsJson.map((qJson) {
final questionMap = qJson as Map<String, dynamic>;
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
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,
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();
// Parse attempt history
final historyJson = json['attemptHistory'] as List<dynamic>? ?? [];
final attemptHistory = historyJson.map((entryJson) {
final entryMap = entryJson as Map<String, dynamic>;
return AttemptHistoryEntry(
timestamp: entryMap['timestamp'] as int? ?? 0,
totalQuestions: entryMap['totalQuestions'] as int? ?? 0,
correctAnswers: entryMap['correctAnswers'] as int? ?? 0,
incorrectAnswers: entryMap['incorrectAnswers'] as int? ?? 0,
skippedAnswers: entryMap['skippedAnswers'] as int? ?? 0,
timeSpentSeconds: entryMap['timeSpentSeconds'] as int?,
);
}).toList();
// Parse incomplete attempt
IncompleteAttempt? incompleteAttempt;
if (json['incompleteAttempt'] != null) {
final incompleteJson = json['incompleteAttempt'] as Map<String, dynamic>;
incompleteAttempt = IncompleteAttempt(
attemptId: incompleteJson['attemptId'] as String? ?? '',
questionIds: (incompleteJson['questionIds'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
startTime: incompleteJson['startTime'] as int? ?? 0,
currentQuestionIndex: incompleteJson['currentQuestionIndex'] as int? ?? 0,
answers: Map<String, dynamic>.from(
incompleteJson['answers'] as Map<String, dynamic>? ?? {},
),
manualOverrides: Map<String, bool>.from(
(incompleteJson['manualOverrides'] as Map<String, dynamic>?)?.map(
(key, value) => MapEntry(key, value as bool),
) ?? {},
),
pausedAt: incompleteJson['pausedAt'] as int? ?? 0,
remainingSeconds: incompleteJson['remainingSeconds'] as int?,
);
}
// 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,
attemptHistory: attemptHistory,
incompleteAttempt: incompleteAttempt,
);
}
/// Get all decks /// Get all decks
List<Deck> getAllDecks() { Future<List<Deck>> getAllDecks() async {
initialize(); await _ensureInitialized();
return _decks.values.toList(); final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
final decks = <Deck>[];
for (final id in deckIds) {
final deck = await getDeck(id);
if (deck != null) {
decks.add(deck);
}
}
return decks;
}
/// Synchronous version for backward compatibility
List<Deck> getAllDecksSync() {
if (!_initialized || _prefs == null) {
// Trigger async initialization but return empty list for now
_ensureInitialized();
return [];
}
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
final decks = <Deck>[];
for (final id in deckIds) {
final deck = getDeckSync(id);
if (deck != null) {
decks.add(deck);
}
}
return decks;
} }
/// Get a deck by ID /// Get a deck by ID
Deck? getDeck(String id) { Future<Deck?> getDeck(String id) async {
initialize(); await _ensureInitialized();
return _decks[id]; final deckJson = _prefs!.getString('$_decksKey:$id');
if (deckJson == null) return null;
try {
final json = jsonDecode(deckJson) as Map<String, dynamic>;
return _jsonToDeck(json);
} catch (e) {
debugPrint('Error loading deck $id: $e');
return null;
}
}
/// Synchronous version for backward compatibility
Deck? getDeckSync(String id) {
if (!_initialized || _prefs == null) {
// Trigger async initialization but return null for now
_ensureInitialized();
return null;
}
final deckJson = _prefs!.getString('$_decksKey:$id');
if (deckJson == null) return null;
try {
final json = jsonDecode(deckJson) as Map<String, dynamic>;
return _jsonToDeck(json);
} catch (e) {
debugPrint('Error loading deck $id: $e');
return null;
}
} }
/// Add or update a deck /// Add or update a deck
void saveDeck(Deck deck) { Future<void> saveDeck(Deck deck) async {
initialize(); await _ensureInitialized();
_decks[deck.id] = deck; final deckJson = jsonEncode(_deckToJson(deck));
await _prefs!.setString('$_decksKey:${deck.id}', deckJson);
// Update deck IDs list
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
if (!deckIds.contains(deck.id)) {
deckIds.add(deck.id);
await _prefs!.setStringList(_deckIdsKey, deckIds);
}
}
/// Synchronous version for backward compatibility
void saveDeckSync(Deck deck) {
if (!_initialized || _prefs == null) {
// Queue for async save
_ensureInitialized().then((_) => saveDeck(deck));
return;
}
final deckJson = jsonEncode(_deckToJson(deck));
_prefs!.setString('$_decksKey:${deck.id}', deckJson);
// Update deck IDs list
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
if (!deckIds.contains(deck.id)) {
deckIds.add(deck.id);
_prefs!.setStringList(_deckIdsKey, deckIds);
}
} }
/// Delete a deck /// Delete a deck
void deleteDeck(String id) { Future<void> deleteDeck(String id) async {
_decks.remove(id); await _ensureInitialized();
await _prefs!.remove('$_decksKey:$id');
// Update deck IDs list
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
deckIds.remove(id);
await _prefs!.setStringList(_deckIdsKey, deckIds);
}
/// Synchronous version for backward compatibility
void deleteDeckSync(String id) {
if (!_initialized || _prefs == null) {
// Queue for async delete
_ensureInitialized().then((_) => deleteDeck(id));
return;
}
_prefs!.remove('$_decksKey:$id');
// Update deck IDs list
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
deckIds.remove(id);
_prefs!.setStringList(_deckIdsKey, deckIds);
} }
/// Check if a deck exists /// Check if a deck exists
bool hasDeck(String id) { Future<bool> hasDeck(String id) async {
initialize(); await _ensureInitialized();
return _decks.containsKey(id); return _prefs!.containsKey('$_decksKey:$id');
}
/// Synchronous version for backward compatibility
bool hasDeckSync(String id) {
if (!_initialized || _prefs == null) return false;
return _prefs!.containsKey('$_decksKey:$id');
} }
/// Get the number of decks /// Get the number of decks
int get deckCount { Future<int> get deckCount async {
initialize(); await _ensureInitialized();
return _decks.length; final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
return deckIds.length;
} }
}
/// Synchronous version for backward compatibility
int get deckCountSync {
if (!_initialized || _prefs == null) return 0;
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
return deckIds.length;
}
}

@ -12,6 +12,7 @@ dependencies:
practice_engine: practice_engine:
path: packages/practice_engine path: packages/practice_engine
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
shared_preferences: ^2.2.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

Loading…
Cancel
Save

Powered by TurnKey Linux.