first commit
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/workout.dart';
|
||||
import '../utils/hmac_utility.dart';
|
||||
import 'database_service.dart';
|
||||
|
||||
/// محرك المزامنة التلقائي
|
||||
/// يراقب الاتصال بالإنترنت ويرسل التمارين المحفوظة محلياً عند توفر الشبكة
|
||||
class SyncService extends GetxService {
|
||||
final DatabaseService _db = Get.find<DatabaseService>();
|
||||
final _storage = const FlutterSecureStorage();
|
||||
final _connectivity = Connectivity();
|
||||
|
||||
StreamSubscription? _connectivitySub;
|
||||
Timer? _syncTimer;
|
||||
|
||||
final _isSyncing = false.obs;
|
||||
final _isOnline = false.obs;
|
||||
final _pendingCount = 0.obs;
|
||||
|
||||
bool get isSyncing => _isSyncing.value;
|
||||
bool get isOnline => _isOnline.value;
|
||||
int get pendingCount => _pendingCount.value;
|
||||
|
||||
static const String _apiEndpointKey = 'api_endpoint';
|
||||
static const String _apiKeyKey = 'api_key';
|
||||
static const String _apiSecretKey = 'api_secret';
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
super.onInit();
|
||||
await _checkConnectivity();
|
||||
_startConnectivityMonitor();
|
||||
_startPeriodicSync();
|
||||
await _updatePendingCount();
|
||||
}
|
||||
|
||||
// ─── Connectivity Monitoring ──────────────────────────────────
|
||||
|
||||
Future<void> _checkConnectivity() async {
|
||||
final results = await _connectivity.checkConnectivity();
|
||||
_isOnline.value = !results.contains(ConnectivityResult.none);
|
||||
}
|
||||
|
||||
void _startConnectivityMonitor() {
|
||||
_connectivitySub = _connectivity.onConnectivityChanged.listen(
|
||||
(List<ConnectivityResult> results) {
|
||||
final wasOffline = !_isOnline.value;
|
||||
_isOnline.value = !results.contains(ConnectivityResult.none);
|
||||
|
||||
// Came back online → trigger sync
|
||||
if (wasOffline && _isOnline.value) {
|
||||
debugPrint('[SYNC] Back online — triggering sync');
|
||||
syncPendingWorkouts();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startPeriodicSync() {
|
||||
// Check for pending syncs every 2 minutes
|
||||
_syncTimer = Timer.periodic(
|
||||
const Duration(minutes: 2),
|
||||
(_) {
|
||||
if (_isOnline.value && !_isSyncing.value) {
|
||||
syncPendingWorkouts();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Credentials Management ───────────────────────────────────
|
||||
|
||||
Future<void> saveCredentials({
|
||||
required String apiEndpoint,
|
||||
required String apiKey,
|
||||
required String apiSecret,
|
||||
}) async {
|
||||
await _storage.write(key: _apiEndpointKey, value: apiEndpoint);
|
||||
await _storage.write(key: _apiKeyKey, value: apiKey);
|
||||
await _storage.write(key: _apiSecretKey, value: apiSecret);
|
||||
}
|
||||
|
||||
Future<Map<String, String?>> getCredentials() async {
|
||||
return {
|
||||
'endpoint': await _storage.read(key: _apiEndpointKey),
|
||||
'apiKey': await _storage.read(key: _apiKeyKey),
|
||||
'apiSecret': await _storage.read(key: _apiSecretKey),
|
||||
};
|
||||
}
|
||||
|
||||
Future<bool> hasCredentials() async {
|
||||
final creds = await getCredentials();
|
||||
return creds['apiKey'] != null &&
|
||||
creds['apiKey']!.isNotEmpty &&
|
||||
creds['apiSecret'] != null &&
|
||||
creds['apiSecret']!.isNotEmpty;
|
||||
}
|
||||
|
||||
// ─── Sync Operations ─────────────────────────────────────────
|
||||
|
||||
/// Queue a workout for sync
|
||||
Future<void> queueWorkout(Workout workout) async {
|
||||
final payload = jsonEncode(workout.toApiPayload());
|
||||
await _db.enqueueSync(workout.id, payload);
|
||||
await _updatePendingCount();
|
||||
|
||||
// Try immediate sync if online
|
||||
if (_isOnline.value) {
|
||||
syncPendingWorkouts();
|
||||
}
|
||||
}
|
||||
|
||||
/// Process all pending sync items
|
||||
Future<void> syncPendingWorkouts() async {
|
||||
if (_isSyncing.value) return;
|
||||
_isSyncing.value = true;
|
||||
|
||||
try {
|
||||
final creds = await getCredentials();
|
||||
if (creds['endpoint'] == null || creds['apiKey'] == null ||
|
||||
creds['apiSecret'] == null) {
|
||||
debugPrint('[SYNC] No API credentials configured');
|
||||
return;
|
||||
}
|
||||
|
||||
final pendingItems = await _db.getPendingSyncs();
|
||||
if (pendingItems.isEmpty) return;
|
||||
|
||||
debugPrint('[SYNC] Processing ${pendingItems.length} pending items');
|
||||
|
||||
for (final item in pendingItems) {
|
||||
await _syncSingleItem(
|
||||
id: item['id'] as int,
|
||||
workoutId: item['workout_id'] as String,
|
||||
payload: item['payload'] as String,
|
||||
endpoint: creds['endpoint']!,
|
||||
apiKey: creds['apiKey']!,
|
||||
apiSecret: creds['apiSecret']!,
|
||||
);
|
||||
}
|
||||
|
||||
await _db.removeSentFromQueue();
|
||||
await _updatePendingCount();
|
||||
} catch (e) {
|
||||
debugPrint('[SYNC] Error during sync: $e');
|
||||
} finally {
|
||||
_isSyncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncSingleItem({
|
||||
required int id,
|
||||
required String workoutId,
|
||||
required String payload,
|
||||
required String endpoint,
|
||||
required String apiKey,
|
||||
required String apiSecret,
|
||||
}) async {
|
||||
try {
|
||||
final headers = HmacUtility.buildHeaders(
|
||||
apiKey: apiKey,
|
||||
apiSecret: apiSecret,
|
||||
payload: payload,
|
||||
);
|
||||
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$endpoint/api/v1/workouts'),
|
||||
headers: headers,
|
||||
body: payload,
|
||||
)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
await _db.updateSyncQueueItem(id, 'sent');
|
||||
await _db.updateSyncStatus(
|
||||
workoutId,
|
||||
SyncStatus.synced,
|
||||
syncedAt: DateTime.now(),
|
||||
);
|
||||
debugPrint('[SYNC] ✅ Workout $workoutId synced');
|
||||
} else {
|
||||
await _db.updateSyncQueueItem(
|
||||
id,
|
||||
'failed',
|
||||
errorMessage: 'HTTP ${response.statusCode}: ${response.body}',
|
||||
);
|
||||
debugPrint('[SYNC] ❌ Failed: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
await _db.updateSyncQueueItem(id, 'failed', errorMessage: e.toString());
|
||||
debugPrint('[SYNC] ❌ Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updatePendingCount() async {
|
||||
final items = await _db.getPendingSyncs();
|
||||
_pendingCount.value = items.length;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_connectivitySub?.cancel();
|
||||
_syncTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user