Change sidebar stats
This commit is contained in:
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