initial
This commit is contained in:
43
lib/features/proxies/model/group_with_proxies.dart
Normal file
43
lib/features/proxies/model/group_with_proxies.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
1
lib/features/proxies/model/model.dart
Normal file
1
lib/features/proxies/model/model.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'group_with_proxies.dart';
|
||||
1
lib/features/proxies/notifier/notifier.dart
Normal file
1
lib/features/proxies/notifier/notifier.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'proxies_notifier.dart';
|
||||
74
lib/features/proxies/notifier/proxies_delay_notifier.dart
Normal file
74
lib/features/proxies/notifier/proxies_delay_notifier.dart
Normal 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();
|
||||
}
|
||||
45
lib/features/proxies/notifier/proxies_notifier.dart
Normal file
45
lib/features/proxies/notifier/proxies_notifier.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
188
lib/features/proxies/view/proxies_page.dart
Normal file
188
lib/features/proxies/view/proxies_page.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
1
lib/features/proxies/view/view.dart
Normal file
1
lib/features/proxies/view/view.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'proxies_page.dart';
|
||||
33
lib/features/proxies/widgets/proxy_tile.dart
Normal file
33
lib/features/proxies/widgets/proxy_tile.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
1
lib/features/proxies/widgets/widgets.dart
Normal file
1
lib/features/proxies/widgets/widgets.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'proxy_tile.dart';
|
||||
Reference in New Issue
Block a user