mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
feat adds proper syncing feature
Signed-off-by: Menno van Leeuwen <menno@vleeuwen.me>
This commit is contained in:
106
lib/providers/simple_sync_provider.dart
Normal file
106
lib/providers/simple_sync_provider.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../services/database_sync_service.dart';
|
||||
|
||||
class SimpleSyncProvider with ChangeNotifier {
|
||||
final DatabaseSyncService _syncService = DatabaseSyncService();
|
||||
|
||||
// Callback for UI refresh after sync
|
||||
VoidCallback? _onSyncCompleteCallback;
|
||||
|
||||
// Getters
|
||||
SyncStatus get status => _syncService.status;
|
||||
String? get lastError => _syncService.lastError;
|
||||
DateTime? get lastSyncTime => _syncService.lastSyncTime;
|
||||
bool get isConfigured => _syncService.isConfigured;
|
||||
bool get isSyncing => status == SyncStatus.downloading ||
|
||||
status == SyncStatus.merging ||
|
||||
status == SyncStatus.uploading;
|
||||
|
||||
// Configuration getters
|
||||
String? get serverUrl => _syncService.serverUrl;
|
||||
String? get username => _syncService.username;
|
||||
String? get password => _syncService.password;
|
||||
String? get remotePath => _syncService.remotePath;
|
||||
|
||||
SimpleSyncProvider() {
|
||||
// Set up callbacks to notify listeners
|
||||
_syncService.onStatusChanged = (_) => notifyListeners();
|
||||
_syncService.onError = (_) => notifyListeners();
|
||||
_syncService.onSyncCompleted = () {
|
||||
notifyListeners();
|
||||
// Trigger UI refresh callback if set
|
||||
_onSyncCompleteCallback?.call();
|
||||
};
|
||||
|
||||
// Load saved configuration and notify listeners when done
|
||||
_loadConfiguration();
|
||||
}
|
||||
|
||||
/// Set callback to refresh UI data after sync completes
|
||||
void setOnSyncCompleteCallback(VoidCallback? callback) {
|
||||
_onSyncCompleteCallback = callback;
|
||||
}
|
||||
|
||||
Future<void> _loadConfiguration() async {
|
||||
await _syncService.loadSavedConfiguration();
|
||||
notifyListeners(); // Notify UI that configuration might be available
|
||||
}
|
||||
|
||||
Future<void> configure({
|
||||
required String serverUrl,
|
||||
required String username,
|
||||
required String password,
|
||||
required String remotePath,
|
||||
}) async {
|
||||
_syncService.configure(
|
||||
serverUrl: serverUrl,
|
||||
username: username,
|
||||
password: password,
|
||||
remotePath: remotePath,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> testConnection() async {
|
||||
return await _syncService.testConnection();
|
||||
}
|
||||
|
||||
Future<void> syncDatabase() async {
|
||||
if (!isConfigured) {
|
||||
throw Exception('Sync not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
await _syncService.syncDatabase();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('SupplementsLog: Sync failed in provider: $e');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_syncService.clearError();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String getStatusText() {
|
||||
switch (status) {
|
||||
case SyncStatus.idle:
|
||||
return 'Ready to sync';
|
||||
case SyncStatus.downloading:
|
||||
return 'Downloading remote database...';
|
||||
case SyncStatus.merging:
|
||||
return 'Merging databases...';
|
||||
case SyncStatus.uploading:
|
||||
return 'Uploading database...';
|
||||
case SyncStatus.completed:
|
||||
return 'Sync completed successfully';
|
||||
case SyncStatus.error:
|
||||
return 'Sync failed: ${lastError ?? 'Unknown error'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -46,31 +46,31 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
await _notificationService.initialize();
|
||||
|
||||
// Set up the callback for handling supplement intake from notifications
|
||||
print('📱 Setting up notification callback...');
|
||||
print('SupplementsLog: 📱 Setting up notification callback...');
|
||||
_notificationService.setTakeSupplementCallback((supplementId, supplementName, units, unitType) {
|
||||
print('📱 === NOTIFICATION CALLBACK TRIGGERED ===');
|
||||
print('📱 Supplement ID: $supplementId');
|
||||
print('📱 Supplement Name: $supplementName');
|
||||
print('📱 Units: $units');
|
||||
print('📱 Unit Type: $unitType');
|
||||
print('SupplementsLog: 📱 === NOTIFICATION CALLBACK TRIGGERED ===');
|
||||
print('SupplementsLog: 📱 Supplement ID: $supplementId');
|
||||
print('SupplementsLog: 📱 Supplement Name: $supplementName');
|
||||
print('SupplementsLog: 📱 Units: $units');
|
||||
print('SupplementsLog: 📱 Unit Type: $unitType');
|
||||
|
||||
// Record the intake when user taps "Take" on notification
|
||||
recordIntake(supplementId, 0.0, unitsTaken: units);
|
||||
print('📱 Intake recorded successfully');
|
||||
print('📱 === CALLBACK COMPLETE ===');
|
||||
print('SupplementsLog: 📱 Intake recorded successfully');
|
||||
print('SupplementsLog: 📱 === CALLBACK COMPLETE ===');
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📱 Recorded intake from notification: $supplementName ($units $unitType)');
|
||||
print('SupplementsLog: 📱 Recorded intake from notification: $supplementName ($units $unitType)');
|
||||
}
|
||||
});
|
||||
print('📱 Notification callback setup complete');
|
||||
print('SupplementsLog: 📱 Notification callback setup complete');
|
||||
|
||||
// Request permissions with error handling
|
||||
try {
|
||||
await _notificationService.requestPermissions();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error requesting notification permissions: $e');
|
||||
print('SupplementsLog: Error requesting notification permissions: $e');
|
||||
}
|
||||
// Continue without notifications rather than crashing
|
||||
}
|
||||
@@ -99,7 +99,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
await _checkPersistentReminders();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error checking persistent reminders: $e');
|
||||
print('SupplementsLog: Error checking persistent reminders: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -120,8 +120,8 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
|
||||
if (currentDate != lastCheckDate) {
|
||||
if (kDebugMode) {
|
||||
print('Date changed detected: ${lastCheckDate} -> ${currentDate}');
|
||||
print('Refreshing today\'s intakes for new day...');
|
||||
print('SupplementsLog: Date changed detected: ${lastCheckDate} -> ${currentDate}');
|
||||
print('SupplementsLog: Refreshing today\'s intakes for new day...');
|
||||
}
|
||||
|
||||
// Date has changed, refresh today's intakes
|
||||
@@ -129,7 +129,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
await loadTodayIntakes();
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Today\'s intakes refreshed for new day');
|
||||
print('SupplementsLog: Today\'s intakes refreshed for new day');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -140,7 +140,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
// For now, we'll check with default settings
|
||||
// In practice, the UI should call checkPersistentRemindersWithSettings
|
||||
if (kDebugMode) {
|
||||
print('📱 Checking persistent reminders with default settings');
|
||||
print('SupplementsLog: 📱 Checking persistent reminders with default settings');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
required int reminderRetryInterval,
|
||||
required int maxRetryAttempts,
|
||||
}) async {
|
||||
print('📱 🔄 MANUAL CHECK: Persistent reminders called from UI');
|
||||
print('SupplementsLog: 📱 🔄 MANUAL CHECK: Persistent reminders called from UI');
|
||||
await _notificationService.checkPersistentReminders(
|
||||
persistentReminders,
|
||||
reminderRetryInterval,
|
||||
@@ -160,7 +160,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
|
||||
// Add a manual trigger method for testing
|
||||
Future<void> triggerRetryCheck() async {
|
||||
print('📱 🚨 MANUAL TRIGGER: Forcing retry check...');
|
||||
print('SupplementsLog: 📱 🚨 MANUAL TRIGGER: Forcing retry check...');
|
||||
await checkPersistentRemindersWithSettings(
|
||||
persistentReminders: true,
|
||||
reminderRetryInterval: 5, // Force 5 minute interval for testing
|
||||
@@ -183,7 +183,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
// App came back to foreground, check if date changed
|
||||
if (kDebugMode) {
|
||||
print('App resumed, checking for date change...');
|
||||
print('SupplementsLog: App resumed, checking for date change...');
|
||||
}
|
||||
forceCheckDateChange();
|
||||
}
|
||||
@@ -191,7 +191,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
|
||||
Future<void> _rescheduleAllNotifications() async {
|
||||
if (kDebugMode) {
|
||||
print('📱 Rescheduling notifications for all active supplements...');
|
||||
print('SupplementsLog: 📱 Rescheduling notifications for all active supplements...');
|
||||
}
|
||||
|
||||
for (final supplement in _supplements) {
|
||||
@@ -200,14 +200,14 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
await _notificationService.scheduleSupplementReminders(supplement);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('📱 Error rescheduling notifications for ${supplement.name}: $e');
|
||||
print('SupplementsLog: 📱 Error rescheduling notifications for ${supplement.name}: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📱 Finished rescheduling notifications');
|
||||
print('SupplementsLog: 📱 Finished rescheduling notifications');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,16 +216,16 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
print('Loading supplements from database...');
|
||||
print('SupplementsLog: Loading supplements from database...');
|
||||
_supplements = await _databaseHelper.getAllSupplements();
|
||||
print('Loaded ${_supplements.length} supplements');
|
||||
print('SupplementsLog: Loaded ${_supplements.length} supplements');
|
||||
for (var supplement in _supplements) {
|
||||
print('Supplement: ${supplement.name}');
|
||||
print('SupplementsLog: Supplement: ${supplement.name}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading supplements: $e');
|
||||
print('SupplementsLog: Error loading supplements: $e');
|
||||
if (kDebugMode) {
|
||||
print('Error loading supplements: $e');
|
||||
print('SupplementsLog: Error loading supplements: $e');
|
||||
}
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
@@ -235,28 +235,28 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
|
||||
Future<void> addSupplement(Supplement supplement) async {
|
||||
try {
|
||||
print('Adding supplement: ${supplement.name}');
|
||||
print('SupplementsLog: Adding supplement: ${supplement.name}');
|
||||
final id = await _databaseHelper.insertSupplement(supplement);
|
||||
print('Supplement inserted with ID: $id');
|
||||
print('SupplementsLog: Supplement inserted with ID: $id');
|
||||
final newSupplement = supplement.copyWith(id: id);
|
||||
|
||||
// Schedule notifications (skip if there's an error)
|
||||
try {
|
||||
await _notificationService.scheduleSupplementReminders(newSupplement);
|
||||
print('Notifications scheduled');
|
||||
print('SupplementsLog: Notifications scheduled');
|
||||
} catch (notificationError) {
|
||||
print('Warning: Could not schedule notifications: $notificationError');
|
||||
print('SupplementsLog: Warning: Could not schedule notifications: $notificationError');
|
||||
}
|
||||
|
||||
await loadSupplements();
|
||||
print('Supplements reloaded, count: ${_supplements.length}');
|
||||
print('SupplementsLog: Supplements reloaded, count: ${_supplements.length}');
|
||||
|
||||
// Trigger sync after adding supplement
|
||||
_triggerSyncIfEnabled();
|
||||
} catch (e) {
|
||||
print('Error adding supplement: $e');
|
||||
print('SupplementsLog: Error adding supplement: $e');
|
||||
if (kDebugMode) {
|
||||
print('Error adding supplement: $e');
|
||||
print('SupplementsLog: Error adding supplement: $e');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
@@ -275,7 +275,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
_triggerSyncIfEnabled();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error updating supplement: $e');
|
||||
print('SupplementsLog: Error updating supplement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,7 +293,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
_triggerSyncIfEnabled();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error deleting supplement: $e');
|
||||
print('SupplementsLog: Error deleting supplement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,7 +323,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error recording intake: $e');
|
||||
print('SupplementsLog: Error recording intake: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,22 +332,22 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
try {
|
||||
final today = DateTime.now();
|
||||
if (kDebugMode) {
|
||||
print('Loading intakes for date: ${today.year}-${today.month}-${today.day}');
|
||||
print('SupplementsLog: Loading intakes for date: ${today.year}-${today.month}-${today.day}');
|
||||
}
|
||||
|
||||
_todayIntakes = await _databaseHelper.getIntakesWithSupplementsForDate(today);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Loaded ${_todayIntakes.length} intakes for today');
|
||||
print('SupplementsLog: Loaded ${_todayIntakes.length} intakes for today');
|
||||
for (var intake in _todayIntakes) {
|
||||
print(' - Supplement ID: ${intake['supplement_id']}, taken at: ${intake['takenAt']}');
|
||||
print('SupplementsLog: - Supplement ID: ${intake['supplement_id']}, taken at: ${intake['takenAt']}');
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error loading today\'s intakes: $e');
|
||||
print('SupplementsLog: Error loading today\'s intakes: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,7 +358,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error loading monthly intakes: $e');
|
||||
print('SupplementsLog: Error loading monthly intakes: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,7 +368,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
return await _databaseHelper.getIntakesWithSupplementsForDate(date);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error loading intakes for date: $e');
|
||||
print('SupplementsLog: Error loading intakes for date: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -388,7 +388,26 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
_triggerSyncIfEnabled();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error deleting intake: $e');
|
||||
print('SupplementsLog: Error deleting intake: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> permanentlyDeleteIntake(int intakeId) async {
|
||||
try {
|
||||
await _databaseHelper.permanentlyDeleteIntake(intakeId);
|
||||
await loadTodayIntakes();
|
||||
// Also refresh monthly intakes if they're loaded
|
||||
if (_monthlyIntakes.isNotEmpty) {
|
||||
await loadMonthlyIntakes(DateTime.now().year, DateTime.now().month);
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
// Trigger sync after permanently deleting intake
|
||||
_triggerSyncIfEnabled();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('SupplementsLog: Error permanently deleting intake: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,7 +423,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
// Method to manually refresh daily status (useful for testing or manual refresh)
|
||||
Future<void> refreshDailyStatus() async {
|
||||
if (kDebugMode) {
|
||||
print('Manually refreshing daily status...');
|
||||
print('SupplementsLog: Manually refreshing daily status...');
|
||||
}
|
||||
_lastDateCheck = DateTime.now();
|
||||
await loadTodayIntakes();
|
||||
@@ -417,20 +436,20 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
final lastCheckDate = DateTime(_lastDateCheck.year, _lastDateCheck.month, _lastDateCheck.day);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Force checking date change...');
|
||||
print('Current date: $currentDate');
|
||||
print('Last check date: $lastCheckDate');
|
||||
print('SupplementsLog: Force checking date change...');
|
||||
print('SupplementsLog: Current date: $currentDate');
|
||||
print('SupplementsLog: Last check date: $lastCheckDate');
|
||||
}
|
||||
|
||||
if (currentDate != lastCheckDate) {
|
||||
if (kDebugMode) {
|
||||
print('Date change detected, refreshing intakes...');
|
||||
print('SupplementsLog: Date change detected, refreshing intakes...');
|
||||
}
|
||||
_lastDateCheck = now;
|
||||
await loadTodayIntakes();
|
||||
} else {
|
||||
if (kDebugMode) {
|
||||
print('No date change detected');
|
||||
print('SupplementsLog: No date change detected');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,7 +464,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error loading archived supplements: $e');
|
||||
print('SupplementsLog: Error loading archived supplements: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,7 +479,7 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
_triggerSyncIfEnabled();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error archiving supplement: $e');
|
||||
print('SupplementsLog: Error archiving supplement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -475,21 +494,21 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver {
|
||||
_triggerSyncIfEnabled();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error unarchiving supplement: $e');
|
||||
print('SupplementsLog: Error unarchiving supplement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteArchivedSupplement(int supplementId) async {
|
||||
try {
|
||||
await _databaseHelper.deleteSupplement(supplementId);
|
||||
await _databaseHelper.permanentlyDeleteSupplement(supplementId);
|
||||
await loadArchivedSupplements(); // Refresh archived supplements
|
||||
|
||||
// Trigger sync after deleting archived supplement
|
||||
// Trigger sync after permanently deleting archived supplement
|
||||
_triggerSyncIfEnabled();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error deleting archived supplement: $e');
|
||||
print('SupplementsLog: Error permanently deleting archived supplement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,417 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/sync_data.dart';
|
||||
import '../models/sync_enums.dart';
|
||||
import '../services/webdav_sync_service.dart';
|
||||
import 'supplement_provider.dart';
|
||||
|
||||
/// Provider for managing sync operations with WebDAV servers
|
||||
class SyncProvider with ChangeNotifier {
|
||||
final WebDAVSyncService _syncService = WebDAVSyncService();
|
||||
final SupplementProvider _supplementProvider;
|
||||
|
||||
SyncOperationStatus _status = SyncOperationStatus.idle;
|
||||
SyncResult? _lastSyncResult;
|
||||
DateTime? _lastSyncTime;
|
||||
String? _currentError;
|
||||
bool _isAutoSyncEnabled = false;
|
||||
bool _autoSyncOnDataChanges = true;
|
||||
SyncFrequency _syncFrequency = SyncFrequency.hourly;
|
||||
ConflictResolutionStrategy _conflictStrategy = ConflictResolutionStrategy.manual;
|
||||
List<SyncConflict> _pendingConflicts = [];
|
||||
double _syncProgress = 0.0;
|
||||
|
||||
// WebDAV Configuration
|
||||
String? _serverUrl;
|
||||
String? _username;
|
||||
String? _detectedServerType;
|
||||
String? _finalWebdavUrl;
|
||||
String? _syncFolderName;
|
||||
bool _isConfigured = false;
|
||||
|
||||
SyncProvider(this._supplementProvider);
|
||||
|
||||
// Getters
|
||||
SyncOperationStatus get status => _status;
|
||||
SyncResult? get lastSyncResult => _lastSyncResult;
|
||||
DateTime? get lastSyncTime => _lastSyncTime;
|
||||
String? get currentError => _currentError;
|
||||
bool get isAutoSyncEnabled => _isAutoSyncEnabled;
|
||||
bool get autoSyncOnDataChanges => _autoSyncOnDataChanges;
|
||||
SyncFrequency get syncFrequency => _syncFrequency;
|
||||
ConflictResolutionStrategy get conflictStrategy => _conflictStrategy;
|
||||
List<SyncConflict> get pendingConflicts => List.unmodifiable(_pendingConflicts);
|
||||
double get syncProgress => _syncProgress;
|
||||
String? get serverUrl => _serverUrl;
|
||||
String? get username => _username;
|
||||
String? get detectedServerType => _detectedServerType;
|
||||
String? get finalWebdavUrl => _finalWebdavUrl;
|
||||
String? get syncFolderName => _syncFolderName;
|
||||
bool get isConfigured => _isConfigured;
|
||||
bool get isSyncing => _status == SyncOperationStatus.syncing;
|
||||
bool get hasError => _currentError != null;
|
||||
bool get hasPendingConflicts => _pendingConflicts.isNotEmpty;
|
||||
|
||||
/// Initialize the sync provider
|
||||
Future<void> initialize() async {
|
||||
await _syncService.initialize();
|
||||
_isConfigured = await _syncService.isConfigured();
|
||||
_lastSyncTime = await _syncService.getLastSyncTime();
|
||||
|
||||
if (_isConfigured) {
|
||||
// Load existing configuration
|
||||
_serverUrl = await _syncService.getServerUrl();
|
||||
_username = await _syncService.getUsername();
|
||||
_syncFolderName = await _syncService.getSyncFolderName();
|
||||
_finalWebdavUrl = await _syncService.getLastWorkingUrl();
|
||||
_detectedServerType = _detectServerTypeFromUrl(_finalWebdavUrl ?? _serverUrl ?? '');
|
||||
}
|
||||
|
||||
// Set up data change callback on supplement provider
|
||||
_supplementProvider.setOnDataChangedCallback(() {
|
||||
triggerAutoSyncOnDataChange();
|
||||
});
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Configure WebDAV connection
|
||||
Future<bool> configure({
|
||||
required String serverUrl,
|
||||
required String username,
|
||||
String? password,
|
||||
String? deviceName,
|
||||
String? syncFolderName,
|
||||
}) async {
|
||||
try {
|
||||
_setStatus(SyncOperationStatus.syncing);
|
||||
_clearError();
|
||||
|
||||
final success = await _syncService.configure(
|
||||
baseUrl: serverUrl,
|
||||
username: username,
|
||||
password: password,
|
||||
deviceName: deviceName,
|
||||
syncFolderName: syncFolderName ?? 'Supplements',
|
||||
);
|
||||
|
||||
if (success) {
|
||||
_serverUrl = serverUrl;
|
||||
_username = username;
|
||||
_syncFolderName = syncFolderName ?? 'Supplements';
|
||||
_isConfigured = true;
|
||||
|
||||
// Get server detection info
|
||||
_finalWebdavUrl = await _syncService.getLastWorkingUrl();
|
||||
_detectedServerType = _detectServerTypeFromUrl(_finalWebdavUrl ?? serverUrl);
|
||||
|
||||
_setStatus(SyncOperationStatus.success);
|
||||
} else {
|
||||
_setError('Failed to configure WebDAV connection', SyncOperationStatus.authenticationError);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (e) {
|
||||
_setError(e.toString(), SyncOperationStatus.authenticationError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Test WebDAV connection
|
||||
Future<bool> testConnection() async {
|
||||
if (!_isConfigured) return false;
|
||||
|
||||
try {
|
||||
_clearError();
|
||||
return await _syncService.testConnection();
|
||||
} catch (e) {
|
||||
_setError(e.toString(), SyncOperationStatus.networkError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear WebDAV configuration
|
||||
Future<void> clearConfiguration() async {
|
||||
await _syncService.clearConfiguration();
|
||||
_serverUrl = null;
|
||||
_username = null;
|
||||
_detectedServerType = null;
|
||||
_finalWebdavUrl = null;
|
||||
_syncFolderName = null;
|
||||
_isConfigured = false;
|
||||
_setStatus(SyncOperationStatus.idle);
|
||||
_clearError();
|
||||
}
|
||||
|
||||
/// Perform manual sync
|
||||
Future<bool> performManualSync() async {
|
||||
if (!_isConfigured || _status == SyncOperationStatus.syncing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
_setStatus(SyncOperationStatus.syncing);
|
||||
_clearError();
|
||||
_syncProgress = 0.0;
|
||||
|
||||
// Check connectivity first
|
||||
if (!await _syncService.hasConnectivity()) {
|
||||
_setError('No internet connection', SyncOperationStatus.networkError);
|
||||
return false;
|
||||
}
|
||||
|
||||
_syncProgress = 0.2;
|
||||
notifyListeners();
|
||||
|
||||
// Perform the sync
|
||||
final result = await _syncService.performSync();
|
||||
_lastSyncResult = result;
|
||||
_lastSyncTime = result.timestamp;
|
||||
|
||||
_syncProgress = 0.8;
|
||||
notifyListeners();
|
||||
|
||||
if (result.success) {
|
||||
if (result.conflicts.isNotEmpty) {
|
||||
_pendingConflicts = result.conflicts;
|
||||
_setStatus(SyncOperationStatus.conflictsDetected);
|
||||
} else {
|
||||
_setStatus(SyncOperationStatus.success);
|
||||
}
|
||||
|
||||
// Reload local data after successful sync
|
||||
await _supplementProvider.loadSupplements();
|
||||
await _supplementProvider.loadTodayIntakes();
|
||||
} else {
|
||||
_setError(result.error ?? 'Sync failed', result.status);
|
||||
}
|
||||
|
||||
_syncProgress = 1.0;
|
||||
notifyListeners();
|
||||
|
||||
return result.success;
|
||||
} catch (e) {
|
||||
_setError(e.toString(), SyncOperationStatus.serverError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a conflict
|
||||
Future<void> resolveConflict(String syncId, ConflictResolution resolution) async {
|
||||
_pendingConflicts.removeWhere((conflict) => conflict.syncId == syncId);
|
||||
|
||||
// TODO: Implement actual conflict resolution logic
|
||||
// This would involve updating the local database with the chosen resolution
|
||||
|
||||
if (_pendingConflicts.isEmpty && _status == SyncOperationStatus.conflictsDetected) {
|
||||
_setStatus(SyncOperationStatus.success);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Resolve all conflicts using the configured strategy
|
||||
Future<void> resolveAllConflicts() async {
|
||||
if (_conflictStrategy == ConflictResolutionStrategy.manual) {
|
||||
return; // Manual conflicts need individual resolution
|
||||
}
|
||||
|
||||
final resolvedConflicts = <SyncConflict>[];
|
||||
|
||||
for (final conflict in _pendingConflicts) {
|
||||
ConflictResolution resolution;
|
||||
|
||||
switch (_conflictStrategy) {
|
||||
case ConflictResolutionStrategy.preferLocal:
|
||||
resolution = ConflictResolution.useLocal;
|
||||
break;
|
||||
case ConflictResolutionStrategy.preferRemote:
|
||||
resolution = ConflictResolution.useRemote;
|
||||
break;
|
||||
case ConflictResolutionStrategy.preferNewer:
|
||||
resolution = conflict.isLocalNewer
|
||||
? ConflictResolution.useLocal
|
||||
: ConflictResolution.useRemote;
|
||||
break;
|
||||
case ConflictResolutionStrategy.manual:
|
||||
continue; // Skip manual conflicts
|
||||
}
|
||||
|
||||
await resolveConflict(conflict.syncId, resolution);
|
||||
resolvedConflicts.add(conflict);
|
||||
}
|
||||
|
||||
if (resolvedConflicts.isNotEmpty) {
|
||||
// Perform another sync to apply resolutions
|
||||
await performManualSync();
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable or disable auto sync
|
||||
Future<void> setAutoSyncEnabled(bool enabled) async {
|
||||
_isAutoSyncEnabled = enabled;
|
||||
notifyListeners();
|
||||
|
||||
if (enabled) {
|
||||
_scheduleNextAutoSync();
|
||||
}
|
||||
// TODO: Cancel existing timers when disabled
|
||||
}
|
||||
|
||||
/// Set sync frequency
|
||||
Future<void> setSyncFrequency(SyncFrequency frequency) async {
|
||||
_syncFrequency = frequency;
|
||||
notifyListeners();
|
||||
|
||||
if (_isAutoSyncEnabled) {
|
||||
_scheduleNextAutoSync();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set conflict resolution strategy
|
||||
Future<void> setConflictResolutionStrategy(ConflictResolutionStrategy strategy) async {
|
||||
_conflictStrategy = strategy;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Enable or disable auto sync on data changes
|
||||
Future<void> setAutoSyncOnDataChanges(bool enabled) async {
|
||||
_autoSyncOnDataChanges = enabled;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Trigger sync automatically when data changes (if enabled)
|
||||
Future<void> triggerAutoSyncOnDataChange() async {
|
||||
if (!_autoSyncOnDataChanges || !_isConfigured || _status == SyncOperationStatus.syncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform sync without blocking the UI
|
||||
performManualSync();
|
||||
}
|
||||
|
||||
/// Get sync statistics from last result
|
||||
SyncStatistics? get lastSyncStatistics => _lastSyncResult?.statistics;
|
||||
|
||||
/// Get formatted last sync time
|
||||
String get formattedLastSyncTime {
|
||||
if (_lastSyncTime == null) return 'Never';
|
||||
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(_lastSyncTime!);
|
||||
|
||||
if (difference.inMinutes < 1) {
|
||||
return 'Just now';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes}m ago';
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours}h ago';
|
||||
} else {
|
||||
return '${difference.inDays}d ago';
|
||||
}
|
||||
}
|
||||
|
||||
/// Get sync status message
|
||||
String get statusMessage {
|
||||
switch (_status) {
|
||||
case SyncOperationStatus.idle:
|
||||
return 'Ready to sync';
|
||||
case SyncOperationStatus.syncing:
|
||||
return 'Syncing... ${(_syncProgress * 100).round()}%';
|
||||
case SyncOperationStatus.success:
|
||||
return 'Sync completed successfully';
|
||||
case SyncOperationStatus.networkError:
|
||||
return 'Network connection failed';
|
||||
case SyncOperationStatus.authenticationError:
|
||||
return 'Authentication failed';
|
||||
case SyncOperationStatus.serverError:
|
||||
return 'Server error occurred';
|
||||
case SyncOperationStatus.conflictsDetected:
|
||||
return '${_pendingConflicts.length} conflict(s) need resolution';
|
||||
case SyncOperationStatus.cancelled:
|
||||
return 'Sync was cancelled';
|
||||
}
|
||||
}
|
||||
|
||||
/// Get device info
|
||||
Future<Map<String, String?>> getDeviceInfo() async {
|
||||
return await _syncService.getDeviceInfo();
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
void _setStatus(SyncOperationStatus status) {
|
||||
_status = status;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String error, SyncOperationStatus status) {
|
||||
_currentError = error;
|
||||
_status = status;
|
||||
_syncProgress = 0.0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _clearError() {
|
||||
_currentError = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _scheduleNextAutoSync() {
|
||||
if (!_isAutoSyncEnabled || _syncFrequency == SyncFrequency.manual) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement actual scheduling logic using Timer or WorkManager
|
||||
// This would schedule the next automatic sync based on the frequency
|
||||
if (kDebugMode) {
|
||||
print('Next auto sync scheduled for ${_syncFrequency.displayName}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect server type from URL for display purposes
|
||||
String _detectServerTypeFromUrl(String url) {
|
||||
final lowerUrl = url.toLowerCase();
|
||||
|
||||
if (lowerUrl.contains('/remote.php/dav/files/')) {
|
||||
return 'Nextcloud';
|
||||
} else if (lowerUrl.contains('/remote.php/webdav/')) {
|
||||
return 'ownCloud';
|
||||
} else if (lowerUrl.contains('nextcloud')) {
|
||||
return 'Nextcloud';
|
||||
} else if (lowerUrl.contains('owncloud')) {
|
||||
return 'ownCloud';
|
||||
} else if (lowerUrl.contains('/webdav/') || lowerUrl.contains('/dav/')) {
|
||||
return 'Generic WebDAV';
|
||||
} else {
|
||||
return 'WebDAV Server';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: Cancel any pending timers or background tasks
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum for conflict resolution choices
|
||||
enum ConflictResolution {
|
||||
useLocal,
|
||||
useRemote,
|
||||
merge, // For future implementation
|
||||
}
|
||||
|
||||
/// Extension methods for ConflictResolution
|
||||
extension ConflictResolutionExtension on ConflictResolution {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ConflictResolution.useLocal:
|
||||
return 'Use Local Version';
|
||||
case ConflictResolution.useRemote:
|
||||
return 'Use Remote Version';
|
||||
case ConflictResolution.merge:
|
||||
return 'Merge Changes';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user