diff --git a/CLAUDE.md b/CLAUDE.md index beca8746..3436adcc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/lib/data/model/server/connection_stat.dart b/lib/data/model/server/connection_stat.dart new file mode 100644 index 00000000..037cfe34 --- /dev/null +++ b/lib/data/model/server/connection_stat.dart @@ -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 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 recentConnections, + @HiveField(8) required double successRate, + }) = _ServerConnectionStats; + + factory ServerConnectionStats.fromJson(Map 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; +} \ No newline at end of file diff --git a/lib/data/model/server/connection_stat.freezed.dart b/lib/data/model/server/connection_stat.freezed.dart new file mode 100644 index 00000000..28c56595 --- /dev/null +++ b/lib/data/model/server/connection_stat.freezed.dart @@ -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 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 get copyWith => _$ConnectionStatCopyWithImpl(this as ConnectionStat, _$identity); + + /// Serializes this ConnectionStat to a JSON map. + Map 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 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 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? 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 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 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? 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 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 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 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 get copyWith => _$ServerConnectionStatsCopyWithImpl(this as ServerConnectionStats, _$identity); + + /// Serializes this ServerConnectionStats to a JSON map. + Map 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 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,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 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 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? 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 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 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 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 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? 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 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 recentConnections = const [], @HiveField(8) required this.successRate}): _recentConnections = recentConnections; + factory _ServerConnectionStats.fromJson(Map 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 _recentConnections; +@override@JsonKey()@HiveField(7) List 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 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 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,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable +as double, + )); +} + + +} + +// dart format on diff --git a/lib/data/model/server/connection_stat.g.dart b/lib/data/model/server/connection_stat.g.dart new file mode 100644 index 00000000..7a6aed05 --- /dev/null +++ b/lib/data/model/server/connection_stat.g.dart @@ -0,0 +1,233 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'connection_stat.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ConnectionStatAdapter extends TypeAdapter { + @override + final typeId = 100; + + @override + ConnectionStat read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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 { + @override + final typeId = 101; + + @override + ServerConnectionStats read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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(), + 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 { + @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 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 _$ConnectionStatToJson(_ConnectionStat instance) => + { + '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 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?) + ?.map((e) => ConnectionStat.fromJson(e as Map)) + .toList() ?? + const [], + successRate: (json['successRate'] as num).toDouble(), +); + +Map _$ServerConnectionStatsToJson( + _ServerConnectionStats instance, +) => { + '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, +}; diff --git a/lib/data/provider/container.g.dart b/lib/data/provider/container.g.dart index 684a765a..28445f4f 100644 --- a/lib/data/provider/container.g.dart +++ b/lib/data/provider/container.g.dart @@ -6,7 +6,7 @@ part of 'container.dart'; // RiverpodGenerator // ************************************************************************** -String _$containerNotifierHash() => r'db8f8a6b6071b7b33fbf79128dfed408a5b9fdad'; +String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/data/provider/server/single.dart b/lib/data/provider/server/single.dart index 215c53be..55b054e1 100644 --- a/lib/data/provider/server/single.dart +++ b/lib/data/provider/server/single.dart @@ -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); diff --git a/lib/data/provider/server/single.g.dart b/lib/data/provider/server/single.g.dart index eb069983..976c6d10 100644 --- a/lib/data/provider/server/single.g.dart +++ b/lib/data/provider/server/single.g.dart @@ -6,7 +6,7 @@ part of 'single.dart'; // RiverpodGenerator // ************************************************************************** -String _$serverNotifierHash() => r'5625b0a4762c28efdbc124809c03b84a51d213b1'; +String _$serverNotifierHash() => r'524647748cc3810c17e5c1cd29e360f3936f5014'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/data/res/store.dart b/lib/data/res/store.dart index efb84973..e35fbc6f 100644 --- a/lib/data/res/store.dart +++ b/lib/data/res/store.dart @@ -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(); static SnippetStore get snippet => getIt(); static HistoryStore get history => getIt(); + static ConnectionStatsStore get connectionStats => getIt(); /// All stores that need backup static List get _allBackup => [ @@ -25,6 +27,7 @@ abstract final class Stores { key, snippet, history, + connectionStats, ]; static Future init() async { @@ -34,6 +37,7 @@ abstract final class Stores { getIt.registerLazySingleton(() => PrivateKeyStore.instance); getIt.registerLazySingleton(() => SnippetStore.instance); getIt.registerLazySingleton(() => HistoryStore.instance); + getIt.registerLazySingleton(() => ConnectionStatsStore.instance); await Future.wait(_allBackup.map((store) => store.init())); } diff --git a/lib/data/store/connection_stats.dart b/lib/data/store/connection_stats.dart new file mode 100644 index 00000000..704bd968 --- /dev/null +++ b/lib/data/store/connection_stats.dart @@ -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 = []; + + 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 getConnectionHistory(String serverId) { + final allKeys = keys().where((key) => key.startsWith(serverId)).toList(); + final stats = []; + + for (final key in allKeys) { + final stat = get( + key, + fromObj: (val) { + if (val is ConnectionStat) return val; + if (val is Map) { + final map = val.toStrDynMap; + if (map == null) return null; + try { + return ConnectionStat.fromJson(map as Map); + } 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 getAllServerStats() { + final serverIds = {}; + final serverNames = {}; + + // 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( + key, + fromObj: (val) { + if (val is ConnectionStat) return val; + if (val is Map) { + final map = val.toStrDynMap; + if (map == null) return null; + try { + return ConnectionStat.fromJson(map as Map); + } catch (e) { + dprint('Parsing ConnectionStat from JSON', e); + } + } + return null; + }, + ); + if (stat != null) { + serverNames[serverId] = stat.serverName; + } + } + } + + final allStats = []; + 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); + } + } +} \ No newline at end of file diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 7477ac14..0d28d5ee 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -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 diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index 495b7fc3..6410254c 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -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.'; + } } diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 010fe805..808d4ff5 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -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.'; + } } diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index b4531e88..4ca13cf2 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -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.'; + } } diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index 47f4d34c..cf2256ab 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -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.'; + } } diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index 8102f572..2cf3a4dd 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -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.'; + } } diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index 22b46015..833fc375 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -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\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。'; + } } diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index ca0a3ce8..d49a6fdf 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -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.'; + } } diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index 6833e039..27413cca 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -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.'; + } } diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index 178f83c4..02435104 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -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\"? Это действие не может быть отменено.'; + } } diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index 5ac62d63..c61208c1 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -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.'; + } } diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 9fb17dcc..4274bf24 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -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\"? Цю дію не можна скасувати.'; + } } diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index 64f64034..6cd9a14c 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -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\" 的連線統計資料嗎?此操作無法撤銷。'; + } } diff --git a/lib/hive/hive_registrar.g.dart b/lib/hive/hive_registrar.g.dart index 5b30f95e..38bfc8ab 100644 --- a/lib/hive/hive_registrar.g.dart +++ b/lib/hive/hive_registrar.g.dart @@ -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()); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9b371012..72860e9c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2a2069ee..a9e6b464 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a2cc7820..acea4ac5 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index eda9cf1e..849a8dce 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index a26f99bb..b93673c5 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 10362754..27d914b8 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index b02c4df9..63a6dc33 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index b8182571..37ac01ef 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 1b1d2e11..a14e6aab 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 3c5ed8e5..ff1cef3e 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 6bc9fed4..0e19625d 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index a54cca43..1796c032 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 1f8a1271..835de0a8 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -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" + } + } + } } \ No newline at end of file diff --git a/lib/view/page/server/connection_stats.dart b/lib/view/page/server/connection_stats.dart new file mode 100644 index 00000000..de1a55bb --- /dev/null +++ b/lib/view/page/server/connection_stats.dart @@ -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 createState() => _ConnectionStatsPageState(); +} + +class _ConnectionStatsPageState extends State { + List _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, + ), + ], + ); + } +} diff --git a/lib/view/page/setting/entries/server.dart b/lib/view/page/setting/entries/server.dart index f032990c..80a41e43 100644 --- a/lib/view/page/setting/entries/server.dart +++ b/lib/view/page/setting/entries/server.dart @@ -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), diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index e75f5558..aabb9dd0 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -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';