Merge branch 'main' of hiddify-github:hiddify/hiddify-next
This commit is contained in:
@@ -18,7 +18,9 @@
|
||||
"unknown": "Unknown",
|
||||
"hidden": "Hidden",
|
||||
"timeout": "timeout",
|
||||
"clipboardExportSuccessMsg": "Added to Clipboard"
|
||||
"clipboardExportSuccessMsg": "Added to Clipboard",
|
||||
"showMore": "Show More",
|
||||
"showLess": "Show Less"
|
||||
},
|
||||
"intro": {
|
||||
"termsAndPolicyCaution(rich)": "by continuing you agree with ${tap(@:about.termsAndConditions)}",
|
||||
@@ -37,14 +39,17 @@
|
||||
"experimentalNotice": "Experimental Features In Use",
|
||||
"experimentalNoticeMsg": "You've enabled some experimental features which might affect connection quality and cause unexpected errors. You can always change or reset these options from Config options page.",
|
||||
"disableExperimentalNotice": "Don't show again"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"traffic": "Live Traffic",
|
||||
"traffic": "Traffic",
|
||||
"trafficLive": "Live Traffic",
|
||||
"trafficTotal": "Total Traffic",
|
||||
"uplink": "Uplink",
|
||||
"downlink": "Downlink",
|
||||
"connection": "Connection"
|
||||
}
|
||||
"connection": "Connection",
|
||||
"speed": "Speed",
|
||||
"totalTransferred": "Total transferred"
|
||||
},
|
||||
"profile": {
|
||||
"overviewPageTitle": "Profiles",
|
||||
@@ -147,10 +152,6 @@
|
||||
"ipInfoSemantics": {
|
||||
"address": "IP address",
|
||||
"country": "Country"
|
||||
},
|
||||
"statsSemantics": {
|
||||
"speed": "Speed",
|
||||
"totalTransferred": "Total transferred"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
|
||||
@@ -33,12 +33,6 @@
|
||||
"experimentalNotice": "Funciones experimentales en uso",
|
||||
"experimentalNoticeMsg": "Ha habilitado algunas funciones experimentales que podrían afectar la calidad de la conexión y provocar errores inesperados. Siempre puede cambiar o restablecer estas opciones desde la página de opciones de configuración.",
|
||||
"disableExperimentalNotice": "No volver a mostrar"
|
||||
},
|
||||
"stats": {
|
||||
"traffic": "Tráfico en vivo",
|
||||
"trafficTotal": "Tráfico total",
|
||||
"uplink": "Enlace ascendente",
|
||||
"downlink": "Enlace descendente"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
"experimentalNotice": "اخطار استفاده از ویژگیهای آزمایشی",
|
||||
"experimentalNoticeMsg": "برخی از ویژگیهای آزمایشی را فعال کردهاید که ممکن است بر کیفیت اتصال تأثیر بگذارد و باعث خطاهای غیرمنتظره شود. همیشه میتوانید این گزینهها را از صفحه تنظیمات کانفیگ تغییر دهید یا بازنشانی کنید.",
|
||||
"disableExperimentalNotice": "دیگر نشان نده"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"traffic": "مصرف لحظهای",
|
||||
"trafficLive": "مصرف لحظهای",
|
||||
"trafficTotal": "مصرف کل",
|
||||
"uplink": "ارسال",
|
||||
"downlink": "دریافت"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"overviewPageTitle": "پروفایلها",
|
||||
|
||||
@@ -35,14 +35,16 @@
|
||||
"experimentalNotice": "Recursos experimentais em uso",
|
||||
"experimentalNoticeMsg": "Você ativou alguns recursos experimentais que podem afetar a qualidade da conexão e causar erros inesperados. Você sempre pode alterar ou redefinir essas opções na página de opções de configuração.",
|
||||
"disableExperimentalNotice": "Não mostrar novamente"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"traffic": "Tráfego ao vivo",
|
||||
"trafficLive": "Tráfego ao vivo",
|
||||
"trafficTotal": "Tráfego total",
|
||||
"uplink": "Ligação ascendente",
|
||||
"downlink": "Link descendente",
|
||||
"connection": "Conexão"
|
||||
}
|
||||
"connection": "Conexão",
|
||||
"speed": "Velocidade",
|
||||
"totalTransferred": "Total transferido"
|
||||
},
|
||||
"profile": {
|
||||
"overviewPageTitle": "Perfis",
|
||||
@@ -145,10 +147,6 @@
|
||||
"ipInfoSemantics": {
|
||||
"address": "Endereço de IP",
|
||||
"country": "País"
|
||||
},
|
||||
"statsSemantics": {
|
||||
"speed": "Velocidade",
|
||||
"totalTransferred": "Total transferido"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
"experimentalNotice": "Экспериментальные функции в использовании",
|
||||
"experimentalNoticeMsg": "Вы включили некоторые экспериментальные функции, которые могут повлиять на качество соединения и вызвать непредвиденные ошибки. Вы всегда можете изменить или сбросить эти параметры на странице параметров конфигурации.",
|
||||
"disableExperimentalNotice": "Больше не показывать"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"traffic": "Текущий трафик",
|
||||
"trafficLive": "Текущий трафик",
|
||||
"trafficTotal": "Трафик",
|
||||
"uplink": "Скорость отправки",
|
||||
"downlink": "Скорость загрузки"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"overviewPageTitle": "Профили",
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
"experimentalNotice": "Kullanımdaki Deneysel Özellikler",
|
||||
"experimentalNoticeMsg": "Bağlantı kalitesini etkileyebilecek ve beklenmeyen hatalara neden olabilecek bazı deneysel özellikleri etkinleştirdiniz. Bu seçenekleri istediğiniz zaman Yapılandırma seçenekleri sayfasından değiştirebilir veya sıfırlayabilirsiniz.",
|
||||
"disableExperimentalNotice": "Bir daha gösterme"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"traffic": "Canlı Trafik",
|
||||
"trafficLive": "Canlı Trafik",
|
||||
"trafficTotal": "Toplam Trafik",
|
||||
"uplink": "Çıkış Yolu",
|
||||
"downlink": "Giriş Yolu"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"overviewPageTitle": "Profiller",
|
||||
|
||||
@@ -35,14 +35,16 @@
|
||||
"experimentalNotice": "使用中的实验功能",
|
||||
"experimentalNoticeMsg": "您启用了一些实验性功能,这些功能可能会影响连接质量并导致意外错误。您始终可以从“配置选项”页面更改或重置这些选项。",
|
||||
"disableExperimentalNotice": "不再显示"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"traffic": "实时流量",
|
||||
"trafficLive": "实时流量",
|
||||
"trafficTotal": "总流量",
|
||||
"uplink": "上行",
|
||||
"downlink": "下行",
|
||||
"connection": "连接"
|
||||
}
|
||||
"connection": "连接",
|
||||
"speed": "速度",
|
||||
"totalTransferred": "总传输量"
|
||||
},
|
||||
"profile": {
|
||||
"overviewPageTitle": "配置文件",
|
||||
@@ -145,10 +147,6 @@
|
||||
"ipInfoSemantics": {
|
||||
"address": "IP地址",
|
||||
"country": "国家"
|
||||
},
|
||||
"statsSemantics": {
|
||||
"speed": "速度",
|
||||
"totalTransferred": "总传输量"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
"experimentalNotice": "使用中的實驗性功能",
|
||||
"experimentalNoticeMsg": "您啟用了一些實驗性功能,這些功能可能會影響連線品質並導致意外錯誤。您始終可以從「配置選項」頁面變更或重設這些選項。",
|
||||
"disableExperimentalNotice": "不再提示"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"traffic": "即時流量",
|
||||
"trafficLive": "即時流量",
|
||||
"trafficTotal": "總流量",
|
||||
"uplink": "上行",
|
||||
"downlink": "下行"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"overviewPageTitle": "設定檔",
|
||||
|
||||
@@ -15,3 +15,5 @@ abstract class Constants {
|
||||
static const cfWarpTermsOfService =
|
||||
"https://www.cloudflare.com/application/terms/";
|
||||
}
|
||||
|
||||
const kAnimationDuration = Duration(milliseconds: 250);
|
||||
|
||||
53
lib/core/widget/animated_text.dart
Normal file
53
lib/core/widget/animated_text.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/model/constants.dart';
|
||||
|
||||
class AnimatedText extends Text {
|
||||
const AnimatedText(
|
||||
super.data, {
|
||||
super.key,
|
||||
super.style,
|
||||
this.duration = kAnimationDuration,
|
||||
this.size = true,
|
||||
this.slide = true,
|
||||
});
|
||||
|
||||
final Duration duration;
|
||||
final bool size;
|
||||
final bool slide;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
duration: duration,
|
||||
transitionBuilder: (child, animation) {
|
||||
child = FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
if (size) {
|
||||
child = SizeTransition(
|
||||
axis: Axis.horizontal,
|
||||
fixedCrossAxisSizeFactor: 1,
|
||||
sizeFactor: Tween<double>(begin: 0.88, end: 1).animate(animation),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
if (slide) {
|
||||
child = SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -0.2),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
child: Text(
|
||||
data!,
|
||||
key: ValueKey<String>(data!),
|
||||
style: style,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
lib/core/widget/spaced_list_widget.dart
Normal file
10
lib/core/widget/spaced_list_widget.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
extension SpacedWidgets on List<Widget> {
|
||||
List<Widget> spaceBy({double? width, double? height}) => [
|
||||
for (int i = 0; i < length; i++) ...[
|
||||
if (i > 0) SizedBox(width: width, height: height),
|
||||
this[i],
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
import 'package:hiddify/core/theme/theme_extensions.dart';
|
||||
import 'package:hiddify/core/widget/animated_text.dart';
|
||||
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
|
||||
import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart';
|
||||
import 'package:hiddify/features/connection/model/connection_status.dart';
|
||||
@@ -160,27 +161,11 @@ class _ConnectionButton extends StatelessWidget {
|
||||
),
|
||||
const Gap(16),
|
||||
ExcludeSemantics(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
transitionBuilder: (child, animation) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -0.2),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
child: AnimatedText(
|
||||
label,
|
||||
key: ValueKey<String>(label),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
|
||||
import 'package:hiddify/features/proxy/active/ip_widget.dart';
|
||||
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
|
||||
import 'package:hiddify/features/stats/notifier/stats_notifier.dart';
|
||||
import 'package:hiddify/gen/fonts.gen.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
@@ -128,13 +129,13 @@ class _StatsColumn extends HookConsumerWidget {
|
||||
_InfoProp(
|
||||
icon: FluentIcons.arrow_bidirectional_up_down_20_regular,
|
||||
text: (stats?.downlinkTotal ?? 0).size(),
|
||||
semanticLabel: t.proxies.statsSemantics.totalTransferred,
|
||||
semanticLabel: t.stats.totalTransferred,
|
||||
),
|
||||
const Gap(8),
|
||||
_InfoProp(
|
||||
icon: FluentIcons.arrow_download_20_regular,
|
||||
text: (stats?.downlink ?? 0).speed(),
|
||||
semanticLabel: t.proxies.statsSemantics.speed,
|
||||
semanticLabel: t.stats.speed,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -165,7 +166,10 @@ class _InfoProp extends StatelessWidget {
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(fontFamily: FontFamily.emoji),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/widget/shimmer_skeleton.dart';
|
||||
import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
|
||||
import 'package:hiddify/features/proxy/active/ip_widget.dart';
|
||||
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
|
||||
import 'package:hiddify/gen/fonts.gen.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 activeProxy = ref.watch(activeProxyNotifierProvider);
|
||||
final ipInfo = ref.watch(ipInfoNotifierProvider);
|
||||
|
||||
Widget propText(String txt) {
|
||||
return Text(
|
||||
txt,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
theme.textTheme.bodySmall?.copyWith(fontFamily: FontFamily.emoji),
|
||||
);
|
||||
}
|
||||
|
||||
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 (activeProxy) {
|
||||
AsyncData(value: final proxy) => buildProp(
|
||||
const Icon(FluentIcons.arrow_routing_20_regular),
|
||||
propText(
|
||||
proxy.selectedName.isNotNullOrBlank
|
||||
? proxy.selectedName!
|
||||
: proxy.name,
|
||||
),
|
||||
),
|
||||
_ => buildProp(
|
||||
const Icon(FluentIcons.arrow_routing_20_regular),
|
||||
propText("..."),
|
||||
),
|
||||
},
|
||||
const Gap(4),
|
||||
switch (ipInfo) {
|
||||
AsyncData(value: final info) => buildProp(
|
||||
IPCountryFlag(
|
||||
countryCode: info.countryCode,
|
||||
size: 16,
|
||||
),
|
||||
IPText(
|
||||
ip: info.ip,
|
||||
onLongPress: () async {
|
||||
ref.read(ipInfoNotifierProvider.notifier).refresh();
|
||||
},
|
||||
constrained: true,
|
||||
),
|
||||
),
|
||||
AsyncLoading() => buildProp(
|
||||
const Icon(FluentIcons.question_circle_20_regular),
|
||||
const ShimmerSkeleton(widthFactor: .85, height: 14),
|
||||
),
|
||||
AsyncError(error: final UnknownIp _) => buildProp(
|
||||
const Icon(FluentIcons.arrow_sync_20_regular),
|
||||
UnknownIPText(
|
||||
text: t.proxies.checkIp,
|
||||
onTap: () async {
|
||||
ref.read(ipInfoNotifierProvider.notifier).refresh();
|
||||
},
|
||||
constrained: true,
|
||||
),
|
||||
),
|
||||
_ => buildProp(
|
||||
const Icon(FluentIcons.error_circle_20_regular),
|
||||
UnknownIPText(
|
||||
text: t.proxies.unknownIp,
|
||||
onTap: () async {
|
||||
ref.read(ipInfoNotifierProvider.notifier).refresh();
|
||||
},
|
||||
constrained: true,
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
87
lib/features/stats/widget/connection_stats_card.dart
Normal file
87
lib/features/stats/widget/connection_stats_card.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/widget/shimmer_skeleton.dart';
|
||||
import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
|
||||
import 'package:hiddify/features/proxy/active/ip_widget.dart';
|
||||
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
|
||||
import 'package:hiddify/features/stats/widget/stats_card.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class ConnectionStatsCard extends HookConsumerWidget {
|
||||
const ConnectionStatsCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final activeProxy = ref.watch(activeProxyNotifierProvider);
|
||||
final ipInfo = ref.watch(ipInfoNotifierProvider);
|
||||
|
||||
return StatsCard(
|
||||
title: t.stats.connection,
|
||||
stats: [
|
||||
switch (activeProxy) {
|
||||
AsyncData(value: final proxy) => (
|
||||
label: const Icon(FluentIcons.arrow_routing_20_regular),
|
||||
data: Text(
|
||||
proxy.selectedName.isNotNullOrBlank
|
||||
? proxy.selectedName!
|
||||
: proxy.name,
|
||||
),
|
||||
semanticLabel: null,
|
||||
),
|
||||
_ => (
|
||||
label: const Icon(FluentIcons.arrow_routing_20_regular),
|
||||
data: const Text("..."),
|
||||
semanticLabel: null,
|
||||
),
|
||||
},
|
||||
switch (ipInfo) {
|
||||
AsyncData(value: final info) => (
|
||||
label: IPCountryFlag(
|
||||
countryCode: info.countryCode,
|
||||
size: 16,
|
||||
),
|
||||
data: IPText(
|
||||
ip: info.ip,
|
||||
onLongPress: () async {
|
||||
ref.read(ipInfoNotifierProvider.notifier).refresh();
|
||||
},
|
||||
constrained: true,
|
||||
),
|
||||
semanticLabel: null,
|
||||
),
|
||||
AsyncLoading() => (
|
||||
label: const Icon(FluentIcons.question_circle_20_regular),
|
||||
data: const ShimmerSkeleton(widthFactor: .85, height: 14),
|
||||
semanticLabel: null,
|
||||
),
|
||||
AsyncError(error: final UnknownIp _) => (
|
||||
label: const Icon(FluentIcons.arrow_sync_20_regular),
|
||||
data: UnknownIPText(
|
||||
text: t.proxies.checkIp,
|
||||
onTap: () async {
|
||||
ref.read(ipInfoNotifierProvider.notifier).refresh();
|
||||
},
|
||||
constrained: true,
|
||||
),
|
||||
semanticLabel: null,
|
||||
),
|
||||
_ => (
|
||||
label: const Icon(FluentIcons.error_circle_20_regular),
|
||||
data: UnknownIPText(
|
||||
text: t.proxies.unknownIp,
|
||||
onTap: () async {
|
||||
ref.read(ipInfoNotifierProvider.notifier).refresh();
|
||||
},
|
||||
constrained: true,
|
||||
),
|
||||
semanticLabel: null,
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
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/core/model/constants.dart';
|
||||
import 'package:hiddify/core/utils/preferences_utils.dart';
|
||||
import 'package:hiddify/core/widget/animated_text.dart';
|
||||
import 'package:hiddify/features/stats/model/stats_entity.dart';
|
||||
import 'package:hiddify/features/stats/notifier/stats_notifier.dart';
|
||||
import 'package:hiddify/features/stats/widget/connection_stats_card.dart';
|
||||
import 'package:hiddify/features/stats/widget/stats_card.dart';
|
||||
import 'package:hiddify/utils/number_formatters.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final showAllSidebarStatsProvider = PreferencesNotifier.createAutoDispose(
|
||||
"show_all_sidebar_stats",
|
||||
false,
|
||||
);
|
||||
|
||||
class SideBarStatsOverview extends HookConsumerWidget {
|
||||
const SideBarStatsOverview({super.key});
|
||||
|
||||
@@ -16,39 +26,114 @@ class SideBarStatsOverview extends HookConsumerWidget {
|
||||
|
||||
final stats =
|
||||
ref.watch(statsNotifierProvider).asData?.value ?? StatsEntity.empty();
|
||||
final showAll = ref.watch(showAllSidebarStatsProvider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ActiveProxySideBarCard(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: SizedBox(
|
||||
height: 18,
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
textStyle: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(showAllSidebarStatsProvider.notifier)
|
||||
.update(!showAll);
|
||||
},
|
||||
icon: AnimatedRotation(
|
||||
turns: showAll ? 1 : 0.5,
|
||||
duration: kAnimationDuration,
|
||||
child: const Icon(
|
||||
FluentIcons.chevron_down_16_regular,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
label: AnimatedText(
|
||||
showAll ? t.general.showLess : t.general.showMore,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const ConnectionStatsCard(),
|
||||
const Gap(8),
|
||||
_StatCard(
|
||||
title: t.home.stats.traffic,
|
||||
firstStat: (
|
||||
label: "↑",
|
||||
data: stats.uplink.speed(),
|
||||
semanticLabel: t.home.stats.uplink,
|
||||
AnimatedCrossFade(
|
||||
crossFadeState:
|
||||
showAll ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
duration: kAnimationDuration,
|
||||
firstChild: StatsCard(
|
||||
title: t.stats.traffic,
|
||||
stats: [
|
||||
(
|
||||
label: const Icon(FluentIcons.arrow_download_16_regular),
|
||||
data: Text(stats.downlink.speed()),
|
||||
semanticLabel: t.stats.speed,
|
||||
),
|
||||
secondStat: (
|
||||
label: "↓",
|
||||
data: stats.downlink.speed(),
|
||||
semanticLabel: t.home.stats.downlink,
|
||||
(
|
||||
label: const Icon(
|
||||
FluentIcons.arrow_bidirectional_up_down_16_regular,
|
||||
),
|
||||
data: Text(stats.downlinkTotal.size()),
|
||||
semanticLabel: t.stats.totalTransferred,
|
||||
),
|
||||
],
|
||||
),
|
||||
secondChild: Column(
|
||||
children: [
|
||||
StatsCard(
|
||||
title: t.stats.trafficLive,
|
||||
stats: [
|
||||
(
|
||||
label: const Text(
|
||||
"↑",
|
||||
style: TextStyle(color: Colors.green),
|
||||
),
|
||||
data: Text(stats.uplink.speed()),
|
||||
semanticLabel: t.stats.uplink,
|
||||
),
|
||||
(
|
||||
label: Text(
|
||||
"↓",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
data: Text(stats.downlink.speed()),
|
||||
semanticLabel: t.stats.downlink,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
_StatCard(
|
||||
title: t.home.stats.trafficTotal,
|
||||
firstStat: (
|
||||
label: "↑",
|
||||
data: stats.uplinkTotal.size(),
|
||||
semanticLabel: t.home.stats.uplink,
|
||||
StatsCard(
|
||||
title: t.stats.trafficTotal,
|
||||
stats: [
|
||||
(
|
||||
label: const Text(
|
||||
"↑",
|
||||
style: TextStyle(color: Colors.green),
|
||||
),
|
||||
secondStat: (
|
||||
label: "↓",
|
||||
data: stats.downlinkTotal.size(),
|
||||
semanticLabel: t.home.stats.downlink,
|
||||
data: Text(stats.uplinkTotal.size()),
|
||||
semanticLabel: t.stats.uplink,
|
||||
),
|
||||
(
|
||||
label: Text(
|
||||
"↓",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
data: Text(stats.downlinkTotal.size()),
|
||||
semanticLabel: t.stats.downlink,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -56,63 +141,3 @@ class SideBarStatsOverview extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatCard extends HookConsumerWidget {
|
||||
const _StatCard({
|
||||
required this.title,
|
||||
required this.firstStat,
|
||||
required this.secondStat,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final ({String label, String data, String semanticLabel}) firstStat;
|
||||
final ({String label, String data, String semanticLabel}) secondStat;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shadowColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title),
|
||||
const Gap(4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
firstStat.label,
|
||||
semanticsLabel: firstStat.semanticLabel,
|
||||
style: const TextStyle(color: Colors.green),
|
||||
),
|
||||
Text(
|
||||
firstStat.data,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
secondStat.label,
|
||||
semanticsLabel: secondStat.semanticLabel,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
Text(
|
||||
secondStat.data,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
94
lib/features/stats/widget/stats_card.dart
Normal file
94
lib/features/stats/widget/stats_card.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/widget/spaced_list_widget.dart';
|
||||
|
||||
typedef PresentableStat = ({Widget label, Widget data, String? semanticLabel});
|
||||
|
||||
class StatsCard extends StatelessWidget {
|
||||
const StatsCard({
|
||||
super.key,
|
||||
this.title,
|
||||
this.titleStyle,
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
this.labelStyle,
|
||||
this.dataStyle,
|
||||
required this.stats,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final TextStyle? titleStyle;
|
||||
final EdgeInsets padding;
|
||||
final TextStyle? labelStyle;
|
||||
final TextStyle? dataStyle;
|
||||
final List<PresentableStat> stats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveTitleStyle =
|
||||
titleStyle ?? Theme.of(context).textTheme.bodySmall;
|
||||
final effectiveLabelStyle = labelStyle ??
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(fontWeight: FontWeight.w300);
|
||||
final effectiveDataStyle = dataStyle ??
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(fontWeight: FontWeight.w300);
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shadowColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
Text(
|
||||
title!,
|
||||
style: effectiveTitleStyle,
|
||||
),
|
||||
const Gap(4),
|
||||
],
|
||||
...stats
|
||||
.map(
|
||||
(stat) {
|
||||
Widget label = IconTheme.merge(
|
||||
data: const IconThemeData(size: 14),
|
||||
child: DefaultTextStyle(
|
||||
style: effectiveLabelStyle!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: stat.label,
|
||||
),
|
||||
);
|
||||
if (stat.semanticLabel != null) {
|
||||
label = Tooltip(
|
||||
message: stat.semanticLabel,
|
||||
verticalOffset: 8,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
label,
|
||||
const Gap(2),
|
||||
DefaultTextStyle(
|
||||
style: effectiveDataStyle!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Flexible(child: stat.data),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
.toList()
|
||||
.spaceBy(height: 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user