This commit is contained in:
problematicconsumer
2023-12-01 12:56:24 +03:30
parent 9c165e178b
commit ed614988a2
181 changed files with 3092 additions and 2341 deletions

View 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));
}

View 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);
}
}

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

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

View 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());
}
}
}

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