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

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

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