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 '../widgets/start_raid_button.dart';
|
||||||
import '../../../gamification/application/quest_service.dart';
|
import '../../../gamification/application/quest_service.dart';
|
||||||
import '../../../workout_runner/application/workout_generator_service.dart';
|
import '../../../workout_runner/application/workout_generator_service.dart';
|
||||||
|
import '../../../../core/utils/connectivity_helper.dart';
|
||||||
|
|
||||||
class HubScreen extends ConsumerStatefulWidget {
|
class HubScreen extends ConsumerStatefulWidget {
|
||||||
const HubScreen({super.key});
|
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(
|
Future<void> _startNextWorkout(
|
||||||
CycleCollection cycle, UserCollection user) async {
|
CycleCollection cycle, UserCollection user) async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -422,7 +443,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
icon: const Icon(Icons.groups,
|
icon: const Icon(Icons.groups,
|
||||||
color: AppTheme.secondaryColor),
|
color: AppTheme.secondaryColor),
|
||||||
tooltip: 'Multiplayer Lobby',
|
tooltip: 'Multiplayer Lobby',
|
||||||
onPressed: _showMultiplayerDialog,
|
onPressed: _handleMultiplayerPress,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
import '../../../core/constants/app_constants.dart';
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
import '../../../core/utils/connectivity_helper.dart';
|
||||||
import '../local/app_database.dart';
|
import '../local/app_database.dart';
|
||||||
import '../repositories/user_repository.dart';
|
import '../repositories/user_repository.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
|
@ -25,9 +29,25 @@ class SyncService {
|
||||||
|
|
||||||
Future<void> sync() async {
|
Future<void> sync() async {
|
||||||
if (_isSyncing) return;
|
if (_isSyncing) return;
|
||||||
|
|
||||||
|
if (!await ConnectivityHelper.hasConnection()) {
|
||||||
|
log('Sync skipped: No internet connection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isSyncing = true;
|
_isSyncing = true;
|
||||||
|
|
||||||
try {
|
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...');
|
log('🔄 Starting Sync...');
|
||||||
|
|
||||||
final dirtyCycles = await (db.select(db.cycles)
|
final dirtyCycles = await (db.select(db.cycles)
|
||||||
|
|
@ -35,7 +55,6 @@ class SyncService {
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
for (var cycle in dirtyCycles) {
|
for (var cycle in dirtyCycles) {
|
||||||
try {
|
|
||||||
if (cycle.serverId == null) {
|
if (cycle.serverId == null) {
|
||||||
log('📤 Pushing new cycle ${cycle.cycleNumber}...');
|
log('📤 Pushing new cycle ${cycle.cycleNumber}...');
|
||||||
final tmsMap = cycle.trainingMaxes
|
final tmsMap = cycle.trainingMaxes
|
||||||
|
|
@ -67,9 +86,6 @@ class SyncService {
|
||||||
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||||
.write(const CyclesCompanion(isDirty: Value(false)));
|
.write(const CyclesCompanion(isDirty: Value(false)));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
log('❌ Failed to sync cycle ${cycle.id}: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final dirtyUser = await (db.select(db.users)
|
final dirtyUser = await (db.select(db.users)
|
||||||
|
|
@ -108,9 +124,9 @@ class SyncService {
|
||||||
|
|
||||||
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
||||||
|
|
||||||
if ((pushData['workouts'] as List).isNotEmpty ||
|
// if ((pushData['workouts'] as List).isNotEmpty || pushData['user_stats'] != null) { ... }
|
||||||
pushData['user_stats'] != null) {
|
|
||||||
log('📤 Pushing data...');
|
log('Checking for updates...');
|
||||||
final response = await apiClient.sync(
|
final response = await apiClient.sync(
|
||||||
lastSyncTimestamp: lastSync ?? '',
|
lastSyncTimestamp: lastSync ?? '',
|
||||||
pushData: pushData,
|
pushData: pushData,
|
||||||
|
|
@ -220,16 +236,34 @@ class SyncService {
|
||||||
|
|
||||||
if (response['server_timestamp'] != null) {
|
if (response['server_timestamp'] != null) {
|
||||||
await _storage.write(
|
await _storage.write(
|
||||||
key: AppConstants.keyLastSync,
|
key: AppConstants.keyLastSync, value: response['server_timestamp']);
|
||||||
value: response['server_timestamp']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log('✅ Sync completed successfully');
|
|
||||||
} catch (e, stack) {
|
Future<T> _retry<T>(
|
||||||
log('❌ Sync failed: $e');
|
Future<T> Function() function, {
|
||||||
log(stack.toString());
|
int maxAttempts = 3,
|
||||||
} finally {
|
Duration initialDelay = const Duration(seconds: 2),
|
||||||
_isSyncing = false;
|
}) 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