172 lines
5.4 KiB
Dart
172 lines
5.4 KiB
Dart
// ignore_for_file: prefer_initializing_formals
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../auth/auth_service.dart';
|
|
import '../../database/database.dart';
|
|
import '../../features/records/records_service.dart';
|
|
import 'cloud_restore_service.dart';
|
|
|
|
enum CloudRestoreStatus {
|
|
idle,
|
|
restoring,
|
|
success,
|
|
partialFailure,
|
|
authRequired,
|
|
unavailable,
|
|
failure,
|
|
}
|
|
|
|
class CloudRestoreUiState {
|
|
final CloudRestoreStatus status;
|
|
final CloudRestoreResult? result;
|
|
final String? message;
|
|
|
|
const CloudRestoreUiState({
|
|
this.status = CloudRestoreStatus.idle,
|
|
this.result,
|
|
this.message,
|
|
});
|
|
|
|
bool get isRestoring => status == CloudRestoreStatus.restoring;
|
|
}
|
|
|
|
class CloudRestoreController extends StateNotifier<CloudRestoreUiState> {
|
|
final CloudRestoreService _restoreService;
|
|
final AuthService _authService;
|
|
final RecordsService? _recordsService;
|
|
final AppDatabase? _db;
|
|
|
|
CloudRestoreController({
|
|
required CloudRestoreService restoreService,
|
|
required AuthService authService,
|
|
RecordsService? recordsService,
|
|
AppDatabase? db,
|
|
}) : _restoreService = restoreService,
|
|
_authService = authService,
|
|
_recordsService = recordsService,
|
|
_db = db,
|
|
super(const CloudRestoreUiState());
|
|
|
|
Future<CloudRestoreResult?> restoreFromMotorCloud() async {
|
|
if (state.isRestoring) return state.result;
|
|
|
|
final user = _authService.currentUser;
|
|
if (user == null || user.isAnonymous) {
|
|
state = const CloudRestoreUiState(
|
|
status: CloudRestoreStatus.authRequired,
|
|
message: 'Sign in with a MotorCloud account to restore ride history.',
|
|
);
|
|
return null;
|
|
}
|
|
if (!_authService.isConfigured) {
|
|
state = const CloudRestoreUiState(
|
|
status: CloudRestoreStatus.unavailable,
|
|
message:
|
|
'MotorCloud is unavailable. Try again when Firebase services are available.',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
state = const CloudRestoreUiState(status: CloudRestoreStatus.restoring);
|
|
try {
|
|
final result = await _restoreService.restore();
|
|
if (_recordsService != null && _db != null) {
|
|
try {
|
|
await _recordsService.recalculateFromDatabase(_db);
|
|
} catch (_) {}
|
|
}
|
|
final hasFailures = result.failures > 0;
|
|
final hasRestoredOrMatched =
|
|
result.hasAnyRestoredData ||
|
|
result.existingRidesMatched > 0 ||
|
|
result.telemetryAlreadyPresent > 0 ||
|
|
result.telemetrySkipped > 0;
|
|
final status = result.requiresAuthentication && !hasRestoredOrMatched
|
|
? CloudRestoreStatus.authRequired
|
|
: (hasFailures && !hasRestoredOrMatched
|
|
? CloudRestoreStatus.failure
|
|
: (hasFailures
|
|
? CloudRestoreStatus.partialFailure
|
|
: CloudRestoreStatus.success));
|
|
state = CloudRestoreUiState(
|
|
status: status,
|
|
result: result,
|
|
message: _formatResult(result),
|
|
);
|
|
return result;
|
|
} on StateError catch (e) {
|
|
final message = e.message;
|
|
state = CloudRestoreUiState(
|
|
status: message.toLowerCase().contains('auth')
|
|
? CloudRestoreStatus.authRequired
|
|
: CloudRestoreStatus.unavailable,
|
|
message: message,
|
|
);
|
|
return null;
|
|
} catch (e) {
|
|
state = CloudRestoreUiState(
|
|
status: CloudRestoreStatus.failure,
|
|
message: 'MotorCloud restore failed: $e',
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
String _formatResult(CloudRestoreResult result) {
|
|
final parts = <String>[];
|
|
if (result.metadataRowsAdded > 0) {
|
|
parts.add('${result.metadataRowsAdded} ride(s) added');
|
|
}
|
|
if (result.telemetrySamplesRestored > 0) {
|
|
parts.add(
|
|
'${result.telemetrySamplesRestored} telemetry sample(s) restored',
|
|
);
|
|
}
|
|
if (result.performanceRunsRestored > 0) {
|
|
parts.add(
|
|
'${result.performanceRunsRestored} performance run(s) restored',
|
|
);
|
|
}
|
|
if (result.rideEventsRestored > 0) {
|
|
parts.add('${result.rideEventsRestored} ride event(s) restored');
|
|
}
|
|
if (result.existingRidesMatched > 0 || result.telemetryAlreadyPresent > 0) {
|
|
final alreadyPresent =
|
|
result.existingRidesMatched > result.telemetryAlreadyPresent
|
|
? result.existingRidesMatched
|
|
: result.telemetryAlreadyPresent;
|
|
parts.add('$alreadyPresent ride(s) already on this device');
|
|
}
|
|
final failureDetails = <String>[];
|
|
if (result.telemetryFailed > 0) {
|
|
failureDetails.add(
|
|
'${result.telemetryFailed} ride(s) pending telemetry upload from source device',
|
|
);
|
|
}
|
|
if (result.telemetryAuthFailures > 0) {
|
|
failureDetails.add('${result.telemetryAuthFailures} ride(s) require re-authentication');
|
|
}
|
|
if (result.metadata.conflicts > 0) {
|
|
failureDetails.add('${result.metadata.conflicts} conflict(s)');
|
|
}
|
|
if (result.metadata.skipped > 0) {
|
|
failureDetails.add('${result.metadata.skipped} skipped');
|
|
}
|
|
|
|
if (result.failures > 0) {
|
|
final explanation = failureDetails.isNotEmpty
|
|
? failureDetails.join('; ')
|
|
: '${result.failures} item(s) need attention';
|
|
parts.add('Unresolved: $explanation');
|
|
}
|
|
if (parts.isEmpty) {
|
|
return 'No restorable MotorCloud rides were found for this account.';
|
|
}
|
|
final prefix = result.failures > 0
|
|
? 'MotorCloud restore partially completed'
|
|
: 'MotorCloud restore complete';
|
|
return '$prefix: ${parts.join(', ')}.';
|
|
}
|
|
}
|