Merge branch 'main' of hiddify-github:hiddify/hiddify-next

This commit is contained in:
Hiddify
2024-03-08 14:20:42 +01:00
17 changed files with 2934 additions and 2800 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -33,13 +33,13 @@
"experimentalNotice": "اخطار استفاده از ویژگی‌های آزمایشی",
"experimentalNoticeMsg": "برخی از ویژگی‌های آزمایشی را فعال کرده‌اید که ممکن است بر کیفیت اتصال تأثیر بگذارد و باعث خطاهای غیرمنتظره شود. همیشه می‌توانید این گزینه‌ها را از صفحه تنظیمات کانفیگ تغییر دهید یا بازنشانی کنید.",
"disableExperimentalNotice": "دیگر نشان نده"
}
},
"stats": {
"traffic": "مصرف لحظه‌ای",
"trafficLive": "مصرف لحظه‌ای",
"trafficTotal": "مصرف کل",
"uplink": "ارسال",
"downlink": "دریافت"
}
},
"profile": {
"overviewPageTitle": "پروفایل‌ها",

View File

@@ -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": {

View File

@@ -33,13 +33,13 @@
"experimentalNotice": "Экспериментальные функции в использовании",
"experimentalNoticeMsg": "Вы включили некоторые экспериментальные функции, которые могут повлиять на качество соединения и вызвать непредвиденные ошибки. Вы всегда можете изменить или сбросить эти параметры на странице параметров конфигурации.",
"disableExperimentalNotice": "Больше не показывать"
}
},
"stats": {
"traffic": "Текущий трафик",
"trafficLive": "Текущий трафик",
"trafficTotal": "Трафик",
"uplink": "Скорость отправки",
"downlink": "Скорость загрузки"
}
},
"profile": {
"overviewPageTitle": "Профили",

View File

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

View File

@@ -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": {

View File

@@ -29,13 +29,13 @@
"experimentalNotice": "使用中的實驗性功能",
"experimentalNoticeMsg": "您啟用了一些實驗性功能,這些功能可能會影響連線品質並導致意外錯誤。您始終可以從「配置選項」頁面變更或重設這些選項。",
"disableExperimentalNotice": "不再提示"
}
},
"stats": {
"traffic": "即時流量",
"trafficLive": "即時流量",
"trafficTotal": "總流量",
"uplink": "上行",
"downlink": "下行"
}
},
"profile": {
"overviewPageTitle": "設定檔",

View File

@@ -15,3 +15,5 @@ abstract class Constants {
static const cfWarpTermsOfService =
"https://www.cloudflare.com/application/terms/";
}
const kAnimationDuration = Duration(milliseconds: 250);

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

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

View File

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

View File

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

View File

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

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

View File

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

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