feat: server conn statistics (#888)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-09-02 19:41:56 +08:00
committed by GitHub
parent 929061213f
commit 2466341999
39 changed files with 2534 additions and 19 deletions

View File

@@ -7,11 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
### Development
- `flutter run` - Run the app in development mode
- `flutter test` - Run all tests
- `dart run fl_build -p PLATFORM` - Build the app for specific platform (see fl_build package)
### Code Generation
- `dart run build_runner build --delete-conflicting-outputs` - Generate code for models with annotations (json_serializable, freezed, hive, riverpod)
- Every time you change model files, run this command to regenerate code (Hive adapters, Riverpod providers, etc.)
- Generated files include: `*.g.dart`, `*.freezed.dart` files
@@ -87,3 +83,13 @@ This is a Flutter application for managing Linux servers with the following key
- AGAIN, NEVER run code formatting commands.
- USE dependency injection via GetIt for services like Stores, Services and etc.
- Generate all l10n files using `flutter gen-l10n` command after modifying ARB files.
- USE `hive_ce` not `hive` package for Hive integration.
- Which no need to config `HiveField` and `HiveType` manually.
- USE widgets and utilities from `fl_lib` package for common functionalities.
- Such as `CustomAppBar`, `context.showRoundDialog`, `Input`, `Btnx.cancelOk`, etc.
- You can use context7 MCP to search `lppcg fl_lib KEYWORD` to find relevant widgets and utilities.
- USE `libL10n` and `l10n` for localization strings.
- `libL10n` is from `fl_lib` package, and `l10n` is from this project.
- Before adding new strings, check if it already exists in `libL10n`.
- Prioritize using strings from `libL10n` to avoid duplication, even if the meaning is not 100% exact, just use the substitution of `libL10n`.
- Split UI into Widget build, Actions, Utils. use `extension on` to achieve this

View File

@@ -0,0 +1,79 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_ce/hive.dart';
part 'connection_stat.freezed.dart';
part 'connection_stat.g.dart';
@freezed
@HiveType(typeId: 100)
abstract class ConnectionStat with _$ConnectionStat {
const factory ConnectionStat({
@HiveField(0) required String serverId,
@HiveField(1) required String serverName,
@HiveField(2) required DateTime timestamp,
@HiveField(3) required ConnectionResult result,
@HiveField(4) @Default('') String errorMessage,
@HiveField(5) required int durationMs,
}) = _ConnectionStat;
factory ConnectionStat.fromJson(Map<String, dynamic> json) =>
_$ConnectionStatFromJson(json);
}
@freezed
@HiveType(typeId: 101)
abstract class ServerConnectionStats with _$ServerConnectionStats {
const factory ServerConnectionStats({
@HiveField(0) required String serverId,
@HiveField(1) required String serverName,
@HiveField(2) required int totalAttempts,
@HiveField(3) required int successCount,
@HiveField(4) required int failureCount,
@HiveField(5) @Default(null) DateTime? lastSuccessTime,
@HiveField(6) @Default(null) DateTime? lastFailureTime,
@HiveField(7) @Default([]) List<ConnectionStat> recentConnections,
@HiveField(8) required double successRate,
}) = _ServerConnectionStats;
factory ServerConnectionStats.fromJson(Map<String, dynamic> json) =>
_$ServerConnectionStatsFromJson(json);
}
@HiveType(typeId: 102)
enum ConnectionResult {
@HiveField(0)
@JsonValue('success')
success,
@HiveField(1)
@JsonValue('timeout')
timeout,
@HiveField(2)
@JsonValue('auth_failed')
authFailed,
@HiveField(3)
@JsonValue('network_error')
networkError,
@HiveField(4)
@JsonValue('unknown_error')
unknownError,
}
extension ConnectionResultExtension on ConnectionResult {
String get displayName {
switch (this) {
case ConnectionResult.success:
return libL10n.success;
case ConnectionResult.timeout:
return '${libL10n.error}(timeout)';
case ConnectionResult.authFailed:
return '${libL10n.error}(auth)';
case ConnectionResult.networkError:
return '${libL10n.error}(${libL10n.network})';
case ConnectionResult.unknownError:
return '${libL10n.error}(${libL10n.unknown})';
}
}
bool get isSuccess => this == ConnectionResult.success;
}

View File

@@ -0,0 +1,585 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'connection_stat.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ConnectionStat {
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) DateTime get timestamp;@HiveField(3) ConnectionResult get result;@HiveField(4) String get errorMessage;@HiveField(5) int get durationMs;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ConnectionStatCopyWith<ConnectionStat> get copyWith => _$ConnectionStatCopyWithImpl<ConnectionStat>(this as ConnectionStat, _$identity);
/// Serializes this ConnectionStat to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
@override
String toString() {
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
}
}
/// @nodoc
abstract mixin class $ConnectionStatCopyWith<$Res> {
factory $ConnectionStatCopyWith(ConnectionStat value, $Res Function(ConnectionStat) _then) = _$ConnectionStatCopyWithImpl;
@useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
});
}
/// @nodoc
class _$ConnectionStatCopyWithImpl<$Res>
implements $ConnectionStatCopyWith<$Res> {
_$ConnectionStatCopyWithImpl(this._self, this._then);
final ConnectionStat _self;
final $Res Function(ConnectionStat) _then;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
return _then(_self.copyWith(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// Adds pattern-matching-related methods to [ConnectionStat].
extension ConnectionStatPatterns on ConnectionStat {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ConnectionStat value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ConnectionStat value) $default,){
final _that = this;
switch (_that) {
case _ConnectionStat():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ConnectionStat value)? $default,){
final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs) $default,) {final _that = this;
switch (_that) {
case _ConnectionStat():
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,) {final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ConnectionStat implements ConnectionStat {
const _ConnectionStat({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.timestamp, @HiveField(3) required this.result, @HiveField(4) this.errorMessage = '', @HiveField(5) required this.durationMs});
factory _ConnectionStat.fromJson(Map<String, dynamic> json) => _$ConnectionStatFromJson(json);
@override@HiveField(0) final String serverId;
@override@HiveField(1) final String serverName;
@override@HiveField(2) final DateTime timestamp;
@override@HiveField(3) final ConnectionResult result;
@override@JsonKey()@HiveField(4) final String errorMessage;
@override@HiveField(5) final int durationMs;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ConnectionStatCopyWith<_ConnectionStat> get copyWith => __$ConnectionStatCopyWithImpl<_ConnectionStat>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ConnectionStatToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
@override
String toString() {
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
}
}
/// @nodoc
abstract mixin class _$ConnectionStatCopyWith<$Res> implements $ConnectionStatCopyWith<$Res> {
factory _$ConnectionStatCopyWith(_ConnectionStat value, $Res Function(_ConnectionStat) _then) = __$ConnectionStatCopyWithImpl;
@override @useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
});
}
/// @nodoc
class __$ConnectionStatCopyWithImpl<$Res>
implements _$ConnectionStatCopyWith<$Res> {
__$ConnectionStatCopyWithImpl(this._self, this._then);
final _ConnectionStat _self;
final $Res Function(_ConnectionStat) _then;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
return _then(_ConnectionStat(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
mixin _$ServerConnectionStats {
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) int get totalAttempts;@HiveField(3) int get successCount;@HiveField(4) int get failureCount;@HiveField(5) DateTime? get lastSuccessTime;@HiveField(6) DateTime? get lastFailureTime;@HiveField(7) List<ConnectionStat> get recentConnections;@HiveField(8) double get successRate;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ServerConnectionStatsCopyWith<ServerConnectionStats> get copyWith => _$ServerConnectionStatsCopyWithImpl<ServerConnectionStats>(this as ServerConnectionStats, _$identity);
/// Serializes this ServerConnectionStats to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other.recentConnections, recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(recentConnections),successRate);
@override
String toString() {
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
}
}
/// @nodoc
abstract mixin class $ServerConnectionStatsCopyWith<$Res> {
factory $ServerConnectionStatsCopyWith(ServerConnectionStats value, $Res Function(ServerConnectionStats) _then) = _$ServerConnectionStatsCopyWithImpl;
@useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
});
}
/// @nodoc
class _$ServerConnectionStatsCopyWithImpl<$Res>
implements $ServerConnectionStatsCopyWith<$Res> {
_$ServerConnectionStatsCopyWithImpl(this._self, this._then);
final ServerConnectionStats _self;
final $Res Function(ServerConnectionStats) _then;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
return _then(_self.copyWith(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
as DateTime?,recentConnections: null == recentConnections ? _self.recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
/// Adds pattern-matching-related methods to [ServerConnectionStats].
extension ServerConnectionStatsPatterns on ServerConnectionStats {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerConnectionStats value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerConnectionStats value) $default,){
final _that = this;
switch (_that) {
case _ServerConnectionStats():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerConnectionStats value)? $default,){
final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate) $default,) {final _that = this;
switch (_that) {
case _ServerConnectionStats():
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,) {final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ServerConnectionStats implements ServerConnectionStats {
const _ServerConnectionStats({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.totalAttempts, @HiveField(3) required this.successCount, @HiveField(4) required this.failureCount, @HiveField(5) this.lastSuccessTime = null, @HiveField(6) this.lastFailureTime = null, @HiveField(7) final List<ConnectionStat> recentConnections = const [], @HiveField(8) required this.successRate}): _recentConnections = recentConnections;
factory _ServerConnectionStats.fromJson(Map<String, dynamic> json) => _$ServerConnectionStatsFromJson(json);
@override@HiveField(0) final String serverId;
@override@HiveField(1) final String serverName;
@override@HiveField(2) final int totalAttempts;
@override@HiveField(3) final int successCount;
@override@HiveField(4) final int failureCount;
@override@JsonKey()@HiveField(5) final DateTime? lastSuccessTime;
@override@JsonKey()@HiveField(6) final DateTime? lastFailureTime;
final List<ConnectionStat> _recentConnections;
@override@JsonKey()@HiveField(7) List<ConnectionStat> get recentConnections {
if (_recentConnections is EqualUnmodifiableListView) return _recentConnections;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_recentConnections);
}
@override@HiveField(8) final double successRate;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ServerConnectionStatsCopyWith<_ServerConnectionStats> get copyWith => __$ServerConnectionStatsCopyWithImpl<_ServerConnectionStats>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ServerConnectionStatsToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other._recentConnections, _recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(_recentConnections),successRate);
@override
String toString() {
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
}
}
/// @nodoc
abstract mixin class _$ServerConnectionStatsCopyWith<$Res> implements $ServerConnectionStatsCopyWith<$Res> {
factory _$ServerConnectionStatsCopyWith(_ServerConnectionStats value, $Res Function(_ServerConnectionStats) _then) = __$ServerConnectionStatsCopyWithImpl;
@override @useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
});
}
/// @nodoc
class __$ServerConnectionStatsCopyWithImpl<$Res>
implements _$ServerConnectionStatsCopyWith<$Res> {
__$ServerConnectionStatsCopyWithImpl(this._self, this._then);
final _ServerConnectionStats _self;
final $Res Function(_ServerConnectionStats) _then;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
return _then(_ServerConnectionStats(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
as DateTime?,recentConnections: null == recentConnections ? _self._recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
// dart format on

View File

@@ -0,0 +1,233 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'connection_stat.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ConnectionStatAdapter extends TypeAdapter<ConnectionStat> {
@override
final typeId = 100;
@override
ConnectionStat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ConnectionStat(
serverId: fields[0] as String,
serverName: fields[1] as String,
timestamp: fields[2] as DateTime,
result: fields[3] as ConnectionResult,
errorMessage: fields[4] == null ? '' : fields[4] as String,
durationMs: (fields[5] as num).toInt(),
);
}
@override
void write(BinaryWriter writer, ConnectionStat obj) {
writer
..writeByte(6)
..writeByte(0)
..write(obj.serverId)
..writeByte(1)
..write(obj.serverName)
..writeByte(2)
..write(obj.timestamp)
..writeByte(3)
..write(obj.result)
..writeByte(4)
..write(obj.errorMessage)
..writeByte(5)
..write(obj.durationMs);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConnectionStatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class ServerConnectionStatsAdapter extends TypeAdapter<ServerConnectionStats> {
@override
final typeId = 101;
@override
ServerConnectionStats read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ServerConnectionStats(
serverId: fields[0] as String,
serverName: fields[1] as String,
totalAttempts: (fields[2] as num).toInt(),
successCount: (fields[3] as num).toInt(),
failureCount: (fields[4] as num).toInt(),
lastSuccessTime: fields[5] == null ? null : fields[5] as DateTime?,
lastFailureTime: fields[6] == null ? null : fields[6] as DateTime?,
recentConnections: fields[7] == null
? []
: (fields[7] as List).cast<ConnectionStat>(),
successRate: (fields[8] as num).toDouble(),
);
}
@override
void write(BinaryWriter writer, ServerConnectionStats obj) {
writer
..writeByte(9)
..writeByte(0)
..write(obj.serverId)
..writeByte(1)
..write(obj.serverName)
..writeByte(2)
..write(obj.totalAttempts)
..writeByte(3)
..write(obj.successCount)
..writeByte(4)
..write(obj.failureCount)
..writeByte(5)
..write(obj.lastSuccessTime)
..writeByte(6)
..write(obj.lastFailureTime)
..writeByte(7)
..write(obj.recentConnections)
..writeByte(8)
..write(obj.successRate);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerConnectionStatsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class ConnectionResultAdapter extends TypeAdapter<ConnectionResult> {
@override
final typeId = 102;
@override
ConnectionResult read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return ConnectionResult.success;
case 1:
return ConnectionResult.timeout;
case 2:
return ConnectionResult.authFailed;
case 3:
return ConnectionResult.networkError;
case 4:
return ConnectionResult.unknownError;
default:
return ConnectionResult.success;
}
}
@override
void write(BinaryWriter writer, ConnectionResult obj) {
switch (obj) {
case ConnectionResult.success:
writer.writeByte(0);
case ConnectionResult.timeout:
writer.writeByte(1);
case ConnectionResult.authFailed:
writer.writeByte(2);
case ConnectionResult.networkError:
writer.writeByte(3);
case ConnectionResult.unknownError:
writer.writeByte(4);
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConnectionResultAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ConnectionStat _$ConnectionStatFromJson(Map<String, dynamic> json) =>
_ConnectionStat(
serverId: json['serverId'] as String,
serverName: json['serverName'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
result: $enumDecode(_$ConnectionResultEnumMap, json['result']),
errorMessage: json['errorMessage'] as String? ?? '',
durationMs: (json['durationMs'] as num).toInt(),
);
Map<String, dynamic> _$ConnectionStatToJson(_ConnectionStat instance) =>
<String, dynamic>{
'serverId': instance.serverId,
'serverName': instance.serverName,
'timestamp': instance.timestamp.toIso8601String(),
'result': _$ConnectionResultEnumMap[instance.result]!,
'errorMessage': instance.errorMessage,
'durationMs': instance.durationMs,
};
const _$ConnectionResultEnumMap = {
ConnectionResult.success: 'success',
ConnectionResult.timeout: 'timeout',
ConnectionResult.authFailed: 'auth_failed',
ConnectionResult.networkError: 'network_error',
ConnectionResult.unknownError: 'unknown_error',
};
_ServerConnectionStats _$ServerConnectionStatsFromJson(
Map<String, dynamic> json,
) => _ServerConnectionStats(
serverId: json['serverId'] as String,
serverName: json['serverName'] as String,
totalAttempts: (json['totalAttempts'] as num).toInt(),
successCount: (json['successCount'] as num).toInt(),
failureCount: (json['failureCount'] as num).toInt(),
lastSuccessTime: json['lastSuccessTime'] == null
? null
: DateTime.parse(json['lastSuccessTime'] as String),
lastFailureTime: json['lastFailureTime'] == null
? null
: DateTime.parse(json['lastFailureTime'] as String),
recentConnections:
(json['recentConnections'] as List<dynamic>?)
?.map((e) => ConnectionStat.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
successRate: (json['successRate'] as num).toDouble(),
);
Map<String, dynamic> _$ServerConnectionStatsToJson(
_ServerConnectionStats instance,
) => <String, dynamic>{
'serverId': instance.serverId,
'serverName': instance.serverName,
'totalAttempts': instance.totalAttempts,
'successCount': instance.successCount,
'failureCount': instance.failureCount,
'lastSuccessTime': instance.lastSuccessTime?.toIso8601String(),
'lastFailureTime': instance.lastFailureTime?.toIso8601String(),
'recentConnections': instance.recentConnections,
'successRate': instance.successRate,
};

View File

@@ -6,7 +6,7 @@ part of 'container.dart';
// RiverpodGenerator
// **************************************************************************
String _$containerNotifierHash() => r'db8f8a6b6071b7b33fbf79128dfed408a5b9fdad';
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -14,6 +14,7 @@ import 'package:server_box/data/helper/system_detector.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_status_update_req.dart';
@@ -145,6 +146,15 @@ class ServerNotifier extends _$ServerNotifier {
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
}
// Record successful connection
Stores.connectionStats.recordConnection(ConnectionStat(
serverId: spi.id,
serverName: spi.name,
timestamp: time1,
result: ConnectionResult.success,
durationMs: spentTime,
));
final sessionId = 'ssh_${spi.id}';
TermSessionManager.add(
id: sessionId,
@@ -156,6 +166,29 @@ class ServerNotifier extends _$ServerNotifier {
TermSessionManager.setActive(sessionId, hasTerminal: false);
} catch (e) {
TryLimiter.inc(sid);
// Determine connection failure type
ConnectionResult failureResult;
if (e.toString().contains('timeout') || e.toString().contains('Timeout')) {
failureResult = ConnectionResult.timeout;
} else if (e.toString().contains('auth') || e.toString().contains('Authentication')) {
failureResult = ConnectionResult.authFailed;
} else if (e.toString().contains('network') || e.toString().contains('Network')) {
failureResult = ConnectionResult.networkError;
} else {
failureResult = ConnectionResult.unknownError;
}
// Record failed connection
Stores.connectionStats.recordConnection(ConnectionStat(
serverId: spi.id,
serverName: spi.name,
timestamp: DateTime.now(),
result: failureResult,
errorMessage: e.toString(),
durationMs: 0,
));
final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString());
updateStatus(newStatus);
updateConnection(ServerConn.failed);

View File

@@ -6,7 +6,7 @@ part of 'single.dart';
// RiverpodGenerator
// **************************************************************************
String _$serverNotifierHash() => r'5625b0a4762c28efdbc124809c03b84a51d213b1';
String _$serverNotifierHash() => r'524647748cc3810c17e5c1cd29e360f3936f5014';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -1,5 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:get_it/get_it.dart';
import 'package:server_box/data/store/connection_stats.dart';
import 'package:server_box/data/store/container.dart';
import 'package:server_box/data/store/history.dart';
import 'package:server_box/data/store/private_key.dart';
@@ -16,6 +17,7 @@ abstract final class Stores {
static PrivateKeyStore get key => getIt<PrivateKeyStore>();
static SnippetStore get snippet => getIt<SnippetStore>();
static HistoryStore get history => getIt<HistoryStore>();
static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();
/// All stores that need backup
static List<HiveStore> get _allBackup => [
@@ -25,6 +27,7 @@ abstract final class Stores {
key,
snippet,
history,
connectionStats,
];
static Future<void> init() async {
@@ -34,6 +37,7 @@ abstract final class Stores {
getIt.registerLazySingleton<PrivateKeyStore>(() => PrivateKeyStore.instance);
getIt.registerLazySingleton<SnippetStore>(() => SnippetStore.instance);
getIt.registerLazySingleton<HistoryStore>(() => HistoryStore.instance);
getIt.registerLazySingleton<ConnectionStatsStore>(() => ConnectionStatsStore.instance);
await Future.wait(_allBackup.map((store) => store.init()));
}

View File

@@ -0,0 +1,190 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
class ConnectionStatsStore extends HiveStore {
ConnectionStatsStore._() : super('connection_stats');
static final instance = ConnectionStatsStore._();
// Record a connection attempt
void recordConnection(ConnectionStat stat) {
final key = '${stat.serverId}_${ShortId.generate()}';
set(key, stat);
_cleanOldRecords(stat.serverId);
}
// Clean records older than 30 days for a specific server
void _cleanOldRecords(String serverId) {
final cutoffTime = DateTime.now().subtract(const Duration(days: 30));
final allKeys = keys().toList();
final keysToDelete = <String>[];
for (final key in allKeys) {
if (key.startsWith(serverId)) {
final parts = key.split('_');
if (parts.length >= 2) {
final timestamp = int.tryParse(parts.last);
if (timestamp != null) {
final recordTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
if (recordTime.isBefore(cutoffTime)) {
keysToDelete.add(key);
}
}
}
}
}
for (final key in keysToDelete) {
remove(key);
}
}
// Get connection stats for a specific server
ServerConnectionStats getServerStats(String serverId, String serverName) {
final allStats = getConnectionHistory(serverId);
if (allStats.isEmpty) {
return ServerConnectionStats(
serverId: serverId,
serverName: serverName,
totalAttempts: 0,
successCount: 0,
failureCount: 0,
recentConnections: [],
successRate: 0.0,
);
}
final totalAttempts = allStats.length;
final successCount = allStats.where((s) => s.result.isSuccess).length;
final failureCount = totalAttempts - successCount;
final successRate = totalAttempts > 0 ? (successCount / totalAttempts) : 0.0;
final successTimes = allStats
.where((s) => s.result.isSuccess)
.map((s) => s.timestamp)
.toList();
final failureTimes = allStats
.where((s) => !s.result.isSuccess)
.map((s) => s.timestamp)
.toList();
DateTime? lastSuccessTime;
DateTime? lastFailureTime;
if (successTimes.isNotEmpty) {
successTimes.sort((a, b) => b.compareTo(a));
lastSuccessTime = successTimes.first;
}
if (failureTimes.isNotEmpty) {
failureTimes.sort((a, b) => b.compareTo(a));
lastFailureTime = failureTimes.first;
}
// Get recent connections (last 20)
final recentConnections = allStats.take(20).toList();
return ServerConnectionStats(
serverId: serverId,
serverName: serverName,
totalAttempts: totalAttempts,
successCount: successCount,
failureCount: failureCount,
lastSuccessTime: lastSuccessTime,
lastFailureTime: lastFailureTime,
recentConnections: recentConnections,
successRate: successRate,
);
}
// Get connection history for a specific server
List<ConnectionStat> getConnectionHistory(String serverId) {
final allKeys = keys().where((key) => key.startsWith(serverId)).toList();
final stats = <ConnectionStat>[];
for (final key in allKeys) {
final stat = get<ConnectionStat>(
key,
fromObj: (val) {
if (val is ConnectionStat) return val;
if (val is Map<dynamic, dynamic>) {
final map = val.toStrDynMap;
if (map == null) return null;
try {
return ConnectionStat.fromJson(map as Map<String, dynamic>);
} catch (e) {
dprint('Parsing ConnectionStat from JSON', e);
}
}
return null;
},
);
if (stat != null) {
stats.add(stat);
}
}
// Sort by timestamp, newest first
stats.sort((a, b) => b.timestamp.compareTo(a.timestamp));
return stats;
}
// Get all servers' stats
List<ServerConnectionStats> getAllServerStats() {
final serverIds = <String>{};
final serverNames = <String, String>{};
// Get all unique server IDs
for (final key in keys()) {
final parts = key.split('_');
if (parts.length >= 2) {
final serverId = parts[0];
serverIds.add(serverId);
// Try to get server name from the stored stat
final stat = get<ConnectionStat>(
key,
fromObj: (val) {
if (val is ConnectionStat) return val;
if (val is Map<dynamic, dynamic>) {
final map = val.toStrDynMap;
if (map == null) return null;
try {
return ConnectionStat.fromJson(map as Map<String, dynamic>);
} catch (e) {
dprint('Parsing ConnectionStat from JSON', e);
}
}
return null;
},
);
if (stat != null) {
serverNames[serverId] = stat.serverName;
}
}
}
final allStats = <ServerConnectionStats>[];
for (final serverId in serverIds) {
final serverName = serverNames[serverId] ?? serverId;
final stats = getServerStats(serverId, serverName);
allStats.add(stats);
}
return allStats;
}
// Clear all connection stats
void clearAll() {
box.clear();
}
// Clear stats for a specific server
void clearServerStats(String serverId) {
final keysToDelete = keys().where((key) => key.startsWith(serverId)).toList();
for (final key in keysToDelete) {
remove(key);
}
}
}

View File

@@ -1621,6 +1621,84 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
String get writeScriptTip;
/// No description provided for @connectionStats.
///
/// In en, this message translates to:
/// **'Connection Statistics'**
String get connectionStats;
/// No description provided for @noConnectionStatsData.
///
/// In en, this message translates to:
/// **'No connection statistics data'**
String get noConnectionStatsData;
/// No description provided for @totalAttempts.
///
/// In en, this message translates to:
/// **'Total'**
String get totalAttempts;
/// No description provided for @lastSuccess.
///
/// In en, this message translates to:
/// **'Last Success'**
String get lastSuccess;
/// No description provided for @lastFailure.
///
/// In en, this message translates to:
/// **'Last Failure'**
String get lastFailure;
/// No description provided for @recentConnections.
///
/// In en, this message translates to:
/// **'Recent Connections'**
String get recentConnections;
/// No description provided for @viewDetails.
///
/// In en, this message translates to:
/// **'View Details'**
String get viewDetails;
/// No description provided for @connectionDetails.
///
/// In en, this message translates to:
/// **'Connection Details'**
String get connectionDetails;
/// No description provided for @clearThisServerStats.
///
/// In en, this message translates to:
/// **'Clear This Server Statistics'**
String get clearThisServerStats;
/// No description provided for @clearAllStatsTitle.
///
/// In en, this message translates to:
/// **'Clear All Statistics'**
String get clearAllStatsTitle;
/// No description provided for @clearAllStatsContent.
///
/// In en, this message translates to:
/// **'Are you sure you want to clear all server connection statistics? This action cannot be undone.'**
String get clearAllStatsContent;
/// No description provided for @clearServerStatsTitle.
///
/// In en, this message translates to:
/// **'Clear {serverName} Statistics'**
String clearServerStatsTitle(String serverName);
/// No description provided for @clearServerStatsContent.
///
/// In en, this message translates to:
/// **'Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.'**
String clearServerStatsContent(String serverName);
}
class _AppLocalizationsDelegate

View File

@@ -851,4 +851,48 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get writeScriptTip =>
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
@override
String get connectionStats => 'Verbindungsstatistiken';
@override
String get noConnectionStatsData => 'Keine Verbindungsstatistikdaten';
@override
String get totalAttempts => 'Gesamt';
@override
String get lastSuccess => 'Letzter Erfolg';
@override
String get lastFailure => 'Letzter Fehler';
@override
String get recentConnections => 'Kürzliche Verbindungen';
@override
String get viewDetails => 'Details anzeigen';
@override
String get connectionDetails => 'Verbindungsdetails';
@override
String get clearThisServerStats => 'Statistiken dieses Servers löschen';
@override
String get clearAllStatsTitle => 'Alle Statistiken löschen';
@override
String get clearAllStatsContent =>
'Sind Sie sicher, dass Sie alle Server-Verbindungsstatistiken löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';
@override
String clearServerStatsTitle(String serverName) {
return '$serverName Statistiken löschen';
}
@override
String clearServerStatsContent(String serverName) {
return 'Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \"$serverName\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';
}
}

View File

@@ -843,4 +843,48 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get writeScriptTip =>
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
@override
String get connectionStats => 'Connection Statistics';
@override
String get noConnectionStatsData => 'No connection statistics data';
@override
String get totalAttempts => 'Total';
@override
String get lastSuccess => 'Last Success';
@override
String get lastFailure => 'Last Failure';
@override
String get recentConnections => 'Recent Connections';
@override
String get viewDetails => 'View Details';
@override
String get connectionDetails => 'Connection Details';
@override
String get clearThisServerStats => 'Clear This Server Statistics';
@override
String get clearAllStatsTitle => 'Clear All Statistics';
@override
String get clearAllStatsContent =>
'Are you sure you want to clear all server connection statistics? This action cannot be undone.';
@override
String clearServerStatsTitle(String serverName) {
return 'Clear $serverName Statistics';
}
@override
String clearServerStatsContent(String serverName) {
return 'Are you sure you want to clear connection statistics for server \"$serverName\"? This action cannot be undone.';
}
}

View File

@@ -852,4 +852,49 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get writeScriptTip =>
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
@override
String get connectionStats => 'Estadísticas de conexión';
@override
String get noConnectionStatsData =>
'No hay datos de estadísticas de conexión';
@override
String get totalAttempts => 'Total';
@override
String get lastSuccess => 'Último éxito';
@override
String get lastFailure => 'Último fallo';
@override
String get recentConnections => 'Conexiones recientes';
@override
String get viewDetails => 'Ver detalles';
@override
String get connectionDetails => 'Detalles de conexión';
@override
String get clearThisServerStats => 'Limpiar estadísticas de este servidor';
@override
String get clearAllStatsTitle => 'Limpiar todas las estadísticas';
@override
String get clearAllStatsContent =>
'¿Estás seguro de que quieres limpiar todas las estadísticas de conexión del servidor? Esta acción no se puede deshacer.';
@override
String clearServerStatsTitle(String serverName) {
return 'Limpiar estadísticas de $serverName';
}
@override
String clearServerStatsContent(String serverName) {
return '¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \"$serverName\"? Esta acción no se puede deshacer.';
}
}

View File

@@ -855,4 +855,49 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get writeScriptTip =>
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.';
@override
String get connectionStats => 'Statistiques de connexion';
@override
String get noConnectionStatsData =>
'Aucune donnée de statistiques de connexion';
@override
String get totalAttempts => 'Total';
@override
String get lastSuccess => 'Dernier succès';
@override
String get lastFailure => 'Dernier échec';
@override
String get recentConnections => 'Connexions récentes';
@override
String get viewDetails => 'Voir les détails';
@override
String get connectionDetails => 'Détails de connexion';
@override
String get clearThisServerStats => 'Effacer les statistiques de ce serveur';
@override
String get clearAllStatsTitle => 'Effacer toutes les statistiques';
@override
String get clearAllStatsContent =>
'Êtes-vous sûr de vouloir effacer toutes les statistiques de connexion des serveurs ? Cette action ne peut pas être annulée.';
@override
String clearServerStatsTitle(String serverName) {
return 'Effacer les statistiques de $serverName';
}
@override
String clearServerStatsContent(String serverName) {
return 'Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \"$serverName\" ? Cette action ne peut pas être annulée.';
}
}

View File

@@ -843,4 +843,48 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get writeScriptTip =>
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
@override
String get connectionStats => 'Statistik Koneksi';
@override
String get noConnectionStatsData => 'Tidak ada data statistik koneksi';
@override
String get totalAttempts => 'Total';
@override
String get lastSuccess => 'Sukses Terakhir';
@override
String get lastFailure => 'Gagal Terakhir';
@override
String get recentConnections => 'Koneksi Terkini';
@override
String get viewDetails => 'Lihat Detail';
@override
String get connectionDetails => 'Detail Koneksi';
@override
String get clearThisServerStats => 'Hapus Statistik Server Ini';
@override
String get clearAllStatsTitle => 'Hapus Semua Statistik';
@override
String get clearAllStatsContent =>
'Apakah Anda yakin ingin menghapus semua statistik koneksi server? Tindakan ini tidak dapat dibatalkan.';
@override
String clearServerStatsTitle(String serverName) {
return 'Hapus Statistik $serverName';
}
@override
String clearServerStatsContent(String serverName) {
return 'Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"$serverName\"? Tindakan ini tidak dapat dibatalkan.';
}
}

View File

@@ -818,4 +818,47 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get writeScriptTip =>
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
@override
String get connectionStats => '接続統計';
@override
String get noConnectionStatsData => '接続統計データがありません';
@override
String get totalAttempts => '総計';
@override
String get lastSuccess => '最後の成功';
@override
String get lastFailure => '最後の失敗';
@override
String get recentConnections => '最近の接続';
@override
String get viewDetails => '詳細を表示';
@override
String get connectionDetails => '接続の詳細';
@override
String get clearThisServerStats => 'このサーバーの統計をクリア';
@override
String get clearAllStatsTitle => 'すべての統計をクリア';
@override
String get clearAllStatsContent => 'すべてのサーバー接続統計を削除してもよろしいですか?この操作は元に戻せません。';
@override
String clearServerStatsTitle(String serverName) {
return '$serverNameの統計をクリア';
}
@override
String clearServerStatsContent(String serverName) {
return 'サーバー\"$serverName\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。';
}
}

View File

@@ -849,4 +849,48 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get writeScriptTip =>
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
@override
String get connectionStats => 'Verbindingsstatistieken';
@override
String get noConnectionStatsData => 'Geen verbindingsstatistiekgegevens';
@override
String get totalAttempts => 'Totaal';
@override
String get lastSuccess => 'Laatst succesvol';
@override
String get lastFailure => 'Laatst gefaald';
@override
String get recentConnections => 'Recente verbindingen';
@override
String get viewDetails => 'Details bekijken';
@override
String get connectionDetails => 'Verbindingsdetails';
@override
String get clearThisServerStats => 'Statistieken van deze server wissen';
@override
String get clearAllStatsTitle => 'Alle statistieken wissen';
@override
String get clearAllStatsContent =>
'Weet u zeker dat u alle serververbindingsstatistieken wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';
@override
String clearServerStatsTitle(String serverName) {
return 'Statistieken van $serverName wissen';
}
@override
String clearServerStatsContent(String serverName) {
return 'Weet u zeker dat u de verbindingsstatistieken voor server \"$serverName\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';
}
}

View File

@@ -846,4 +846,48 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get writeScriptTip =>
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
@override
String get connectionStats => 'Estatísticas de conexão';
@override
String get noConnectionStatsData => 'Não há dados de estatísticas de conexão';
@override
String get totalAttempts => 'Total';
@override
String get lastSuccess => 'Último sucesso';
@override
String get lastFailure => 'Última falha';
@override
String get recentConnections => 'Conexões recentes';
@override
String get viewDetails => 'Ver detalhes';
@override
String get connectionDetails => 'Detalhes da conexão';
@override
String get clearThisServerStats => 'Limpar estatísticas deste servidor';
@override
String get clearAllStatsTitle => 'Limpar todas as estatísticas';
@override
String get clearAllStatsContent =>
'Tem certeza de que deseja limpar todas as estatísticas de conexão do servidor? Esta ação não pode ser desfeita.';
@override
String clearServerStatsTitle(String serverName) {
return 'Limpar estatísticas de $serverName';
}
@override
String clearServerStatsContent(String serverName) {
return 'Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \"$serverName\"? Esta ação não pode ser desfeita.';
}
}

View File

@@ -848,4 +848,48 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get writeScriptTip =>
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
@override
String get connectionStats => 'Статистика соединений';
@override
String get noConnectionStatsData => 'Нет данных статистики соединений';
@override
String get totalAttempts => 'Общее';
@override
String get lastSuccess => 'Последний успех';
@override
String get lastFailure => 'Последний сбой';
@override
String get recentConnections => 'Недавние соединения';
@override
String get viewDetails => 'Просмотр деталей';
@override
String get connectionDetails => 'Детали соединения';
@override
String get clearThisServerStats => 'Очистить статистику этого сервера';
@override
String get clearAllStatsTitle => 'Очистить всю статистику';
@override
String get clearAllStatsContent =>
'Вы уверены, что хотите очистить всю статистику соединений сервера? Это действие не может быть отменено.';
@override
String clearServerStatsTitle(String serverName) {
return 'Очистить статистику $serverName';
}
@override
String clearServerStatsContent(String serverName) {
return 'Вы уверены, что хотите очистить статистику соединений для сервера \"$serverName\"? Это действие не может быть отменено.';
}
}

View File

@@ -843,4 +843,48 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get writeScriptTip =>
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
@override
String get connectionStats => 'Bağlantı İstatistikleri';
@override
String get noConnectionStatsData => 'Bağlantı istatistik verisi yok';
@override
String get totalAttempts => 'Toplam';
@override
String get lastSuccess => 'Son Başarı';
@override
String get lastFailure => 'Son Başarısızlık';
@override
String get recentConnections => 'Son Bağlantılar';
@override
String get viewDetails => 'Detayları Görüntüle';
@override
String get connectionDetails => 'Bağlantı Detayları';
@override
String get clearThisServerStats => 'Bu Sunucu İstatistiklerini Temizle';
@override
String get clearAllStatsTitle => 'Tüm İstatistikleri Temizle';
@override
String get clearAllStatsContent =>
'Tüm sunucu bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';
@override
String clearServerStatsTitle(String serverName) {
return '$serverName İstatistiklerini Temizle';
}
@override
String clearServerStatsContent(String serverName) {
return '\"$serverName\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';
}
}

