Merge branch 'main' of hiddify-github:hiddify/hiddify-next
This commit is contained in:
@@ -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,25 +161,9 @@ 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(
|
||||
label,
|
||||
key: ValueKey<String>(label),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
child: AnimatedText(
|
||||
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(),
|
||||
const Gap(8),
|
||||
_StatCard(
|
||||
title: t.home.stats.traffic,
|
||||
firstStat: (
|
||||
label: "↑",
|
||||
data: stats.uplink.speed(),
|
||||
semanticLabel: t.home.stats.uplink,
|
||||
),
|
||||
secondStat: (
|
||||
label: "↓",
|
||||
data: stats.downlink.speed(),
|
||||
semanticLabel: t.home.stats.downlink,
|
||||
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.trafficTotal,
|
||||
firstStat: (
|
||||
label: "↑",
|
||||
data: stats.uplinkTotal.size(),
|
||||
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,
|
||||
),
|
||||
(
|
||||
label: const Icon(
|
||||
FluentIcons.arrow_bidirectional_up_down_16_regular,
|
||||
),
|
||||
data: Text(stats.downlinkTotal.size()),
|
||||
semanticLabel: t.stats.totalTransferred,
|
||||
),
|
||||
],
|
||||
),
|
||||
secondStat: (
|
||||
label: "↓",
|
||||
data: stats.downlinkTotal.size(),
|
||||
semanticLabel: t.home.stats.downlink,
|
||||
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),
|
||||
StatsCard(
|
||||
title: t.stats.trafficTotal,
|
||||
stats: [
|
||||
(
|
||||
label: const Text(
|
||||
"↑",
|
||||
style: TextStyle(color: Colors.green),
|
||||
),
|
||||
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