import 'dart:io'; import 'package:hiddify/core/haptic/haptic_service.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/preferences/service_preferences.dart'; import 'package:hiddify/features/connection/data/connection_data_providers.dart'; import 'package:hiddify/features/connection/data/connection_repository.dart'; import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; part 'connection_notifier.g.dart'; @Riverpod(keepAlive: true) class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { @override Stream build() async* { if (Platform.isIOS) { await _connectionRepo.setup().mapLeft((l) { loggy.error("error setting up connection repository", l); }).run(); } ref.listen( activeProfileProvider.select((value) => value.asData?.value), (previous, next) async { if (previous == null) return; final shouldReconnect = next == null || previous.id != next.id; if (shouldReconnect) { await reconnect(next); } }, ); yield* _connectionRepo.watchConnectionStatus().doOnData((event) { if (event case Disconnected(connectionFailure: final _?) when PlatformUtils.isDesktop) { ref.read(startedByUserProvider.notifier).update(false); } loggy.info("connection status: ${event.format()}"); }); } ConnectionRepository get _connectionRepo => ref.read(connectionRepositoryProvider); Future mayConnect() async { if (state case AsyncData(:final value)) { if (value case Disconnected()) return _connect(); } } Future toggleConnection() async { final haptic = ref.read(hapticServiceProvider.notifier); if (state case AsyncError()) { await haptic.lightImpact(); await _connect(); } else if (state case AsyncData(:final value)) { switch (value) { case Disconnected(): await haptic.lightImpact(); await ref.read(startedByUserProvider.notifier).update(true); await _connect(); case Connected(): await haptic.mediumImpact(); await ref.read(startedByUserProvider.notifier).update(false); await _disconnect(); default: loggy.warning("switching status, debounce"); } } } Future reconnect(ProfileEntity? profile) async { if (state case AsyncData(:final value) when value == const Connected()) { if (profile == null) { loggy.info("no active profile, disconnecting"); return _disconnect(); } loggy.info("active profile changed, reconnecting"); await ref.read(startedByUserProvider.notifier).update(true); await _connectionRepo .reconnect( profile.id, profile.name, ref.read(disableMemoryLimitProvider), ) .mapLeft((err) { loggy.warning("error reconnecting", err); state = AsyncError(err, StackTrace.current); }).run(); } } Future abortConnection() async { if (state case AsyncData(:final value)) { switch (value) { case Connected() || Connecting(): loggy.debug("aborting connection"); await _disconnect(); default: } } } Future _connect() async { final activeProfile = await ref.read(activeProfileProvider.future); await _connectionRepo .connect( activeProfile!.id, activeProfile.name, ref.read(disableMemoryLimitProvider), ) .mapLeft((err) async { loggy.warning("error connecting", err); loggy.warning( err); //Go err is not normal object to see the go errors are string and need to be dumped if (err.toString().contains("panic")) { await Sentry.captureException(Exception(err.toString())); } await ref.read(startedByUserProvider.notifier).update(false); state = AsyncError(err, StackTrace.current); }).run(); } Future _disconnect() async { await _connectionRepo.disconnect().mapLeft((err) { loggy.warning("error disconnecting", err); state = AsyncError(err, StackTrace.current); }).run(); } } @Riverpod(keepAlive: true) Future serviceRunning(ServiceRunningRef ref) => ref .watch( connectionNotifierProvider.selectAsync((data) => data.isConnected), ) .onError((error, stackTrace) => false);