import 'dart:convert'; import 'package:dartx/dartx.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/features/profile/details/profile_details_state.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/features/profile/model/profile_failure.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:uuid/uuid.dart'; part 'profile_details_notifier.g.dart'; @riverpod class ProfileDetailsNotifier extends _$ProfileDetailsNotifier with AppLogger { @override Future build( String id, { String? url, String? profileName, }) async { if (id == 'new') { return ProfileDetailsState( profile: RemoteProfileEntity( id: const Uuid().v4(), active: true, name: profileName ?? "", url: url ?? "", lastUpdate: DateTime.now(), ), ); } final failureOrProfile = await _profilesRepo.getById(id).run(); return failureOrProfile.match( (err) { loggy.warning('failed to load profile', err); throw err; }, (profile) async { if (profile == null) { loggy.warning('profile with id: [$id] does not exist'); throw const ProfileNotFoundFailure(); } _originalProfile = profile; final result = await _profilesRepo.generateConfig(id).run(); var configContent = result.fold( (failure) => throw Exception('Failed to generate config: $failure'), (config) => config, ); if (configContent.isNotEmpty) { try { final jsonObject = jsonDecode(configContent); List> res = []; if (jsonObject is Map && jsonObject['outbounds'] is List) { for (var outbound in jsonObject['outbounds'] as List) { if (outbound is Map && outbound['type'] != null && !['selector', 'urltest', 'dns', 'block'].contains(outbound['type']) && !['direct', 'bypass', 'direct-fragment'].contains(outbound['tag'])) { res.add(outbound); } } } else { // print('No outbounds found in the config'); } configContent = '{"outbounds": ${json.encode(res)}}'; } catch (e) { // print('Error parsing JSON: $e'); } } else { // print('Config content is null or empty'); } return ProfileDetailsState(profile: profile, isEditing: true, configContent: configContent); }, ); } ProfileRepository get _profilesRepo => ref.read(profileRepositoryProvider).requireValue; ProfileEntity? _originalProfile; void setField({ String? name, String? url, Option? updateInterval, String? configContent, }) { if (state case AsyncData(:final value)) { state = AsyncData( value.copyWith( profile: value.profile.map( remote: (rp) => rp.copyWith( name: name ?? rp.name, url: url ?? rp.url, options: updateInterval == null ? rp.options : updateInterval.fold( () => null, (t) => ProfileOptions( updateInterval: Duration(hours: t), ), ), ), local: (lp) => lp.copyWith(name: name ?? lp.name), ), configContentChanged: value.configContentChanged || value.configContent != configContent, configContent: configContent ?? value.configContent, ), ); } } Future save() async { if (state case AsyncData(:final value)) { if (value.save case AsyncLoading()) return; final profile = value.profile; Either? failureOrSuccess; state = AsyncData(value.copyWith(save: const AsyncLoading())); switch (profile) { case RemoteProfileEntity(): loggy.debug( 'saving profile, url: [${profile.url}], name: [${profile.name}]', ); if (profile.name.isBlank || profile.url.isBlank) { loggy.debug('save: invalid arguments'); } else if (value.isEditing) { if (_originalProfile case RemoteProfileEntity(:final url) when url == profile.url) { loggy.debug('editing profile'); failureOrSuccess = await _profilesRepo.patch(profile).run(); if (failureOrSuccess.isRight()) { failureOrSuccess = await _profilesRepo .updateContent( profile.id, value.configContent, ) .run(); } } else { loggy.debug('updating profile'); failureOrSuccess = await _profilesRepo.updateSubscription(profile, patchBaseProfile: true).run(); if (failureOrSuccess.isRight()) { failureOrSuccess = await _profilesRepo .updateContent( profile.id, value.configContent, ) .run(); } } } else { loggy.debug('adding profile, url: [${profile.url}]'); failureOrSuccess = await _profilesRepo.add(profile).run(); } case LocalProfileEntity() when value.isEditing: loggy.debug('editing profile'); failureOrSuccess = await _profilesRepo.patch(profile).run(); default: loggy.warning("local profile can't be added manually"); } state = AsyncData( value.copyWith( save: failureOrSuccess?.fold( (l) => AsyncError(l, StackTrace.current), (_) => const AsyncData(null), ) ?? value.save, showErrorMessages: true, ), ); } } Future updateProfile() async { if (state case AsyncData(:final value)) { if (value.update?.isLoading ?? false || !value.isEditing) return; if (value.profile case LocalProfileEntity()) { loggy.warning("local profile can't be updated"); return; } final profile = value.profile; state = AsyncData(value.copyWith(update: const AsyncLoading())); final failureOrUpdatedProfile = await _profilesRepo.updateSubscription(profile as RemoteProfileEntity).flatMap((_) => _profilesRepo.getById(id)).run(); state = AsyncData( value.copyWith( update: failureOrUpdatedProfile.match( (l) => AsyncError(l, StackTrace.current), (_) => const AsyncData(null), ), profile: failureOrUpdatedProfile.match( (_) => profile, (updatedProfile) => updatedProfile ?? profile, ), ), ); } } Future delete() async { if (state case AsyncData(:final value)) { if (value.delete case AsyncLoading()) return; final profile = value.profile; state = AsyncData(value.copyWith(delete: const AsyncLoading())); state = AsyncData( value.copyWith( delete: await AsyncValue.guard(() async { await _profilesRepo.deleteById(profile.id).getOrElse((l) => throw l).run(); }), ), ); } } }