Change proxies flow

This commit is contained in:
problematicconsumer
2023-08-29 19:32:31 +03:30
parent 375cb8a945
commit e8eb55ac8d
15 changed files with 335 additions and 229 deletions

View File

@@ -1,43 +0,0 @@
import 'package:combine/combine.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/domain/clash/clash.dart';
part 'group_with_proxies.freezed.dart';
@freezed
class GroupWithProxies with _$GroupWithProxies {
const GroupWithProxies._();
const factory GroupWithProxies({
required ClashProxyGroup group,
required List<ClashProxy> proxies,
}) = _GroupWithProxies;
static Future<List<GroupWithProxies>> fromProxies(
List<ClashProxy> proxies,
) async {
final stopWatch = Stopwatch()..start();
final res = await CombineWorker().execute(
() {
final result = <GroupWithProxies>[];
for (final proxy in proxies) {
if (proxy is ClashProxyGroup) {
// if (mode != TunnelMode.global && proxy.name == "GLOBAL") continue;
if (proxy.name == "GLOBAL") continue;
final current = <ClashProxy>[];
for (final name in proxy.all) {
current.addAll(proxies.where((e) => e.name == name).toList());
}
result.add(GroupWithProxies(group: proxy, proxies: current));
}
}
return result;
},
);
debugPrint(
"computed grouped proxies in [${stopWatch.elapsedMilliseconds}ms]",
);
return res;
}
}

View File

@@ -1 +0,0 @@
export 'group_with_proxies.dart';

View File

@@ -1,83 +0,0 @@
import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:hiddify/core/prefs/misc_prefs.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
part 'proxies_delay_notifier.g.dart';
// TODO: rewrite
@Riverpod(keepAlive: true)
class ProxiesDelayNotifier extends _$ProxiesDelayNotifier with AppLogger {
@override
Map<String, int> build() {
ref.onDispose(
() {
loggy.debug("disposing");
_currentTest?.cancel();
},
);
ref.listen(
activeProfileProvider.selectAsync((value) => value?.id),
(prev, next) async {
if (await prev != await next) ref.invalidateSelf();
},
);
return {};
}
ClashFacade get _clash => ref.read(coreFacadeProvider);
StreamSubscription? _currentTest;
Future<void> testDelay(Iterable<String> proxies) async {
final testUrl = ref.read(connectionTestUrlProvider);
final concurrent = ref.read(concurrentTestCountProvider);
loggy.info(
'testing delay for [${proxies.length}] proxies with [$testUrl], [$concurrent] at a time',
);
// cancel possible running test
await _currentTest?.cancel();
// reset previous
state = state.filterNot((entry) => proxies.contains(entry.key));
void setDelay(String name, int delay) {
state = {
...state
..update(
name,
(_) => delay,
ifAbsent: () => delay,
),
};
}
_currentTest = Stream.fromIterable(proxies)
.bufferCount(concurrent)
.asyncMap(
(chunk) => Future.wait(
chunk.map(
(e) async => setDelay(
e,
await _clash
.testDelay(e, testUrl: testUrl)
.getOrElse((l) => -1)
.run(),
),
),
),
)
.listen((event) {});
}
Future<void> cancelDelayTest() async => _currentTest?.cancel();
}

View File

