Change sidebar stats

This commit is contained in:
problematicconsumer
2024-03-08 15:07:45 +03:30
parent d73330cf0b
commit ff62e9951a
17 changed files with 2929 additions and 2797 deletions

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

View File

@@ -128,13 +128,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,
),
],
),

View File

@@ -1,115 +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: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,
);
}
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(),
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,
),
],
),
],
),
),
);
}
}

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