Migrate to singbox

This commit is contained in:
problematicconsumer
2023-08-19 22:27:23 +03:30
parent 14369d0a03
commit 684acc555d
124 changed files with 3408 additions and 2047 deletions

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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),
],
);
}
}

View File

@@ -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),
];
}
}

View File

@@ -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();

View File

@@ -5,7 +5,6 @@ import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/features/common/active_profile/has_any_profile_notifier.dart';
import 'package:hiddify/features/common/clash/clash_controller.dart';
import 'package:hiddify/features/common/common.dart';
import 'package:hiddify/features/home/widgets/widgets.dart';
import 'package:hiddify/utils/utils.dart';
@@ -22,18 +21,6 @@ class HomePage extends HookConsumerWidget {
final hasAnyProfile = ref.watch(hasAnyProfileProvider);
final activeProfile = ref.watch(activeProfileProvider);
ref.listen(
clashControllerProvider,
(_, next) {
if (next case AsyncError(:final error)) {
CustomToast.error(
t.presentError(error),
duration: const Duration(seconds: 10),
).show(context);
}
},
);
return Scaffold(
body: Stack(
alignment: Alignment.bottomCenter,

View File

@@ -3,8 +3,11 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/theme/theme.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/gen/assets.gen.dart';
import 'package:hiddify/utils/alerts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
@@ -17,12 +20,71 @@ class ConnectionButton extends HookConsumerWidget {
final t = ref.watch(translationsProvider);
final connectionStatus = ref.watch(connectivityControllerProvider);
final Color connectionLogoColor = connectionStatus.isConnected
? ConnectionButtonColor.connected
: ConnectionButtonColor.disconnected;
ref.listen(
connectivityControllerProvider,
(_, next) {
if (next case AsyncError(:final error)) {
CustomToast.error(t.presentError(error)).show(context);
}
if (next
case AsyncData(value: Disconnected(:final connectionFailure?))) {
CustomAlertDialog(
message: connectionFailure.present(t),
).show(context);
}
},
);
final bool intractable = !connectionStatus.isSwitching;
switch (connectionStatus) {
case AsyncData(value: final status):
final Color connectionLogoColor = status.isConnected
? ConnectionButtonColor.connected
: ConnectionButtonColor.disconnected;
return _ConnectionButton(
onTap: () => ref
.read(connectivityControllerProvider.notifier)
.toggleConnection(),
enabled: !status.isSwitching,
label: status.present(t),
buttonColor: connectionLogoColor,
);
case AsyncError():
return _ConnectionButton(
onTap: () => ref
.read(connectivityControllerProvider.notifier)
.toggleConnection(),
enabled: true,
label: const Disconnected().present(t),
buttonColor: ConnectionButtonColor.disconnected,
);
default:
// HACK
return _ConnectionButton(
onTap: () {},
enabled: false,
label: "",
buttonColor: Colors.red,
);
}
}
}
class _ConnectionButton extends StatelessWidget {
const _ConnectionButton({
required this.onTap,
required this.enabled,
required this.label,
required this.buttonColor,
});
final VoidCallback onTap;
final bool enabled;
final String label;
final Color buttonColor;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -33,7 +95,7 @@ class ConnectionButton extends HookConsumerWidget {
boxShadow: [
BoxShadow(
blurRadius: 16,
color: connectionLogoColor.withOpacity(0.5),
color: buttonColor.withOpacity(0.5),
),
],
),
@@ -43,26 +105,24 @@ class ConnectionButton extends HookConsumerWidget {
shape: const CircleBorder(),
color: Colors.white,
child: InkWell(
onTap: () async {
await ref
.read(connectivityControllerProvider.notifier)
.toggleConnection();
},
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(36),
child: Assets.images.logo.svg(
colorFilter: ColorFilter.mode(
connectionLogoColor,
buttonColor,
BlendMode.srcIn,
),
),
),
),
).animate(target: intractable ? 0 : 1).blurXY(end: 1),
).animate(target: intractable ? 0 : 1).scaleXY(end: .88),
).animate(target: enabled ? 0 : 1).blurXY(end: 1),
)
.animate(target: enabled ? 0 : 1)
.scaleXY(end: .88, curve: Curves.easeIn),
const Gap(16),
Text(
connectionStatus.present(t).sentenceCase,
label.sentenceCase,
style: Theme.of(context).textTheme.bodyLarge,
),
],

View File