@@ -1,11 +1,9 @@
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/proxies/model/model.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -14,31 +12,51 @@ part 'proxies_notifier.g.dart';
@Riverpod(keepAlive: true)
class ProxiesNotifier extends _$ProxiesNotifier with AppLogger {
@override
Future<List<GroupWithProxies>> build() async {
loggy.debug('building');
if (!await ref.watch(serviceRunningProvider.future)) {
Stream<List<OutboundGroup>> build() async* {
loggy.debug("building");
final serviceRunning = await ref.watch(serviceRunningProvider.future);
if (!serviceRunning) {
throw const CoreServiceNotRunning();
}
return _clash.getProxies().flatMap(
(proxies) {
return TaskEither(
() async => right(await GroupWithProxies.fromProxies(proxies)),
yield* ref.watch(coreFacadeProvider).watchOutbounds().map(
(event) => event.getOrElse(
(f) {
loggy.warning("error receiving proxies", f);
throw f;
},
),
);
},
).getOrElse((l) {
loggy.warning("failed receiving proxies: $l");
throw l;
}).run();
}
ClashFacade get _clash => ref.read(coreFacadeProvider);
Future<void> changeProxy(String groupTag, String outboundTag) async {
loggy.debug(
"changing proxy, group: [$groupTag] - outbound: [$outboundTag]",
);
if (state case AsyncData(value: final outbounds)) {
await ref
.read(coreFacadeProvider)
.selectOutbound(groupTag, outboundTag)
.getOrElse((l) {
loggy.warning("error selecting outbound", l);
throw l;
}).run();
state = AsyncData(
[
...outbounds.map(
(e) => e.tag == groupTag ? e.copyWith(selected: outboundTag) : e,
),
],
).copyWithPrevious(state);
}
}
Future<void> changeProxy(String selectorName, String proxyName) async {
loggy.debug("changing proxy, selector: $selectorName - proxy: $proxyName ");
await _clash
.changeProxy(selectorName, proxyName)
.getOrElse((l) => throw l)
.run();
ref.invalidateSelf();
Future<void> urlTest(String groupTag) async {
loggy.debug("testing group: [$groupTag]");
if (state case AsyncData()) {
await ref.read(coreFacadeProvider).urlTest(groupTag).getOrElse((l) {
loggy.warning("error testing group", l);
throw l;
}).run();
}
}
}

View File

@@ -3,7 +3,6 @@ import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/features/common/common.dart';
import 'package:hiddify/features/proxies/notifier/notifier.dart';
import 'package:hiddify/features/proxies/notifier/proxies_delay_notifier.dart';
import 'package:hiddify/features/proxies/widgets/widgets.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -18,7 +17,6 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
final asyncProxies = ref.watch(proxiesNotifierProvider);
final notifier = ref.watch(proxiesNotifierProvider.notifier);
final delays = ref.watch(proxiesDelayNotifierProvider);
final selectActiveProxyMutation = useMutation(
initialOnFailure: (error) =>
@@ -47,55 +45,33 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
);
}
final select = groups.first;
final group = groups.first;
return Scaffold(
body: CustomScrollView(
slivers: [
NestedTabAppBar(
title: Text(t.proxies.pageTitle.titleCase),
actions: [
PopupMenuButton(
itemBuilder: (_) {
return [
PopupMenuItem(
onTap: ref
.read(proxiesDelayNotifierProvider.notifier)
.cancelDelayTest,
child: Text(
t.proxies.cancelTestButtonText.sentenceCase,
),
),
];
},
),
],
),
NestedTabAppBar(title: Text(t.proxies.pageTitle.titleCase)),
SliverLayoutBuilder(
builder: (context, constraints) {
final width = constraints.crossAxisExtent;
if (!PlatformUtils.isDesktop && width < 648) {
return SliverList.builder(
itemBuilder: (_, index) {
final proxy = select.proxies[index];
final proxy = group.items[index];
return ProxyTile(
proxy,
selected: select.group.now == proxy.name,
delay: delays[proxy.name],
selected: group.selected == proxy.tag,
onSelect: () async {
if (selectActiveProxyMutation.state.isInProgress) {
return;
}
selectActiveProxyMutation.setFuture(
notifier.changeProxy(
select.group.name,
proxy.name,
),
notifier.changeProxy(group.tag, proxy.tag),
);
},
);
},
itemCount: select.proxies.length,
itemCount: group.items.length,
);
}
@@ -105,36 +81,31 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
mainAxisExtent: 68,
),
itemBuilder: (context, index) {
final proxy = select.proxies[index];
final proxy = group.items[index];
return ProxyTile(
proxy,
selected: select.group.now == proxy.name,
delay: delays[proxy.name],
selected: group.selected == proxy.tag,
onSelect: () async {
if (selectActiveProxyMutation.state.isInProgress) {
return;
}
selectActiveProxyMutation.setFuture(
notifier.changeProxy(
select.group.name,
proxy.name,
group.tag,
proxy.tag,
),
);
},
);
},
itemCount: select.proxies.length,
itemCount: group.items.length,
);
},
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async =>
// TODO: improve
ref.read(proxiesDelayNotifierProvider.notifier).testDelay(
select.proxies.map((e) => e.name),
),
onPressed: () async => notifier.urlTest(group.tag),
tooltip: t.proxies.delayTestTooltip.titleCase,
child: const Icon(Icons.bolt),
),

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ProxyTile extends HookConsumerWidget {
@@ -8,13 +8,11 @@ class ProxyTile extends HookConsumerWidget {
super.key,
required this.selected,
required this.onSelect,
this.delay,
});
final ClashProxy proxy;
final OutboundGroupItem proxy;
final bool selected;
final VoidCallback onSelect;
final int? delay;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -23,10 +21,7 @@ class ProxyTile extends HookConsumerWidget {
return ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text(
switch (proxy) {
ClashProxyGroup(:final name) => name.toUpperCase(),
ClashProxyItem(:final name) => name,
},
proxy.tag,
overflow: TextOverflow.ellipsis,
),
leading: Padding(
@@ -40,38 +35,12 @@ class ProxyTile extends HookConsumerWidget {
),
),
),
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(text: proxy.type.label),
// if (proxy.udp)
// WidgetSpan(
// child: Padding(
// padding: const EdgeInsets.symmetric(horizontal: 4),
// child: DecoratedBox(
// decoration: BoxDecoration(
// border: Border.all(
// color: theme.colorScheme.tertiaryContainer,
// ),
// borderRadius: BorderRadius.circular(6),
// ),
// child: Text(
// " UDP ",
// style: TextStyle(
// fontSize: theme.textTheme.labelSmall?.fontSize,
// ),
// ),
// ),
// ),
// ),
if (proxy case ClashProxyGroup(:final now)) ...[
TextSpan(text: " ($now)"),
],
],
),
subtitle: Text(
proxy.type.label,
overflow: TextOverflow.ellipsis,
),
trailing: delay != null ? Text(delay.toString()) : null,
trailing:
proxy.urlTestDelay != 0 ? Text(proxy.urlTestDelay.toString()) : null,
selected: selected,
onTap: onSelect,
horizontalTitleGap: 4,