Migrate to singbox
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.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';
|
||||
|
||||
part 'clash_controller.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ClashController extends _$ClashController with AppLogger {
|
||||
Profile? _oldProfile;
|
||||
|
||||
@override
|
||||
Future<void> build() async {
|
||||
final clash = ref.watch(clashFacadeProvider);
|
||||
|
||||
final overridesListener = ref.listen(
|
||||
prefsControllerProvider.select((value) => value.clash),
|
||||
(_, overrides) async {
|
||||
loggy.debug("new clash overrides received, patching...");
|
||||
await clash.patchOverrides(overrides).getOrElse((l) => throw l).run();
|
||||
},
|
||||
);
|
||||
final overrides = overridesListener.read();
|
||||
|
||||
final activeProfile = await ref.watch(activeProfileProvider.future);
|
||||
final oldProfile = _oldProfile;
|
||||
_oldProfile = activeProfile;
|
||||
if (activeProfile != null) {
|
||||
if (oldProfile == null ||
|
||||
oldProfile.id != activeProfile.id ||
|
||||
oldProfile.lastUpdate != activeProfile.lastUpdate) {
|
||||
loggy.debug("profile changed or updated, updating clash core");
|
||||
await clash
|
||||
.changeConfigs(activeProfile.id)
|
||||
.call(clash.patchOverrides(overrides))
|
||||
.getOrElse((error) {
|
||||
loggy.warning("failed to change or patch configs, $error");
|
||||
throw error;
|
||||
}).run();
|
||||
}
|
||||
} else {
|
||||
if (oldProfile != null) {
|
||||
loggy.debug("active profile removed, resetting clash");
|
||||
await clash
|
||||
.changeConfigs(Constants.configFileName)
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:hiddify/core/prefs/prefs.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/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@@ -11,13 +11,16 @@ part 'clash_mode.g.dart';
|
||||
class ClashMode extends _$ClashMode with AppLogger {
|
||||
@override
|
||||
Future<TunnelMode?> build() async {
|
||||
final clash = ref.watch(clashFacadeProvider);
|
||||
await ref.watch(clashControllerProvider.future);
|
||||
final clash = ref.watch(coreFacadeProvider);
|
||||
if (!await ref.watch(serviceRunningProvider.future)) {
|
||||
return null;
|
||||
}
|
||||
ref.watch(prefsControllerProvider.select((value) => value.clash.mode));
|
||||
return clash
|
||||
.getConfigs()
|
||||
.map((r) => r.mode)
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
return clash.getConfigs().map((r) => r.mode).getOrElse(
|
||||
(l) {
|
||||
loggy.warning("fetching clash mode: $l");
|
||||
throw l;
|
||||
},
|
||||
).run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:hiddify/features/common/clash/clash_controller.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/features/common/window/window_controller.dart';
|
||||
import 'package:hiddify/features/logs/notifier/notifier.dart';
|
||||
import 'package:hiddify/features/system_tray/controller/system_tray_controller.dart';
|
||||
import 'package:hiddify/utils/platform_utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -12,9 +12,8 @@ part 'common_controllers.g.dart';
|
||||
@Riverpod(keepAlive: true)
|
||||
void commonControllers(CommonControllersRef ref) {
|
||||
ref.listen(
|
||||
clashControllerProvider,
|
||||
logsNotifierProvider,
|
||||
(previous, next) {},
|
||||
fireImmediately: true,
|
||||
);
|
||||
ref.listen(
|
||||
connectivityControllerProvider,
|
||||
|
||||
@@ -1,62 +1,90 @@
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/services/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/services/service_providers.dart';
|
||||
import 'package:hiddify/domain/core_facade.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';
|
||||
|
||||
part 'connectivity_controller.g.dart';
|
||||
|
||||
// TODO: test and improve
|
||||
// TODO: abort connection on clash error
|
||||
@Riverpod(keepAlive: true)
|
||||
class ConnectivityController extends _$ConnectivityController with AppLogger {
|
||||
@override
|
||||
ConnectionStatus build() {
|
||||
state = const Disconnected();
|
||||
final connection = _connectivity
|
||||
.watchConnectionStatus()
|
||||
.map(ConnectionStatus.fromBool)
|
||||
.listen((event) => state = event);
|
||||
|
||||
// currently changes wont take effect while connected
|
||||
Stream<ConnectionStatus> build() {
|
||||
ref.listen(
|
||||
prefsControllerProvider.select((value) => value.network),
|
||||
(_, next) => _networkPrefs = next,
|
||||
fireImmediately: true,
|
||||
activeProfileProvider.select((value) => value.asData?.value),
|
||||
(previous, next) async {
|
||||
if (previous == null) return;
|
||||
final shouldReconnect = previous != next;
|
||||
if (shouldReconnect) {
|
||||
loggy.debug("active profile modified, reconnect");
|
||||
await reconnect();
|
||||
}
|
||||
},
|
||||
);
|
||||
ref.listen(
|
||||
prefsControllerProvider
|
||||
.select((value) => (value.clash.httpPort!, value.clash.socksPort!)),
|
||||
(_, next) => _ports = (http: next.$1, socks: next.$2),
|
||||
fireImmediately: true,
|
||||
);
|
||||
|
||||
ref.onDispose(connection.cancel);
|
||||
return state;
|
||||
return _connectivity.watchConnectionStatus();
|
||||
}
|
||||
|
||||
ConnectivityService get _connectivity =>
|
||||
ref.watch(connectivityServiceProvider);
|
||||
|
||||
late ({int http, int socks}) _ports;
|
||||
// ignore: unused_field
|
||||
late NetworkPrefs _networkPrefs;
|
||||
CoreFacade get _connectivity => ref.watch(coreFacadeProvider);
|
||||
|
||||
Future<void> toggleConnection() async {
|
||||
switch (state) {
|
||||
case Disconnected():
|
||||
if (!await _connectivity.grantVpnPermission()) {
|
||||
state = const Disconnected(ConnectivityFailure.unexpected());
|
||||
return;
|
||||
}
|
||||
await _connectivity.connect(
|
||||
httpPort: _ports.http,
|
||||
socksPort: _ports.socks,
|
||||
);
|
||||
case Connected():
|
||||
await _connectivity.disconnect();
|
||||
default:
|
||||
if (state case AsyncError()) {
|
||||
await _connect();
|
||||
} else if (state case AsyncData(:final value)) {
|
||||
switch (value) {
|
||||
case Disconnected():
|
||||
await _connect();
|
||||
case Connected():
|
||||
await _disconnect();
|
||||
default:
|
||||
loggy.warning("switching status, debounce");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reconnect() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
if (value case Connected()) {
|
||||
loggy.debug("reconnecting");
|
||||
await _disconnect();
|
||||
await _connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> abortConnection() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
switch (value) {
|
||||
case Connected() || Connecting():
|
||||
loggy.debug("aborting connection");
|
||||
await _disconnect();
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connect() async {
|
||||
final activeProfile = await ref.read(activeProfileProvider.future);
|
||||
await _connectivity
|
||||
.changeConfig(activeProfile!.id)
|
||||
.andThen(_connectivity.connect)
|
||||
.mapLeft((l) {
|
||||
loggy.warning("error connecting: $l");
|
||||
state = AsyncError(l, StackTrace.current);
|
||||
}).run();
|
||||
}
|
||||
|
||||
Future<void> _disconnect() async {
|
||||
await _connectivity.disconnect().mapLeft((l) {
|
||||
loggy.warning("error disconnecting: $l");
|
||||
state = AsyncError(l, StackTrace.current);
|
||||
}).run();
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<bool> serviceRunning(ServiceRunningRef ref) => ref
|
||||
.watch(
|
||||
connectivityControllerProvider.selectAsync((data) => data.isConnected),
|
||||
)
|
||||
.onError((error, stackTrace) => false);
|
||||
|
||||
@@ -22,73 +22,85 @@ class TrafficChart extends HookConsumerWidget {
|
||||
|
||||
switch (asyncTraffics) {
|
||||
case AsyncData(value: final traffics):
|
||||
final latest =
|
||||
traffics.lastOrNull ?? const Traffic(upload: 0, download: 0);
|
||||
final latestUploadData = formatByteSpeed(latest.upload);
|
||||
final latestDownloadData = formatByteSpeed(latest.download);
|
||||
|
||||
final uploadChartSpots = traffics.takeLast(chartSteps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.upload.toDouble()),
|
||||
);
|
||||
final downloadChartSpots = traffics.takeLast(chartSteps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.download.toDouble()),
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
// mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 68,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
gridData: const FlGridData(show: false),
|
||||
lineTouchData: const LineTouchData(enabled: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: uploadChartSpots.toList(),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: downloadChartSpots.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↑"),
|
||||
Text(latestUploadData.size),
|
||||
Text(latestUploadData.unit),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↓"),
|
||||
Text(latestDownloadData.size),
|
||||
Text(latestDownloadData.unit),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
// TODO: handle loading and error
|
||||
return _Chart(traffics, chartSteps);
|
||||
case AsyncLoading(:final value):
|
||||
if (value == null) return const SizedBox();
|
||||
return _Chart(value, chartSteps);
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Chart extends StatelessWidget {
|
||||
const _Chart(this.records, this.steps);
|
||||
|
||||
final List<Traffic> records;
|
||||
final int steps;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final latest = records.lastOrNull ?? const Traffic(upload: 0, download: 0);
|
||||
final latestUploadData = formatByteSpeed(latest.upload);
|
||||
final latestDownloadData = formatByteSpeed(latest.download);
|
||||
|
||||
final uploadChartSpots = records.takeLast(steps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.upload.toDouble()),
|
||||
);
|
||||
final downloadChartSpots = records.takeLast(steps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.download.toDouble()),
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 68,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
gridData: const FlGridData(show: false),
|
||||
lineTouchData: const LineTouchData(enabled: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: uploadChartSpots.toList(),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: downloadChartSpots.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↑"),
|
||||
Text(latestUploadData.size),
|
||||
Text(latestUploadData.unit),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↓"),
|
||||
Text(latestDownloadData.size),
|
||||
Text(latestDownloadData.unit),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@@ -13,28 +14,37 @@ class TrafficNotifier extends _$TrafficNotifier with AppLogger {
|
||||
int get _steps => 100;
|
||||
|
||||
@override
|
||||
Stream<List<Traffic>> build() {
|
||||
return Stream.periodic(const Duration(seconds: 1)).asyncMap(
|
||||
(_) async {
|
||||
return ref.read(clashFacadeProvider).getTraffic().match(
|
||||
(f) {
|
||||
loggy.warning('failed to watch clash traffic: $f');
|
||||
return const ClashTraffic(upload: 0, download: 0);
|
||||
},
|
||||
(traffic) => traffic,
|
||||
).run();
|
||||
},
|
||||
).map(
|
||||
(event) => switch (state) {
|
||||
AsyncData(:final value) => [
|
||||
...value.takeLast(_steps - 1),
|
||||
Traffic(upload: event.upload, download: event.download),
|
||||
],
|
||||
_ => List.generate(
|
||||
_steps,
|
||||
(index) => const Traffic(upload: 0, download: 0),
|
||||
)
|
||||
},
|
||||
);
|
||||
Stream<List<Traffic>> build() async* {
|
||||
final serviceRunning = await ref.watch(serviceRunningProvider.future);
|
||||
if (serviceRunning) {
|
||||
yield* ref.watch(coreFacadeProvider).watchTraffic().map(
|
||||
(event) => _mapToState(
|
||||
event
|
||||
.getOrElse((_) => const ClashTraffic(upload: 0, download: 0)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
yield* Stream.periodic(const Duration(seconds: 1)).asyncMap(
|
||||
(_) async {
|
||||
return const ClashTraffic(upload: 0, download: 0);
|
||||
},
|
||||
).map(_mapToState);
|
||||
}
|
||||
}
|
||||
|
||||
List<Traffic> _mapToState(ClashTraffic event) {
|
||||
final previous = state.valueOrNull ??
|
||||
List.generate(
|
||||
_steps,
|
||||
(index) => const Traffic(upload: 0, download: 0),
|
||||
);
|
||||
while (previous.length < _steps) {
|
||||
loggy.debug("previous short, adding");
|
||||
previous.insert(0, const Traffic(upload: 0, download: 0));
|
||||
}
|
||||
return [
|
||||
...previous.takeLast(_steps - 1),
|
||||
Traffic(upload: event.upload, download: event.download),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,12 @@ class WindowController extends _$WindowController
|
||||
await windowManager.close();
|
||||
}
|
||||
|
||||
Future<void> quit() async {
|
||||
loggy.debug("quitting");
|
||||
await windowManager.close();
|
||||
await windowManager.destroy();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onWindowClose() async {
|
||||
await windowManager.hide();
|
||||
|
||||
Reference in New Issue
Block a user