Add stats overview
This commit is contained in:
@@ -299,16 +299,8 @@ class ProfileSubscriptionInfo extends HookConsumerWidget {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: formatByte(subInfo.consumption, unit: 3).size),
|
||||
const TextSpan(text: " / "),
|
||||
TextSpan(text: formatByte(subInfo.total, unit: 3).size),
|
||||
const TextSpan(text: " "),
|
||||
TextSpan(text: t.profile.subscription.gigaByte),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
subInfo.consumption.sizeOf(subInfo.total),
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
|
||||
23
lib/features/common/stats/stats_notifier.dart
Normal file
23
lib/features/common/stats/stats_notifier.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'stats_notifier.g.dart';
|
||||
|
||||
@riverpod
|
||||
class StatsNotifier extends _$StatsNotifier with AppLogger {
|
||||
@override
|
||||
Stream<CoreStatus> build() async* {
|
||||
final serviceRunning = await ref.watch(serviceRunningProvider.future);
|
||||
if (serviceRunning) {
|
||||
yield* ref
|
||||
.watch(coreFacadeProvider)
|
||||
.watchCoreStatus()
|
||||
.map((event) => event.getOrElse((_) => CoreStatus.empty()));
|
||||
} else {
|
||||
yield* Stream.value(CoreStatus.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
104
lib/features/common/stats/stats_overview.dart
Normal file
104
lib/features/common/stats/stats_overview.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/features/common/stats/stats_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
class StatsOverview extends HookConsumerWidget {
|
||||
const StatsOverview({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final stats =
|
||||
ref.watch(statsNotifierProvider).asData?.value ?? CoreStatus.empty();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_StatCard(
|
||||
title: t.home.stats.traffic.titleCase,
|
||||
firstStat: (
|
||||
label: t.home.stats.uplink.titleCase,
|
||||
data: stats.uplink.speed(),
|
||||
),
|
||||
secondStat: (
|
||||
label: t.home.stats.downlink.titleCase,
|
||||
data: stats.downlink.speed(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
_StatCard(
|
||||
title: t.home.stats.trafficTotal.titleCase,
|
||||
firstStat: (
|
||||
label: t.home.stats.uplink.titleCase,
|
||||
data: stats.uplinkTotal.size(),
|
||||
),
|
||||
secondStat: (
|
||||
label: t.home.stats.downlink.titleCase,
|
||||
data: stats.downlinkTotal.size(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatCard extends HookConsumerWidget {
|
||||
const _StatCard({
|
||||
required this.title,
|
||||
required this.firstStat,
|
||||
required this.secondStat,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final ({String label, String data}) firstStat;
|
||||
final ({String label, String data}) 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,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
Text(firstStat.data),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
secondStat.label,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
Text(secondStat.data),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/features/common/traffic/traffic_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
// TODO: test implementation, rewrite
|
||||
class TrafficChart extends HookConsumerWidget {
|
||||
const TrafficChart({
|
||||
super.key,
|
||||
this.chartSteps = 20,
|
||||
});
|
||||
|
||||
final int chartSteps;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asyncTraffics = ref.watch(trafficNotifierProvider);
|
||||
|
||||
switch (asyncTraffics) {
|
||||
case AsyncData(value: final traffics):
|
||||
return _Chart(traffics, chartSteps);
|
||||
case AsyncLoading(:final value):
|
||||
if (value == null) return const SizedBox();
|
||||
return _Chart(value, chartSteps);
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Chart extends StatelessWidget {
|
||||
const _Chart(this.records, this.steps);
|
||||
|
||||
final List<Traffic> records;
|
||||
final int steps;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final latest = records.lastOrNull ?? const Traffic(upload: 0, download: 0);
|
||||
final latestUploadData = formatByteSpeed(latest.upload);
|
||||
final latestDownloadData = formatByteSpeed(latest.download);
|
||||
|
||||
final uploadChartSpots = records.takeLast(steps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.upload.toDouble()),
|
||||
);
|
||||
final downloadChartSpots = records.takeLast(steps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.download.toDouble()),
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 68,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
gridData: const FlGridData(show: false),
|
||||
lineTouchData: const LineTouchData(enabled: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: uploadChartSpots.toList(),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: downloadChartSpots.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↑"),
|
||||
Text(latestUploadData.size),
|
||||
Text(latestUploadData.unit),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↓"),
|
||||
Text(latestDownloadData.size),
|
||||
Text(latestDownloadData.unit),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'traffic_notifier.g.dart';
|
||||
|
||||
// TODO: improve
|
||||
@riverpod
|
||||
class TrafficNotifier extends _$TrafficNotifier with AppLogger {
|
||||
int get _steps => 100;
|
||||
|
||||
@override
|
||||
Stream<List<Traffic>> build() async* {
|
||||
final serviceRunning = await ref.watch(serviceRunningProvider.future);
|
||||
if (serviceRunning) {
|
||||
// TODO: temporary!
|
||||
yield* ref.watch(coreFacadeProvider).watchCoreStatus().map((event) {
|
||||
return event.map(
|
||||
(a) => ClashTraffic(upload: a.uplink, download: a.downlink),
|
||||
);
|
||||
}).map(
|
||||
(event) => _mapToState(
|
||||
event.getOrElse((_) => const ClashTraffic(upload: 0, download: 0)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
yield* Stream.periodic(const Duration(seconds: 1)).asyncMap(
|
||||
(_) async {
|
||||
return const ClashTraffic(upload: 0, download: 0);
|
||||
},
|
||||
).map(_mapToState);
|
||||
}
|
||||
}
|
||||
|
||||
List<Traffic> _mapToState(ClashTraffic event) {
|
||||
final previous = state.valueOrNull ??
|
||||
List.generate(
|
||||
_steps,
|
||||
(index) => const Traffic(upload: 0, download: 0),
|
||||
);
|
||||
while (previous.length < _steps) {
|
||||
loggy.debug("previous short, adding");
|
||||
previous.insert(0, const Traffic(upload: 0, download: 0));
|
||||
}
|
||||
return [
|
||||
...previous.takeLast(_steps - 1),
|
||||
Traffic(upload: event.upload, download: event.download),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user