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 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 saveEvaluationHistories( List 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> 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; final histories = jsonList .map( (json) => EvaluationHistory.fromJson(json as Map), ) .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> 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 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 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 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 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 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 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 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 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 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 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'; }