View File

@@ -849,4 +849,48 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get writeScriptTip =>
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
@override
String get connectionStats => 'Статистика з\'єднань';
@override
String get noConnectionStatsData => 'Немає даних статистики з\'єднань';
@override
String get totalAttempts => 'Загальна кількість';
@override
String get lastSuccess => 'Останній успіх';
@override
String get lastFailure => 'Остання помилка';
@override
String get recentConnections => 'Останні з\'єднання';
@override
String get viewDetails => 'Переглянути деталі';
@override
String get connectionDetails => 'Деталі з\'єднання';
@override
String get clearThisServerStats => 'Очистити статистику цього сервера';
@override
String get clearAllStatsTitle => 'Очистити всю статистику';
@override
String get clearAllStatsContent =>
'Ви впевнені, що хочете очистити всю статистику з\'єднань сервера? Цю дію не можна скасувати.';
@override
String clearServerStatsTitle(String serverName) {
return 'Очистити статистику $serverName';
}
@override
String clearServerStatsContent(String serverName) {
return 'Ви впевнені, що хочете очистити статистику з\'єднань для сервера \"$serverName\"? Цю дію не можна скасувати.';
}
}

View File

@@ -803,6 +803,49 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get writeScriptTip =>
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
@override
String get connectionStats => '连接统计';
@override
String get noConnectionStatsData => '暂无连接统计数据';
@override
String get totalAttempts => '总次数';
@override
String get lastSuccess => '最后成功';
@override
String get lastFailure => '最后失败';
@override
String get recentConnections => '最近连接记录';
@override
String get viewDetails => '查看详情';
@override
String get connectionDetails => '连接详情';
@override
String get clearThisServerStats => '清空此服务器统计';
@override
String get clearAllStatsTitle => '清空所有统计';
@override
String get clearAllStatsContent => '确定要清空所有服务器的连接统计数据吗?此操作无法撤销。';
@override
String clearServerStatsTitle(String serverName) {
return '清空 $serverName 统计';
}
@override
String clearServerStatsContent(String serverName) {
return '确定要清空服务器 \"$serverName\" 的连接统计数据吗?此操作无法撤销。';
}
}
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
@@ -1604,4 +1647,47 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get writeScriptTip =>
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
@override
String get connectionStats => '連線統計';
@override
String get noConnectionStatsData => '暫無連線統計資料';
@override
String get totalAttempts => '總次數';
@override
String get lastSuccess => '最後成功';
@override
String get lastFailure => '最後失敗';
@override
String get recentConnections => '最近連線記錄';
@override
String get viewDetails => '檢視詳情';
@override
String get connectionDetails => '連線詳情';
@override
String get clearThisServerStats => '清空此伺服器統計';
@override
String get clearAllStatsTitle => '清空所有統計';
@override
String get clearAllStatsContent => '確定要清空所有伺服器的連線統計資料嗎?此操作無法撤銷。';
@override
String clearServerStatsTitle(String serverName) {
return '清空 $serverName 統計';
}
@override
String clearServerStatsContent(String serverName) {
return '確定要清空伺服器 \"$serverName\" 的連線統計資料嗎?此操作無法撤銷。';
}
}

