326 lines
9.7 KiB
Dart
326 lines
9.7 KiB
Dart
import 'dart:convert';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../models/evaluation_history.dart';
|
|
|
|
/// Service for managing local storage using SharedPreferences
|
|
///
|
|
/// Handles storage of evaluation history, cache management,
|
|
/// and other persistent data (excluding secure credentials)
|
|
///
|
|
/// Usage example:
|
|
/// ```dart
|
|
/// final storageService = StorageService();
|
|
///
|
|
/// // Save evaluation history
|
|
/// await storageService.saveEvaluationHistory(history);
|
|
///
|
|
/// // Load evaluation history
|
|
/// final histories = await storageService.loadEvaluationHistory();
|
|
///
|
|
/// // Clear all data
|
|
/// await storageService.clearAllData();
|
|
/// ```
|
|
class StorageService {
|
|
static const String _evaluationHistoryKey = 'evaluation_history';
|
|
static const String _lastSyncTimeKey = 'last_sync_time';
|
|
static const String _cacheVersionKey = 'cache_version';
|
|
static const int _maxHistoryItems = 100; // Maximum history items to keep
|
|
|
|
/// Save evaluation history to local storage
|
|
///
|
|
/// Appends new history item to existing list
|
|
/// Automatically trims list if it exceeds max items
|
|
///
|
|
/// [history] - The evaluation history to save
|
|
Future<void> saveEvaluationHistory(EvaluationHistory history) async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
// Load existing history
|
|
final histories = await loadEvaluationHistory();
|
|
|
|
// Add new history at the beginning
|
|
histories.insert(0, history);
|
|
|
|
// Trim if exceeds max items
|
|
if (histories.length > _maxHistoryItems) {
|
|
histories.removeRange(_maxHistoryItems, histories.length);
|
|
}
|
|
|
|
// Convert to JSON and save
|
|
final jsonList = histories.map((h) => h.toJson()).toList();
|
|
final jsonString = jsonEncode(jsonList);
|
|
await prefs.setString(_evaluationHistoryKey, jsonString);
|
|
} catch (e) {
|
|
throw StorageException('Failed to save evaluation history: $e');
|
|
}
|
|
}
|
|
|
|
/// Save multiple evaluation histories at once
|
|
///
|
|
/// Useful for batch operations
|
|
///
|
|
/// [histories] - List of evaluation histories to save
|
|
Future<void> saveEvaluationHistories(
|
|
List<EvaluationHistory> histories,
|
|
) async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
// Load existing history
|
|
final existingHistories = await loadEvaluationHistory();
|
|
|
|
// Merge new histories at the beginning
|
|
final mergedHistories = [...histories, ...existingHistories];
|
|
|
|
// Trim if exceeds max items
|
|
final trimmedHistories = mergedHistories.length > _maxHistoryItems
|
|
? mergedHistories.sublist(0, _maxHistoryItems)
|
|
: mergedHistories;
|
|
|
|
// Convert to JSON and save
|
|
final jsonList = trimmedHistories.map((h) => h.toJson()).toList();
|
|
final jsonString = jsonEncode(jsonList);
|
|
await prefs.setString(_evaluationHistoryKey, jsonString);
|
|
} catch (e) {
|
|
throw StorageException('Failed to save evaluation histories: $e');
|
|
}
|
|
}
|
|
|
|
/// Load evaluation history from local storage
|
|
///
|
|
/// Returns list of evaluation histories sorted by timestamp (newest first)
|
|
/// Returns empty list if no history exists
|
|
Future<List<EvaluationHistory>> loadEvaluationHistory() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final jsonString = prefs.getString(_evaluationHistoryKey);
|
|
|
|
if (jsonString == null || jsonString.isEmpty) {
|
|
return [];
|
|
}
|
|
|
|
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
|
final histories = jsonList
|
|
.map(
|
|
(json) => EvaluationHistory.fromJson(json as Map<String, dynamic>),
|
|
)
|
|
.toList();
|
|
|
|
// Sort by timestamp (newest first)
|
|
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
|
|
|
return histories;
|
|
} catch (e) {
|
|
throw StorageException('Failed to load evaluation history: $e');
|
|
}
|
|
}
|
|
|
|
/// Get evaluation history for a specific course
|
|
///
|
|
/// [courseId] - The course ID to filter by
|
|
/// Returns list of histories for the specified course
|
|
Future<List<EvaluationHistory>> getHistoryByCourse(String courseId) async {
|
|
try {
|
|
final allHistories = await loadEvaluationHistory();
|
|
return allHistories.where((h) => h.course.id == courseId).toList();
|
|
} catch (e) {
|
|
throw StorageException('Failed to get history by course: $e');
|
|
}
|
|
}
|
|
|
|
/// Get successful evaluation count
|
|
///
|
|
/// Returns the total number of successful evaluations
|
|
Future<int> getSuccessfulEvaluationCount() async {
|
|
try {
|
|
final histories = await loadEvaluationHistory();
|
|
return histories.where((h) => h.success).length;
|
|
} catch (e) {
|
|
throw StorageException('Failed to get successful evaluation count: $e');
|
|
}
|
|
}
|
|
|
|
/// Get failed evaluation count
|
|
///
|
|
/// Returns the total number of failed evaluations
|
|
Future<int> getFailedEvaluationCount() async {
|
|
try {
|
|
final histories = await loadEvaluationHistory();
|
|
return histories.where((h) => !h.success).length;
|
|
} catch (e) {
|
|
throw StorageException('Failed to get failed evaluation count: $e');
|
|
}
|
|
}
|
|
|
|
/// Clear evaluation history
|
|
///
|
|
/// Removes all stored evaluation history
|
|
Future<void> clearEvaluationHistory() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_evaluationHistoryKey);
|
|
} catch (e) {
|
|
throw StorageException('Failed to clear evaluation history: $e');
|
|
}
|
|
}
|
|
|
|
/// Save last sync time
|
|
///
|
|
/// Records when the last data synchronization occurred
|
|
///
|
|
/// [time] - The timestamp to save
|
|
Future<void> saveLastSyncTime(DateTime time) async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_lastSyncTimeKey, time.toIso8601String());
|
|
} catch (e) {
|
|
throw StorageException('Failed to save last sync time: $e');
|
|
}
|
|
}
|
|
|
|
/// Load last sync time
|
|
///
|
|
/// Returns the timestamp of the last synchronization
|
|
/// Returns null if no sync has occurred
|
|
Future<DateTime?> loadLastSyncTime() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final timeString = prefs.getString(_lastSyncTimeKey);
|
|
|
|
if (timeString == null) {
|
|
return null;
|
|
}
|
|
|
|
return DateTime.parse(timeString);
|
|
} catch (e) {
|
|
throw StorageException('Failed to load last sync time: $e');
|
|
}
|
|
}
|
|
|
|
/// Get cache version
|
|
///
|
|
/// Returns the current cache version number
|
|
/// Used for cache invalidation when data structure changes
|
|
Future<int> getCacheVersion() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
return prefs.getInt(_cacheVersionKey) ?? 1;
|
|
} catch (e) {
|
|
throw StorageException('Failed to get cache version: $e');
|
|
}
|
|
}
|
|
|
|
/// Set cache version
|
|
///
|
|
/// Updates the cache version number
|
|
///
|
|
/// [version] - The new version number
|
|
Future<void> setCacheVersion(int version) async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setInt(_cacheVersionKey, version);
|
|
} catch (e) {
|
|
throw StorageException('Failed to set cache version: $e');
|
|
}
|
|
}
|
|
|
|
/// Clear cache if version mismatch
|
|
///
|
|
/// Compares current cache version with expected version
|
|
/// Clears cache if they don't match
|
|
///
|
|
/// [expectedVersion] - The expected cache version
|
|
/// Returns true if cache was cleared, false otherwise
|
|
Future<bool> clearCacheIfVersionMismatch(int expectedVersion) async {
|
|
try {
|
|
final currentVersion = await getCacheVersion();
|
|
|
|
if (currentVersion != expectedVersion) {
|
|
await clearEvaluationHistory();
|
|
await setCacheVersion(expectedVersion);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} catch (e) {
|
|
throw StorageException('Failed to check cache version: $e');
|
|
}
|
|
}
|
|
|
|
/// Clear all local data
|
|
///
|
|
/// Removes all data stored by this service
|
|
/// Does NOT clear secure storage (credentials)
|
|
/// Does NOT clear theme preferences
|
|
Future<void> clearAllData() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_evaluationHistoryKey);
|
|
await prefs.remove(_lastSyncTimeKey);
|
|
// Keep cache version to maintain compatibility
|
|
} catch (e) {
|
|
throw StorageException('Failed to clear all data: $e');
|
|
}
|
|
}
|
|
|
|
/// Get storage statistics
|
|
///
|
|
/// Returns information about stored data
|
|
Future<StorageStats> getStorageStats() async {
|
|
try {
|
|
final histories = await loadEvaluationHistory();
|
|
final lastSync = await loadLastSyncTime();
|
|
final cacheVersion = await getCacheVersion();
|
|
|
|
return StorageStats(
|
|
totalHistoryItems: histories.length,
|
|
successfulEvaluations: histories.where((h) => h.success).length,
|
|
failedEvaluations: histories.where((h) => !h.success).length,
|
|
lastSyncTime: lastSync,
|
|
cacheVersion: cacheVersion,
|
|
);
|
|
} catch (e) {
|
|
throw StorageException('Failed to get storage stats: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Storage statistics data class
|
|
class StorageStats {
|
|
final int totalHistoryItems;
|
|
final int successfulEvaluations;
|
|
final int failedEvaluations;
|
|
final DateTime? lastSyncTime;
|
|
final int cacheVersion;
|
|
|
|
StorageStats({
|
|
required this.totalHistoryItems,
|
|
required this.successfulEvaluations,
|
|
required this.failedEvaluations,
|
|
this.lastSyncTime,
|
|
required this.cacheVersion,
|
|
});
|
|
|
|
@override
|
|
String toString() {
|
|
return 'StorageStats('
|
|
'total: $totalHistoryItems, '
|
|
'success: $successfulEvaluations, '
|
|
'failed: $failedEvaluations, '
|
|
'lastSync: $lastSyncTime, '
|
|
'cacheVersion: $cacheVersion'
|
|
')';
|
|
}
|
|
}
|
|
|
|
/// Exception thrown when storage operations fail
|
|
class StorageException implements Exception {
|
|
final String message;
|
|
|
|
StorageException(this.message);
|
|
|
|
@override
|
|
String toString() => 'StorageException: $message';
|
|
}
|