parent
09cc51c059
commit
9f06568eab
@ -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();
|
|
||||||
|
static const String _decksKey = 'decks';
|
||||||
final Map<String, Deck> _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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in new issue