View File

@@ -3,12 +3,16 @@
// Check in to version control
import 'package:hive_ce/hive.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
import 'package:server_box/hive/hive_adapters.dart';
extension HiveRegistrar on HiveInterface {
void registerAdapters() {
registerAdapter(ConnectionResultAdapter());
registerAdapter(ConnectionStatAdapter());
registerAdapter(NetViewTypeAdapter());
registerAdapter(PrivateKeyInfoAdapter());
registerAdapter(ServerConnectionStatsAdapter());
registerAdapter(ServerCustomAdapter());
registerAdapter(ServerFuncBtnAdapter());
registerAdapter(SnippetAdapter());
@@ -21,8 +25,11 @@ extension HiveRegistrar on HiveInterface {
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
void registerAdapters() {
registerAdapter(ConnectionResultAdapter());
registerAdapter(ConnectionStatAdapter());
registerAdapter(NetViewTypeAdapter());
registerAdapter(PrivateKeyInfoAdapter());
registerAdapter(ServerConnectionStatsAdapter());
registerAdapter(ServerCustomAdapter());
registerAdapter(ServerFuncBtnAdapter());
registerAdapter(SnippetAdapter());

View File

@@ -249,5 +249,32 @@
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
"write": "Schreiben",
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen."
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.",
"connectionStats": "Verbindungsstatistiken",
"noConnectionStatsData": "Keine Verbindungsstatistikdaten",
"totalAttempts": "Gesamt",
"lastSuccess": "Letzter Erfolg",
"lastFailure": "Letzter Fehler",
"recentConnections": "Kürzliche Verbindungen",
"viewDetails": "Details anzeigen",
"connectionDetails": "Verbindungsdetails",
"clearThisServerStats": "Statistiken dieses Servers löschen",
"clearAllStatsTitle": "Alle Statistiken löschen",
"clearAllStatsContent": "Sind Sie sicher, dass Sie alle Server-Verbindungsstatistiken löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"clearServerStatsTitle": "{serverName} Statistiken löschen",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \"{serverName}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
"write": "Write",
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content."
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.",
"connectionStats": "Connection Statistics",
"noConnectionStatsData": "No connection statistics data",
"totalAttempts": "Total",
"lastSuccess": "Last Success",
"lastFailure": "Last Failure",
"recentConnections": "Recent Connections",
"viewDetails": "View Details",
"connectionDetails": "Connection Details",
"clearThisServerStats": "Clear This Server Statistics",
"clearAllStatsTitle": "Clear All Statistics",
"clearAllStatsContent": "Are you sure you want to clear all server connection statistics? This action cannot be undone.",
"clearServerStatsTitle": "Clear {serverName} Statistics",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
"write": "Escribir",
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script."
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.",
"connectionStats": "Estadísticas de conexión",
"noConnectionStatsData": "No hay datos de estadísticas de conexión",
"totalAttempts": "Total",
"lastSuccess": "Último éxito",
"lastFailure": "Último fallo",
"recentConnections": "Conexiones recientes",
"viewDetails": "Ver detalles",
"connectionDetails": "Detalles de conexión",
"clearThisServerStats": "Limpiar estadísticas de este servidor",
"clearAllStatsTitle": "Limpiar todas las estadísticas",
"clearAllStatsContent": "¿Estás seguro de que quieres limpiar todas las estadísticas de conexión del servidor? Esta acción no se puede deshacer.",
"clearServerStatsTitle": "Limpiar estadísticas de {serverName}",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \"{serverName}\"? Esta acción no se puede deshacer.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
"write": "Écrire",
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script."
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script.",
"connectionStats": "Statistiques de connexion",
"noConnectionStatsData": "Aucune donnée de statistiques de connexion",
"totalAttempts": "Total",
"lastSuccess": "Dernier succès",
"lastFailure": "Dernier échec",
"recentConnections": "Connexions récentes",
"viewDetails": "Voir les détails",
"connectionDetails": "Détails de connexion",
"clearThisServerStats": "Effacer les statistiques de ce serveur",
"clearAllStatsTitle": "Effacer toutes les statistiques",
"clearAllStatsContent": "Êtes-vous sûr de vouloir effacer toutes les statistiques de connexion des serveurs ? Cette action ne peut pas être annulée.",
"clearServerStatsTitle": "Effacer les statistiques de {serverName}",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \"{serverName}\" ? Cette action ne peut pas être annulée.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
"write": "Tulis",
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut."
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.",
"connectionStats": "Statistik Koneksi",
"noConnectionStatsData": "Tidak ada data statistik koneksi",
"totalAttempts": "Total",
"lastSuccess": "Sukses Terakhir",
"lastFailure": "Gagal Terakhir",
"recentConnections": "Koneksi Terkini",
"viewDetails": "Lihat Detail",
"connectionDetails": "Detail Koneksi",
"clearThisServerStats": "Hapus Statistik Server Ini",
"clearAllStatsTitle": "Hapus Semua Statistik",
"clearAllStatsContent": "Apakah Anda yakin ingin menghapus semua statistik koneksi server? Tindakan ini tidak dapat dibatalkan.",
"clearServerStatsTitle": "Hapus Statistik {serverName}",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"{serverName}\"? Tindakan ini tidak dapat dibatalkan.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "WOLWake-on-LANを設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
"write": "書き込み",
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。"
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。",
"connectionStats": "接続統計",
"noConnectionStatsData": "接続統計データがありません",
"totalAttempts": "総計",
"lastSuccess": "最後の成功",
"lastFailure": "最後の失敗",
"recentConnections": "最近の接続",
"viewDetails": "詳細を表示",
"connectionDetails": "接続の詳細",
"clearThisServerStats": "このサーバーの統計をクリア",
"clearAllStatsTitle": "すべての統計をクリア",
"clearAllStatsContent": "すべてのサーバー接続統計を削除してもよろしいですか?この操作は元に戻せません。",
"clearServerStatsTitle": "{serverName}の統計をクリア",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "サーバー\"{serverName}\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
"write": "Schrijven",
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren."
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.",
"connectionStats": "Verbindingsstatistieken",
"noConnectionStatsData": "Geen verbindingsstatistiekgegevens",
"totalAttempts": "Totaal",
"lastSuccess": "Laatst succesvol",
"lastFailure": "Laatst gefaald",
"recentConnections": "Recente verbindingen",
"viewDetails": "Details bekijken",
"connectionDetails": "Verbindingsdetails",
"clearThisServerStats": "Statistieken van deze server wissen",
"clearAllStatsTitle": "Alle statistieken wissen",
"clearAllStatsContent": "Weet u zeker dat u alle serververbindingsstatistieken wilt wissen? Deze actie kan niet ongedaan worden gemaakt.",
"clearServerStatsTitle": "Statistieken van {serverName} wissen",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "Weet u zeker dat u de verbindingsstatistieken voor server \"{serverName}\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
"write": "Escrita",
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script."
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.",
"connectionStats": "Estatísticas de conexão",
"noConnectionStatsData": "Não há dados de estatísticas de conexão",
"totalAttempts": "Total",
"lastSuccess": "Último sucesso",
"lastFailure": "Última falha",
"recentConnections": "Conexões recentes",
"viewDetails": "Ver detalhes",
"connectionDetails": "Detalhes da conexão",
"clearThisServerStats": "Limpar estatísticas deste servidor",
"clearAllStatsTitle": "Limpar todas as estatísticas",
"clearAllStatsContent": "Tem certeza de que deseja limpar todas as estatísticas de conexão do servidor? Esta ação não pode ser desfeita.",
"clearServerStatsTitle": "Limpar estatísticas de {serverName}",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \"{serverName}\"? Esta ação não pode ser desfeita.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
"write": "Запись",
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.",
"connectionStats": "Статистика соединений",
"noConnectionStatsData": "Нет данных статистики соединений",
"totalAttempts": "Общее",
"lastSuccess": "Последний успех",
"lastFailure": "Последний сбой",
"recentConnections": "Недавние соединения",
"viewDetails": "Просмотр деталей",
"connectionDetails": "Детали соединения",
"clearThisServerStats": "Очистить статистику этого сервера",
"clearAllStatsTitle": "Очистить всю статистику",
"clearAllStatsContent": "Вы уверены, что хотите очистить всю статистику соединений сервера? Это действие не может быть отменено.",
"clearServerStatsTitle": "Очистить статистику {serverName}",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "Вы уверены, что хотите очистить статистику соединений для сервера \"{serverName}\"? Это действие не может быть отменено.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
"write": "Yaz",
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz."
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.",
"connectionStats": "Bağlantı İstatistikleri",
"noConnectionStatsData": "Bağlantı istatistik verisi yok",
"totalAttempts": "Toplam",
"lastSuccess": "Son Başarı",
"lastFailure": "Son Başarısızlık",
"recentConnections": "Son Bağlantılar",
"viewDetails": "Detayları Görüntüle",
"connectionDetails": "Bağlantı Detayları",
"clearThisServerStats": "Bu Sunucu İstatistiklerini Temizle",
"clearAllStatsTitle": "Tüm İstatistikleri Temizle",
"clearAllStatsContent": "Tüm sunucu bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"clearServerStatsTitle": "{serverName} İstatistiklerini Temizle",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "\"{serverName}\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
"write": "Записати",
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта."
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.",
"connectionStats": "Статистика з'єднань",
"noConnectionStatsData": "Немає даних статистики з'єднань",
"totalAttempts": "Загальна кількість",
"lastSuccess": "Останній успіх",
"lastFailure": "Остання помилка",
"recentConnections": "Останні з'єднання",
"viewDetails": "Переглянути деталі",
"connectionDetails": "Деталі з'єднання",
"clearThisServerStats": "Очистити статистику цього сервера",
"clearAllStatsTitle": "Очистити всю статистику",
"clearAllStatsContent": "Ви впевнені, що хочете очистити всю статистику з'єднань сервера? Цю дію не можна скасувати.",
"clearServerStatsTitle": "Очистити статистику {serverName}",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "Ви впевнені, що хочете очистити статистику з'єднань для сервера \"{serverName}\"? Цю дію не можна скасувати.",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
"write": "写",
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。",
"connectionStats": "连接统计",
"noConnectionStatsData": "暂无连接统计数据",
"totalAttempts": "总次数",
"lastSuccess": "最后成功",
"lastFailure": "最后失败",
"recentConnections": "最近连接记录",
"viewDetails": "查看详情",
"connectionDetails": "连接详情",
"clearThisServerStats": "清空此服务器统计",
"clearAllStatsTitle": "清空所有统计",
"clearAllStatsContent": "确定要清空所有服务器的连接统计数据吗?此操作无法撤销。",
"clearServerStatsTitle": "清空 {serverName} 统计",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "确定要清空服务器 \"{serverName}\" 的连接统计数据吗?此操作无法撤销。",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -249,5 +249,32 @@
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
"write": "寫入",
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。",
"connectionStats": "連線統計",
"noConnectionStatsData": "暫無連線統計資料",
"totalAttempts": "總次數",
"lastSuccess": "最後成功",
"lastFailure": "最後失敗",
"recentConnections": "最近連線記錄",
"viewDetails": "檢視詳情",
"connectionDetails": "連線詳情",
"clearThisServerStats": "清空此伺服器統計",
"clearAllStatsTitle": "清空所有統計",
"clearAllStatsContent": "確定要清空所有伺服器的連線統計資料嗎?此操作無法撤銷。",
"clearServerStatsTitle": "清空 {serverName} 統計",
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"clearServerStatsContent": "確定要清空伺服器 \"{serverName}\" 的連線統計資料嗎?此操作無法撤銷。",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
}
}

View File

@@ -0,0 +1,360 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
import 'package:server_box/data/res/store.dart';
class ConnectionStatsPage extends StatefulWidget {
const ConnectionStatsPage({super.key});
@override
State<ConnectionStatsPage> createState() => _ConnectionStatsPageState();
}
class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
List<ServerConnectionStats> _serverStats = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadStats();
}
void _loadStats() {
setState(() {
_isLoading = true;
});
final stats = Stores.connectionStats.getAllServerStats();
setState(() {
_serverStats = stats;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: Text(l10n.connectionStats),
actions: [
IconButton(
onPressed: _loadStats,
icon: const Icon(Icons.refresh),
tooltip: libL10n.refresh,
),
IconButton(
onPressed: _showClearAllDialog,
icon: const Icon(Icons.clear_all, color: Colors.red),
tooltip: libL10n.clear,
),
],
),
body: _buildBody,
);
}
Widget get _buildBody {
if (_isLoading) {
return const Center(child: SizedLoading.large);
}
if (_serverStats.isEmpty) {
return Center(child: Text(l10n.noConnectionStatsData));
}
return ListView.builder(
itemCount: _serverStats.length,
itemBuilder: (context, index) {
final stats = _serverStats[index];
return _buildServerStatsCard(stats);
},
);
}
Widget _buildServerStatsCard(ServerConnectionStats stats) {
final successRate = stats.totalAttempts == 0
? 'N/A'
: '${(stats.successRate * 100).toStringAsFixed(1)}%';
final lastSuccessTime = stats.lastSuccessTime;
final lastFailureTime = stats.lastFailureTime;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
stats.serverName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
Text(
'${libL10n.success}: $successRate%',
style: TextStyle(
fontSize: 16,
color: stats.successRate >= 0.8
? Colors.green
: stats.successRate >= 0.5
? Colors.orange
: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(
l10n.totalAttempts,
stats.totalAttempts.toString(),
Icons.all_inclusive,
),
_buildStatItem(
libL10n.success,
stats.successCount.toString(),
Icons.check_circle,
Colors.green,
),
_buildStatItem(
libL10n.fail,
stats.failureCount.toString(),
Icons.error,
Colors.red,
),
],
),
if (lastSuccessTime != null || lastFailureTime != null) ...[
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
if (lastSuccessTime != null)
_buildTimeItem(
l10n.lastSuccess,
lastSuccessTime,
Icons.check_circle,
Colors.green,
),
if (lastFailureTime != null)
_buildTimeItem(
l10n.lastFailure,
lastFailureTime,
Icons.error,
Colors.red,
),
],
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.recentConnections,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () => _showServerDetailsDialog(stats),
child: Text(l10n.viewDetails),
),
],
),
const SizedBox(height: 8),
...stats.recentConnections.take(3).map(_buildConnectionItem),
],
),
),
);
}
Widget _buildStatItem(
String label,
String value,
IconData icon, [
Color? color,
]) {
return Column(
children: [
Icon(icon, size: 24, color: color ?? Colors.grey),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
],
);
}
Widget _buildTimeItem(
String label,
DateTime time,
IconData icon,
Color color,
) {
final timeStr = time.simple();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 16, color: color),
UIs.width7,
Text(
'$label: ',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
Text(timeStr, style: const TextStyle(fontSize: 12)),
],
),
);
}
Widget _buildConnectionItem(ConnectionStat stat) {
final timeStr = stat.timestamp.simple();
final isSuccess = stat.result.isSuccess;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(
isSuccess ? Icons.check_circle : Icons.error,
size: 16,
color: isSuccess ? Colors.green : Colors.red,
),
UIs.width7,
Text(timeStr, style: const TextStyle(fontSize: 12)),
UIs.width7,
Expanded(
child: Text(
isSuccess
? '${libL10n.success} (${stat.durationMs}ms)'
: stat.result.displayName,
style: TextStyle(
fontSize: 12,
color: isSuccess ? Colors.green : Colors.red,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
extension on _ConnectionStatsPageState {
void _showServerDetailsDialog(ServerConnectionStats stats) {
context.showRoundDialog(
title: '${stats.serverName} - ${l10n.connectionDetails}',
child: SizedBox(
width: double.maxFinite,
height: MediaQuery.sizeOf(context).height * 0.7,
child: ListView.separated(
itemCount: stats.recentConnections.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
final stat = stats.recentConnections[index];
final timeStr = stat.timestamp.simple();
final isSuccess = stat.result.isSuccess;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
isSuccess ? Icons.check_circle : Icons.error,
color: isSuccess ? Colors.green : Colors.red,
),
title: Text(timeStr),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isSuccess
? '${libL10n.success} (${stat.durationMs}ms)'
: '${libL10n.fail}: ${stat.result.displayName}',
style: TextStyle(
color: isSuccess ? Colors.green : Colors.red,
),
),
if (!isSuccess && stat.errorMessage.isNotEmpty)
Text(
stat.errorMessage,
style: const TextStyle(fontSize: 11, color: Colors.grey),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
),
),
actions: [
TextButton(onPressed: context.pop, child: Text(libL10n.close)),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_showClearServerStatsDialog(stats);
},
child: Text(
l10n.clearThisServerStats,
style: TextStyle(color: Colors.red),
),
),
],
);
}
void _showClearAllDialog() {
context.showRoundDialog(
title: l10n.clearAllStatsTitle,
child: Text(l10n.clearAllStatsContent),
actions: [
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
CountDownBtn(
onTap: () {
context.pop();
Stores.connectionStats.clearAll();
_loadStats();
},
text: libL10n.ok,
afterColor: Colors.red,
),
],
);
}
void _showClearServerStatsDialog(ServerConnectionStats stats) {
context.showRoundDialog(
title: l10n.clearServerStatsTitle(stats.serverName),
child: Text(l10n.clearServerStatsContent(stats.serverName)),
actions: [
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
CountDownBtn(
onTap: () {
context.pop();
Stores.connectionStats.clearServerStats(stats.serverId);
_loadStats();
},
text: libL10n.ok,
afterColor: Colors.red,
),
],
);
}
}

View File

@@ -9,6 +9,7 @@ extension _Server on _AppSettingsPageState {
_buildNetViewType(),
_buildServerSeq(),
_buildServerDetailCardSeq(),
_buildConnectionStats(),
_buildDeleteServers(),
_buildCpuView(),
_buildServerMore(),
@@ -38,6 +39,22 @@ extension _Server on _AppSettingsPageState {
);
}
Widget _buildConnectionStats() {
return ListTile(
leading: const Icon(Icons.analytics, size: _kIconSize),
title: const Text('连接统计'),
subtitle: const Text('查看服务器连接成功率和历史记录'),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ConnectionStatsPage(),
),
);
},
);
}
Widget _buildDeleteServers() {
return ListTile(
title: Text(l10n.deleteServers),

View File

@@ -17,6 +17,7 @@ import 'package:server_box/data/store/setting.dart';
import 'package:server_box/generated/l10n/l10n.dart';
import 'package:server_box/view/page/backup.dart';
import 'package:server_box/view/page/private_key/list.dart';
import 'package:server_box/view/page/server/connection_stats.dart';
import 'package:server_box/view/page/setting/platform/android.dart';
import 'package:server_box/view/page/setting/platform/ios.dart';
import 'package:server_box/view/page/setting/platform/platform_pub.dart';