init cc
This commit is contained in:
369
lib/core/database/README.md
Normal file
369
lib/core/database/README.md
Normal file
@@ -0,0 +1,369 @@
|
||||
# Hive Database Configuration
|
||||
|
||||
This directory contains the complete Hive database setup for local storage and caching in the Flutter app.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The Hive database system is organized into the following components:
|
||||
|
||||
```
|
||||
lib/core/database/
|
||||
├── hive_service.dart # Hive initialization and box management
|
||||
├── models/
|
||||
│ ├── app_settings.dart # App settings model with Hive adapter
|
||||
│ ├── app_settings.g.dart # Generated Hive adapter
|
||||
│ ├── cache_item.dart # Generic cache wrapper model
|
||||
│ ├── cache_item.g.dart # Generated Hive adapter
|
||||
│ ├── user_preferences.dart # User preferences model
|
||||
│ └── user_preferences.g.dart # Generated Hive adapter
|
||||
├── repositories/
|
||||
│ ├── settings_repository.dart # Settings repository implementation
|
||||
│ ├── cache_repository.dart # Cache repository implementation
|
||||
│ └── user_preferences_repository.dart # User preferences repository
|
||||
├── providers/
|
||||
│ └── database_providers.dart # Riverpod providers for database access
|
||||
└── examples/
|
||||
└── database_usage_example.dart # Usage examples
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 1. HiveService
|
||||
- **Initialization**: Sets up Hive for Flutter and registers type adapters
|
||||
- **Box Management**: Manages three main boxes (appSettingsBox, cacheBox, userDataBox)
|
||||
- **Migration Support**: Handles database schema migrations
|
||||
- **Error Handling**: Comprehensive error handling and logging
|
||||
- **Maintenance**: Database compaction and cleanup utilities
|
||||
|
||||
### 2. Models with Type Adapters
|
||||
|
||||
#### AppSettings (TypeId: 0)
|
||||
- Application-wide configuration
|
||||
- Theme mode, locale, notifications
|
||||
- Cache settings and auto-update preferences
|
||||
- Custom settings support
|
||||
- Expiration logic for settings refresh
|
||||
|
||||
#### CacheItem (TypeId: 1)
|
||||
- Generic cache wrapper for any data type
|
||||
- Expiration logic with TTL support
|
||||
- Metadata support for cache categorization
|
||||
- Version control for cache migrations
|
||||
- Statistics and performance monitoring
|
||||
|
||||
#### UserPreferences (TypeId: 2)
|
||||
- User-specific settings and data
|
||||
- Favorites management
|
||||
- Last accessed tracking
|
||||
- Preference categories with type safety
|
||||
- User activity statistics
|
||||
|
||||
### 3. Repository Pattern
|
||||
- **SettingsRepository**: Application settings management
|
||||
- **CacheRepository**: Generic caching with TTL and expiration
|
||||
- **UserPreferencesRepository**: User-specific data management
|
||||
|
||||
### 4. Riverpod Integration
|
||||
- State management with providers
|
||||
- Reactive updates when data changes
|
||||
- Type-safe access to cached data
|
||||
- Automatic cache invalidation
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Initialization
|
||||
```dart
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize Hive database service
|
||||
await HiveService.initialize();
|
||||
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Management
|
||||
```dart
|
||||
class SettingsScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
final notifier = ref.read(appSettingsProvider.notifier);
|
||||
|
||||
return SwitchListTile(
|
||||
title: const Text('Dark Mode'),
|
||||
value: settings.themeMode == 'dark',
|
||||
onChanged: (value) => notifier.updateThemeMode(value ? 'dark' : 'light'),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caching Data
|
||||
```dart
|
||||
class DataService {
|
||||
final CacheRepository _cache;
|
||||
|
||||
Future<UserData> getUserData(String userId) async {
|
||||
// Try cache first
|
||||
final cachedData = _cache.get<UserData>('user_$userId');
|
||||
if (cachedData != null) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
final userData = await api.fetchUserData(userId);
|
||||
|
||||
// Cache with 1 hour expiration
|
||||
await _cache.put<UserData>(
|
||||
key: 'user_$userId',
|
||||
data: userData,
|
||||
expirationDuration: const Duration(hours: 1),
|
||||
metadata: {'type': 'user_data', 'userId': userId},
|
||||
);
|
||||
|
||||
return userData;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### User Preferences
|
||||
```dart
|
||||
class ProfileScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final preferences = ref.watch(userPreferencesProvider(null));
|
||||
final notifier = ref.read(userPreferencesProvider(null).notifier);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Compact Mode'),
|
||||
value: preferences?.getPreference<bool>('compactMode', false) ?? false,
|
||||
onChanged: (value) => notifier.setPreference('compactMode', value),
|
||||
),
|
||||
// Favorites management
|
||||
IconButton(
|
||||
icon: Icon(preferences?.isFavorite('item123') == true
|
||||
? Icons.favorite : Icons.favorite_border),
|
||||
onPressed: () {
|
||||
if (preferences?.isFavorite('item123') == true) {
|
||||
notifier.removeFavorite('item123');
|
||||
} else {
|
||||
notifier.addFavorite('item123');
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Database Boxes
|
||||
|
||||
### appSettingsBox
|
||||
- **Purpose**: Application-wide settings
|
||||
- **Key**: String-based keys (e.g., 'app_settings')
|
||||
- **Data**: AppSettings objects
|
||||
- **Usage**: Theme, language, cache strategy, feature flags
|
||||
|
||||
### cacheBox
|
||||
- **Purpose**: Generic caching with TTL
|
||||
- **Key**: String-based cache keys
|
||||
- **Data**: CacheItem objects wrapping any data type
|
||||
- **Usage**: API responses, user data, computed values
|
||||
|
||||
### userDataBox
|
||||
- **Purpose**: User-specific preferences and data
|
||||
- **Key**: User ID or 'current_user_preferences' for default
|
||||
- **Data**: UserPreferences objects
|
||||
- **Usage**: User settings, favorites, activity tracking
|
||||
|
||||
## Cache Strategies
|
||||
|
||||
### Write-Through Cache
|
||||
```dart
|
||||
Future<void> updateUserData(UserData data) async {
|
||||
// Update API
|
||||
await api.updateUser(data);
|
||||
|
||||
// Update cache
|
||||
await cacheRepository.put<UserData>(
|
||||
key: 'user_${data.id}',
|
||||
data: data,
|
||||
expirationDuration: const Duration(hours: 1),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Cache-Aside Pattern
|
||||
```dart
|
||||
Future<UserData> getUserData(String userId) async {
|
||||
// Check cache first
|
||||
var userData = cacheRepository.get<UserData>('user_$userId');
|
||||
|
||||
if (userData == null) {
|
||||
// Cache miss - fetch from API
|
||||
userData = await api.fetchUser(userId);
|
||||
|
||||
// Store in cache
|
||||
await cacheRepository.put<UserData>(
|
||||
key: 'user_$userId',
|
||||
data: userData,
|
||||
expirationDuration: const Duration(minutes: 30),
|
||||
);
|
||||
}
|
||||
|
||||
return userData;
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### 1. Lazy Loading
|
||||
- Boxes are opened only when needed
|
||||
- Data is loaded on-demand
|
||||
|
||||
### 2. Batch Operations
|
||||
```dart
|
||||
Future<void> cacheMultipleItems(Map<String, dynamic> items) async {
|
||||
final futures = items.entries.map((entry) =>
|
||||
cacheRepository.put<dynamic>(
|
||||
key: entry.key,
|
||||
data: entry.value,
|
||||
expirationDuration: const Duration(hours: 1),
|
||||
)
|
||||
);
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Cache Maintenance
|
||||
```dart
|
||||
// Automatic cleanup of expired items
|
||||
final result = await cacheRepository.performMaintenance();
|
||||
print('Cleaned ${result['expiredItemsRemoved']} expired items');
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Repository Level
|
||||
- All repository methods have comprehensive try-catch blocks
|
||||
- Errors are logged with stack traces
|
||||
- Graceful degradation (return defaults on errors)
|
||||
|
||||
### Provider Level
|
||||
- State management handles loading/error states
|
||||
- Automatic retry mechanisms
|
||||
- User-friendly error messages
|
||||
|
||||
### Service Level
|
||||
- Database initialization errors are handled gracefully
|
||||
- Box corruption recovery
|
||||
- Migration failure handling
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Version Control
|
||||
```dart
|
||||
class AppSettings {
|
||||
final int version; // Used for migrations
|
||||
|
||||
// Migration logic in repository
|
||||
AppSettings _migrateSettings(AppSettings oldSettings) {
|
||||
if (oldSettings.version < 2) {
|
||||
// Perform migration to version 2
|
||||
return oldSettings.copyWith(
|
||||
version: 2,
|
||||
// Add new fields with defaults
|
||||
newField: defaultValue,
|
||||
);
|
||||
}
|
||||
return oldSettings;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Backup
|
||||
```dart
|
||||
// Export data before migration
|
||||
final backup = settingsRepository.exportSettings();
|
||||
final userBackup = userPreferencesRepository.exportUserPreferences();
|
||||
|
||||
// Perform migration
|
||||
await HiveService.migrate();
|
||||
|
||||
// Restore if migration fails
|
||||
if (migrationFailed) {
|
||||
await settingsRepository.importSettings(backup);
|
||||
await userPreferencesRepository.importUserPreferences(userBackup);
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring and Analytics
|
||||
|
||||
### Database Statistics
|
||||
```dart
|
||||
final stats = ref.watch(databaseStatsProvider);
|
||||
print('Total items: ${stats['totalItems']}');
|
||||
print('Cache hit rate: ${stats['cache']['hitRate']}%');
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
```dart
|
||||
final cacheStats = await cacheRepository.getStats();
|
||||
print('Cache performance:');
|
||||
print('- Hit rate: ${(cacheStats.hitRate * 100).toStringAsFixed(1)}%');
|
||||
print('- Valid items: ${cacheStats.validItems}');
|
||||
print('- Expired items: ${cacheStats.expiredItems}');
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Key Naming Convention
|
||||
- Use descriptive, hierarchical keys: `user_123_profile`
|
||||
- Include type information: `api_response_movies_popular`
|
||||
- Use consistent separators: underscores or colons
|
||||
|
||||
### 2. TTL Management
|
||||
- Short TTL for frequently changing data (5-30 minutes)
|
||||
- Medium TTL for stable data (1-24 hours)
|
||||
- Long TTL for static data (1-7 days)
|
||||
- Permanent cache only for user preferences
|
||||
|
||||
### 3. Data Validation
|
||||
- Validate data before caching
|
||||
- Check data integrity on retrieval
|
||||
- Handle corrupted data gracefully
|
||||
|
||||
### 4. Memory Management
|
||||
- Regular cleanup of expired items
|
||||
- Monitor cache size and performance
|
||||
- Use appropriate data structures
|
||||
|
||||
### 5. Security
|
||||
- Don't cache sensitive data without encryption
|
||||
- Clear caches on logout
|
||||
- Validate user access to cached data
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Test repository methods with mock boxes
|
||||
- Validate data serialization/deserialization
|
||||
- Test error handling scenarios
|
||||
|
||||
### Integration Tests
|
||||
- Test full database workflows
|
||||
- Validate Riverpod provider interactions
|
||||
- Test migration scenarios
|
||||
|
||||
### Performance Tests
|
||||
- Measure cache hit rates
|
||||
- Test with large datasets
|
||||
- Monitor memory usage
|
||||
|
||||
This Hive database setup provides a robust, scalable foundation for local data storage and caching in your Flutter application.
|
||||
412
lib/core/database/examples/database_usage_example.dart
Normal file
412
lib/core/database/examples/database_usage_example.dart
Normal file
@@ -0,0 +1,412 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/database_providers.dart';
|
||||
|
||||
/// Example widget showing how to use the Hive database system
|
||||
class DatabaseUsageExample extends ConsumerWidget {
|
||||
const DatabaseUsageExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Database Usage Example'),
|
||||
),
|
||||
body: const SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_AppSettingsSection(),
|
||||
SizedBox(height: 24),
|
||||
_CacheSection(),
|
||||
SizedBox(height: 24),
|
||||
_UserPreferencesSection(),
|
||||
SizedBox(height: 24),
|
||||
_DatabaseStatsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// App Settings Section
|
||||
class _AppSettingsSection extends ConsumerWidget {
|
||||
const _AppSettingsSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
final notifier = ref.read(appSettingsProvider.notifier);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('App Settings', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Theme Mode
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Theme Mode'),
|
||||
DropdownButton<String>(
|
||||
value: settings.themeMode,
|
||||
onChanged: (value) => notifier.updateThemeMode(value!),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'system', child: Text('System')),
|
||||
DropdownMenuItem(value: 'light', child: Text('Light')),
|
||||
DropdownMenuItem(value: 'dark', child: Text('Dark')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Notifications
|
||||
SwitchListTile(
|
||||
title: const Text('Notifications'),
|
||||
value: settings.notificationsEnabled,
|
||||
onChanged: (value) => notifier.updateNotificationsEnabled(value),
|
||||
),
|
||||
|
||||
// Analytics
|
||||
SwitchListTile(
|
||||
title: const Text('Analytics'),
|
||||
value: settings.analyticsEnabled,
|
||||
onChanged: (value) => notifier.updateAnalyticsEnabled(value),
|
||||
),
|
||||
|
||||
// Cache Strategy
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Cache Strategy'),
|
||||
DropdownButton<String>(
|
||||
value: settings.cacheStrategy,
|
||||
onChanged: (value) => notifier.updateCacheStrategy(value!),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'minimal', child: Text('Minimal')),
|
||||
DropdownMenuItem(value: 'normal', child: Text('Normal')),
|
||||
DropdownMenuItem(value: 'aggressive', child: Text('Aggressive')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () => notifier.resetToDefault(),
|
||||
child: const Text('Reset to Default'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache Section
|
||||
class _CacheSection extends ConsumerWidget {
|
||||
const _CacheSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cacheRepository = ref.watch(cacheRepositoryProvider);
|
||||
final cacheStatsAsyncValue = ref.watch(cacheStatsProvider);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Cache Management', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Cache Statistics
|
||||
cacheStatsAsyncValue.when(
|
||||
data: (stats) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Total Items: ${stats.totalItems}'),
|
||||
Text('Valid Items: ${stats.validItems}'),
|
||||
Text('Expired Items: ${stats.expiredItems}'),
|
||||
Text('Hit Rate: ${(stats.hitRate * 100).toStringAsFixed(1)}%'),
|
||||
],
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (error, _) => Text('Error: $error'),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Cache Actions
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
// Example: Cache some data
|
||||
await cacheRepository.put<String>(
|
||||
key: 'example_key',
|
||||
data: 'Hello, Cache!',
|
||||
expirationDuration: const Duration(minutes: 30),
|
||||
metadata: {'type': 'example', 'created_by': 'user'},
|
||||
);
|
||||
ref.invalidate(cacheStatsProvider);
|
||||
},
|
||||
child: const Text('Add Cache Item'),
|
||||
),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final data = cacheRepository.get<String>('example_key');
|
||||
if (data != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Cached data: $data')),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No cached data found')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Get Cache Item'),
|
||||
),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final expiredCount = await cacheRepository.clearExpired();
|
||||
ref.invalidate(cacheStatsProvider);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Cleared $expiredCount expired items')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Clear Expired'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// User Preferences Section
|
||||
class _UserPreferencesSection extends ConsumerWidget {
|
||||
const _UserPreferencesSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final userPreferences = ref.watch(userPreferencesProvider(null));
|
||||
final notifier = ref.read(userPreferencesProvider(null).notifier);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('User Preferences', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
if (userPreferences == null)
|
||||
Column(
|
||||
children: [
|
||||
const Text('No user preferences found'),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await notifier.createUserPreferences(
|
||||
userId: 'demo_user',
|
||||
displayName: 'Demo User',
|
||||
email: 'demo@example.com',
|
||||
preferences: {
|
||||
'compactMode': false,
|
||||
'sortBy': 'name',
|
||||
'gridColumns': 2,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('Create Demo User'),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('User ID: ${userPreferences.userId}'),
|
||||
Text('Display Name: ${userPreferences.displayName}'),
|
||||
Text('Email: ${userPreferences.email ?? 'Not set'}'),
|
||||
Text('Favorites: ${userPreferences.favoriteItems.length}'),
|
||||
Text('Preferences: ${userPreferences.preferences.length}'),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Preference Controls
|
||||
SwitchListTile(
|
||||
title: const Text('Compact Mode'),
|
||||
value: userPreferences.getPreference<bool>('compactMode', false),
|
||||
onChanged: (value) => notifier.setPreference('compactMode', value),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Actions
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await notifier.addFavorite('item_${DateTime.now().millisecondsSinceEpoch}');
|
||||
},
|
||||
child: const Text('Add Random Favorite'),
|
||||
),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await notifier.updateLastAccessed('demo_item');
|
||||
},
|
||||
child: const Text('Update Last Accessed'),
|
||||
),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await notifier.clearPreferences();
|
||||
},
|
||||
child: const Text('Clear User Data'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Database Statistics Section
|
||||
class _DatabaseStatsSection extends ConsumerWidget {
|
||||
const _DatabaseStatsSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final databaseStats = ref.watch(databaseStatsProvider);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Database Statistics', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Text('Total Items: ${databaseStats['totalItems']}'),
|
||||
|
||||
if (databaseStats['settings'] != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Text('Settings:', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
...((databaseStats['settings'] as Map<String, dynamic>).entries.map(
|
||||
(entry) => Text(' ${entry.key}: ${entry.value}'),
|
||||
)),
|
||||
],
|
||||
|
||||
if (databaseStats['cache'] != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Text('Cache:', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
...((databaseStats['cache'] as Map<String, dynamic>).entries.map(
|
||||
(entry) => Text(' ${entry.key}: ${entry.value}'),
|
||||
)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper functions for demonstrating cache operations
|
||||
class CacheExamples {
|
||||
static Future<void> demonstrateCaching(WidgetRef ref) async {
|
||||
final cacheRepository = ref.read(cacheRepositoryProvider);
|
||||
|
||||
// Store different types of data
|
||||
await cacheRepository.put<String>(
|
||||
key: 'user_token',
|
||||
data: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
expirationDuration: const Duration(hours: 1),
|
||||
metadata: {'type': 'auth_token'},
|
||||
);
|
||||
|
||||
await cacheRepository.put<Map<String, dynamic>>(
|
||||
key: 'user_profile',
|
||||
data: {
|
||||
'id': 123,
|
||||
'name': 'John Doe',
|
||||
'email': 'john@example.com',
|
||||
},
|
||||
expirationDuration: const Duration(minutes: 30),
|
||||
metadata: {'type': 'user_data'},
|
||||
);
|
||||
|
||||
await cacheRepository.put<List<String>>(
|
||||
key: 'recent_searches',
|
||||
data: ['flutter', 'dart', 'hive'],
|
||||
expirationDuration: const Duration(days: 7),
|
||||
metadata: {'type': 'user_activity'},
|
||||
);
|
||||
|
||||
// Retrieve data
|
||||
final token = cacheRepository.get<String>('user_token');
|
||||
final profile = cacheRepository.get<Map<String, dynamic>>('user_profile');
|
||||
final searches = cacheRepository.get<List<String>>('recent_searches');
|
||||
|
||||
print('Token: $token');
|
||||
print('Profile: $profile');
|
||||
print('Searches: $searches');
|
||||
}
|
||||
|
||||
static Future<void> demonstrateUserPreferences(WidgetRef ref) async {
|
||||
final repository = ref.read(userPreferencesRepositoryProvider);
|
||||
|
||||
// Create user preferences
|
||||
final preferences = await repository.createUserPreferences(
|
||||
userId: 'user_123',
|
||||
displayName: 'Alice Smith',
|
||||
email: 'alice@example.com',
|
||||
preferences: {
|
||||
'theme': 'dark',
|
||||
'notifications': true,
|
||||
'language': 'en',
|
||||
'autoBackup': false,
|
||||
},
|
||||
);
|
||||
|
||||
print('Created preferences: $preferences');
|
||||
|
||||
// Update preferences
|
||||
await repository.setPreference('theme', 'light', 'user_123');
|
||||
await repository.addFavorite('movie_456', 'user_123');
|
||||
await repository.updateLastAccessed('series_789', 'user_123');
|
||||
|
||||
// Retrieve data
|
||||
final theme = repository.getPreference<String>('theme', 'system', 'user_123');
|
||||
final favorites = repository.getFavorites('user_123');
|
||||
final recentItems = repository.getRecentlyAccessed(userId: 'user_123');
|
||||
|
||||
print('Theme: $theme');
|
||||
print('Favorites: $favorites');
|
||||
print('Recent items: $recentItems');
|
||||
}
|
||||
}
|
||||
180
lib/core/database/hive_service.dart
Normal file
180
lib/core/database/hive_service.dart
Normal file
@@ -0,0 +1,180 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'models/app_settings.dart';
|
||||
import 'models/cache_item.dart';
|
||||
import 'models/user_preferences.dart';
|
||||
|
||||
/// Hive database service for initialization and box management
|
||||
class HiveService {
|
||||
static const String _appSettingsBoxName = 'appSettingsBox';
|
||||
static const String _cacheBoxName = 'cacheBox';
|
||||
static const String _userDataBoxName = 'userDataBox';
|
||||
|
||||
// Private boxes - access through getters
|
||||
static Box<AppSettings>? _appSettingsBox;
|
||||
static Box<CacheItem>? _cacheBox;
|
||||
static Box<UserPreferences>? _userDataBox;
|
||||
|
||||
/// Initialize Hive database
|
||||
static Future<void> initialize() async {
|
||||
try {
|
||||
// Initialize Hive for Flutter
|
||||
await Hive.initFlutter();
|
||||
|
||||
// Register type adapters
|
||||
await _registerAdapters();
|
||||
|
||||
// Open boxes
|
||||
await _openBoxes();
|
||||
|
||||
debugPrint('✅ Hive initialized successfully');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Failed to initialize Hive: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Register all Hive type adapters
|
||||
static Future<void> _registerAdapters() async {
|
||||
// Register adapters only if not already registered
|
||||
if (!Hive.isAdapterRegistered(0)) {
|
||||
Hive.registerAdapter(AppSettingsAdapter());
|
||||
}
|
||||
if (!Hive.isAdapterRegistered(1)) {
|
||||
Hive.registerAdapter(CacheItemAdapter());
|
||||
}
|
||||
if (!Hive.isAdapterRegistered(2)) {
|
||||
Hive.registerAdapter(UserPreferencesAdapter());
|
||||
}
|
||||
}
|
||||
|
||||
/// Open all required Hive boxes
|
||||
static Future<void> _openBoxes() async {
|
||||
try {
|
||||
_appSettingsBox = await Hive.openBox<AppSettings>(_appSettingsBoxName);
|
||||
_cacheBox = await Hive.openBox<CacheItem>(_cacheBoxName);
|
||||
_userDataBox = await Hive.openBox<UserPreferences>(_userDataBoxName);
|
||||
} catch (e) {
|
||||
debugPrint('Error opening Hive boxes: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get app settings box
|
||||
static Box<AppSettings> get appSettingsBox {
|
||||
if (_appSettingsBox == null || !_appSettingsBox!.isOpen) {
|
||||
throw StateError('AppSettings box is not initialized. Call HiveService.initialize() first.');
|
||||
}
|
||||
return _appSettingsBox!;
|
||||
}
|
||||
|
||||
/// Get cache box
|
||||
static Box<CacheItem> get cacheBox {
|
||||
if (_cacheBox == null || !_cacheBox!.isOpen) {
|
||||
throw StateError('Cache box is not initialized. Call HiveService.initialize() first.');
|
||||
}
|
||||
return _cacheBox!;
|
||||
}
|
||||
|
||||
/// Get user data box
|
||||
static Box<UserPreferences> get userDataBox {
|
||||
if (_userDataBox == null || !_userDataBox!.isOpen) {
|
||||
throw StateError('UserData box is not initialized. Call HiveService.initialize() first.');
|
||||
}
|
||||
return _userDataBox!;
|
||||
}
|
||||
|
||||
/// Check if Hive is initialized
|
||||
static bool get isInitialized {
|
||||
return _appSettingsBox != null &&
|
||||
_cacheBox != null &&
|
||||
_userDataBox != null &&
|
||||
_appSettingsBox!.isOpen &&
|
||||
_cacheBox!.isOpen &&
|
||||
_userDataBox!.isOpen;
|
||||
}
|
||||
|
||||
/// Close all boxes (call this when app is terminated)
|
||||
static Future<void> closeAll() async {
|
||||
try {
|
||||
await _appSettingsBox?.close();
|
||||
await _cacheBox?.close();
|
||||
await _userDataBox?.close();
|
||||
debugPrint('✅ All Hive boxes closed successfully');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error closing Hive boxes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all data (use with caution - for testing or reset functionality)
|
||||
static Future<void> clearAll() async {
|
||||
try {
|
||||
await _appSettingsBox?.clear();
|
||||
await _cacheBox?.clear();
|
||||
await _userDataBox?.clear();
|
||||
debugPrint('✅ All Hive boxes cleared successfully');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error clearing Hive boxes: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact all boxes to optimize storage
|
||||
static Future<void> compactAll() async {
|
||||
try {
|
||||
await _appSettingsBox?.compact();
|
||||
await _cacheBox?.compact();
|
||||
await _userDataBox?.compact();
|
||||
debugPrint('✅ All Hive boxes compacted successfully');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error compacting Hive boxes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get database statistics
|
||||
static Map<String, dynamic> getStats() {
|
||||
return {
|
||||
'isInitialized': isInitialized,
|
||||
'appSettingsCount': _appSettingsBox?.length ?? 0,
|
||||
'cacheItemsCount': _cacheBox?.length ?? 0,
|
||||
'userDataCount': _userDataBox?.length ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Perform database migration if needed
|
||||
static Future<void> migrate() async {
|
||||
try {
|
||||
// Check current version and migrate if needed
|
||||
final currentVersion = _appSettingsBox?.get('db_version')?.version ?? 1;
|
||||
const latestVersion = 1;
|
||||
|
||||
if (currentVersion < latestVersion) {
|
||||
await _performMigration(currentVersion, latestVersion);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error during database migration: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform database migration from one version to another
|
||||
static Future<void> _performMigration(int fromVersion, int toVersion) async {
|
||||
debugPrint('🔄 Migrating database from version $fromVersion to $toVersion');
|
||||
|
||||
// Add migration logic here as needed
|
||||
// Example:
|
||||
// if (fromVersion < 2) {
|
||||
// await _migrateToVersion2();
|
||||
// }
|
||||
|
||||
// Update database version
|
||||
await _appSettingsBox?.put('db_version', AppSettings(
|
||||
version: toVersion,
|
||||
themeMode: 'system',
|
||||
locale: 'en',
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
|
||||
debugPrint('✅ Database migration completed');
|
||||
}
|
||||
}
|
||||
212
lib/core/database/models/app_settings.dart
Normal file
212
lib/core/database/models/app_settings.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'app_settings.g.dart';
|
||||
|
||||
/// App settings model for storing application-wide configuration
|
||||
@HiveType(typeId: 0)
|
||||
class AppSettings extends HiveObject {
|
||||
@HiveField(0)
|
||||
final int version;
|
||||
|
||||
@HiveField(1)
|
||||
final String themeMode; // 'light', 'dark', 'system'
|
||||
|
||||
@HiveField(2)
|
||||
final String locale; // Language code (e.g., 'en', 'es')
|
||||
|
||||
@HiveField(3)
|
||||
final bool notificationsEnabled;
|
||||
|
||||
@HiveField(4)
|
||||
final bool analyticsEnabled;
|
||||
|
||||
@HiveField(5)
|
||||
final String cacheStrategy; // 'aggressive', 'normal', 'minimal'
|
||||
|
||||
@HiveField(6)
|
||||
final int cacheExpirationHours;
|
||||
|
||||
@HiveField(7)
|
||||
final bool autoUpdateEnabled;
|
||||
|
||||
@HiveField(8)
|
||||
final DateTime lastUpdated;
|
||||
|
||||
@HiveField(9)
|
||||
final Map<String, dynamic>? customSettings;
|
||||
|
||||
AppSettings({
|
||||
this.version = 1,
|
||||
this.themeMode = 'system',
|
||||
this.locale = 'en',
|
||||
this.notificationsEnabled = true,
|
||||
this.analyticsEnabled = false,
|
||||
this.cacheStrategy = 'normal',
|
||||
this.cacheExpirationHours = 24,
|
||||
this.autoUpdateEnabled = true,
|
||||
required this.lastUpdated,
|
||||
this.customSettings,
|
||||
});
|
||||
|
||||
/// Create a copy with modified fields
|
||||
AppSettings copyWith({
|
||||
int? version,
|
||||
String? themeMode,
|
||||
String? locale,
|
||||
bool? notificationsEnabled,
|
||||
bool? analyticsEnabled,
|
||||
String? cacheStrategy,
|
||||
int? cacheExpirationHours,
|
||||
bool? autoUpdateEnabled,
|
||||
DateTime? lastUpdated,
|
||||
Map<String, dynamic>? customSettings,
|
||||
}) {
|
||||
return AppSettings(
|
||||
version: version ?? this.version,
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
locale: locale ?? this.locale,
|
||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||
analyticsEnabled: analyticsEnabled ?? this.analyticsEnabled,
|
||||
cacheStrategy: cacheStrategy ?? this.cacheStrategy,
|
||||
cacheExpirationHours: cacheExpirationHours ?? this.cacheExpirationHours,
|
||||
autoUpdateEnabled: autoUpdateEnabled ?? this.autoUpdateEnabled,
|
||||
lastUpdated: lastUpdated ?? this.lastUpdated,
|
||||
customSettings: customSettings ?? this.customSettings,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create default app settings
|
||||
factory AppSettings.defaultSettings() {
|
||||
return AppSettings(
|
||||
version: 1,
|
||||
themeMode: 'system',
|
||||
locale: 'en',
|
||||
notificationsEnabled: true,
|
||||
analyticsEnabled: false,
|
||||
cacheStrategy: 'normal',
|
||||
cacheExpirationHours: 24,
|
||||
autoUpdateEnabled: true,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to Map for JSON serialization
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'version': version,
|
||||
'themeMode': themeMode,
|
||||
'locale': locale,
|
||||
'notificationsEnabled': notificationsEnabled,
|
||||
'analyticsEnabled': analyticsEnabled,
|
||||
'cacheStrategy': cacheStrategy,
|
||||
'cacheExpirationHours': cacheExpirationHours,
|
||||
'autoUpdateEnabled': autoUpdateEnabled,
|
||||
'lastUpdated': lastUpdated.toIso8601String(),
|
||||
'customSettings': customSettings,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create from Map for JSON deserialization
|
||||
factory AppSettings.fromMap(Map<String, dynamic> map) {
|
||||
return AppSettings(
|
||||
version: map['version'] ?? 1,
|
||||
themeMode: map['themeMode'] ?? 'system',
|
||||
locale: map['locale'] ?? 'en',
|
||||
notificationsEnabled: map['notificationsEnabled'] ?? true,
|
||||
analyticsEnabled: map['analyticsEnabled'] ?? false,
|
||||
cacheStrategy: map['cacheStrategy'] ?? 'normal',
|
||||
cacheExpirationHours: map['cacheExpirationHours'] ?? 24,
|
||||
autoUpdateEnabled: map['autoUpdateEnabled'] ?? true,
|
||||
lastUpdated: DateTime.parse(map['lastUpdated'] ?? DateTime.now().toIso8601String()),
|
||||
customSettings: map['customSettings']?.cast<String, dynamic>(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Check if settings are expired (for auto-refresh logic)
|
||||
bool isExpired({int maxAgeHours = 168}) { // Default 1 week
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(lastUpdated);
|
||||
return difference.inHours > maxAgeHours;
|
||||
}
|
||||
|
||||
/// Get custom setting value
|
||||
T? getCustomSetting<T>(String key) {
|
||||
return customSettings?[key] as T?;
|
||||
}
|
||||
|
||||
/// Set custom setting value
|
||||
AppSettings setCustomSetting(String key, dynamic value) {
|
||||
final updatedCustomSettings = Map<String, dynamic>.from(customSettings ?? {});
|
||||
updatedCustomSettings[key] = value;
|
||||
|
||||
return copyWith(
|
||||
customSettings: updatedCustomSettings,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove custom setting
|
||||
AppSettings removeCustomSetting(String key) {
|
||||
if (customSettings == null) return this;
|
||||
|
||||
final updatedCustomSettings = Map<String, dynamic>.from(customSettings!);
|
||||
updatedCustomSettings.remove(key);
|
||||
|
||||
return copyWith(
|
||||
customSettings: updatedCustomSettings.isNotEmpty ? updatedCustomSettings : null,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings{version: $version, themeMode: $themeMode, locale: $locale, '
|
||||
'notificationsEnabled: $notificationsEnabled, analyticsEnabled: $analyticsEnabled, '
|
||||
'cacheStrategy: $cacheStrategy, cacheExpirationHours: $cacheExpirationHours, '
|
||||
'autoUpdateEnabled: $autoUpdateEnabled, lastUpdated: $lastUpdated, '
|
||||
'customSettings: $customSettings}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is AppSettings &&
|
||||
other.version == version &&
|
||||
other.themeMode == themeMode &&
|
||||
other.locale == locale &&
|
||||
other.notificationsEnabled == notificationsEnabled &&
|
||||
other.analyticsEnabled == analyticsEnabled &&
|
||||
other.cacheStrategy == cacheStrategy &&
|
||||
other.cacheExpirationHours == cacheExpirationHours &&
|
||||
other.autoUpdateEnabled == autoUpdateEnabled &&
|
||||
other.lastUpdated == lastUpdated &&
|
||||
_mapEquals(other.customSettings, customSettings);
|
||||
}
|
||||
|
||||
bool _mapEquals(Map<String, dynamic>? a, Map<String, dynamic>? b) {
|
||||
if (a == null) return b == null;
|
||||
if (b == null) return false;
|
||||
if (a.length != b.length) return false;
|
||||
for (final key in a.keys) {
|
||||
if (!b.containsKey(key) || a[key] != b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
version,
|
||||
themeMode,
|
||||
locale,
|
||||
notificationsEnabled,
|
||||
analyticsEnabled,
|
||||
cacheStrategy,
|
||||
cacheExpirationHours,
|
||||
autoUpdateEnabled,
|
||||
lastUpdated,
|
||||
customSettings?.hashCode ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/core/database/models/app_settings.g.dart
Normal file
68
lib/core/database/models/app_settings.g.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
AppSettings read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AppSettings(
|
||||
version: fields[0] as int,
|
||||
themeMode: fields[1] as String,
|
||||
locale: fields[2] as String,
|
||||
notificationsEnabled: fields[3] as bool,
|
||||
analyticsEnabled: fields[4] as bool,
|
||||
cacheStrategy: fields[5] as String,
|
||||
cacheExpirationHours: fields[6] as int,
|
||||
autoUpdateEnabled: fields[7] as bool,
|
||||
lastUpdated: fields[8] as DateTime,
|
||||
customSettings: (fields[9] as Map?)?.cast<String, dynamic>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AppSettings obj) {
|
||||
writer
|
||||
..writeByte(10)
|
||||
..writeByte(0)
|
||||
..write(obj.version)
|
||||
..writeByte(1)
|
||||
..write(obj.themeMode)
|
||||
..writeByte(2)
|
||||
..write(obj.locale)
|
||||
..writeByte(3)
|
||||
..write(obj.notificationsEnabled)
|
||||
..writeByte(4)
|
||||
..write(obj.analyticsEnabled)
|
||||
..writeByte(5)
|
||||
..write(obj.cacheStrategy)
|
||||
..writeByte(6)
|
||||
..write(obj.cacheExpirationHours)
|
||||
..writeByte(7)
|
||||
..write(obj.autoUpdateEnabled)
|
||||
..writeByte(8)
|
||||
..write(obj.lastUpdated)
|
||||
..writeByte(9)
|
||||
..write(obj.customSettings);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AppSettingsAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
274
lib/core/database/models/cache_item.dart
Normal file
274
lib/core/database/models/cache_item.dart
Normal file
@@ -0,0 +1,274 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'cache_item.g.dart';
|
||||
|
||||
/// Generic cache wrapper for storing any data with expiration logic
|
||||
@HiveType(typeId: 1)
|
||||
class CacheItem extends HiveObject {
|
||||
@HiveField(0)
|
||||
@override
|
||||
final String key;
|
||||
|
||||
@HiveField(1)
|
||||
final dynamic data;
|
||||
|
||||
@HiveField(2)
|
||||
final DateTime createdAt;
|
||||
|
||||
@HiveField(3)
|
||||
final DateTime expiresAt;
|
||||
|
||||
@HiveField(4)
|
||||
final String dataType; // Type identifier for runtime type safety
|
||||
|
||||
@HiveField(5)
|
||||
final Map<String, dynamic>? metadata; // Additional cache metadata
|
||||
|
||||
@HiveField(6)
|
||||
final int version; // Cache version for migration support
|
||||
|
||||
CacheItem({
|
||||
required this.key,
|
||||
required this.data,
|
||||
required this.createdAt,
|
||||
required this.expiresAt,
|
||||
required this.dataType,
|
||||
this.metadata,
|
||||
this.version = 1,
|
||||
});
|
||||
|
||||
/// Create a new cache item with expiration time
|
||||
factory CacheItem.create({
|
||||
required String key,
|
||||
required dynamic data,
|
||||
required Duration expirationDuration,
|
||||
Map<String, dynamic>? metadata,
|
||||
int version = 1,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
return CacheItem(
|
||||
key: key,
|
||||
data: data,
|
||||
createdAt: now,
|
||||
expiresAt: now.add(expirationDuration),
|
||||
dataType: data.runtimeType.toString(),
|
||||
metadata: metadata,
|
||||
version: version,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a cache item that never expires
|
||||
factory CacheItem.permanent({
|
||||
required String key,
|
||||
required dynamic data,
|
||||
Map<String, dynamic>? metadata,
|
||||
int version = 1,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
return CacheItem(
|
||||
key: key,
|
||||
data: data,
|
||||
createdAt: now,
|
||||
expiresAt: DateTime(9999), // Far future date
|
||||
dataType: data.runtimeType.toString(),
|
||||
metadata: metadata,
|
||||
version: version,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check if the cache item is expired
|
||||
bool get isExpired {
|
||||
return DateTime.now().isAfter(expiresAt);
|
||||
}
|
||||
|
||||
/// Check if the cache item is still valid
|
||||
bool get isValid {
|
||||
return !isExpired;
|
||||
}
|
||||
|
||||
/// Get the age of the cache item
|
||||
Duration get age {
|
||||
return DateTime.now().difference(createdAt);
|
||||
}
|
||||
|
||||
/// Get time remaining until expiration
|
||||
Duration get timeUntilExpiry {
|
||||
final now = DateTime.now();
|
||||
if (now.isAfter(expiresAt)) {
|
||||
return Duration.zero;
|
||||
}
|
||||
return expiresAt.difference(now);
|
||||
}
|
||||
|
||||
/// Check if cache item will expire within the given duration
|
||||
bool willExpireWithin(Duration duration) {
|
||||
final checkTime = DateTime.now().add(duration);
|
||||
return expiresAt.isBefore(checkTime);
|
||||
}
|
||||
|
||||
/// Create a refreshed copy with new expiration time
|
||||
CacheItem refresh(Duration newExpirationDuration) {
|
||||
final now = DateTime.now();
|
||||
return CacheItem(
|
||||
key: key,
|
||||
data: data,
|
||||
createdAt: createdAt, // Keep original creation time
|
||||
expiresAt: now.add(newExpirationDuration),
|
||||
dataType: dataType,
|
||||
metadata: metadata,
|
||||
version: version,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with updated data
|
||||
CacheItem updateData(dynamic newData, {Duration? newExpirationDuration}) {
|
||||
final now = DateTime.now();
|
||||
return CacheItem(
|
||||
key: key,
|
||||
data: newData,
|
||||
createdAt: now, // Update creation time for new data
|
||||
expiresAt: newExpirationDuration != null
|
||||
? now.add(newExpirationDuration)
|
||||
: expiresAt,
|
||||
dataType: dataType,
|
||||
metadata: metadata,
|
||||
version: version,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with updated metadata
|
||||
CacheItem updateMetadata(Map<String, dynamic> newMetadata) {
|
||||
return CacheItem(
|
||||
key: key,
|
||||
data: data,
|
||||
createdAt: createdAt,
|
||||
expiresAt: expiresAt,
|
||||
dataType: dataType,
|
||||
metadata: {...(metadata ?? {}), ...newMetadata},
|
||||
version: version,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get metadata value
|
||||
V? getMetadata<V>(String key) {
|
||||
return metadata?[key] as V?;
|
||||
}
|
||||
|
||||
/// Convert to Map for JSON serialization
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'key': key,
|
||||
'data': data,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'expiresAt': expiresAt.toIso8601String(),
|
||||
'dataType': dataType,
|
||||
'metadata': metadata,
|
||||
'version': version,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create from Map (useful for migration or external data)
|
||||
static CacheItem fromMap(Map<String, dynamic> map) {
|
||||
return CacheItem(
|
||||
key: map['key'] as String,
|
||||
data: map['data'],
|
||||
createdAt: DateTime.parse(map['createdAt'] as String),
|
||||
expiresAt: DateTime.parse(map['expiresAt'] as String),
|
||||
dataType: map['dataType'] as String,
|
||||
metadata: map['metadata']?.cast<String, dynamic>(),
|
||||
version: map['version'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CacheItem{key: $key, dataType: $dataType, createdAt: $createdAt, '
|
||||
'expiresAt: $expiresAt, isExpired: $isExpired, version: $version}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is CacheItem &&
|
||||
other.key == key &&
|
||||
other.data == data &&
|
||||
other.createdAt == createdAt &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.dataType == dataType &&
|
||||
_mapEquals(other.metadata, metadata) &&
|
||||
other.version == version;
|
||||
}
|
||||
|
||||
bool _mapEquals(Map<String, dynamic>? a, Map<String, dynamic>? b) {
|
||||
if (a == null) return b == null;
|
||||
if (b == null) return false;
|
||||
if (a.length != b.length) return false;
|
||||
for (final key in a.keys) {
|
||||
if (!b.containsKey(key) || a[key] != b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
key,
|
||||
data,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
dataType,
|
||||
metadata?.hashCode ?? 0,
|
||||
version,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache statistics for monitoring cache performance
|
||||
class CacheStats {
|
||||
final int totalItems;
|
||||
final int validItems;
|
||||
final int expiredItems;
|
||||
final DateTime oldestItem;
|
||||
final DateTime newestItem;
|
||||
final Map<String, int> typeCount;
|
||||
|
||||
const CacheStats({
|
||||
required this.totalItems,
|
||||
required this.validItems,
|
||||
required this.expiredItems,
|
||||
required this.oldestItem,
|
||||
required this.newestItem,
|
||||
required this.typeCount,
|
||||
});
|
||||
|
||||
double get hitRate {
|
||||
if (totalItems == 0) return 0.0;
|
||||
return validItems / totalItems;
|
||||
}
|
||||
|
||||
double get expirationRate {
|
||||
if (totalItems == 0) return 0.0;
|
||||
return expiredItems / totalItems;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'totalItems': totalItems,
|
||||
'validItems': validItems,
|
||||
'expiredItems': expiredItems,
|
||||
'oldestItem': oldestItem.toIso8601String(),
|
||||
'newestItem': newestItem.toIso8601String(),
|
||||
'typeCount': typeCount,
|
||||
'hitRate': hitRate,
|
||||
'expirationRate': expirationRate,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CacheStats{total: $totalItems, valid: $validItems, expired: $expiredItems, '
|
||||
'hitRate: ${(hitRate * 100).toStringAsFixed(1)}%, '
|
||||
'types: ${typeCount.keys.join(', ')}}';
|
||||
}
|
||||
}
|
||||
59
lib/core/database/models/cache_item.g.dart
Normal file
59
lib/core/database/models/cache_item.g.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cache_item.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class CacheItemAdapter extends TypeAdapter<CacheItem> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
CacheItem read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CacheItem(
|
||||
key: fields[0] as String,
|
||||
data: fields[1] as dynamic,
|
||||
createdAt: fields[2] as DateTime,
|
||||
expiresAt: fields[3] as DateTime,
|
||||
dataType: fields[4] as String,
|
||||
metadata: (fields[5] as Map?)?.cast<String, dynamic>(),
|
||||
version: fields[6] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CacheItem obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.key)
|
||||
..writeByte(1)
|
||||
..write(obj.data)
|
||||
..writeByte(2)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(3)
|
||||
..write(obj.expiresAt)
|
||||
..writeByte(4)
|
||||
..write(obj.dataType)
|
||||
..writeByte(5)
|
||||
..write(obj.metadata)
|
||||
..writeByte(6)
|
||||
..write(obj.version);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CacheItemAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
379
lib/core/database/models/user_preferences.dart
Normal file
379
lib/core/database/models/user_preferences.dart
Normal file
@@ -0,0 +1,379 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'user_preferences.g.dart';
|
||||
|
||||
/// User preferences model for storing user-specific settings and data
|
||||
@HiveType(typeId: 2)
|
||||
class UserPreferences extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String userId;
|
||||
|
||||
@HiveField(1)
|
||||
final String displayName;
|
||||
|
||||
@HiveField(2)
|
||||
final String? email;
|
||||
|
||||
@HiveField(3)
|
||||
final String? avatarUrl;
|
||||
|
||||
@HiveField(4)
|
||||
final Map<String, dynamic> preferences;
|
||||
|
||||
@HiveField(5)
|
||||
final List<String> favoriteItems;
|
||||
|
||||
@HiveField(6)
|
||||
final Map<String, DateTime> lastAccessed;
|
||||
|
||||
@HiveField(7)
|
||||
final DateTime createdAt;
|
||||
|
||||
@HiveField(8)
|
||||
final DateTime lastUpdated;
|
||||
|
||||
@HiveField(9)
|
||||
final int version;
|
||||
|
||||
UserPreferences({
|
||||
required this.userId,
|
||||
required this.displayName,
|
||||
this.email,
|
||||
this.avatarUrl,
|
||||
this.preferences = const {},
|
||||
this.favoriteItems = const [],
|
||||
this.lastAccessed = const {},
|
||||
required this.createdAt,
|
||||
required this.lastUpdated,
|
||||
this.version = 1,
|
||||
});
|
||||
|
||||
/// Create a new user preferences instance
|
||||
factory UserPreferences.create({
|
||||
required String userId,
|
||||
required String displayName,
|
||||
String? email,
|
||||
String? avatarUrl,
|
||||
Map<String, dynamic>? preferences,
|
||||
List<String>? favoriteItems,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
return UserPreferences(
|
||||
userId: userId,
|
||||
displayName: displayName,
|
||||
email: email,
|
||||
avatarUrl: avatarUrl,
|
||||
preferences: preferences ?? {},
|
||||
favoriteItems: favoriteItems ?? [],
|
||||
lastAccessed: {},
|
||||
createdAt: now,
|
||||
lastUpdated: now,
|
||||
version: 1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create default user preferences for anonymous user
|
||||
factory UserPreferences.anonymous() {
|
||||
final now = DateTime.now();
|
||||
return UserPreferences(
|
||||
userId: 'anonymous',
|
||||
displayName: 'Anonymous User',
|
||||
preferences: _getDefaultPreferences(),
|
||||
favoriteItems: [],
|
||||
lastAccessed: {},
|
||||
createdAt: now,
|
||||
lastUpdated: now,
|
||||
version: 1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get default preferences
|
||||
static Map<String, dynamic> _getDefaultPreferences() {
|
||||
return {
|
||||
// UI Preferences
|
||||
'compactMode': false,
|
||||
'showThumbnails': true,
|
||||
'gridColumns': 2,
|
||||
'sortBy': 'name',
|
||||
'sortOrder': 'asc', // 'asc' or 'desc'
|
||||
|
||||
// Notification Preferences
|
||||
'pushNotifications': true,
|
||||
'emailNotifications': false,
|
||||
'notificationTypes': ['updates', 'recommendations'],
|
||||
|
||||
// Privacy Preferences
|
||||
'analyticsOptIn': false,
|
||||
'shareUsageData': false,
|
||||
'personalizedRecommendations': true,
|
||||
|
||||
// Content Preferences
|
||||
'autoPlay': false,
|
||||
'highQualityImages': true,
|
||||
'downloadQuality': 'medium', // 'low', 'medium', 'high'
|
||||
'cacheSize': 500, // MB
|
||||
|
||||
// Accessibility
|
||||
'textSize': 'normal', // 'small', 'normal', 'large'
|
||||
'highContrast': false,
|
||||
'reduceAnimations': false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a copy with modified fields
|
||||
UserPreferences copyWith({
|
||||
String? userId,
|
||||
String? displayName,
|
||||
String? email,
|
||||
String? avatarUrl,
|
||||
Map<String, dynamic>? preferences,
|
||||
List<String>? favoriteItems,
|
||||
Map<String, DateTime>? lastAccessed,
|
||||
DateTime? createdAt,
|
||||
DateTime? lastUpdated,
|
||||
int? version,
|
||||
}) {
|
||||
return UserPreferences(
|
||||
userId: userId ?? this.userId,
|
||||
displayName: displayName ?? this.displayName,
|
||||
email: email ?? this.email,
|
||||
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||
preferences: preferences ?? this.preferences,
|
||||
favoriteItems: favoriteItems ?? this.favoriteItems,
|
||||
lastAccessed: lastAccessed ?? this.lastAccessed,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
lastUpdated: lastUpdated ?? DateTime.now(),
|
||||
version: version ?? this.version,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get a preference value with type safety
|
||||
T getPreference<T>(String key, T defaultValue) {
|
||||
final value = preferences[key];
|
||||
if (value is T) {
|
||||
return value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// Set a preference value
|
||||
UserPreferences setPreference(String key, dynamic value) {
|
||||
final updatedPreferences = Map<String, dynamic>.from(preferences);
|
||||
updatedPreferences[key] = value;
|
||||
|
||||
return copyWith(
|
||||
preferences: updatedPreferences,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove a preference
|
||||
UserPreferences removePreference(String key) {
|
||||
final updatedPreferences = Map<String, dynamic>.from(preferences);
|
||||
updatedPreferences.remove(key);
|
||||
|
||||
return copyWith(
|
||||
preferences: updatedPreferences,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Add item to favorites
|
||||
UserPreferences addFavorite(String itemId) {
|
||||
if (favoriteItems.contains(itemId)) return this;
|
||||
|
||||
final updatedFavorites = List<String>.from(favoriteItems);
|
||||
updatedFavorites.add(itemId);
|
||||
|
||||
return copyWith(
|
||||
favoriteItems: updatedFavorites,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove item from favorites
|
||||
UserPreferences removeFavorite(String itemId) {
|
||||
if (!favoriteItems.contains(itemId)) return this;
|
||||
|
||||
final updatedFavorites = List<String>.from(favoriteItems);
|
||||
updatedFavorites.remove(itemId);
|
||||
|
||||
return copyWith(
|
||||
favoriteItems: updatedFavorites,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Check if item is favorite
|
||||
bool isFavorite(String itemId) {
|
||||
return favoriteItems.contains(itemId);
|
||||
}
|
||||
|
||||
/// Update last accessed time for an item
|
||||
UserPreferences updateLastAccessed(String itemId) {
|
||||
final updatedLastAccessed = Map<String, DateTime>.from(lastAccessed);
|
||||
updatedLastAccessed[itemId] = DateTime.now();
|
||||
|
||||
return copyWith(
|
||||
lastAccessed: updatedLastAccessed,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get last accessed time for an item
|
||||
DateTime? getLastAccessed(String itemId) {
|
||||
return lastAccessed[itemId];
|
||||
}
|
||||
|
||||
/// Get recently accessed items (sorted by most recent)
|
||||
List<String> getRecentlyAccessed({int limit = 10}) {
|
||||
final entries = lastAccessed.entries.toList();
|
||||
entries.sort((a, b) => b.value.compareTo(a.value));
|
||||
return entries.take(limit).map((e) => e.key).toList();
|
||||
}
|
||||
|
||||
/// Clean old last accessed entries (older than specified days)
|
||||
UserPreferences cleanOldAccess({int maxAgeDays = 30}) {
|
||||
final cutoffDate = DateTime.now().subtract(Duration(days: maxAgeDays));
|
||||
final updatedLastAccessed = Map<String, DateTime>.from(lastAccessed);
|
||||
|
||||
updatedLastAccessed.removeWhere((key, value) => value.isBefore(cutoffDate));
|
||||
|
||||
return copyWith(
|
||||
lastAccessed: updatedLastAccessed,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get user statistics
|
||||
Map<String, dynamic> getStats() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'displayName': displayName,
|
||||
'totalFavorites': favoriteItems.length,
|
||||
'totalAccessedItems': lastAccessed.length,
|
||||
'recentlyAccessed': getRecentlyAccessed(limit: 5),
|
||||
'accountAge': DateTime.now().difference(createdAt).inDays,
|
||||
'lastActive': lastUpdated,
|
||||
'preferences': preferences.keys.length,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to Map for JSON serialization
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'displayName': displayName,
|
||||
'email': email,
|
||||
'avatarUrl': avatarUrl,
|
||||
'preferences': preferences,
|
||||
'favoriteItems': favoriteItems,
|
||||
'lastAccessed': lastAccessed.map((k, v) => MapEntry(k, v.toIso8601String())),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'lastUpdated': lastUpdated.toIso8601String(),
|
||||
'version': version,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create from Map for JSON deserialization
|
||||
factory UserPreferences.fromMap(Map<String, dynamic> map) {
|
||||
return UserPreferences(
|
||||
userId: map['userId'] as String,
|
||||
displayName: map['displayName'] as String,
|
||||
email: map['email'] as String?,
|
||||
avatarUrl: map['avatarUrl'] as String?,
|
||||
preferences: Map<String, dynamic>.from(map['preferences'] ?? {}),
|
||||
favoriteItems: List<String>.from(map['favoriteItems'] ?? []),
|
||||
lastAccessed: (map['lastAccessed'] as Map<String, dynamic>?)
|
||||
?.map((k, v) => MapEntry(k, DateTime.parse(v as String))) ?? {},
|
||||
createdAt: DateTime.parse(map['createdAt'] as String),
|
||||
lastUpdated: DateTime.parse(map['lastUpdated'] as String),
|
||||
version: map['version'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check if preferences need migration
|
||||
bool needsMigration() {
|
||||
const currentVersion = 1;
|
||||
return version < currentVersion;
|
||||
}
|
||||
|
||||
/// Migrate preferences to current version
|
||||
UserPreferences migrate() {
|
||||
if (!needsMigration()) return this;
|
||||
|
||||
var migrated = this;
|
||||
|
||||
// Add migration logic here as needed
|
||||
// Example:
|
||||
// if (version < 2) {
|
||||
// migrated = _migrateToVersion2(migrated);
|
||||
// }
|
||||
|
||||
return migrated.copyWith(version: 1);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserPreferences{userId: $userId, displayName: $displayName, '
|
||||
'favorites: ${favoriteItems.length}, preferences: ${preferences.keys.length}, '
|
||||
'lastUpdated: $lastUpdated}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is UserPreferences &&
|
||||
other.userId == userId &&
|
||||
other.displayName == displayName &&
|
||||
other.email == email &&
|
||||
other.avatarUrl == avatarUrl &&
|
||||
_mapEquals(other.preferences, preferences) &&
|
||||
_listEquals(other.favoriteItems, favoriteItems) &&
|
||||
_dateMapEquals(other.lastAccessed, lastAccessed) &&
|
||||
other.createdAt == createdAt &&
|
||||
other.lastUpdated == lastUpdated &&
|
||||
other.version == version;
|
||||
}
|
||||
|
||||
bool _mapEquals(Map<String, dynamic> a, Map<String, dynamic> b) {
|
||||
if (a.length != b.length) return false;
|
||||
for (final key in a.keys) {
|
||||
if (!b.containsKey(key) || a[key] != b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _listEquals(List<String> a, List<String> b) {
|
||||
if (a.length != b.length) return false;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _dateMapEquals(Map<String, DateTime> a, Map<String, DateTime> b) {
|
||||
if (a.length != b.length) return false;
|
||||
for (final key in a.keys) {
|
||||
if (!b.containsKey(key) || a[key] != b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
userId,
|
||||
displayName,
|
||||
email,
|
||||
avatarUrl,
|
||||
preferences.hashCode,
|
||||
favoriteItems.hashCode,
|
||||
lastAccessed.hashCode,
|
||||
createdAt,
|
||||
lastUpdated,
|
||||
version,
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/core/database/models/user_preferences.g.dart
Normal file
68
lib/core/database/models/user_preferences.g.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user_preferences.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class UserPreferencesAdapter extends TypeAdapter<UserPreferences> {
|
||||
@override
|
||||
final int typeId = 2;
|
||||
|
||||
@override
|
||||
UserPreferences read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return UserPreferences(
|
||||
userId: fields[0] as String,
|
||||
displayName: fields[1] as String,
|
||||
email: fields[2] as String?,
|
||||
avatarUrl: fields[3] as String?,
|
||||
preferences: (fields[4] as Map).cast<String, dynamic>(),
|
||||
favoriteItems: (fields[5] as List).cast<String>(),
|
||||
lastAccessed: (fields[6] as Map).cast<String, DateTime>(),
|
||||
createdAt: fields[7] as DateTime,
|
||||
lastUpdated: fields[8] as DateTime,
|
||||
version: fields[9] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, UserPreferences obj) {
|
||||
writer
|
||||
..writeByte(10)
|
||||
..writeByte(0)
|
||||
..write(obj.userId)
|
||||
..writeByte(1)
|
||||
..write(obj.displayName)
|
||||
..writeByte(2)
|
||||
..write(obj.email)
|
||||
..writeByte(3)
|
||||
..write(obj.avatarUrl)
|
||||
..writeByte(4)
|
||||
..write(obj.preferences)
|
||||
..writeByte(5)
|
||||
..write(obj.favoriteItems)
|
||||
..writeByte(6)
|
||||
..write(obj.lastAccessed)
|
||||
..writeByte(7)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(8)
|
||||
..write(obj.lastUpdated)
|
||||
..writeByte(9)
|
||||
..write(obj.version);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is UserPreferencesAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
438
lib/core/database/providers/database_providers.dart
Normal file
438
lib/core/database/providers/database_providers.dart
Normal file
@@ -0,0 +1,438 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../repositories/settings_repository.dart';
|
||||
import '../repositories/cache_repository.dart';
|
||||
import '../repositories/user_preferences_repository.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/cache_item.dart';
|
||||
import '../models/user_preferences.dart';
|
||||
|
||||
/// Providers for database repositories and services
|
||||
|
||||
/// Settings repository provider
|
||||
final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
|
||||
return SettingsRepository();
|
||||
});
|
||||
|
||||
/// Cache repository provider
|
||||
final cacheRepositoryProvider = Provider<CacheRepository>((ref) {
|
||||
return CacheRepository();
|
||||
});
|
||||
|
||||
/// User preferences repository provider
|
||||
final userPreferencesRepositoryProvider = Provider<UserPreferencesRepository>((ref) {
|
||||
return UserPreferencesRepository();
|
||||
});
|
||||
|
||||
/// Current app settings provider
|
||||
final appSettingsProvider = StateNotifierProvider<AppSettingsNotifier, AppSettings>((ref) {
|
||||
final repository = ref.watch(settingsRepositoryProvider);
|
||||
return AppSettingsNotifier(repository);
|
||||
});
|
||||
|
||||
/// App settings notifier
|
||||
class AppSettingsNotifier extends StateNotifier<AppSettings> {
|
||||
final SettingsRepository _repository;
|
||||
|
||||
AppSettingsNotifier(this._repository) : super(AppSettings.defaultSettings()) {
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
void _loadSettings() {
|
||||
try {
|
||||
state = _repository.getSettings();
|
||||
} catch (e) {
|
||||
// Keep default settings if loading fails
|
||||
state = AppSettings.defaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update theme mode
|
||||
Future<void> updateThemeMode(String themeMode) async {
|
||||
try {
|
||||
await _repository.updateThemeMode(themeMode);
|
||||
state = state.copyWith(themeMode: themeMode);
|
||||
} catch (e) {
|
||||
// Handle error - maybe show a snackbar
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update locale
|
||||
Future<void> updateLocale(String locale) async {
|
||||
try {
|
||||
await _repository.updateLocale(locale);
|
||||
state = state.copyWith(locale: locale);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update notifications enabled
|
||||
Future<void> updateNotificationsEnabled(bool enabled) async {
|
||||
try {
|
||||
await _repository.updateNotificationsEnabled(enabled);
|
||||
state = state.copyWith(notificationsEnabled: enabled);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update analytics enabled
|
||||
Future<void> updateAnalyticsEnabled(bool enabled) async {
|
||||
try {
|
||||
await _repository.updateAnalyticsEnabled(enabled);
|
||||
state = state.copyWith(analyticsEnabled: enabled);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update cache strategy
|
||||
Future<void> updateCacheStrategy(String strategy) async {
|
||||
try {
|
||||
await _repository.updateCacheStrategy(strategy);
|
||||
state = state.copyWith(cacheStrategy: strategy);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update cache expiration hours
|
||||
Future<void> updateCacheExpirationHours(int hours) async {
|
||||
try {
|
||||
await _repository.updateCacheExpirationHours(hours);
|
||||
state = state.copyWith(cacheExpirationHours: hours);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update auto update enabled
|
||||
Future<void> updateAutoUpdateEnabled(bool enabled) async {
|
||||
try {
|
||||
await _repository.updateAutoUpdateEnabled(enabled);
|
||||
state = state.copyWith(autoUpdateEnabled: enabled);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set custom setting
|
||||
Future<void> setCustomSetting(String key, dynamic value) async {
|
||||
try {
|
||||
await _repository.setCustomSetting(key, value);
|
||||
state = state.setCustomSetting(key, value);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove custom setting
|
||||
Future<void> removeCustomSetting(String key) async {
|
||||
try {
|
||||
await _repository.removeCustomSetting(key);
|
||||
state = state.removeCustomSetting(key);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset to default settings
|
||||
Future<void> resetToDefault() async {
|
||||
try {
|
||||
await _repository.resetToDefault();
|
||||
state = AppSettings.defaultSettings();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Theme mode provider (derived from app settings)
|
||||
final themeModeProvider = Provider<ThemeMode>((ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
switch (settings.themeMode) {
|
||||
case 'light':
|
||||
return ThemeMode.light;
|
||||
case 'dark':
|
||||
return ThemeMode.dark;
|
||||
case 'system':
|
||||
default:
|
||||
return ThemeMode.system;
|
||||
}
|
||||
});
|
||||
|
||||
/// Current locale provider (derived from app settings)
|
||||
final localeProvider = Provider<String>((ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
return settings.locale;
|
||||
});
|
||||
|
||||
/// Notifications enabled provider
|
||||
final notificationsEnabledProvider = Provider<bool>((ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
return settings.notificationsEnabled;
|
||||
});
|
||||
|
||||
/// Analytics enabled provider
|
||||
final analyticsEnabledProvider = Provider<bool>((ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
return settings.analyticsEnabled;
|
||||
});
|
||||
|
||||
/// Cache strategy provider
|
||||
final cacheStrategyProvider = Provider<String>((ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
return settings.cacheStrategy;
|
||||
});
|
||||
|
||||
/// Cache expiration hours provider
|
||||
final cacheExpirationHoursProvider = Provider<int>((ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
return settings.cacheExpirationHours;
|
||||
});
|
||||
|
||||
/// Auto update enabled provider
|
||||
final autoUpdateEnabledProvider = Provider<bool>((ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
return settings.autoUpdateEnabled;
|
||||
});
|
||||
|
||||
/// Cache statistics provider
|
||||
final cacheStatsProvider = FutureProvider<CacheStats>((ref) async {
|
||||
final repository = ref.watch(cacheRepositoryProvider);
|
||||
return repository.getStats();
|
||||
});
|
||||
|
||||
/// Cache maintenance provider
|
||||
final cacheMaintenanceProvider = FutureProvider.family<Map<String, dynamic>, bool>(
|
||||
(ref, performMaintenance) async {
|
||||
if (!performMaintenance) return {};
|
||||
|
||||
final repository = ref.watch(cacheRepositoryProvider);
|
||||
return repository.performMaintenance();
|
||||
},
|
||||
);
|
||||
|
||||
/// Cache item provider for a specific key
|
||||
final cacheItemProvider = Provider.family<CacheItem?, String>((ref, key) {
|
||||
final repository = ref.watch(cacheRepositoryProvider);
|
||||
return repository.getCacheItem(key);
|
||||
});
|
||||
|
||||
/// Cache data provider for a specific key with type safety
|
||||
final cacheDataProvider = Provider.family<dynamic, String>((ref, key) {
|
||||
final repository = ref.watch(cacheRepositoryProvider);
|
||||
return repository.get(key);
|
||||
});
|
||||
|
||||
/// Database statistics provider
|
||||
final databaseStatsProvider = Provider<Map<String, dynamic>>((ref) {
|
||||
final settingsRepo = ref.watch(settingsRepositoryProvider);
|
||||
final cacheRepo = ref.watch(cacheRepositoryProvider);
|
||||
|
||||
final settingsStats = settingsRepo.getSettingsStats();
|
||||
final cacheStats = cacheRepo.getStats();
|
||||
|
||||
return {
|
||||
'settings': settingsStats,
|
||||
'cache': cacheStats.toMap(),
|
||||
'totalItems': settingsStats['totalSettingsInBox'] + cacheStats.totalItems,
|
||||
};
|
||||
});
|
||||
|
||||
/// Provider to clear expired cache items
|
||||
final clearExpiredCacheProvider = FutureProvider<int>((ref) async {
|
||||
final repository = ref.watch(cacheRepositoryProvider);
|
||||
return repository.clearExpired();
|
||||
});
|
||||
|
||||
/// Provider to get cache keys by pattern
|
||||
final cacheKeysByPatternProvider = Provider.family<List<String>, String>((ref, pattern) {
|
||||
final repository = ref.watch(cacheRepositoryProvider);
|
||||
return repository.getKeysByPattern(pattern);
|
||||
});
|
||||
|
||||
/// Provider to get cache keys by type
|
||||
final cacheKeysByTypeProvider = Provider.family<List<String>, String>((ref, type) {
|
||||
final repository = ref.watch(cacheRepositoryProvider);
|
||||
return repository.getKeysByType(type);
|
||||
});
|
||||
|
||||
/// Current user preferences provider
|
||||
final userPreferencesProvider = StateNotifierProvider.family<UserPreferencesNotifier, UserPreferences?, String?>((ref, userId) {
|
||||
final repository = ref.watch(userPreferencesRepositoryProvider);
|
||||
return UserPreferencesNotifier(repository, userId);
|
||||
});
|
||||
|
||||
/// User preferences notifier
|
||||
class UserPreferencesNotifier extends StateNotifier<UserPreferences?> {
|
||||
final UserPreferencesRepository _repository;
|
||||
final String? _userId;
|
||||
|
||||
UserPreferencesNotifier(this._repository, this._userId) : super(null) {
|
||||
_loadUserPreferences();
|
||||
}
|
||||
|
||||
void _loadUserPreferences() {
|
||||
try {
|
||||
state = _repository.getUserPreferences(_userId);
|
||||
} catch (e) {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new user preferences
|
||||
Future<void> createUserPreferences({
|
||||
required String userId,
|
||||
required String displayName,
|
||||
String? email,
|
||||
String? avatarUrl,
|
||||
Map<String, dynamic>? preferences,
|
||||
List<String>? favoriteItems,
|
||||
}) async {
|
||||
try {
|
||||
final newPreferences = await _repository.createUserPreferences(
|
||||
userId: userId,
|
||||
displayName: displayName,
|
||||
email: email,
|
||||
avatarUrl: avatarUrl,
|
||||
preferences: preferences,
|
||||
favoriteItems: favoriteItems,
|
||||
);
|
||||
state = newPreferences;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update profile information
|
||||
Future<void> updateProfile({
|
||||
String? displayName,
|
||||
String? email,
|
||||
String? avatarUrl,
|
||||
}) async {
|
||||
if (state == null) return;
|
||||
|
||||
try {
|
||||
await _repository.updateProfile(
|
||||
userId: _userId,
|
||||
displayName: displayName,
|
||||
email: email,
|
||||
avatarUrl: avatarUrl,
|
||||
);
|
||||
|
||||
state = state!.copyWith(
|
||||
displayName: displayName ?? state!.displayName,
|
||||
email: email ?? state!.email,
|
||||
avatarUrl: avatarUrl ?? state!.avatarUrl,
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set preference
|
||||
Future<void> setPreference(String key, dynamic value) async {
|
||||
if (state == null) return;
|
||||
|
||||
try {
|
||||
await _repository.setPreference(key, value, _userId);
|
||||
state = state!.setPreference(key, value);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove preference
|
||||
Future<void> removePreference(String key) async {
|
||||
if (state == null) return;
|
||||
|
||||
try {
|
||||
await _repository.removePreference(key, _userId);
|
||||
state = state!.removePreference(key);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add favorite
|
||||
Future<void> addFavorite(String itemId) async {
|
||||
if (state == null) return;
|
||||
|
||||
try {
|
||||
await _repository.addFavorite(itemId, _userId);
|
||||
state = state!.addFavorite(itemId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove favorite
|
||||
Future<void> removeFavorite(String itemId) async {
|
||||
if (state == null) return;
|
||||
|
||||
try {
|
||||
await _repository.removeFavorite(itemId, _userId);
|
||||
state = state!.removeFavorite(itemId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update last accessed
|
||||
Future<void> updateLastAccessed(String itemId) async {
|
||||
if (state == null) return;
|
||||
|
||||
try {
|
||||
await _repository.updateLastAccessed(itemId, _userId);
|
||||
state = state!.updateLastAccessed(itemId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear user preferences
|
||||
Future<void> clearPreferences() async {
|
||||
try {
|
||||
await _repository.clearUserPreferences(_userId);
|
||||
state = null;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// User preference provider for specific key
|
||||
final userPreferenceProvider = Provider.family.autoDispose<dynamic, (String, dynamic, String?)>((ref, params) {
|
||||
final (key, defaultValue, userId) = params;
|
||||
final repository = ref.watch(userPreferencesRepositoryProvider);
|
||||
return repository.getPreference(key, defaultValue, userId);
|
||||
});
|
||||
|
||||
/// User favorites provider
|
||||
final userFavoritesProvider = Provider.family<List<String>, String?>((ref, userId) {
|
||||
final repository = ref.watch(userPreferencesRepositoryProvider);
|
||||
return repository.getFavorites(userId);
|
||||
});
|
||||
|
||||
/// Recently accessed provider
|
||||
final recentlyAccessedProvider = Provider.family<List<String>, (int, String?)>((ref, params) {
|
||||
final (limit, userId) = params;
|
||||
final repository = ref.watch(userPreferencesRepositoryProvider);
|
||||
return repository.getRecentlyAccessed(limit: limit, userId: userId);
|
||||
});
|
||||
|
||||
/// Is favorite provider
|
||||
final isFavoriteProvider = Provider.family<bool, (String, String?)>((ref, params) {
|
||||
final (itemId, userId) = params;
|
||||
final repository = ref.watch(userPreferencesRepositoryProvider);
|
||||
return repository.isFavorite(itemId, userId);
|
||||
});
|
||||
|
||||
/// User stats provider
|
||||
final userStatsProvider = Provider.family<Map<String, dynamic>, String?>((ref, userId) {
|
||||
final repository = ref.watch(userPreferencesRepositoryProvider);
|
||||
return repository.getUserStats(userId);
|
||||
});
|
||||
480
lib/core/database/repositories/cache_repository.dart
Normal file
480
lib/core/database/repositories/cache_repository.dart
Normal file
@@ -0,0 +1,480 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../hive_service.dart';
|
||||
import '../models/cache_item.dart';
|
||||
|
||||
/// Repository for managing cached data using Hive
|
||||
class CacheRepository {
|
||||
/// Store data in cache with expiration
|
||||
Future<void> put<T>({
|
||||
required String key,
|
||||
required T data,
|
||||
required Duration expirationDuration,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final cacheItem = CacheItem.create(
|
||||
key: key,
|
||||
data: data,
|
||||
expirationDuration: expirationDuration,
|
||||
metadata: metadata,
|
||||
);
|
||||
await box.put(key, cacheItem);
|
||||
debugPrint('✅ Cache item stored: $key');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error storing cache item $key: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Store permanent data in cache (never expires)
|
||||
Future<void> putPermanent<T>({
|
||||
required String key,
|
||||
required T data,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final cacheItem = CacheItem.permanent(
|
||||
key: key,
|
||||
data: data,
|
||||
metadata: metadata,
|
||||
);
|
||||
await box.put(key, cacheItem);
|
||||
debugPrint('✅ Permanent cache item stored: $key');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error storing permanent cache item $key: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get data from cache
|
||||
T? get<T>(String key) {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final cacheItem = box.get(key);
|
||||
|
||||
if (cacheItem == null) {
|
||||
debugPrint('📭 Cache miss: $key');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cacheItem.isExpired) {
|
||||
debugPrint('⏰ Cache expired: $key');
|
||||
// Optionally remove expired item
|
||||
delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
debugPrint('✅ Cache hit: $key');
|
||||
return cacheItem.data as T?;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting cache item $key: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cache item with full metadata
|
||||
CacheItem? getCacheItem(String key) {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final cacheItem = box.get(key);
|
||||
|
||||
if (cacheItem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cacheItem.isExpired) {
|
||||
// Optionally remove expired item
|
||||
delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return cacheItem;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting cache item $key: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if key exists and is valid (not expired)
|
||||
bool contains(String key) {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final cacheItem = box.get(key);
|
||||
|
||||
if (cacheItem == null) return false;
|
||||
if (cacheItem.isExpired) {
|
||||
delete(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error checking cache item $key: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if key exists regardless of expiration
|
||||
bool containsKey(String key) {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
return box.containsKey(key);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error checking key $key: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete specific cache item
|
||||
Future<void> delete(String key) async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
await box.delete(key);
|
||||
debugPrint('🗑️ Cache item deleted: $key');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error deleting cache item $key: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete multiple cache items
|
||||
Future<void> deleteMultiple(List<String> keys) async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
for (final key in keys) {
|
||||
await box.delete(key);
|
||||
}
|
||||
debugPrint('🗑️ Multiple cache items deleted: ${keys.length} items');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error deleting multiple cache items: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all expired items
|
||||
Future<int> cleanExpiredItems() async {
|
||||
return await clearExpired();
|
||||
}
|
||||
|
||||
/// Clear all expired items
|
||||
Future<int> clearExpired() async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final expiredKeys = <String>[];
|
||||
final now = DateTime.now();
|
||||
|
||||
for (final key in box.keys) {
|
||||
final cacheItem = box.get(key);
|
||||
if (cacheItem != null && now.isAfter(cacheItem.expiresAt)) {
|
||||
expiredKeys.add(key as String);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in expiredKeys) {
|
||||
await box.delete(key);
|
||||
}
|
||||
|
||||
debugPrint('🧹 Cleared ${expiredKeys.length} expired cache items');
|
||||
return expiredKeys.length;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error clearing expired items: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all cache items
|
||||
Future<void> clearAll() async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final count = box.length;
|
||||
await box.clear();
|
||||
debugPrint('🧹 Cleared all cache items: $count items');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error clearing all cache items: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear cache items by pattern
|
||||
Future<int> clearByPattern(Pattern pattern) async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final keysToDelete = <String>[];
|
||||
|
||||
for (final key in box.keys) {
|
||||
if (key is String && key.contains(pattern)) {
|
||||
keysToDelete.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToDelete) {
|
||||
await box.delete(key);
|
||||
}
|
||||
|
||||
debugPrint('🧹 Cleared ${keysToDelete.length} cache items matching pattern: $pattern');
|
||||
return keysToDelete.length;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error clearing cache items by pattern: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear cache items by type
|
||||
Future<int> clearByType(String dataType) async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final keysToDelete = <String>[];
|
||||
|
||||
for (final key in box.keys) {
|
||||
final cacheItem = box.get(key);
|
||||
if (cacheItem != null && cacheItem.dataType == dataType) {
|
||||
keysToDelete.add(key as String);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToDelete) {
|
||||
await box.delete(key);
|
||||
}
|
||||
|
||||
debugPrint('🧹 Cleared ${keysToDelete.length} cache items of type: $dataType');
|
||||
return keysToDelete.length;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error clearing cache items by type: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh cache item with new expiration
|
||||
Future<bool> refresh(String key, Duration newExpirationDuration) async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final cacheItem = box.get(key);
|
||||
|
||||
if (cacheItem == null) return false;
|
||||
|
||||
final refreshedItem = cacheItem.refresh(newExpirationDuration);
|
||||
await box.put(key, refreshedItem);
|
||||
|
||||
debugPrint('🔄 Cache item refreshed: $key');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error refreshing cache item $key: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update cache item data
|
||||
Future<bool> update<T>(String key, T newData, {Duration? newExpirationDuration}) async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final cacheItem = box.get(key);
|
||||
|
||||
if (cacheItem == null) return false;
|
||||
|
||||
final updatedItem = cacheItem.updateData(newData, newExpirationDuration: newExpirationDuration);
|
||||
await box.put(key, updatedItem);
|
||||
|
||||
debugPrint('📝 Cache item updated: $key');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error updating cache item $key: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all keys in cache
|
||||
List<String> getAllKeys() {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
return box.keys.cast<String>().toList();
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting all keys: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get keys by pattern
|
||||
List<String> getKeysByPattern(Pattern pattern) {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
return box.keys
|
||||
.cast<String>()
|
||||
.where((key) => key.contains(pattern))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting keys by pattern: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get keys by data type
|
||||
List<String> getKeysByType(String dataType) {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final keys = <String>[];
|
||||
|
||||
for (final key in box.keys) {
|
||||
final cacheItem = box.get(key);
|
||||
if (cacheItem != null && cacheItem.dataType == dataType) {
|
||||
keys.add(key as String);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting keys by type: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cache statistics
|
||||
CacheStats getStats() {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final now = DateTime.now();
|
||||
var validItems = 0;
|
||||
var expiredItems = 0;
|
||||
DateTime? oldestItem;
|
||||
DateTime? newestItem;
|
||||
final typeCount = <String, int>{};
|
||||
|
||||
for (final key in box.keys) {
|
||||
final cacheItem = box.get(key);
|
||||
if (cacheItem == null) continue;
|
||||
|
||||
// Count by expiration status
|
||||
if (now.isAfter(cacheItem.expiresAt)) {
|
||||
expiredItems++;
|
||||
} else {
|
||||
validItems++;
|
||||
}
|
||||
|
||||
// Track oldest and newest items
|
||||
if (oldestItem == null || cacheItem.createdAt.isBefore(oldestItem)) {
|
||||
oldestItem = cacheItem.createdAt;
|
||||
}
|
||||
if (newestItem == null || cacheItem.createdAt.isAfter(newestItem)) {
|
||||
newestItem = cacheItem.createdAt;
|
||||
}
|
||||
|
||||
// Count by type
|
||||
typeCount[cacheItem.dataType] = (typeCount[cacheItem.dataType] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return CacheStats(
|
||||
totalItems: box.length,
|
||||
validItems: validItems,
|
||||
expiredItems: expiredItems,
|
||||
oldestItem: oldestItem ?? DateTime.now(),
|
||||
newestItem: newestItem ?? DateTime.now(),
|
||||
typeCount: typeCount,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting cache stats: $e');
|
||||
return CacheStats(
|
||||
totalItems: 0,
|
||||
validItems: 0,
|
||||
expiredItems: 0,
|
||||
oldestItem: DateTime.now(),
|
||||
newestItem: DateTime.now(),
|
||||
typeCount: const {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cache size in bytes (approximate)
|
||||
int getApproximateSize() {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
// This is an approximation as Hive doesn't provide exact size
|
||||
return box.length * 1024; // Assume average 1KB per item
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting cache size: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact cache storage
|
||||
Future<void> compact() async {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
await box.compact();
|
||||
debugPrint('✅ Cache storage compacted');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error compacting cache: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Export cache data (for debugging or backup)
|
||||
Map<String, dynamic> exportCache({bool includeExpired = false}) {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
final now = DateTime.now();
|
||||
final exportData = <String, dynamic>{};
|
||||
|
||||
for (final key in box.keys) {
|
||||
final cacheItem = box.get(key);
|
||||
if (cacheItem == null) continue;
|
||||
|
||||
if (!includeExpired && now.isAfter(cacheItem.expiresAt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
exportData[key as String] = cacheItem.toMap();
|
||||
}
|
||||
|
||||
return exportData;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error exporting cache: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch cache changes for a specific key
|
||||
Stream<CacheItem?> watch(String key) {
|
||||
try {
|
||||
final box = HiveService.cacheBox;
|
||||
return box.watch(key: key).map((event) => event.value as CacheItem?);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error watching cache key $key: $e');
|
||||
return Stream.value(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform cache maintenance (cleanup expired items, compact storage)
|
||||
Future<Map<String, dynamic>> performMaintenance() async {
|
||||
try {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
// Get stats before maintenance
|
||||
final statsBefore = getStats();
|
||||
|
||||
// Clear expired items
|
||||
final expiredCount = await clearExpired();
|
||||
|
||||
// Compact storage
|
||||
await compact();
|
||||
|
||||
// Get stats after maintenance
|
||||
final statsAfter = getStats();
|
||||
|
||||
final maintenanceTime = DateTime.now().difference(startTime);
|
||||
|
||||
final result = {
|
||||
'expiredItemsRemoved': expiredCount,
|
||||
'itemsBefore': statsBefore.totalItems,
|
||||
'itemsAfter': statsAfter.totalItems,
|
||||
'maintenanceTimeMs': maintenanceTime.inMilliseconds,
|
||||
'completedAt': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
debugPrint('🔧 Cache maintenance completed: $result');
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error during cache maintenance: $e');
|
||||
return {'error': e.toString()};
|
||||
}
|
||||
}
|
||||
}
|
||||
249
lib/core/database/repositories/settings_repository.dart
Normal file
249
lib/core/database/repositories/settings_repository.dart
Normal file
@@ -0,0 +1,249 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../hive_service.dart';
|
||||
import '../models/app_settings.dart';
|
||||
|
||||
/// Repository for managing application settings using Hive
|
||||
class SettingsRepository {
|
||||
static const String _defaultKey = 'app_settings';
|
||||
|
||||
/// Get the current app settings
|
||||
AppSettings getSettings() {
|
||||
try {
|
||||
final box = HiveService.appSettingsBox;
|
||||
final settings = box.get(_defaultKey);
|
||||
|
||||
if (settings == null) {
|
||||
// Return default settings if none exist
|
||||
final defaultSettings = AppSettings.defaultSettings();
|
||||
saveSettings(defaultSettings);
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
// Check if settings need migration
|
||||
if (settings.version < 1) {
|
||||
final migratedSettings = _migrateSettings(settings);
|
||||
saveSettings(migratedSettings);
|
||||
return migratedSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error getting settings: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
|
||||
// Return default settings on error
|
||||
return AppSettings.defaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/// Save app settings
|
||||
Future<void> saveSettings(AppSettings settings) async {
|
||||
try {
|
||||
final box = HiveService.appSettingsBox;
|
||||
final updatedSettings = settings.copyWith(lastUpdated: DateTime.now());
|
||||
await box.put(_defaultKey, updatedSettings);
|
||||
debugPrint('✅ Settings saved successfully');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error saving settings: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update theme mode
|
||||
Future<void> updateThemeMode(String themeMode) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.copyWith(themeMode: themeMode);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Update locale
|
||||
Future<void> updateLocale(String locale) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.copyWith(locale: locale);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Update notifications enabled
|
||||
Future<void> updateNotificationsEnabled(bool enabled) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.copyWith(notificationsEnabled: enabled);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Update analytics enabled
|
||||
Future<void> updateAnalyticsEnabled(bool enabled) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.copyWith(analyticsEnabled: enabled);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Update cache strategy
|
||||
Future<void> updateCacheStrategy(String strategy) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.copyWith(cacheStrategy: strategy);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Update cache expiration hours
|
||||
Future<void> updateCacheExpirationHours(int hours) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.copyWith(cacheExpirationHours: hours);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Update auto update enabled
|
||||
Future<void> updateAutoUpdateEnabled(bool enabled) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.copyWith(autoUpdateEnabled: enabled);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Set custom setting
|
||||
Future<void> setCustomSetting(String key, dynamic value) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.setCustomSetting(key, value);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Get custom setting
|
||||
T? getCustomSetting<T>(String key) {
|
||||
final settings = getSettings();
|
||||
return settings.getCustomSetting<T>(key);
|
||||
}
|
||||
|
||||
/// Remove custom setting
|
||||
Future<void> removeCustomSetting(String key) async {
|
||||
final currentSettings = getSettings();
|
||||
final updatedSettings = currentSettings.removeCustomSetting(key);
|
||||
await saveSettings(updatedSettings);
|
||||
}
|
||||
|
||||
/// Reset to default settings
|
||||
Future<void> resetToDefault() async {
|
||||
final defaultSettings = AppSettings.defaultSettings();
|
||||
await saveSettings(defaultSettings);
|
||||
debugPrint('✅ Settings reset to default');
|
||||
}
|
||||
|
||||
/// Export settings to Map (for backup)
|
||||
Map<String, dynamic> exportSettings() {
|
||||
try {
|
||||
final settings = getSettings();
|
||||
return settings.toMap();
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error exporting settings: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Import settings from Map (for restore)
|
||||
Future<bool> importSettings(Map<String, dynamic> settingsMap) async {
|
||||
try {
|
||||
final settings = AppSettings.fromMap(settingsMap);
|
||||
await saveSettings(settings);
|
||||
debugPrint('✅ Settings imported successfully');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error importing settings: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if settings exist
|
||||
bool hasSettings() {
|
||||
try {
|
||||
final box = HiveService.appSettingsBox;
|
||||
return box.containsKey(_defaultKey);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error checking settings existence: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all settings (use with caution)
|
||||
Future<void> clearSettings() async {
|
||||
try {
|
||||
final box = HiveService.appSettingsBox;
|
||||
await box.delete(_defaultKey);
|
||||
debugPrint('✅ Settings cleared');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error clearing settings: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get settings statistics
|
||||
Map<String, dynamic> getSettingsStats() {
|
||||
try {
|
||||
final settings = getSettings();
|
||||
final box = HiveService.appSettingsBox;
|
||||
|
||||
return {
|
||||
'hasCustomSettings': settings.customSettings?.isNotEmpty ?? false,
|
||||
'customSettingsCount': settings.customSettings?.length ?? 0,
|
||||
'lastUpdated': settings.lastUpdated.toIso8601String(),
|
||||
'version': settings.version,
|
||||
'settingsAge': DateTime.now().difference(settings.lastUpdated).inDays,
|
||||
'isExpired': settings.isExpired(),
|
||||
'totalSettingsInBox': box.length,
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting settings stats: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate settings from older version
|
||||
AppSettings _migrateSettings(AppSettings oldSettings) {
|
||||
debugPrint('🔄 Migrating settings from version ${oldSettings.version} to 1');
|
||||
|
||||
// Perform any necessary migrations here
|
||||
// For now, just update the version and timestamp
|
||||
return oldSettings.copyWith(
|
||||
version: 1,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Validate settings
|
||||
bool validateSettings(AppSettings settings) {
|
||||
try {
|
||||
// Basic validation
|
||||
if (settings.version < 1) return false;
|
||||
if (settings.cacheExpirationHours < 1) return false;
|
||||
if (!['light', 'dark', 'system'].contains(settings.themeMode)) return false;
|
||||
if (!['aggressive', 'normal', 'minimal'].contains(settings.cacheStrategy)) return false;
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error validating settings: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch settings changes
|
||||
Stream<AppSettings> watchSettings() {
|
||||
try {
|
||||
final box = HiveService.appSettingsBox;
|
||||
return box.watch(key: _defaultKey).map((event) {
|
||||
final settings = event.value as AppSettings?;
|
||||
return settings ?? AppSettings.defaultSettings();
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error watching settings: $e');
|
||||
return Stream.value(AppSettings.defaultSettings());
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact settings storage
|
||||
Future<void> compact() async {
|
||||
try {
|
||||
final box = HiveService.appSettingsBox;
|
||||
await box.compact();
|
||||
debugPrint('✅ Settings storage compacted');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error compacting settings: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
329
lib/core/database/repositories/user_preferences_repository.dart
Normal file
329
lib/core/database/repositories/user_preferences_repository.dart
Normal file
@@ -0,0 +1,329 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../hive_service.dart';
|
||||
import '../models/user_preferences.dart';
|
||||
|
||||
/// Repository for managing user preferences using Hive
|
||||
class UserPreferencesRepository {
|
||||
static const String _defaultKey = 'current_user_preferences';
|
||||
|
||||
/// Get the current user preferences (alias for getUserPreferences)
|
||||
UserPreferences? getPreferences([String? userId]) {
|
||||
return getUserPreferences(userId);
|
||||
}
|
||||
|
||||
/// Get the current user preferences
|
||||
UserPreferences? getUserPreferences([String? userId]) {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
final key = userId ?? _defaultKey;
|
||||
|
||||
final preferences = box.get(key);
|
||||
if (preferences == null) return null;
|
||||
|
||||
// Check if preferences need migration
|
||||
if (preferences.needsMigration()) {
|
||||
final migratedPreferences = preferences.migrate();
|
||||
saveUserPreferences(migratedPreferences, userId);
|
||||
return migratedPreferences;
|
||||
}
|
||||
|
||||
return preferences;
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error getting user preferences: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Save user preferences
|
||||
Future<void> saveUserPreferences(UserPreferences preferences, [String? userId]) async {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
final key = userId ?? _defaultKey;
|
||||
|
||||
final updatedPreferences = preferences.copyWith(lastUpdated: DateTime.now());
|
||||
await box.put(key, updatedPreferences);
|
||||
|
||||
debugPrint('✅ User preferences saved for key: $key');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ Error saving user preferences: $e');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new user preferences
|
||||
Future<UserPreferences> createUserPreferences({
|
||||
required String userId,
|
||||
required String displayName,
|
||||
String? email,
|
||||
String? avatarUrl,
|
||||
Map<String, dynamic>? preferences,
|
||||
List<String>? favoriteItems,
|
||||
}) async {
|
||||
final userPreferences = UserPreferences.create(
|
||||
userId: userId,
|
||||
displayName: displayName,
|
||||
email: email,
|
||||
avatarUrl: avatarUrl,
|
||||
preferences: preferences,
|
||||
favoriteItems: favoriteItems,
|
||||
);
|
||||
|
||||
await saveUserPreferences(userPreferences, userId);
|
||||
return userPreferences;
|
||||
}
|
||||
|
||||
/// Update user profile information
|
||||
Future<void> updateProfile({
|
||||
String? userId,
|
||||
String? displayName,
|
||||
String? email,
|
||||
String? avatarUrl,
|
||||
}) async {
|
||||
final currentPreferences = getUserPreferences(userId);
|
||||
if (currentPreferences == null) return;
|
||||
|
||||
final updatedPreferences = currentPreferences.copyWith(
|
||||
displayName: displayName ?? currentPreferences.displayName,
|
||||
email: email ?? currentPreferences.email,
|
||||
avatarUrl: avatarUrl ?? currentPreferences.avatarUrl,
|
||||
);
|
||||
|
||||
await saveUserPreferences(updatedPreferences, userId);
|
||||
}
|
||||
|
||||
/// Set a user preference
|
||||
Future<void> setPreference(String key, dynamic value, [String? userId]) async {
|
||||
final currentPreferences = getUserPreferences(userId);
|
||||
if (currentPreferences == null) return;
|
||||
|
||||
final updatedPreferences = currentPreferences.setPreference(key, value);
|
||||
await saveUserPreferences(updatedPreferences, userId);
|
||||
}
|
||||
|
||||
/// Get a user preference with type safety
|
||||
T getPreference<T>(String key, T defaultValue, [String? userId]) {
|
||||
final preferences = getUserPreferences(userId);
|
||||
if (preferences == null) return defaultValue;
|
||||
|
||||
return preferences.getPreference<T>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// Remove a user preference
|
||||
Future<void> removePreference(String key, [String? userId]) async {
|
||||
final currentPreferences = getUserPreferences(userId);
|
||||
if (currentPreferences == null) return;
|
||||
|
||||
final updatedPreferences = currentPreferences.removePreference(key);
|
||||
await saveUserPreferences(updatedPreferences, userId);
|
||||
}
|
||||
|
||||
/// Add item to favorites
|
||||
Future<void> addFavorite(String itemId, [String? userId]) async {
|
||||
final currentPreferences = getUserPreferences(userId);
|
||||
if (currentPreferences == null) return;
|
||||
|
||||
final updatedPreferences = currentPreferences.addFavorite(itemId);
|
||||
await saveUserPreferences(updatedPreferences, userId);
|
||||
}
|
||||
|
||||
/// Remove item from favorites
|
||||
Future<void> removeFavorite(String itemId, [String? userId]) async {
|
||||
final currentPreferences = getUserPreferences(userId);
|
||||
if (currentPreferences == null) return;
|
||||
|
||||
final updatedPreferences = currentPreferences.removeFavorite(itemId);
|
||||
await saveUserPreferences(updatedPreferences, userId);
|
||||
}
|
||||
|
||||
/// Check if item is favorite
|
||||
bool isFavorite(String itemId, [String? userId]) {
|
||||
final preferences = getUserPreferences(userId);
|
||||
return preferences?.isFavorite(itemId) ?? false;
|
||||
}
|
||||
|
||||
/// Get all favorite items
|
||||
List<String> getFavorites([String? userId]) {
|
||||
final preferences = getUserPreferences(userId);
|
||||
return preferences?.favoriteItems ?? [];
|
||||
}
|
||||
|
||||
/// Update last accessed time for an item
|
||||
Future<void> updateLastAccessed(String itemId, [String? userId]) async {
|
||||
final currentPreferences = getUserPreferences(userId);
|
||||
if (currentPreferences == null) return;
|
||||
|
||||
final updatedPreferences = currentPreferences.updateLastAccessed(itemId);
|
||||
await saveUserPreferences(updatedPreferences, userId);
|
||||
}
|
||||
|
||||
/// Get last accessed time for an item
|
||||
DateTime? getLastAccessed(String itemId, [String? userId]) {
|
||||
final preferences = getUserPreferences(userId);
|
||||
return preferences?.getLastAccessed(itemId);
|
||||
}
|
||||
|
||||
/// Get recently accessed items
|
||||
List<String> getRecentlyAccessed({int limit = 10, String? userId}) {
|
||||
final preferences = getUserPreferences(userId);
|
||||
return preferences?.getRecentlyAccessed(limit: limit) ?? [];
|
||||
}
|
||||
|
||||
/// Clean old access records
|
||||
Future<void> cleanOldAccess({int maxAgeDays = 30, String? userId}) async {
|
||||
final currentPreferences = getUserPreferences(userId);
|
||||
if (currentPreferences == null) return;
|
||||
|
||||
final updatedPreferences = currentPreferences.cleanOldAccess(maxAgeDays: maxAgeDays);
|
||||
await saveUserPreferences(updatedPreferences, userId);
|
||||
}
|
||||
|
||||
/// Get user statistics
|
||||
Map<String, dynamic> getUserStats([String? userId]) {
|
||||
final preferences = getUserPreferences(userId);
|
||||
return preferences?.getStats() ?? {};
|
||||
}
|
||||
|
||||
/// Export user preferences to Map (for backup)
|
||||
Map<String, dynamic> exportUserPreferences([String? userId]) {
|
||||
try {
|
||||
final preferences = getUserPreferences(userId);
|
||||
return preferences?.toMap() ?? {};
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error exporting user preferences: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Import user preferences from Map (for restore)
|
||||
Future<bool> importUserPreferences(Map<String, dynamic> preferencesMap, [String? userId]) async {
|
||||
try {
|
||||
final preferences = UserPreferences.fromMap(preferencesMap);
|
||||
await saveUserPreferences(preferences, userId);
|
||||
debugPrint('✅ User preferences imported successfully');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error importing user preferences: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if user preferences exist
|
||||
bool hasUserPreferences([String? userId]) {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
final key = userId ?? _defaultKey;
|
||||
return box.containsKey(key);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error checking user preferences existence: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear user preferences (use with caution)
|
||||
Future<void> clearUserPreferences([String? userId]) async {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
final key = userId ?? _defaultKey;
|
||||
await box.delete(key);
|
||||
debugPrint('✅ User preferences cleared for key: $key');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error clearing user preferences: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all user IDs that have preferences stored
|
||||
List<String> getAllUserIds() {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
return box.keys.cast<String>().where((key) => key != _defaultKey).toList();
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting all user IDs: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete preferences for a specific user
|
||||
Future<void> deleteUserPreferences(String userId) async {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
await box.delete(userId);
|
||||
debugPrint('✅ User preferences deleted for user: $userId');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error deleting user preferences: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get multiple users' preferences
|
||||
Map<String, UserPreferences> getMultipleUserPreferences(List<String> userIds) {
|
||||
final result = <String, UserPreferences>{};
|
||||
|
||||
for (final userId in userIds) {
|
||||
final preferences = getUserPreferences(userId);
|
||||
if (preferences != null) {
|
||||
result[userId] = preferences;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Validate user preferences
|
||||
bool validateUserPreferences(UserPreferences preferences) {
|
||||
try {
|
||||
// Basic validation
|
||||
if (preferences.userId.isEmpty) return false;
|
||||
if (preferences.displayName.isEmpty) return false;
|
||||
if (preferences.version < 1) return false;
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error validating user preferences: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch user preferences changes
|
||||
Stream<UserPreferences?> watchUserPreferences([String? userId]) {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
final key = userId ?? _defaultKey;
|
||||
return box.watch(key: key).map((event) => event.value as UserPreferences?);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error watching user preferences: $e');
|
||||
return Stream.value(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact user preferences storage
|
||||
Future<void> compact() async {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
await box.compact();
|
||||
debugPrint('✅ User preferences storage compacted');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error compacting user preferences: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Map<String, dynamic> getStorageStats() {
|
||||
try {
|
||||
final box = HiveService.userDataBox;
|
||||
final allUserIds = getAllUserIds();
|
||||
|
||||
return {
|
||||
'totalUsers': allUserIds.length,
|
||||
'hasDefaultUser': hasUserPreferences(),
|
||||
'totalEntries': box.length,
|
||||
'userIds': allUserIds,
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error getting storage stats: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user