feat: add connectivity check for multiplayer training and retry logic for sync
This commit is contained in:
parent
1e573904f2
commit
2e6052e69d
3 changed files with 267 additions and 200 deletions
12
lib/src/core/utils/connectivity_helper.dart
Normal file
12
lib/src/core/utils/connectivity_helper.dart
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import 'dart:io';
|
||||
|
||||
class ConnectivityHelper {
|
||||
static Future<bool> hasConnection() async {
|
||||
try {
|
||||
final result = await InternetAddress.lookup('8.8.8.8');
|
||||
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
|
||||
} on SocketException catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import '../widgets/level_display.dart';
|
|||
import '../widgets/start_raid_button.dart';
|
||||
import '../../../gamification/application/quest_service.dart';
|
||||
import '../../../workout_runner/application/workout_generator_service.dart';
|
||||
import '../../../../core/utils/connectivity_helper.dart';
|
||||
|
||||
class HubScreen extends ConsumerStatefulWidget {
|
||||
const HubScreen({super.key});
|
||||
|
|
@ -51,6 +52,26 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> _handleMultiplayerPress() async {
|
||||
final hasInternet = await ConnectivityHelper.hasConnection();
|
||||
|
||||
if (!hasInternet) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Für Multiplayer wird eine aktive Internetverbindung benötigt.'), //TODO: make this in l10n
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted) _showMultiplayerDialog();
|
||||
}
|
||||
|
||||
Future<void> _startNextWorkout(
|
||||
CycleCollection cycle, UserCollection user) async {
|
||||
try {
|
||||
|
|
@ -422,7 +443,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
icon: const Icon(Icons.groups,
|
||||
color: AppTheme.secondaryColor),
|
||||
tooltip: 'Multiplayer Lobby',
|
||||
onPressed: _showMultiplayerDialog,
|
||||
onPressed: _handleMultiplayerPress,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../../../../main.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
import '../../../core/utils/connectivity_helper.dart';
|
||||
import '../local/app_database.dart';
|
||||
import '../repositories/user_repository.dart';
|
||||
import 'api_client.dart';
|
||||
|
|
@ -25,9 +29,25 @@ class SyncService {
|
|||
|
||||
Future<void> sync() async {
|
||||
if (_isSyncing) return;
|
||||
|
||||
if (!await ConnectivityHelper.hasConnection()) {
|
||||
log('Sync skipped: No internet connection');
|
||||
return;
|
||||
}
|
||||
|
||||
_isSyncing = true;
|
||||
|
||||
try {
|
||||
await _retry(_performSync, maxAttempts: 3);
|
||||
log('✅ Sync completed successfully');
|
||||
} catch (e) {
|
||||
log('❌ Sync failed finally: $e');
|
||||
} finally {
|
||||
_isSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performSync() async {
|
||||
log('🔄 Starting Sync...');
|
||||
|
||||
final dirtyCycles = await (db.select(db.cycles)
|
||||
|
|
@ -35,7 +55,6 @@ class SyncService {
|
|||
.get();
|
||||
|
||||
for (var cycle in dirtyCycles) {
|
||||
try {
|
||||
if (cycle.serverId == null) {
|
||||
log('📤 Pushing new cycle ${cycle.cycleNumber}...');
|
||||
final tmsMap = cycle.trainingMaxes
|
||||
|
|
@ -67,9 +86,6 @@ class SyncService {
|
|||
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||
.write(const CyclesCompanion(isDirty: Value(false)));
|
||||
}
|
||||
} catch (e) {
|
||||
log('❌ Failed to sync cycle ${cycle.id}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final dirtyUser = await (db.select(db.users)
|
||||
|
|
@ -108,9 +124,9 @@ class SyncService {
|
|||
|
||||
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
||||
|
||||
if ((pushData['workouts'] as List).isNotEmpty ||
|
||||
pushData['user_stats'] != null) {
|
||||
log('📤 Pushing data...');
|
||||
// if ((pushData['workouts'] as List).isNotEmpty || pushData['user_stats'] != null) { ... }
|
||||
|
||||
log('Checking for updates...');
|
||||
final response = await apiClient.sync(
|
||||
lastSyncTimestamp: lastSync ?? '',
|
||||
pushData: pushData,
|
||||
|
|
@ -220,16 +236,34 @@ class SyncService {
|
|||
|
||||
if (response['server_timestamp'] != null) {
|
||||
await _storage.write(
|
||||
key: AppConstants.keyLastSync,
|
||||
value: response['server_timestamp']);
|
||||
key: AppConstants.keyLastSync, value: response['server_timestamp']);
|
||||
}
|
||||
}
|
||||
log('✅ Sync completed successfully');
|
||||
} catch (e, stack) {
|
||||
log('❌ Sync failed: $e');
|
||||
log(stack.toString());
|
||||
} finally {
|
||||
_isSyncing = false;
|
||||
|
||||
Future<T> _retry<T>(
|
||||
Future<T> Function() function, {
|
||||
int maxAttempts = 3,
|
||||
Duration initialDelay = const Duration(seconds: 2),
|
||||
}) async {
|
||||
int attempts = 0;
|
||||
while (true) {
|
||||
try {
|
||||
attempts++;
|
||||
return await function();
|
||||
} catch (e) {
|
||||
final shouldRetry = (e is SocketException) ||
|
||||
(e.toString().contains('500')) ||
|
||||
(e.toString().contains('502')) ||
|
||||
(e.toString().contains('timeout'));
|
||||
|
||||
if (attempts >= maxAttempts || !shouldRetry) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
final delay = initialDelay * (1 << (attempts - 1)); // 2s, 4s, 8s
|
||||
log('⚠️ Sync attempt $attempts failed. Retrying in ${delay.inSeconds}s... Error: $e');
|
||||
await Future.delayed(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue