From 8f4d40026b535083b9d5864b29568977e6dbf0bc Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 10 Feb 2024 17:32:45 +0330 Subject: [PATCH] Add desktop active proxy info --- assets/translations/strings_en.i18n.json | 3 +- lib/features/home/widget/home_page.dart | 9 +- .../active/active_proxy_sidebar_card.dart | 109 ++++++++++++++++++ .../stats/widget/side_bar_stats_overview.dart | 3 + lib/singbox/service/ffi_singbox_service.dart | 38 +++++- 5 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 lib/features/proxy/active/active_proxy_sidebar_card.dart diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 22a68525..291896e0 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -38,7 +38,8 @@ "traffic": "Live Traffic", "trafficTotal": "Total Traffic", "uplink": "Uplink", - "downlink": "Downlink" + "downlink": "Downlink", + "connection": "Connection" } }, "profile": { diff --git a/lib/features/home/widget/home_page.dart b/lib/features/home/widget/home_page.dart index 9b43ba45..66025e0e 100644 --- a/lib/features/home/widget/home_page.dart +++ b/lib/features/home/widget/home_page.dart @@ -62,18 +62,17 @@ class HomePage extends HookConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( + const Expanded( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - const ConnectionButton(), - if (Platform.isAndroid || Platform.isIOS) - const ActiveProxyDelayIndicator(), + ConnectionButton(), + ActiveProxyDelayIndicator(), ], ), ), - if (Platform.isAndroid || Platform.isIOS) + if (MediaQuery.sizeOf(context).width < 840) const ActiveProxyFooter(), ], ), diff --git a/lib/features/proxy/active/active_proxy_sidebar_card.dart b/lib/features/proxy/active/active_proxy_sidebar_card.dart new file mode 100644 index 00000000..a3302e42 --- /dev/null +++ b/lib/features/proxy/active/active_proxy_sidebar_card.dart @@ -0,0 +1,109 @@ +import 'package:circle_flags/circle_flags.dart'; +import 'package:dartx/dartx.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/widget/skeleton_widget.dart'; +import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class ActiveProxySideBarCard extends HookConsumerWidget { + const ActiveProxySideBarCard({super.key}); + + Widget buildProp(Widget icon, Widget child) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + icon, + const Gap(4), + Flexible(child: child), + ], + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final t = ref.watch(translationsProvider); + final asyncState = ref.watch(activeProxyNotifierProvider); + + Widget propText(String txt) { + return Text( + txt, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ); + } + + return Theme( + data: theme.copyWith( + iconTheme: theme.iconTheme.copyWith(size: 14), + ), + child: Card( + margin: EdgeInsets.zero, + shadowColor: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t.home.stats.connection), + const Gap(4), + switch (asyncState) { + AsyncData(:final value) => buildProp( + const Icon(FluentIcons.arrow_routing_20_regular), + propText( + value.proxy.selectedName.isNotNullOrBlank + ? value.proxy.selectedName! + : value.proxy.name, + ), + ), + _ => buildProp( + const Icon(FluentIcons.arrow_routing_20_regular), + propText("..."), + ), + }, + const Gap(4), + () { + if (asyncState case AsyncData(:final value)) { + switch (value.ipInfo) { + case AsyncData(value: final ipInfo?): + return buildProp( + CircleFlag(ipInfo.countryCode, size: 12), + propText(ipInfo.ip), + ); + case AsyncError(): + return buildProp( + const Icon(FluentIcons.error_circle_20_regular), + propText(t.general.unknown), + ); + case AsyncLoading(): + return buildProp( + const Icon(FluentIcons.question_circle_20_regular), + const Skeleton(height: 14, widthFactor: .85) + .animate( + onPlay: (controller) => controller.loop(), + ) + .shimmer( + duration: 1000.ms, + angle: 45, + color: Theme.of(context).colorScheme.secondary, + ), + ); + } + } + + return buildProp( + const Icon(FluentIcons.question_circle_20_regular), + propText(t.general.unknown), + ); + }(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/stats/widget/side_bar_stats_overview.dart b/lib/features/stats/widget/side_bar_stats_overview.dart index a1b6ff6b..d0783fe4 100644 --- a/lib/features/stats/widget/side_bar_stats_overview.dart +++ b/lib/features/stats/widget/side_bar_stats_overview.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/proxy/active/active_proxy_sidebar_card.dart'; import 'package:hiddify/features/stats/model/stats_entity.dart'; import 'package:hiddify/features/stats/notifier/stats_notifier.dart'; import 'package:hiddify/utils/number_formatters.dart'; @@ -21,6 +22,8 @@ class SideBarStatsOverview extends HookConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ + const ActiveProxySideBarCard(), + const Gap(8), _StatCard( title: t.home.stats.traffic, firstStat: ( diff --git a/lib/singbox/service/ffi_singbox_service.dart b/lib/singbox/service/ffi_singbox_service.dart index c065fd62..b07a575c 100644 --- a/lib/singbox/service/ffi_singbox_service.dart +++ b/lib/singbox/service/ffi_singbox_service.dart @@ -320,8 +320,42 @@ class FFISingboxService with InfraLogger implements SingboxService { @override Stream> watchActiveOutbounds() { - // TODO: implement watchActiveOutbounds - throw UnimplementedError(); + final logger = newLoggy("[ActiveGroupsClient]"); + final receiver = ReceivePort('active groups receiver'); + final outboundsStream = receiver.asBroadcastStream( + onCancel: (_) { + logger.debug("stopping"); + final err = _box.stopCommandClient(12).cast().toDartString(); + if (err.isNotEmpty) { + logger.error("failed stopping: $err"); + } + receiver.close(); + }, + ).map( + (event) { + if (event case String _) { + if (event.startsWith('error:')) { + logger.error(event); + throw event.replaceFirst('error:', ""); + } + return (jsonDecode(event) as List).map((e) { + return SingboxOutboundGroup.fromJson(e as Map); + }).toList(); + } + logger.error("unexpected type, msg: $event"); + throw "invalid type"; + }, + ); + + final err = _box + .startCommandClient(12, receiver.sendPort.nativePort) + .cast() + .toDartString(); + if (err.isNotEmpty) { + logger.error("error starting: $err"); + throw err; + } + return outboundsStream; } @override