@@ -10,14 +10,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'logs_notifier.g.dart';
// TODO: rewrite
@riverpod
@Riverpod(keepAlive: true)
class LogsNotifier extends _$LogsNotifier with AppLogger {
static const maxLength = 1000;
@override
Stream<LogsState> build() {
state = const AsyncData(LogsState());
return ref.read(clashFacadeProvider).watchLogs().asyncMap(
return ref.read(coreFacadeProvider).watchLogs().asyncMap(
(event) async {
_logs = [
event.getOrElse((l) => throw l),
@@ -32,16 +32,15 @@ class LogsNotifier extends _$LogsNotifier with AppLogger {
);
}
var _logs = <ClashLog>[];
var _logs = <String>[];
final _debouncer = CallbackDebouncer(const Duration(milliseconds: 200));
LogLevel? _levelFilter;
String _filter = "";
Future<List<ClashLog>> _computeLogs() async {
Future<List<String>> _computeLogs() async {
if (_levelFilter == null && _filter.isEmpty) return _logs;
return _logs.where((e) {
return (_filter.isEmpty || e.message.contains(_filter)) &&
(_levelFilter == null || e.level == _levelFilter);
return _filter.isEmpty || e.contains(_filter);
}).toList();
}

View File

@@ -8,7 +8,7 @@ class LogsState with _$LogsState {
const LogsState._();
const factory LogsState({
@Default([]) List<ClashLog> logs,
@Default([]) List<String> logs,
@Default("") String filter,
LogLevel? levelFilter,
}) = _LogsState;

View File

@@ -10,6 +10,7 @@ import 'package:hiddify/features/logs/notifier/notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
import 'package:tint/tint.dart';
class LogsPage extends HookConsumerWidget {
const LogsPage({super.key});
@@ -80,19 +81,7 @@ class LogsPage extends HookConsumerWidget {
children: [
ListTile(
dense: true,
title: Text.rich(
TextSpan(
children: [
TextSpan(text: log.timeStamp),
const TextSpan(text: " "),
TextSpan(
text: log.level.name.toUpperCase(),
style: TextStyle(color: log.level.color),
),
],
),
),
subtitle: Text(log.message),
subtitle: Text(log.strip()),
),
if (index != 0)
const Divider(

View File

@@ -24,7 +24,7 @@ class GroupWithProxies with _$GroupWithProxies {
final result = <GroupWithProxies>[];
for (final proxy in proxies) {
if (proxy is ClashProxyGroup) {
if (mode != TunnelMode.global && proxy.name == "GLOBAL") continue;
// 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());

View File

@@ -32,7 +32,7 @@ class ProxiesDelayNotifier extends _$ProxiesDelayNotifier with AppLogger {
return {};
}
ClashFacade get _clash => ref.read(clashFacadeProvider);
ClashFacade get _clash => ref.read(coreFacadeProvider);
StreamSubscription? _currentTest;
Future<void> testDelay(Iterable<String> proxies) async {

View File

@@ -3,8 +3,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/features/common/clash/clash_controller.dart';
import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/features/common/clash/clash_mode.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';
@@ -16,23 +17,23 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger {
@override
Future<List<GroupWithProxies>> build() async {
loggy.debug('building');
await ref.watch(clashControllerProvider.future);
if (!await ref.watch(serviceRunningProvider.future)) {
throw const CoreServiceNotRunning();
}
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();
return _clash.getProxies().flatMap(
(proxies) {
return TaskEither(
() async => right(await GroupWithProxies.fromProxies(proxies, mode)),
);
},
).getOrElse((l) {
loggy.warning("failed receiving proxies: $l");
throw l;
}).run();
}
ClashFacade get _clash => ref.read(clashFacadeProvider);
ClashFacade get _clash => ref.read(coreFacadeProvider);
Future<void> changeProxy(String selectorName, String proxyName) async {
loggy.debug("changing proxy, selector: $selectorName - proxy: $proxyName ");

View File

@@ -20,7 +20,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
final notifier = ref.watch(proxiesNotifierProvider.notifier);
final asyncProxies = ref.watch(proxiesNotifierProvider);
final proxies = asyncProxies.value ?? [];
final proxies = asyncProxies.asData?.value ?? [];
final delays = ref.watch(proxiesDelayNotifierProvider);
final selectActiveProxyMutation = useMutation(
@@ -163,7 +163,10 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
NestedTabAppBar(
title: Text(t.proxies.pageTitle.titleCase),
),
SliverErrorBodyPlaceholder(t.presentError(error)),
SliverErrorBodyPlaceholder(
t.presentError(error),
icon: null,
),
],
),
);

View File

@@ -19,15 +19,61 @@ class ProxyTile extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return ListTile(
title: Text(
proxy.name,
switch (proxy) {
ClashProxyGroup(:final name) => name.toUpperCase(),
ClashProxyItem(:final name) => name,
},
overflow: TextOverflow.ellipsis,
),
subtitle: Text(proxy.type.label),
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Container(
width: 6,
height: double.maxFinite,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: selected ? theme.colorScheme.primary : Colors.transparent,
),
),
),
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)"),
],
],
),
),
trailing: delay != null ? Text(delay.toString()) : null,
selected: selected,
onTap: onSelect,
horizontalTitleGap: 4,
);
}
}

View File

@@ -1,103 +1,100 @@
import 'package:flutter/material.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/features/settings/widgets/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
// import 'package:flutter/material.dart';
// import 'package:hiddify/core/core_providers.dart';
// import 'package:hiddify/core/prefs/prefs.dart';
// import 'package:hiddify/domain/clash/clash.dart';
// import 'package:hiddify/features/settings/widgets/widgets.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:recase/recase.dart';
class ClashOverridesPage extends HookConsumerWidget {
const ClashOverridesPage({super.key});
// class ClashOverridesPage extends HookConsumerWidget {
// const ClashOverridesPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final t = ref.watch(translationsProvider);
final overrides =
ref.watch(prefsControllerProvider.select((value) => value.clash));
final notifier = ref.watch(prefsControllerProvider.notifier);
// final overrides =
// ref.watch(prefsControllerProvider.select((value) => value.clash));
// final notifier = ref.watch(prefsControllerProvider.notifier);
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text(t.settings.clash.sectionTitle.titleCase),
pinned: true,
),
SliverList.list(
children: [
InputOverrideTile(
title: t.settings.clash.overrides.httpPort,
value: overrides.httpPort,
resetValue: ClashConfig.initial.httpPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(httpPort: value),
),
),
InputOverrideTile(
title: t.settings.clash.overrides.socksPort,
value: overrides.socksPort,
resetValue: ClashConfig.initial.socksPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(socksPort: value),
),
),
InputOverrideTile(
title: t.settings.clash.overrides.redirPort,
value: overrides.redirPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(redirPort: value),
),
),
InputOverrideTile(
title: t.settings.clash.overrides.tproxyPort,
value: overrides.tproxyPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(tproxyPort: value),
),
),
InputOverrideTile(
title: t.settings.clash.overrides.mixedPort,
value: overrides.mixedPort,
resetValue: ClashConfig.initial.mixedPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(mixedPort: value),
),
),
ToggleOverrideTile(
title: t.settings.clash.overrides.allowLan,
value: overrides.allowLan,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(allowLan: value),
),
),
ToggleOverrideTile(
title: t.settings.clash.overrides.ipv6,
value: overrides.ipv6,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(ipv6: value),
),
),
ChoiceOverrideTile(
title: t.settings.clash.overrides.mode,
value: overrides.mode,
options: TunnelMode.values,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(mode: value),
),
),
ChoiceOverrideTile(
title: t.settings.clash.overrides.logLevel,
value: overrides.logLevel,
options: LogLevel.values,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(logLevel: value),
),
),
],
),
],
),
);
}
}
// return Scaffold(
// body: CustomScrollView(
// slivers: [
// SliverAppBar(
// title: Text(t.settings.clash.sectionTitle.titleCase),
// pinned: true,
// ),
// SliverList.list(
// children: [
// InputOverrideTile(
// title: t.settings.clash.overrides.httpPort,
// value: overrides.httpPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(httpPort: value),
// ),
// ),
// InputOverrideTile(
// title: t.settings.clash.overrides.socksPort,
// value: overrides.socksPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(socksPort: value),
// ),
// ),
// InputOverrideTile(
// title: t.settings.clash.overrides.redirPort,
// value: overrides.redirPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(redirPort: value),
// ),
// ),
// InputOverrideTile(
// title: t.settings.clash.overrides.tproxyPort,
// value: overrides.tproxyPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(tproxyPort: value),
// ),
// ),
// InputOverrideTile(
// title: t.settings.clash.overrides.mixedPort,
// value: overrides.mixedPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(mixedPort: value),
// ),
// ),
// ToggleOverrideTile(
// title: t.settings.clash.overrides.allowLan,
// value: overrides.allowLan,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(allowLan: value),
// ),
// ),
// ToggleOverrideTile(
// title: t.settings.clash.overrides.ipv6,
// value: overrides.ipv6,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(ipv6: value),
// ),
// ),
// ChoiceOverrideTile(
// title: t.settings.clash.overrides.mode,
// value: overrides.mode,
// options: TunnelMode.values,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(mode: value),
// ),
// ),
// ChoiceOverrideTile(
// title: t.settings.clash.overrides.logLevel,
// value: overrides.logLevel,
// options: LogLevel.values,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(logLevel: value),
// ),
// ),
// ],
// ),
// ],
// ),
// );
// }
// }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/features/settings/widgets/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
@@ -13,7 +12,7 @@ class SettingsPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
const divider = Divider(indent: 16, endIndent: 16);
// const divider = Divider(indent: 16, endIndent: 16);
return Scaffold(
appBar: AppBar(
@@ -29,18 +28,18 @@ class SettingsPage extends HookConsumerWidget {
t.settings.general.sectionTitle.titleCase,
),
const AppearanceSettingTiles(),
divider,
_SettingsSectionHeader(t.settings.network.sectionTitle.titleCase),
const NetworkSettingTiles(),
divider,
ListTile(
title: Text(t.settings.clash.sectionTitle.titleCase),
leading: const Icon(Icons.edit_document),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () async {
await const ClashOverridesRoute().push(context);
},
),
// divider,
// _SettingsSectionHeader(t.settings.network.sectionTitle.titleCase),
// const NetworkSettingTiles(),
// divider,
// ListTile(
// title: Text(t.settings.clash.sectionTitle.titleCase),
// leading: const Icon(Icons.edit_document),
// contentPadding: const EdgeInsets.symmetric(horizontal: 16),
// onTap: () async {
// await const ClashOverridesRoute().push(context);
// },
// ),
const Gap(16),
],
),

