Refactor
This commit is contained in:
10
lib/features/stats/data/stats_data_providers.dart
Normal file
10
lib/features/stats/data/stats_data_providers.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:hiddify/features/stats/data/stats_repository.dart';
|
||||
import 'package:hiddify/singbox/service/singbox_service_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'stats_data_providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
StatsRepository statsRepository(StatsRepositoryRef ref) {
|
||||
return StatsRepositoryImpl(singbox: ref.watch(singboxServiceProvider));
|
||||
}
|
||||
33
lib/features/stats/data/stats_repository.dart
Normal file
33
lib/features/stats/data/stats_repository.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/core/utils/exception_handler.dart';
|
||||
import 'package:hiddify/features/stats/model/stats_entity.dart';
|
||||
import 'package:hiddify/features/stats/model/stats_failure.dart';
|
||||
import 'package:hiddify/singbox/service/singbox_service.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
|
||||
abstract interface class StatsRepository {
|
||||
Stream<Either<StatsFailure, StatsEntity>> watchStats();
|
||||
}
|
||||
|
||||
class StatsRepositoryImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements StatsRepository {
|
||||
StatsRepositoryImpl({required this.singbox});
|
||||
|
||||
final SingboxService singbox;
|
||||
|
||||
@override
|
||||
Stream<Either<StatsFailure, StatsEntity>> watchStats() {
|
||||
return singbox
|
||||
.watchStats()
|
||||
.map(
|
||||
(event) => StatsEntity(
|
||||
uplink: event.uplink,
|
||||
downlink: event.downlink,
|
||||
uplinkTotal: event.downlink,
|
||||
downlinkTotal: event.downlinkTotal,
|
||||
),
|
||||
)
|
||||
.handleExceptions(StatsUnexpectedFailure.new);
|
||||
}
|
||||
}
|
||||
22
lib/features/stats/model/stats_entity.dart
Normal file
22
lib/features/stats/model/stats_entity.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'stats_entity.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class StatsEntity with _$StatsEntity {
|
||||
const StatsEntity._();
|
||||
|
||||
const factory StatsEntity({
|
||||
required int uplink,
|
||||
required int downlink,
|
||||
required int uplinkTotal,
|
||||
required int downlinkTotal,
|
||||
}) = _StatsEntity;
|
||||
|
||||
factory StatsEntity.empty() => const StatsEntity(
|
||||
uplink: 0,
|
||||
downlink: 0,
|
||||
uplinkTotal: 0,
|
||||
downlinkTotal: 0,
|
||||
);
|
||||
}
|
||||
26
lib/features/stats/model/stats_failure.dart
Normal file
26
lib/features/stats/model/stats_failure.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
|
||||
part 'stats_failure.freezed.dart';
|
||||
|
||||
@freezed
|
||||
sealed class StatsFailure with _$StatsFailure, Failure {
|
||||
const StatsFailure._();
|
||||
|
||||
@With<UnexpectedFailure>()
|
||||
const factory StatsFailure.unexpected([
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
]) = StatsUnexpectedFailure;
|
||||
|
||||
@override
|
||||
({String type, String? message}) present(TranslationsEn t) {
|
||||
return switch (this) {
|
||||
StatsUnexpectedFailure() => (
|
||||
type: t.failure.unexpected,
|
||||
message: null,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
23
lib/features/stats/notifier/stats_notifier.dart
Normal file
23
lib/features/stats/notifier/stats_notifier.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
|
||||
import 'package:hiddify/features/stats/data/stats_data_providers.dart';
|
||||
import 'package:hiddify/features/stats/model/stats_entity.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'stats_notifier.g.dart';
|
||||
|
||||
@riverpod
|
||||
class StatsNotifier extends _$StatsNotifier with AppLogger {
|
||||
@override
|
||||
Stream<StatsEntity> build() async* {
|
||||
final serviceRunning = await ref.watch(serviceRunningProvider.future);
|
||||
if (serviceRunning) {
|
||||
yield* ref
|
||||
.watch(statsRepositoryProvider)
|
||||
.watchStats()
|
||||
.map((event) => event.getOrElse((_) => StatsEntity.empty()));
|
||||
} else {
|
||||
yield* Stream.value(StatsEntity.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
115
lib/features/stats/widget/side_bar_stats_overview.dart
Normal file
115
lib/features/stats/widget/side_bar_stats_overview.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/features/stats/model/stats_entity.dart';
|
||||
import 'package:hiddify/features/stats/notifier/stats_notifier.dart';
|
||||
import 'package:hiddify/utils/number_formatters.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class SideBarStatsOverview extends HookConsumerWidget {
|
||||
const SideBarStatsOverview({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final stats =
|
||||
ref.watch(statsNotifierProvider).asData?.value ?? StatsEntity.empty();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_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,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
_StatCard(
|
||||
title: t.home.stats.trafficTotal,
|
||||
firstStat: (
|
||||
label: "↑",
|
||||
data: stats.uplinkTotal.size(),
|
||||
semanticLabel: t.home.stats.uplink,
|
||||
),
|
||||
secondStat: (
|
||||
label: "↓",
|
||||
data: stats.downlinkTotal.size(),
|
||||
semanticLabel: t.home.stats.downlink,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user