This commit is contained in:
problematicconsumer
2023-07-06 17:18:41 +03:30
commit b617c95f62
352 changed files with 21017 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
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,
TunnelMode? mode,
) 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;
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

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

View File

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

View File

@@ -0,0 +1,74 @@
import 'dart:async';
import 'package:dartx/dartx.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(clashFacadeProvider);
StreamSubscription? _currentTest;
Future<void> testDelay(Iterable<String> proxies) async {
loggy.debug('testing delay for [${proxies.length}] proxies');
// 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(5)
.asyncMap(
(chunk) => Future.wait(
chunk.map(
(e) async => setDelay(
e,
await _clash.testDelay(e).getOrElse((l) => -1).run(),
),
),
),
)
.listen((event) {});
}
Future<void> cancelDelayTest() async => _currentTest?.cancel();
}

View File

@@ -0,0 +1,45 @@
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/features/common/clash/clash_controller.dart';
import 'package:hiddify/features/common/clash/clash_mode.dart';
import 'package:hiddify/features/proxies/model/model.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'proxies_notifier.g.dart';
@Riverpod(keepAlive: true)
class ProxiesNotifier extends _$ProxiesNotifier with AppLogger {
@override
Future<List<GroupWithProxies>> build() async {
loggy.debug('building');
await ref.watch(clashControllerProvider.future);
final mode = await ref.watch(clashModeProvider.future);
return _clash
.getProxies()
.flatMap(
(proxies) {
return TaskEither(
() async =>
right(await GroupWithProxies.fromProxies(proxies, mode)),
);
},
)
.getOrElse((l) => throw l)
.run();
}
ClashFacade get _clash => ref.read(clashFacadeProvider);
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();
}
}

View File

@@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
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';
import 'package:recase/recase.dart';
// TODO: rewrite, bugs with scroll
class ProxiesPage extends HookConsumerWidget with PresLogger {
const ProxiesPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final notifier = ref.watch(proxiesNotifierProvider.notifier);
final asyncProxies = ref.watch(proxiesNotifierProvider);
final proxies = asyncProxies.value ?? [];
final delays = ref.watch(proxiesDelayNotifierProvider);
final selectActiveProxyMutation = useMutation(
initialOnFailure: (error) =>
CustomToast.error(t.presentError(error)).show(context),
);
final tabController = useTabController(
initialLength: proxies.length,
keys: [proxies.length],
);
switch (asyncProxies) {
case AsyncData(value: final proxies):
if (proxies.isEmpty) {
return Scaffold(
body: CustomScrollView(
slivers: [
NestedTabAppBar(
title: Text(t.proxies.pageTitle.titleCase),
),
SliverFillRemaining(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(t.proxies.emptyProxiesMsg.titleCase),
],
),
),
],
),
);
}
final tabs = [
for (final groupWithProxies in proxies)
Tab(
child: Text(
groupWithProxies.group.name.toUpperCase(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
)
];
final tabViews = [
for (final groupWithProxies in proxies)
SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(
groupWithProxies.group.name,
),
slivers: <Widget>[
SliverList.builder(
itemBuilder: (_, index) {
final proxy = groupWithProxies.proxies[index];
return ProxyTile(
proxy,
selected: groupWithProxies.group.now == proxy.name,
delay: delays[proxy.name],
onSelect: () async {
if (selectActiveProxyMutation
.state.isInProgress) {
return;
}
selectActiveProxyMutation.setFuture(
notifier.changeProxy(
groupWithProxies.group.name,
proxy.name,
),
);
},
);
},
itemCount: groupWithProxies.proxies.length,
),
],
);
},
),
),
];
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return <Widget>[
NestedTabAppBar(
title: Text(t.proxies.pageTitle.titleCase),
forceElevated: innerBoxIsScrolled,
actions: [
PopupMenuButton(
itemBuilder: (_) {
return [
PopupMenuItem(
onTap: ref
.read(proxiesDelayNotifierProvider.notifier)
.cancelDelayTest,
child: Text(
t.proxies.cancelTestButtonText.sentenceCase,
),
),
];
},
),
],
bottom: TabBar(
controller: tabController,
isScrollable: true,
tabs: tabs,
),
),
];
},
body: TabBarView(
controller: tabController,
children: tabViews,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async =>
// TODO: improve
ref.read(proxiesDelayNotifierProvider.notifier).testDelay(
proxies[tabController.index].proxies.map((e) => e.name),
),
tooltip: t.proxies.delayTestTooltip.titleCase,
child: const Icon(Icons.bolt),
),
);
case AsyncError(:final error):
return Scaffold(
body: CustomScrollView(
slivers: [
NestedTabAppBar(
title: Text(t.proxies.pageTitle.titleCase),
),
SliverErrorBodyPlaceholder(t.presentError(error)),
],
),
);
case AsyncLoading():
return Scaffold(
body: CustomScrollView(
slivers: [
NestedTabAppBar(
title: Text(t.proxies.pageTitle.titleCase),
),
const SliverLoadingBodyPlaceholder(),
],
),
);
// TODO: remove
default:
return const Scaffold();
}
}
}

View File

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

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
// TODO: rewrite
class ProxyTile extends HookConsumerWidget {
const ProxyTile(
this.proxy, {
super.key,
required this.selected,
required this.onSelect,
this.delay,
});
final ClashProxy proxy;
final bool selected;
final VoidCallback onSelect;
final int? delay;
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListTile(
title: Text(
proxy.name,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(proxy.type.label),
trailing: delay != null ? Text(delay.toString()) : null,
selected: selected,
onTap: onSelect,
);
}
}

View File

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