View File

@@ -1,36 +1,36 @@
import 'package:flutter/material.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
// import 'package:flutter/material.dart';
// import 'package:hiddify/core/core_providers.dart';
// import 'package:hiddify/core/prefs/prefs.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:recase/recase.dart';
class NetworkSettingTiles extends HookConsumerWidget {
const NetworkSettingTiles({super.key});
// class NetworkSettingTiles extends HookConsumerWidget {
// const NetworkSettingTiles({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final t = ref.watch(translationsProvider);
final prefs =
ref.watch(prefsControllerProvider.select((value) => value.network));
final notifier = ref.watch(prefsControllerProvider.notifier);
// final prefs =
// ref.watch(prefsControllerProvider.select((value) => value.network));
// final notifier = ref.watch(prefsControllerProvider.notifier);
return Column(
children: [
SwitchListTile(
title: Text(t.settings.network.systemProxy.titleCase),
subtitle: Text(t.settings.network.systemProxyMsg),
value: prefs.systemProxy,
onChanged: (value) => notifier.patchNetworkPrefs(systemProxy: value),
),
SwitchListTile(
title: Text(t.settings.network.bypassPrivateNetworks.titleCase),
subtitle: Text(t.settings.network.bypassPrivateNetworksMsg),
value: prefs.bypassPrivateNetworks,
onChanged: (value) =>
notifier.patchNetworkPrefs(bypassPrivateNetworks: value),
),
],
);
}
}
// return Column(
// children: [
// SwitchListTile(
// title: Text(t.settings.network.systemProxy.titleCase),
// subtitle: Text(t.settings.network.systemProxyMsg),
// value: prefs.systemProxy,
// onChanged: (value) => notifier.patchNetworkPrefs(systemProxy: value),
// ),
// SwitchListTile(
// title: Text(t.settings.network.bypassPrivateNetworks.titleCase),
// subtitle: Text(t.settings.network.bypassPrivateNetworksMsg),
// value: prefs.bypassPrivateNetworks,
// onChanged: (value) =>
// notifier.patchNetworkPrefs(bypassPrivateNetworks: value),
// ),
// ],
// );
// }
// }

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
@@ -27,7 +25,7 @@ class SystemTrayController extends _$SystemTrayController
_initialized = true;
}
final connection = ref.watch(connectivityControllerProvider);
final connection = await ref.watch(connectivityControllerProvider.future);
final mode =
ref.watch(clashModeProvider.select((value) => value.valueOrNull));
@@ -104,8 +102,9 @@ class SystemTrayController extends _$SystemTrayController
return ref.read(connectivityControllerProvider.notifier).toggleConnection();
}
// TODO rewrite
Future<void> handleClickExitApp(MenuItem menuItem) async {
exit(0);
await ref.read(connectivityControllerProvider.notifier).abortConnection();
await trayManager.destroy();
return ref.read(windowControllerProvider.notifier).quit();
}
}