mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-15 04:34:34 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc8e9b4bb1 | ||
|
|
ec4b633889 | ||
|
|
e51804fa70 | ||
|
|
2466341999 | ||
|
|
929061213f | ||
|
|
6b52679942 | ||
|
|
efc0315c93 | ||
|
|
8e4c2a7cde | ||
|
|
4ec7f5895e | ||
|
|
ee22cdb55f |
95
CLAUDE.md
Normal file
95
CLAUDE.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
### Development
|
||||
|
||||
- `flutter run` - Run the app in development mode
|
||||
- `dart run fl_build -p PLATFORM` - Build the app for specific platform (see fl_build package)
|
||||
- `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
|
||||
|
||||
### Testing
|
||||
|
||||
- `flutter test` - Run unit tests
|
||||
- `flutter test test/battery_test.dart` - Run specific test file
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a Flutter application for managing Linux servers with the following key architectural components:
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `lib/core/` - Core utilities, extensions, and routing
|
||||
- `lib/data/` - Data layer with models, providers, and storage
|
||||
- `model/` - Data models organized by feature (server, container, ssh, etc.)
|
||||
- `provider/` - Riverpod providers for state management
|
||||
- `store/` - Local storage implementations using Hive
|
||||
- `lib/view/` - UI layer with pages and widgets
|
||||
- `lib/generated/` - Generated localization files
|
||||
- `lib/hive/` - Hive adapters for local storage
|
||||
|
||||
### Key Technologies
|
||||
|
||||
- **State Management**: Riverpod with code generation (riverpod_annotation)
|
||||
- **Local Storage**: Hive for persistent data with generated adapters
|
||||
- **SSH/SFTP**: Custom dartssh2 fork for server connections
|
||||
- **Terminal**: Custom xterm.dart fork for SSH terminal interface
|
||||
- **Networking**: dio for HTTP requests
|
||||
- **Charts**: fl_chart for server status visualization
|
||||
- **Localization**: Flutter's built-in i18n with ARB files
|
||||
- **Code Generation**: Uses build_runner with json_serializable, freezed, hive_generator, riverpod_generator
|
||||
|
||||
### Data Models
|
||||
|
||||
- Server management models in `lib/data/model/server/`
|
||||
- Container/Docker models in `lib/data/model/container/`
|
||||
- SSH and SFTP models in respective directories
|
||||
- Most models use freezed for immutability and json_annotation for serialization
|
||||
|
||||
### Features
|
||||
|
||||
- Server status monitoring (CPU, memory, disk, network)
|
||||
- SSH terminal with virtual keyboard
|
||||
- SFTP file browser
|
||||
- Docker container management
|
||||
- Process and systemd service management
|
||||
- Server snippets and custom commands
|
||||
- Multi-language support (12+ languages)
|
||||
- Cross-platform support (iOS, Android, macOS, Linux, Windows)
|
||||
|
||||
### State Management Pattern
|
||||
|
||||
- Uses Riverpod providers for dependency injection and state management
|
||||
- Uses Freezed for immutable state models
|
||||
- Providers are organized by feature in `lib/data/provider/`
|
||||
- State is often persisted using Hive stores in `lib/data/store/`
|
||||
|
||||
### Build System
|
||||
|
||||
- Uses custom `fl_build` package for cross-platform building
|
||||
- `make.dart` script handles pre/post build tasks (metadata generation)
|
||||
- Supports building for multiple platforms with platform-specific configurations
|
||||
- Many dependencies are custom forks hosted on GitHub (dartssh2, xterm, fl_lib, etc.)
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Never run code formatting commands** - The codebase has specific formatting that should not be changed
|
||||
- **Always run code generation** after modifying models with annotations (freezed, json_serializable, hive, riverpod)
|
||||
- Generated files (`*.g.dart`, `*.freezed.dart`) should not be manually edited
|
||||
- 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
|
||||
@@ -748,7 +748,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -758,7 +758,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -884,7 +884,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -894,7 +894,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -912,7 +912,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -922,7 +922,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -943,7 +943,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -956,7 +956,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
@@ -982,7 +982,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -995,7 +995,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1018,7 +1018,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1031,7 +1031,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1054,7 +1054,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1066,7 +1066,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
@@ -1095,7 +1095,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1107,7 +1107,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
PRODUCT_NAME = ServerBox;
|
||||
@@ -1133,7 +1133,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1145,7 +1145,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
PRODUCT_NAME = ServerBox;
|
||||
|
||||
@@ -63,6 +63,7 @@ abstract final class SSHConfig {
|
||||
void addServer() {
|
||||
if (currentHost != null && currentHost != '*' && hostname != null) {
|
||||
final spi = Spi(
|
||||
id: ShortId.generate(),
|
||||
name: currentHost,
|
||||
ip: hostname,
|
||||
port: port,
|
||||
|
||||
@@ -55,8 +55,8 @@ enum StatusCmdType implements ShellCmdType {
|
||||
uptime('uptime'),
|
||||
conn('cat /proc/net/snmp'),
|
||||
disk(
|
||||
'lsblk --bytes --json --output '
|
||||
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
||||
'(lsblk --bytes --json --output '
|
||||
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID 2>/dev/null && echo "LSBLK_SUCCESS") || df -k'
|
||||
),
|
||||
mem("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||
tempType('cat /sys/class/thermal/thermal_zone*/type'),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_ce_flutter/adapters.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/view/page/server/tab/tab.dart';
|
||||
@@ -8,10 +9,17 @@ import 'package:server_box/view/page/snippet/list.dart';
|
||||
import 'package:server_box/view/page/ssh/tab.dart';
|
||||
import 'package:server_box/view/page/storage/local.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
|
||||
@HiveType(typeId: 103)
|
||||
enum AppTab {
|
||||
@HiveField(0)
|
||||
server,
|
||||
@HiveField(1)
|
||||
ssh,
|
||||
@HiveField(2)
|
||||
file,
|
||||
@HiveField(3)
|
||||
snippet
|
||||
//settings,
|
||||
;
|
||||
@@ -93,4 +101,35 @@ enum AppTab {
|
||||
static List<NavigationRailDestination> get navRailDestinations {
|
||||
return AppTab.values.map((e) => e.navRailDestination).toList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Helper function to parse AppTab list from stored object
|
||||
static List<AppTab> parseAppTabsFromObj(dynamic val) {
|
||||
if (val is List) {
|
||||
final tabs = <AppTab>[];
|
||||
for (final e in val) {
|
||||
final tab = _parseAppTabFromElement(e);
|
||||
if (tab != null) {
|
||||
tabs.add(tab);
|
||||
}
|
||||
}
|
||||
if (tabs.isNotEmpty) return tabs;
|
||||
}
|
||||
return AppTab.values;
|
||||
}
|
||||
|
||||
/// Helper function to parse a single AppTab from various element types
|
||||
static AppTab? _parseAppTabFromElement(dynamic e) {
|
||||
if (e is AppTab) {
|
||||
return e;
|
||||
} else if (e is String) {
|
||||
return AppTab.values.firstWhereOrNull((t) => t.name == e);
|
||||
} else if (e is int) {
|
||||
if (e >= 0 && e < AppTab.values.length) {
|
||||
return AppTab.values[e];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
52
lib/data/model/app/tab.g.dart
Normal file
52
lib/data/model/app/tab.g.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AppTabAdapter extends TypeAdapter<AppTab> {
|
||||
@override
|
||||
final typeId = 103;
|
||||
|
||||
@override
|
||||
AppTab read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return AppTab.server;
|
||||
case 1:
|
||||
return AppTab.ssh;
|
||||
case 2:
|
||||
return AppTab.file;
|
||||
case 3:
|
||||
return AppTab.snippet;
|
||||
default:
|
||||
return AppTab.server;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AppTab obj) {
|
||||
switch (obj) {
|
||||
case AppTab.server:
|
||||
writer.writeByte(0);
|
||||
case AppTab.ssh:
|
||||
writer.writeByte(1);
|
||||
case AppTab.file:
|
||||
writer.writeByte(2);
|
||||
case AppTab.snippet:
|
||||
writer.writeByte(3);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AppTabAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/container/status.dart';
|
||||
import 'package:server_box/data/model/container/type.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
|
||||
@@ -10,7 +11,7 @@ sealed class ContainerPs {
|
||||
final String? image = null;
|
||||
String? get name;
|
||||
String? get cmd;
|
||||
bool get running;
|
||||
ContainerStatus get status;
|
||||
|
||||
String? cpu;
|
||||
String? mem;
|
||||
@@ -51,7 +52,7 @@ final class PodmanPs implements ContainerPs {
|
||||
String? get cmd => command?.firstOrNull;
|
||||
|
||||
@override
|
||||
bool get running => exited != true;
|
||||
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);
|
||||
|
||||
@override
|
||||
void parseStats(String s) {
|
||||
@@ -121,10 +122,7 @@ final class DockerPs implements ContainerPs {
|
||||
String? get cmd => null;
|
||||
|
||||
@override
|
||||
bool get running {
|
||||
if (state?.contains('Exited') == true) return false;
|
||||
return true;
|
||||
}
|
||||
ContainerStatus get status => ContainerStatus.fromDockerState(state);
|
||||
|
||||
@override
|
||||
void parseStats(String s) {
|
||||
|
||||
70
lib/data/model/container/status.dart
Normal file
70
lib/data/model/container/status.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
|
||||
/// Represents the various states a container can be in.
|
||||
/// Supports both Docker and Podman container status parsing.
|
||||
enum ContainerStatus {
|
||||
running,
|
||||
exited,
|
||||
created,
|
||||
paused,
|
||||
restarting,
|
||||
removing,
|
||||
dead,
|
||||
unknown;
|
||||
|
||||
/// Check if the container is actively running
|
||||
bool get isRunning => this == ContainerStatus.running;
|
||||
|
||||
/// Check if the container can be started
|
||||
bool get canStart =>
|
||||
this == ContainerStatus.exited ||
|
||||
this == ContainerStatus.created ||
|
||||
this == ContainerStatus.dead;
|
||||
|
||||
/// Check if the container can be stopped
|
||||
bool get canStop =>
|
||||
this == ContainerStatus.running || this == ContainerStatus.paused;
|
||||
|
||||
/// Check if the container can be restarted
|
||||
bool get canRestart =>
|
||||
this != ContainerStatus.removing && this != ContainerStatus.unknown;
|
||||
|
||||
/// Parse Docker container status string to ContainerStatus
|
||||
static ContainerStatus fromDockerState(String? state) {
|
||||
if (state == null || state.isEmpty) return ContainerStatus.unknown;
|
||||
|
||||
final lowerState = state.toLowerCase();
|
||||
|
||||
if (lowerState.startsWith('up')) return ContainerStatus.running;
|
||||
if (lowerState.contains('exited')) return ContainerStatus.exited;
|
||||
if (lowerState.contains('created')) return ContainerStatus.created;
|
||||
if (lowerState.contains('paused')) return ContainerStatus.paused;
|
||||
if (lowerState.contains('restarting')) return ContainerStatus.restarting;
|
||||
if (lowerState.contains('removing')) return ContainerStatus.removing;
|
||||
if (lowerState.contains('dead')) return ContainerStatus.dead;
|
||||
|
||||
return ContainerStatus.unknown;
|
||||
}
|
||||
|
||||
/// Parse Podman container status from exited boolean
|
||||
static ContainerStatus fromPodmanExited(bool? exited) {
|
||||
if (exited == true) return ContainerStatus.exited;
|
||||
if (exited == false) return ContainerStatus.running;
|
||||
return ContainerStatus.unknown;
|
||||
}
|
||||
|
||||
/// Get display string for the status
|
||||
String get displayName {
|
||||
return switch (this) {
|
||||
ContainerStatus.running => l10n.running,
|
||||
ContainerStatus.exited => libL10n.exit,
|
||||
ContainerStatus.created => 'Created',
|
||||
ContainerStatus.paused => 'Paused',
|
||||
ContainerStatus.restarting => 'Restarting',
|
||||
ContainerStatus.removing => 'Removing',
|
||||
ContainerStatus.dead => 'Dead',
|
||||
ContainerStatus.unknown => libL10n.unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
79
lib/data/model/server/connection_stat.dart
Normal file
79
lib/data/model/server/connection_stat.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
part 'connection_stat.freezed.dart';
|
||||
part 'connection_stat.g.dart';
|
||||
|
||||
@freezed
|
||||
@HiveType(typeId: 100)
|
||||
abstract class ConnectionStat with _$ConnectionStat {
|
||||
const factory ConnectionStat({
|
||||
@HiveField(0) required String serverId,
|
||||
@HiveField(1) required String serverName,
|
||||
@HiveField(2) required DateTime timestamp,
|
||||
@HiveField(3) required ConnectionResult result,
|
||||
@HiveField(4) @Default('') String errorMessage,
|
||||
@HiveField(5) required int durationMs,
|
||||
}) = _ConnectionStat;
|
||||
|
||||
factory ConnectionStat.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConnectionStatFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@HiveType(typeId: 101)
|
||||
abstract class ServerConnectionStats with _$ServerConnectionStats {
|
||||
const factory ServerConnectionStats({
|
||||
@HiveField(0) required String serverId,
|
||||
@HiveField(1) required String serverName,
|
||||
@HiveField(2) required int totalAttempts,
|
||||
@HiveField(3) required int successCount,
|
||||
@HiveField(4) required int failureCount,
|
||||
@HiveField(5) @Default(null) DateTime? lastSuccessTime,
|
||||
@HiveField(6) @Default(null) DateTime? lastFailureTime,
|
||||
@HiveField(7) @Default([]) List<ConnectionStat> recentConnections,
|
||||
@HiveField(8) required double successRate,
|
||||
}) = _ServerConnectionStats;
|
||||
|
||||
factory ServerConnectionStats.fromJson(Map<String, dynamic> json) =>
|
||||
_$ServerConnectionStatsFromJson(json);
|
||||
}
|
||||
|
||||
@HiveType(typeId: 102)
|
||||
enum ConnectionResult {
|
||||
@HiveField(0)
|
||||
@JsonValue('success')
|
||||
success,
|
||||
@HiveField(1)
|
||||
@JsonValue('timeout')
|
||||
timeout,
|
||||
@HiveField(2)
|
||||
@JsonValue('auth_failed')
|
||||
authFailed,
|
||||
@HiveField(3)
|
||||
@JsonValue('network_error')
|
||||
networkError,
|
||||
@HiveField(4)
|
||||
@JsonValue('unknown_error')
|
||||
unknownError,
|
||||
}
|
||||
|
||||
extension ConnectionResultExtension on ConnectionResult {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ConnectionResult.success:
|
||||
return libL10n.success;
|
||||
case ConnectionResult.timeout:
|
||||
return '${libL10n.error}(timeout)';
|
||||
case ConnectionResult.authFailed:
|
||||
return '${libL10n.error}(auth)';
|
||||
case ConnectionResult.networkError:
|
||||
return '${libL10n.error}(${libL10n.network})';
|
||||
case ConnectionResult.unknownError:
|
||||
return '${libL10n.error}(${libL10n.unknown})';
|
||||
}
|
||||
}
|
||||
|
||||
bool get isSuccess => this == ConnectionResult.success;
|
||||
}
|
||||
585
lib/data/model/server/connection_stat.freezed.dart
Normal file
585
lib/data/model/server/connection_stat.freezed.dart
Normal file
@@ -0,0 +1,585 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'connection_stat.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ConnectionStat {
|
||||
|
||||
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) DateTime get timestamp;@HiveField(3) ConnectionResult get result;@HiveField(4) String get errorMessage;@HiveField(5) int get durationMs;
|
||||
/// Create a copy of ConnectionStat
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConnectionStatCopyWith<ConnectionStat> get copyWith => _$ConnectionStatCopyWithImpl<ConnectionStat>(this as ConnectionStat, _$identity);
|
||||
|
||||
/// Serializes this ConnectionStat to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ConnectionStatCopyWith<$Res> {
|
||||
factory $ConnectionStatCopyWith(ConnectionStat value, $Res Function(ConnectionStat) _then) = _$ConnectionStatCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ConnectionStatCopyWithImpl<$Res>
|
||||
implements $ConnectionStatCopyWith<$Res> {
|
||||
_$ConnectionStatCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ConnectionStat _self;
|
||||
final $Res Function(ConnectionStat) _then;
|
||||
|
||||
/// Create a copy of ConnectionStat
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
|
||||
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
|
||||
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ConnectionStat].
|
||||
extension ConnectionStatPatterns on ConnectionStat {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ConnectionStat value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ConnectionStat value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ConnectionStat value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat() when $default != null:
|
||||
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat():
|
||||
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat() when $default != null:
|
||||
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _ConnectionStat implements ConnectionStat {
|
||||
const _ConnectionStat({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.timestamp, @HiveField(3) required this.result, @HiveField(4) this.errorMessage = '', @HiveField(5) required this.durationMs});
|
||||
factory _ConnectionStat.fromJson(Map<String, dynamic> json) => _$ConnectionStatFromJson(json);
|
||||
|
||||
@override@HiveField(0) final String serverId;
|
||||
@override@HiveField(1) final String serverName;
|
||||
@override@HiveField(2) final DateTime timestamp;
|
||||
@override@HiveField(3) final ConnectionResult result;
|
||||
@override@JsonKey()@HiveField(4) final String errorMessage;
|
||||
@override@HiveField(5) final int durationMs;
|
||||
|
||||
/// Create a copy of ConnectionStat
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ConnectionStatCopyWith<_ConnectionStat> get copyWith => __$ConnectionStatCopyWithImpl<_ConnectionStat>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ConnectionStatToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ConnectionStatCopyWith<$Res> implements $ConnectionStatCopyWith<$Res> {
|
||||
factory _$ConnectionStatCopyWith(_ConnectionStat value, $Res Function(_ConnectionStat) _then) = __$ConnectionStatCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ConnectionStatCopyWithImpl<$Res>
|
||||
implements _$ConnectionStatCopyWith<$Res> {
|
||||
__$ConnectionStatCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ConnectionStat _self;
|
||||
final $Res Function(_ConnectionStat) _then;
|
||||
|
||||
/// Create a copy of ConnectionStat
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
|
||||
return _then(_ConnectionStat(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
|
||||
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
|
||||
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ServerConnectionStats {
|
||||
|
||||
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) int get totalAttempts;@HiveField(3) int get successCount;@HiveField(4) int get failureCount;@HiveField(5) DateTime? get lastSuccessTime;@HiveField(6) DateTime? get lastFailureTime;@HiveField(7) List<ConnectionStat> get recentConnections;@HiveField(8) double get successRate;
|
||||
/// Create a copy of ServerConnectionStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ServerConnectionStatsCopyWith<ServerConnectionStats> get copyWith => _$ServerConnectionStatsCopyWithImpl<ServerConnectionStats>(this as ServerConnectionStats, _$identity);
|
||||
|
||||
/// Serializes this ServerConnectionStats to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other.recentConnections, recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(recentConnections),successRate);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ServerConnectionStatsCopyWith<$Res> {
|
||||
factory $ServerConnectionStatsCopyWith(ServerConnectionStats value, $Res Function(ServerConnectionStats) _then) = _$ServerConnectionStatsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ServerConnectionStatsCopyWithImpl<$Res>
|
||||
implements $ServerConnectionStatsCopyWith<$Res> {
|
||||
_$ServerConnectionStatsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ServerConnectionStats _self;
|
||||
final $Res Function(ServerConnectionStats) _then;
|
||||
|
||||
/// Create a copy of ServerConnectionStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
|
||||
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
|
||||
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,recentConnections: null == recentConnections ? _self.recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
|
||||
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ServerConnectionStats].
|
||||
extension ServerConnectionStatsPatterns on ServerConnectionStats {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerConnectionStats value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerConnectionStats value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerConnectionStats value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats() when $default != null:
|
||||
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats():
|
||||
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats() when $default != null:
|
||||
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _ServerConnectionStats implements ServerConnectionStats {
|
||||
const _ServerConnectionStats({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.totalAttempts, @HiveField(3) required this.successCount, @HiveField(4) required this.failureCount, @HiveField(5) this.lastSuccessTime = null, @HiveField(6) this.lastFailureTime = null, @HiveField(7) final List<ConnectionStat> recentConnections = const [], @HiveField(8) required this.successRate}): _recentConnections = recentConnections;
|
||||
factory _ServerConnectionStats.fromJson(Map<String, dynamic> json) => _$ServerConnectionStatsFromJson(json);
|
||||
|
||||
@override@HiveField(0) final String serverId;
|
||||
@override@HiveField(1) final String serverName;
|
||||
@override@HiveField(2) final int totalAttempts;
|
||||
@override@HiveField(3) final int successCount;
|
||||
@override@HiveField(4) final int failureCount;
|
||||
@override@JsonKey()@HiveField(5) final DateTime? lastSuccessTime;
|
||||
@override@JsonKey()@HiveField(6) final DateTime? lastFailureTime;
|
||||
final List<ConnectionStat> _recentConnections;
|
||||
@override@JsonKey()@HiveField(7) List<ConnectionStat> get recentConnections {
|
||||
if (_recentConnections is EqualUnmodifiableListView) return _recentConnections;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_recentConnections);
|
||||
}
|
||||
|
||||
@override@HiveField(8) final double successRate;
|
||||
|
||||
/// Create a copy of ServerConnectionStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ServerConnectionStatsCopyWith<_ServerConnectionStats> get copyWith => __$ServerConnectionStatsCopyWithImpl<_ServerConnectionStats>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ServerConnectionStatsToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other._recentConnections, _recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(_recentConnections),successRate);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ServerConnectionStatsCopyWith<$Res> implements $ServerConnectionStatsCopyWith<$Res> {
|
||||
factory _$ServerConnectionStatsCopyWith(_ServerConnectionStats value, $Res Function(_ServerConnectionStats) _then) = __$ServerConnectionStatsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ServerConnectionStatsCopyWithImpl<$Res>
|
||||
implements _$ServerConnectionStatsCopyWith<$Res> {
|
||||
__$ServerConnectionStatsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ServerConnectionStats _self;
|
||||
final $Res Function(_ServerConnectionStats) _then;
|
||||
|
||||
/// Create a copy of ServerConnectionStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
|
||||
return _then(_ServerConnectionStats(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
|
||||
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
|
||||
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,recentConnections: null == recentConnections ? _self._recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
|
||||
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
233
lib/data/model/server/connection_stat.g.dart
Normal file
233
lib/data/model/server/connection_stat.g.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'connection_stat.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ConnectionStatAdapter extends TypeAdapter<ConnectionStat> {
|
||||
@override
|
||||
final typeId = 100;
|
||||
|
||||
@override
|
||||
ConnectionStat read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ConnectionStat(
|
||||
serverId: fields[0] as String,
|
||||
serverName: fields[1] as String,
|
||||
timestamp: fields[2] as DateTime,
|
||||
result: fields[3] as ConnectionResult,
|
||||
errorMessage: fields[4] == null ? '' : fields[4] as String,
|
||||
durationMs: (fields[5] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ConnectionStat obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.serverId)
|
||||
..writeByte(1)
|
||||
..write(obj.serverName)
|
||||
..writeByte(2)
|
||||
..write(obj.timestamp)
|
||||
..writeByte(3)
|
||||
..write(obj.result)
|
||||
..writeByte(4)
|
||||
..write(obj.errorMessage)
|
||||
..writeByte(5)
|
||||
..write(obj.durationMs);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ConnectionStatAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class ServerConnectionStatsAdapter extends TypeAdapter<ServerConnectionStats> {
|
||||
@override
|
||||
final typeId = 101;
|
||||
|
||||
@override
|
||||
ServerConnectionStats read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ServerConnectionStats(
|
||||
serverId: fields[0] as String,
|
||||
serverName: fields[1] as String,
|
||||
totalAttempts: (fields[2] as num).toInt(),
|
||||
successCount: (fields[3] as num).toInt(),
|
||||
failureCount: (fields[4] as num).toInt(),
|
||||
lastSuccessTime: fields[5] == null ? null : fields[5] as DateTime?,
|
||||
lastFailureTime: fields[6] == null ? null : fields[6] as DateTime?,
|
||||
recentConnections: fields[7] == null
|
||||
? []
|
||||
: (fields[7] as List).cast<ConnectionStat>(),
|
||||
successRate: (fields[8] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServerConnectionStats obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.serverId)
|
||||
..writeByte(1)
|
||||
..write(obj.serverName)
|
||||
..writeByte(2)
|
||||
..write(obj.totalAttempts)
|
||||
..writeByte(3)
|
||||
..write(obj.successCount)
|
||||
..writeByte(4)
|
||||
..write(obj.failureCount)
|
||||
..writeByte(5)
|
||||
..write(obj.lastSuccessTime)
|
||||
..writeByte(6)
|
||||
..write(obj.lastFailureTime)
|
||||
..writeByte(7)
|
||||
..write(obj.recentConnections)
|
||||
..writeByte(8)
|
||||
..write(obj.successRate);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ServerConnectionStatsAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class ConnectionResultAdapter extends TypeAdapter<ConnectionResult> {
|
||||
@override
|
||||
final typeId = 102;
|
||||
|
||||
@override
|
||||
ConnectionResult read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return ConnectionResult.success;
|
||||
case 1:
|
||||
return ConnectionResult.timeout;
|
||||
case 2:
|
||||
return ConnectionResult.authFailed;
|
||||
case 3:
|
||||
return ConnectionResult.networkError;
|
||||
case 4:
|
||||
return ConnectionResult.unknownError;
|
||||
default:
|
||||
return ConnectionResult.success;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ConnectionResult obj) {
|
||||
switch (obj) {
|
||||
case ConnectionResult.success:
|
||||
writer.writeByte(0);
|
||||
case ConnectionResult.timeout:
|
||||
writer.writeByte(1);
|
||||
case ConnectionResult.authFailed:
|
||||
writer.writeByte(2);
|
||||
case ConnectionResult.networkError:
|
||||
writer.writeByte(3);
|
||||
case ConnectionResult.unknownError:
|
||||
writer.writeByte(4);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ConnectionResultAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ConnectionStat _$ConnectionStatFromJson(Map<String, dynamic> json) =>
|
||||
_ConnectionStat(
|
||||
serverId: json['serverId'] as String,
|
||||
serverName: json['serverName'] as String,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
result: $enumDecode(_$ConnectionResultEnumMap, json['result']),
|
||||
errorMessage: json['errorMessage'] as String? ?? '',
|
||||
durationMs: (json['durationMs'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ConnectionStatToJson(_ConnectionStat instance) =>
|
||||
<String, dynamic>{
|
||||
'serverId': instance.serverId,
|
||||
'serverName': instance.serverName,
|
||||
'timestamp': instance.timestamp.toIso8601String(),
|
||||
'result': _$ConnectionResultEnumMap[instance.result]!,
|
||||
'errorMessage': instance.errorMessage,
|
||||
'durationMs': instance.durationMs,
|
||||
};
|
||||
|
||||
const _$ConnectionResultEnumMap = {
|
||||
ConnectionResult.success: 'success',
|
||||
ConnectionResult.timeout: 'timeout',
|
||||
ConnectionResult.authFailed: 'auth_failed',
|
||||
ConnectionResult.networkError: 'network_error',
|
||||
ConnectionResult.unknownError: 'unknown_error',
|
||||
};
|
||||
|
||||
_ServerConnectionStats _$ServerConnectionStatsFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ServerConnectionStats(
|
||||
serverId: json['serverId'] as String,
|
||||
serverName: json['serverName'] as String,
|
||||
totalAttempts: (json['totalAttempts'] as num).toInt(),
|
||||
successCount: (json['successCount'] as num).toInt(),
|
||||
failureCount: (json['failureCount'] as num).toInt(),
|
||||
lastSuccessTime: json['lastSuccessTime'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastSuccessTime'] as String),
|
||||
lastFailureTime: json['lastFailureTime'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastFailureTime'] as String),
|
||||
recentConnections:
|
||||
(json['recentConnections'] as List<dynamic>?)
|
||||
?.map((e) => ConnectionStat.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
successRate: (json['successRate'] as num).toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ServerConnectionStatsToJson(
|
||||
_ServerConnectionStats instance,
|
||||
) => <String, dynamic>{
|
||||
'serverId': instance.serverId,
|
||||
'serverName': instance.serverName,
|
||||
'totalAttempts': instance.totalAttempts,
|
||||
'successCount': instance.successCount,
|
||||
'failureCount': instance.failureCount,
|
||||
'lastSuccessTime': instance.lastSuccessTime?.toIso8601String(),
|
||||
'lastFailureTime': instance.lastFailureTime?.toIso8601String(),
|
||||
'recentConnections': instance.recentConnections,
|
||||
'successRate': instance.successRate,
|
||||
};
|
||||
@@ -44,22 +44,49 @@ class Disk with EquatableMixin {
|
||||
static List<Disk> parse(String raw) {
|
||||
final list = <Disk>[];
|
||||
raw = raw.trim();
|
||||
try {
|
||||
if (raw.startsWith('{')) {
|
||||
// Parse JSON output from lsblk command
|
||||
final Map<String, dynamic> jsonData = json.decode(raw);
|
||||
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
|
||||
|
||||
if (raw.isEmpty) {
|
||||
dprint('Empty disk info data received');
|
||||
return list;
|
||||
}
|
||||
|
||||
for (final device in blockdevices) {
|
||||
// Process each device
|
||||
_processTopLevelDevice(device, list);
|
||||
try {
|
||||
// Check if we have lsblk JSON output with success marker
|
||||
if (raw.startsWith('{')) {
|
||||
// Extract JSON part (excluding the success marker if present)
|
||||
final jsonEnd = raw.indexOf('\nLSBLK_SUCCESS');
|
||||
final jsonPart = jsonEnd > 0 ? raw.substring(0, jsonEnd) : raw;
|
||||
|
||||
try {
|
||||
final Map<String, dynamic> jsonData = json.decode(jsonPart);
|
||||
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
|
||||
|
||||
for (final device in blockdevices) {
|
||||
// Process each device
|
||||
_processTopLevelDevice(device, list);
|
||||
}
|
||||
|
||||
// If we successfully parsed JSON and have valid disks, return them
|
||||
if (list.isNotEmpty) {
|
||||
return list;
|
||||
}
|
||||
} on FormatException catch (e) {
|
||||
Loggers.app.warning('JSON parsing failed, falling back to df -k output: $e');
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Error processing JSON disk data, falling back to df -k output: $e', e);
|
||||
}
|
||||
} else {
|
||||
// Fallback to the old parsing method in case of non-JSON output
|
||||
}
|
||||
|
||||
// Check if we have df -k output (fallback case)
|
||||
if (raw.contains('Filesystem') && raw.contains('Mounted on')) {
|
||||
return _parseWithOldMethod(raw);
|
||||
}
|
||||
|
||||
// If we reach here, both parsing methods failed
|
||||
Loggers.app.warning('Unable to parse disk info with any method');
|
||||
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Failed to parse disk info: $e', e);
|
||||
Loggers.app.warning('Failed to parse disk info with both methods: $e', e);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -88,6 +115,32 @@ class Disk with EquatableMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse filesystem fields from device data
|
||||
static ({BigInt size, BigInt used, BigInt avail, int usedPercent}) _parseFilesystemFields(Map<String, dynamic> device) {
|
||||
// Helper function to parse size strings safely
|
||||
BigInt parseSize(String? sizeStr) {
|
||||
if (sizeStr == null || sizeStr.isEmpty || sizeStr == 'null' || sizeStr == '0') {
|
||||
return BigInt.zero;
|
||||
}
|
||||
return (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
}
|
||||
|
||||
// Helper function to parse percentage strings
|
||||
int parsePercent(String? percentStr) {
|
||||
if (percentStr == null || percentStr.isEmpty || percentStr == 'null') {
|
||||
return 0;
|
||||
}
|
||||
return int.tryParse(percentStr.replaceAll('%', '')) ?? 0;
|
||||
}
|
||||
|
||||
return (
|
||||
size: parseSize(device['fssize']?.toString()),
|
||||
used: parseSize(device['fsused']?.toString()),
|
||||
avail: parseSize(device['fsavail']?.toString()),
|
||||
usedPercent: parsePercent(device['fsuse%']?.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Process a single device without recursively processing its children
|
||||
static Disk? _processSingleDevice(Map<String, dynamic> device) {
|
||||
final fstype = device['fstype']?.toString();
|
||||
@@ -102,20 +155,7 @@ class Disk with EquatableMixin {
|
||||
return null;
|
||||
}
|
||||
|
||||
final sizeStr = device['fssize']?.toString() ?? '0';
|
||||
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
final usedStr = device['fsused']?.toString() ?? '0';
|
||||
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
final availStr = device['fsavail']?.toString() ?? '0';
|
||||
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
// Parse fsuse% which is usually in the format "45%"
|
||||
String usePercentStr = device['fsuse%']?.toString() ?? '0';
|
||||
usePercentStr = usePercentStr.replaceAll('%', '');
|
||||
final usedPercent = int.tryParse(usePercentStr) ?? 0;
|
||||
|
||||
final fsFields = _parseFilesystemFields(device);
|
||||
final name = device['name']?.toString();
|
||||
final kname = device['kname']?.toString();
|
||||
final uuid = device['uuid']?.toString();
|
||||
@@ -124,10 +164,10 @@ class Disk with EquatableMixin {
|
||||
path: path,
|
||||
fsTyp: fstype,
|
||||
mount: mountpoint,
|
||||
usedPercent: usedPercent,
|
||||
used: used,
|
||||
size: size,
|
||||
avail: avail,
|
||||
usedPercent: fsFields.usedPercent,
|
||||
used: fsFields.used,
|
||||
size: fsFields.size,
|
||||
avail: fsFields.avail,
|
||||
name: name,
|
||||
kname: kname,
|
||||
uuid: uuid,
|
||||
@@ -155,20 +195,7 @@ class Disk with EquatableMixin {
|
||||
|
||||
// Handle common filesystem cases or parent devices with children
|
||||
if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
|
||||
final sizeStr = device['fssize']?.toString() ?? '0';
|
||||
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
final usedStr = device['fsused']?.toString() ?? '0';
|
||||
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
final availStr = device['fsavail']?.toString() ?? '0';
|
||||
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
// Parse fsuse% which is usually in the format "45%"
|
||||
String usePercentStr = device['fsuse%']?.toString() ?? '0';
|
||||
usePercentStr = usePercentStr.replaceAll('%', '');
|
||||
final usedPercent = int.tryParse(usePercentStr) ?? 0;
|
||||
|
||||
final fsFields = _parseFilesystemFields(device);
|
||||
final name = device['name']?.toString();
|
||||
final kname = device['kname']?.toString();
|
||||
final uuid = device['uuid']?.toString();
|
||||
@@ -177,10 +204,10 @@ class Disk with EquatableMixin {
|
||||
path: path,
|
||||
fsTyp: fstype,
|
||||
mount: mount,
|
||||
usedPercent: usedPercent,
|
||||
used: used,
|
||||
size: size,
|
||||
avail: avail,
|
||||
usedPercent: fsFields.usedPercent,
|
||||
used: fsFields.used,
|
||||
size: fsFields.size,
|
||||
avail: fsFields.avail,
|
||||
name: name,
|
||||
kname: kname,
|
||||
uuid: uuid,
|
||||
|
||||
@@ -38,11 +38,6 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
|
||||
@override
|
||||
ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {
|
||||
this.client = client;
|
||||
this.userName = userName;
|
||||
this.hostId = hostId;
|
||||
this.context = context;
|
||||
|
||||
final type = Stores.container.getType(hostId);
|
||||
final initialState = ContainerState(type: type);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'container.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$containerNotifierHash() => r'db8f8a6b6071b7b33fbf79128dfed408a5b9fdad';
|
||||
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'private_key.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$privateKeyNotifierHash() =>
|
||||
r'404836a4409f64d305c1e22f4a57b52985a57b68';
|
||||
r'12edd05dca29d1cbc9e2a3e047c3d417d22f7bb7';
|
||||
|
||||
/// See also [PrivateKeyNotifier].
|
||||
@ProviderFor(PrivateKeyNotifier)
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'all.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$serversNotifierHash() => r'2ae641188f772794a32e8700c008f51ba0cc1ec9';
|
||||
String _$serversNotifierHash() => r'2b29ad3027a203c7a20bfd0142d384a503cbbcaa';
|
||||
|
||||
/// See also [ServersNotifier].
|
||||
@ProviderFor(ServersNotifier)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'single.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$serverNotifierHash() => r'5625b0a4762c28efdbc124809c03b84a51d213b1';
|
||||
String _$serverNotifierHash() => r'524647748cc3810c17e5c1cd29e360f3936f5014';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'snippet.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$snippetNotifierHash() => r'caf0361f9a0346fb99cb90f032f1ceb29446dd71';
|
||||
String _$snippetNotifierHash() => r'8285c7edf905a4aaa41cd8b65b0a6755c8b97fc9';
|
||||
|
||||
/// See also [SnippetNotifier].
|
||||
@ProviderFor(SnippetNotifier)
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
abstract class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 1231;
|
||||
static const int script = 68;
|
||||
static const int build = 1241;
|
||||
static const int script = 69;
|
||||
}
|
||||
|
||||
@@ -1,4 +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';
|
||||
@@ -6,25 +8,37 @@ import 'package:server_box/data/store/server.dart';
|
||||
import 'package:server_box/data/store/setting.dart';
|
||||
import 'package:server_box/data/store/snippet.dart';
|
||||
|
||||
final GetIt getIt = GetIt.instance;
|
||||
|
||||
abstract final class Stores {
|
||||
static final setting = SettingStore.instance;
|
||||
static final server = ServerStore.instance;
|
||||
static final container = ContainerStore.instance;
|
||||
static final key = PrivateKeyStore.instance;
|
||||
static final snippet = SnippetStore.instance;
|
||||
static final history = HistoryStore.instance;
|
||||
static SettingStore get setting => getIt<SettingStore>();
|
||||
static ServerStore get server => getIt<ServerStore>();
|
||||
static ContainerStore get container => getIt<ContainerStore>();
|
||||
static PrivateKeyStore get key => getIt<PrivateKeyStore>();
|
||||
static SnippetStore get snippet => getIt<SnippetStore>();
|
||||
static HistoryStore get history => getIt<HistoryStore>();
|
||||
static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();
|
||||
|
||||
/// All stores that need backup
|
||||
static final List<HiveStore> _allBackup = [
|
||||
SettingStore.instance,
|
||||
ServerStore.instance,
|
||||
ContainerStore.instance,
|
||||
PrivateKeyStore.instance,
|
||||
SnippetStore.instance,
|
||||
HistoryStore.instance,
|
||||
];
|
||||
static List<HiveStore> get _allBackup => [
|
||||
setting,
|
||||
server,
|
||||
container,
|
||||
key,
|
||||
snippet,
|
||||
history,
|
||||
connectionStats,
|
||||
];
|
||||
|
||||
static Future<void> init() async {
|
||||
getIt.registerLazySingleton<SettingStore>(() => SettingStore.instance);
|
||||
getIt.registerLazySingleton<ServerStore>(() => ServerStore.instance);
|
||||
getIt.registerLazySingleton<ContainerStore>(() => ContainerStore.instance);
|
||||
getIt.registerLazySingleton<PrivateKeyStore>(() => PrivateKeyStore.instance);
|
||||
getIt.registerLazySingleton<SnippetStore>(() => SnippetStore.instance);
|
||||
getIt.registerLazySingleton<HistoryStore>(() => HistoryStore.instance);
|
||||
getIt.registerLazySingleton<ConnectionStatsStore>(() => ConnectionStatsStore.instance);
|
||||
|
||||
await Future.wait(_allBackup.map((store) => store.init()));
|
||||
}
|
||||
|
||||
|
||||
190
lib/data/store/connection_stats.dart
Normal file
190
lib/data/store/connection_stats.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/server/connection_stat.dart';
|
||||
|
||||
class ConnectionStatsStore extends HiveStore {
|
||||
ConnectionStatsStore._() : super('connection_stats');
|
||||
|
||||
static final instance = ConnectionStatsStore._();
|
||||
|
||||
// Record a connection attempt
|
||||
void recordConnection(ConnectionStat stat) {
|
||||
final key = '${stat.serverId}_${ShortId.generate()}';
|
||||
set(key, stat);
|
||||
_cleanOldRecords(stat.serverId);
|
||||
}
|
||||
|
||||
// Clean records older than 30 days for a specific server
|
||||
void _cleanOldRecords(String serverId) {
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(days: 30));
|
||||
final allKeys = keys().toList();
|
||||
final keysToDelete = <String>[];
|
||||
|
||||
for (final key in allKeys) {
|
||||
if (key.startsWith(serverId)) {
|
||||
final parts = key.split('_');
|
||||
if (parts.length >= 2) {
|
||||
final timestamp = int.tryParse(parts.last);
|
||||
if (timestamp != null) {
|
||||
final recordTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
if (recordTime.isBefore(cutoffTime)) {
|
||||
keysToDelete.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToDelete) {
|
||||
remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Get connection stats for a specific server
|
||||
ServerConnectionStats getServerStats(String serverId, String serverName) {
|
||||
final allStats = getConnectionHistory(serverId);
|
||||
|
||||
if (allStats.isEmpty) {
|
||||
return ServerConnectionStats(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
totalAttempts: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
recentConnections: [],
|
||||
successRate: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
final totalAttempts = allStats.length;
|
||||
final successCount = allStats.where((s) => s.result.isSuccess).length;
|
||||
final failureCount = totalAttempts - successCount;
|
||||
final successRate = totalAttempts > 0 ? (successCount / totalAttempts) : 0.0;
|
||||
|
||||
final successTimes = allStats
|
||||
.where((s) => s.result.isSuccess)
|
||||
.map((s) => s.timestamp)
|
||||
.toList();
|
||||
final failureTimes = allStats
|
||||
.where((s) => !s.result.isSuccess)
|
||||
.map((s) => s.timestamp)
|
||||
.toList();
|
||||
|
||||
DateTime? lastSuccessTime;
|
||||
DateTime? lastFailureTime;
|
||||
|
||||
if (successTimes.isNotEmpty) {
|
||||
successTimes.sort((a, b) => b.compareTo(a));
|
||||
lastSuccessTime = successTimes.first;
|
||||
}
|
||||
|
||||
if (failureTimes.isNotEmpty) {
|
||||
failureTimes.sort((a, b) => b.compareTo(a));
|
||||
lastFailureTime = failureTimes.first;
|
||||
}
|
||||
|
||||
// Get recent connections (last 20)
|
||||
final recentConnections = allStats.take(20).toList();
|
||||
|
||||
return ServerConnectionStats(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
totalAttempts: totalAttempts,
|
||||
successCount: successCount,
|
||||
failureCount: failureCount,
|
||||
lastSuccessTime: lastSuccessTime,
|
||||
lastFailureTime: lastFailureTime,
|
||||
recentConnections: recentConnections,
|
||||
successRate: successRate,
|
||||
);
|
||||
}
|
||||
|
||||
// Get connection history for a specific server
|
||||
List<ConnectionStat> getConnectionHistory(String serverId) {
|
||||
final allKeys = keys().where((key) => key.startsWith(serverId)).toList();
|
||||
final stats = <ConnectionStat>[];
|
||||
|
||||
for (final key in allKeys) {
|
||||
final stat = get<ConnectionStat>(
|
||||
key,
|
||||
fromObj: (val) {
|
||||
if (val is ConnectionStat) return val;
|
||||
if (val is Map<dynamic, dynamic>) {
|
||||
final map = val.toStrDynMap;
|
||||
if (map == null) return null;
|
||||
try {
|
||||
return ConnectionStat.fromJson(map as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
dprint('Parsing ConnectionStat from JSON', e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
if (stat != null) {
|
||||
stats.add(stat);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp, newest first
|
||||
stats.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Get all servers' stats
|
||||
List<ServerConnectionStats> getAllServerStats() {
|
||||
final serverIds = <String>{};
|
||||
final serverNames = <String, String>{};
|
||||
|
||||
// Get all unique server IDs
|
||||
for (final key in keys()) {
|
||||
final parts = key.split('_');
|
||||
if (parts.length >= 2) {
|
||||
final serverId = parts[0];
|
||||
serverIds.add(serverId);
|
||||
|
||||
// Try to get server name from the stored stat
|
||||
final stat = get<ConnectionStat>(
|
||||
key,
|
||||
fromObj: (val) {
|
||||
if (val is ConnectionStat) return val;
|
||||
if (val is Map<dynamic, dynamic>) {
|
||||
final map = val.toStrDynMap;
|
||||
if (map == null) return null;
|
||||
try {
|
||||
return ConnectionStat.fromJson(map as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
dprint('Parsing ConnectionStat from JSON', e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
if (stat != null) {
|
||||
serverNames[serverId] = stat.serverName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final allStats = <ServerConnectionStats>[];
|
||||
for (final serverId in serverIds) {
|
||||
final serverName = serverNames[serverId] ?? serverId;
|
||||
final stats = getServerStats(serverId, serverName);
|
||||
allStats.add(stats);
|
||||
}
|
||||
|
||||
return allStats;
|
||||
}
|
||||
|
||||
// Clear all connection stats
|
||||
void clearAll() {
|
||||
box.clear();
|
||||
}
|
||||
|
||||
// Clear stats for a specific server
|
||||
void clearServerStats(String serverId) {
|
||||
final keysToDelete = keys().where((key) => key.startsWith(serverId)).toList();
|
||||
for (final key in keysToDelete) {
|
||||
remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/app/menu/server_func.dart';
|
||||
import 'package:server_box/data/model/app/net_view.dart';
|
||||
import 'package:server_box/data/model/app/server_detail_card.dart';
|
||||
import 'package:server_box/data/model/app/tab.dart';
|
||||
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||
import 'package:server_box/data/res/default.dart';
|
||||
|
||||
@@ -22,10 +23,7 @@ class SettingStore extends HiveStore {
|
||||
// late final launchPage = property('launchPage', Defaults.launchPageIdx);
|
||||
|
||||
/// Disk view: amount / IO
|
||||
late final serverTabPreferDiskAmount = propertyDefault(
|
||||
'serverTabPreferDiskAmount',
|
||||
false,
|
||||
);
|
||||
late final serverTabPreferDiskAmount = propertyDefault('serverTabPreferDiskAmount', false);
|
||||
|
||||
/// Bigger for bigger font size
|
||||
/// 1.0 means 100%
|
||||
@@ -70,20 +68,14 @@ class SettingStore extends HiveStore {
|
||||
late final locale = propertyDefault('locale', '');
|
||||
|
||||
// SSH virtual key (ctrl | alt) auto turn off
|
||||
late final sshVirtualKeyAutoOff = propertyDefault(
|
||||
'sshVirtualKeyAutoOff',
|
||||
true,
|
||||
);
|
||||
late final sshVirtualKeyAutoOff = propertyDefault('sshVirtualKeyAutoOff', true);
|
||||
|
||||
late final editorFontSize = propertyDefault('editorFontSize', 12.5);
|
||||
|
||||
// Editor theme
|
||||
late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme);
|
||||
|
||||
late final editorDarkTheme = propertyDefault(
|
||||
'editorDarkTheme',
|
||||
Defaults.editorDarkTheme,
|
||||
);
|
||||
late final editorDarkTheme = propertyDefault('editorDarkTheme', Defaults.editorDarkTheme);
|
||||
|
||||
late final fullScreen = propertyDefault('fullScreen', false);
|
||||
|
||||
@@ -113,29 +105,20 @@ class SettingStore extends HiveStore {
|
||||
);
|
||||
|
||||
// Only valid on iOS
|
||||
late final autoUpdateHomeWidget = propertyDefault(
|
||||
'autoUpdateHomeWidget',
|
||||
isIOS,
|
||||
);
|
||||
late final autoUpdateHomeWidget = propertyDefault('autoUpdateHomeWidget', isIOS);
|
||||
|
||||
late final autoCheckAppUpdate = propertyDefault('autoCheckAppUpdate', true);
|
||||
|
||||
/// Display server tab function buttons on the bottom of each server card if [true]
|
||||
///
|
||||
/// Otherwise, display them on the top of server detail page
|
||||
late final moveServerFuncs = propertyDefault(
|
||||
'moveOutServerTabFuncBtns',
|
||||
false,
|
||||
);
|
||||
late final moveServerFuncs = propertyDefault('moveOutServerTabFuncBtns', false);
|
||||
|
||||
/// Whether use `rm -r` to delete directory on SFTP
|
||||
late final sftpRmrDir = propertyDefault('sftpRmrDir', false);
|
||||
|
||||
/// Whether use system's primary color as the app's primary color
|
||||
late final useSystemPrimaryColor = propertyDefault(
|
||||
'useSystemPrimaryColor',
|
||||
false,
|
||||
);
|
||||
late final useSystemPrimaryColor = propertyDefault('useSystemPrimaryColor', false);
|
||||
|
||||
/// Only valid on iOS / Android / Windows
|
||||
late final useBioAuth = propertyDefault('useBioAuth', false);
|
||||
@@ -151,10 +134,7 @@ class SettingStore extends HiveStore {
|
||||
late final sftpOpenLastPath = propertyDefault('sftpOpenLastPath', true);
|
||||
|
||||
/// Show folders first in SFTP file browser
|
||||
late final sftpShowFoldersFirst = propertyDefault(
|
||||
'sftpShowFoldersFirst',
|
||||
true,
|
||||
);
|
||||
late final sftpShowFoldersFirst = propertyDefault('sftpShowFoldersFirst', true);
|
||||
|
||||
/// Show tip of suspend
|
||||
late final showSuspendTip = propertyDefault('showSuspendTip', true);
|
||||
@@ -162,10 +142,7 @@ class SettingStore extends HiveStore {
|
||||
/// Whether collapse UI items by default
|
||||
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
|
||||
|
||||
late final serverFuncBtns = listProperty(
|
||||
'serverBtns',
|
||||
defaultValue: ServerFuncBtn.defaultIdxs,
|
||||
);
|
||||
late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs);
|
||||
|
||||
/// Docker is more popular than podman, set to `false` to use docker
|
||||
late final usePodman = propertyDefault('usePodman', false);
|
||||
@@ -180,16 +157,10 @@ class SettingStore extends HiveStore {
|
||||
late final containerParseStat = propertyDefault('containerParseStat', true);
|
||||
|
||||
/// Auto refresh container status
|
||||
late final containerAutoRefresh = propertyDefault(
|
||||
'containerAutoRefresh',
|
||||
true,
|
||||
);
|
||||
late final containerAutoRefresh = propertyDefault('containerAutoRefresh', true);
|
||||
|
||||
/// Use double column servers page on Desktop
|
||||
late final doubleColumnServersPage = propertyDefault(
|
||||
'doubleColumnServersPage',
|
||||
true,
|
||||
);
|
||||
late final doubleColumnServersPage = propertyDefault('doubleColumnServersPage', true);
|
||||
|
||||
/// Ignore local network device (eg: br-xxx, ovs-system...)
|
||||
/// when building traffic view on server tab
|
||||
@@ -244,8 +215,7 @@ class SettingStore extends HiveStore {
|
||||
/// Record the position and size of the window.
|
||||
late final windowState = property<WindowState>(
|
||||
'windowState',
|
||||
fromObj: (raw) =>
|
||||
WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),
|
||||
fromObj: (raw) => WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),
|
||||
toObj: (state) => state == null ? null : jsonEncode(state.toJson()),
|
||||
);
|
||||
|
||||
@@ -258,10 +228,7 @@ class SettingStore extends HiveStore {
|
||||
late final sftpEditor = propertyDefault('sftpEditor', '');
|
||||
|
||||
/// Preferred terminal emulator command on desktop
|
||||
late final desktopTerminal = propertyDefault(
|
||||
'desktopTerminal',
|
||||
'x-terminal-emulator',
|
||||
);
|
||||
late final desktopTerminal = propertyDefault('desktopTerminal', 'x-terminal-emulator');
|
||||
|
||||
/// Run foreground service on Android, if the SSH terminal is running
|
||||
late final fgService = propertyDefault('fgService', false);
|
||||
@@ -280,4 +247,14 @@ class SettingStore extends HiveStore {
|
||||
|
||||
/// Whether to read SSH config from ~/.ssh/config on first time
|
||||
late final firstTimeReadSSHCfg = propertyDefault('firstTimeReadSSHCfg', true);
|
||||
|
||||
/// Tabs at home page
|
||||
late final homeTabs = listProperty(
|
||||
'homeTabs',
|
||||
defaultValue: AppTab.values,
|
||||
fromObj: AppTab.parseAppTabsFromObj,
|
||||
toObj: (val) {
|
||||
return val?.map((e) => e.name).toList() ?? [];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1621,6 +1621,114 @@ 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);
|
||||
|
||||
/// No description provided for @homeTabs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Home Tabs'**
|
||||
String get homeTabs;
|
||||
|
||||
/// No description provided for @homeTabsCustomizeDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Customize which tabs appear on the home page and their order'**
|
||||
String get homeTabsCustomizeDesc;
|
||||
|
||||
/// No description provided for @reset.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset'**
|
||||
String get reset;
|
||||
|
||||
/// No description provided for @availableTabs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Available Tabs'**
|
||||
String get availableTabs;
|
||||
|
||||
/// No description provided for @atLeastOneTab.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'At least one tab must be selected'**
|
||||
String get atLeastOneTab;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -851,4 +851,64 @@ 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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Home-Tabs';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge';
|
||||
|
||||
@override
|
||||
String get reset => 'Zurücksetzen';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Verfügbare Tabs';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'Mindestens ein Tab muss ausgewählt sein';
|
||||
}
|
||||
|
||||
@@ -843,4 +843,64 @@ 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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Home Tabs';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Customize which tabs appear on the home page and their order';
|
||||
|
||||
@override
|
||||
String get reset => 'Reset';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Available Tabs';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'At least one tab must be selected';
|
||||
}
|
||||
|
||||
@@ -852,4 +852,65 @@ 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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Pestañas de inicio';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Personaliza qué pestañas aparecen en la página de inicio y su orden';
|
||||
|
||||
@override
|
||||
String get reset => 'Restablecer';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Pestañas disponibles';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'Al menos una pestaña debe estar seleccionada';
|
||||
}
|
||||
|
||||
@@ -855,4 +855,65 @@ 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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Onglets d\'accueil';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Personnalisez les onglets qui apparaissent sur la page d\'accueil et leur ordre';
|
||||
|
||||
@override
|
||||
String get reset => 'Réinitialiser';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Onglets disponibles';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'Au moins un onglet doit être sélectionné';
|
||||
}
|
||||
|
||||
@@ -843,4 +843,64 @@ 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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Tab Beranda';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Sesuaikan tab mana yang muncul di halaman beranda dan urutannya';
|
||||
|
||||
@override
|
||||
String get reset => 'Reset';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Tab Tersedia';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'Setidaknya satu tab harus dipilih';
|
||||
}
|
||||
|
||||
@@ -818,4 +818,62 @@ 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\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'ホームタブ';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc => 'ホームページに表示するタブとその順序をカスタマイズします';
|
||||
|
||||
@override
|
||||
String get reset => 'リセット';
|
||||
|
||||
@override
|
||||
String get availableTabs => '利用可能なタブ';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => '少なくとも1つのタブを選択する必要があります';
|
||||
}
|
||||
|
||||
@@ -849,4 +849,65 @@ 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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Home-tabbladen';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde';
|
||||
|
||||
@override
|
||||
String get reset => 'Resetten';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Beschikbare tabbladen';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab =>
|
||||
'Er moet minimaal één tabblad worden geselecteerd';
|
||||
}
|
||||
|
||||
@@ -846,4 +846,64 @@ 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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Abas iniciais';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Personalize quais abas aparecem na página inicial e sua ordem';
|
||||
|
||||
@override
|
||||
String get reset => 'Redefinir';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Abas disponíveis';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'Pelo menos uma aba deve ser selecionada';
|
||||
}
|
||||
|
||||
@@ -848,4 +848,64 @@ 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\"? Это действие не может быть отменено.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Вкладки дома';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Настройте, какие вкладки появляются на главной странице и их порядок';
|
||||
|
||||
@override
|
||||
String get reset => 'Сброс';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Доступные вкладки';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'Должна быть выбрана хотя бы одна вкладка';
|
||||
}
|
||||
|
||||
@@ -843,4 +843,64 @@ 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.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Ana Sayfa Sekmeleri';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin';
|
||||
|
||||
@override
|
||||
String get reset => 'Sıfırla';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Mevcut Sekmeler';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'En az bir sekme seçilmelidir';
|
||||
}
|
||||
|
||||
@@ -849,4 +849,64 @@ 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\"? Цю дію не можна скасувати.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => 'Домашні вкладки';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc =>
|
||||
'Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок';
|
||||
|
||||
@override
|
||||
String get reset => 'Скинути';
|
||||
|
||||
@override
|
||||
String get availableTabs => 'Доступні вкладки';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => 'Потрібно вибрати принаймні одну вкладку';
|
||||
}
|
||||
|
||||
@@ -803,6 +803,64 @@ 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\" 的连接统计数据吗?此操作无法撤销。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => '主页标签';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc => '自定义主页上显示的标签及其顺序';
|
||||
|
||||
@override
|
||||
String get reset => '重置';
|
||||
|
||||
@override
|
||||
String get availableTabs => '可用标签';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => '至少需要选择一个标签';
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||
@@ -1604,4 +1662,62 @@ 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\" 的連線統計資料嗎?此操作無法撤銷。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeTabs => '主頁標籤';
|
||||
|
||||
@override
|
||||
String get homeTabsCustomizeDesc => '自訂主頁上顯示的標籤及其順序';
|
||||
|
||||
@override
|
||||
String get reset => '重置';
|
||||
|
||||
@override
|
||||
String get availableTabs => '可用標籤';
|
||||
|
||||
@override
|
||||
String get atLeastOneTab => '至少需要選擇一個標籤';
|
||||
}
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
// Check in to version control
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:server_box/data/model/app/tab.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(AppTabAdapter());
|
||||
registerAdapter(ConnectionResultAdapter());
|
||||
registerAdapter(ConnectionStatAdapter());
|
||||
registerAdapter(NetViewTypeAdapter());
|
||||
registerAdapter(PrivateKeyInfoAdapter());
|
||||
registerAdapter(ServerConnectionStatsAdapter());
|
||||
registerAdapter(ServerCustomAdapter());
|
||||
registerAdapter(ServerFuncBtnAdapter());
|
||||
registerAdapter(SnippetAdapter());
|
||||
@@ -21,8 +27,12 @@ extension HiveRegistrar on HiveInterface {
|
||||
|
||||
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(AppTabAdapter());
|
||||
registerAdapter(ConnectionResultAdapter());
|
||||
registerAdapter(ConnectionStatAdapter());
|
||||
registerAdapter(NetViewTypeAdapter());
|
||||
registerAdapter(PrivateKeyInfoAdapter());
|
||||
registerAdapter(ServerConnectionStatsAdapter());
|
||||
registerAdapter(ServerCustomAdapter());
|
||||
registerAdapter(ServerFuncBtnAdapter());
|
||||
registerAdapter(SnippetAdapter());
|
||||
|
||||
@@ -121,7 +121,7 @@ final class _IntroPage extends StatelessWidget {
|
||||
IntroPage.title(text: l10n.backupPassword, big: true),
|
||||
SizedBox(height: padTop * 0.5),
|
||||
Text(
|
||||
'${l10n.backupTip}\n\n${l10n.backupPasswordTip}',
|
||||
l10n.backupTip,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -148,10 +148,7 @@ final class _IntroPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => ctx.pop(false), child: Text(libL10n.cancel)),
|
||||
TextButton(onPressed: () => ctx.pop(true), child: Text(libL10n.ok)),
|
||||
],
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
if (result == true) {
|
||||
final pwd = controller.text.trim();
|
||||
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Home-Tabs",
|
||||
"homeTabsCustomizeDesc": "Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge",
|
||||
"reset": "Zurücksetzen",
|
||||
"availableTabs": "Verfügbare Tabs",
|
||||
"atLeastOneTab": "Mindestens ein Tab muss ausgewählt sein"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Home Tabs",
|
||||
"homeTabsCustomizeDesc": "Customize which tabs appear on the home page and their order",
|
||||
"reset": "Reset",
|
||||
"availableTabs": "Available Tabs",
|
||||
"atLeastOneTab": "At least one tab must be selected"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Pestañas de inicio",
|
||||
"homeTabsCustomizeDesc": "Personaliza qué pestañas aparecen en la página de inicio y su orden",
|
||||
"reset": "Restablecer",
|
||||
"availableTabs": "Pestañas disponibles",
|
||||
"atLeastOneTab": "Al menos una pestaña debe estar seleccionada"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Onglets d'accueil",
|
||||
"homeTabsCustomizeDesc": "Personnalisez les onglets qui apparaissent sur la page d'accueil et leur ordre",
|
||||
"reset": "Réinitialiser",
|
||||
"availableTabs": "Onglets disponibles",
|
||||
"atLeastOneTab": "Au moins un onglet doit être sélectionné"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Tab Beranda",
|
||||
"homeTabsCustomizeDesc": "Sesuaikan tab mana yang muncul di halaman beranda dan urutannya",
|
||||
"reset": "Reset",
|
||||
"availableTabs": "Tab Tersedia",
|
||||
"atLeastOneTab": "Setidaknya satu tab harus dipilih"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "ホームタブ",
|
||||
"homeTabsCustomizeDesc": "ホームページに表示するタブとその順序をカスタマイズします",
|
||||
"reset": "リセット",
|
||||
"availableTabs": "利用可能なタブ",
|
||||
"atLeastOneTab": "少なくとも1つのタブを選択する必要があります"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Home-tabbladen",
|
||||
"homeTabsCustomizeDesc": "Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde",
|
||||
"reset": "Resetten",
|
||||
"availableTabs": "Beschikbare tabbladen",
|
||||
"atLeastOneTab": "Er moet minimaal één tabblad worden geselecteerd"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Abas iniciais",
|
||||
"homeTabsCustomizeDesc": "Personalize quais abas aparecem na página inicial e sua ordem",
|
||||
"reset": "Redefinir",
|
||||
"availableTabs": "Abas disponíveis",
|
||||
"atLeastOneTab": "Pelo menos uma aba deve ser selecionada"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Вкладки дома",
|
||||
"homeTabsCustomizeDesc": "Настройте, какие вкладки появляются на главной странице и их порядок",
|
||||
"reset": "Сброс",
|
||||
"availableTabs": "Доступные вкладки",
|
||||
"atLeastOneTab": "Должна быть выбрана хотя бы одна вкладка"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Ana Sayfa Sekmeleri",
|
||||
"homeTabsCustomizeDesc": "Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin",
|
||||
"reset": "Sıfırla",
|
||||
"availableTabs": "Mevcut Sekmeler",
|
||||
"atLeastOneTab": "En az bir sekme seçilmelidir"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "Домашні вкладки",
|
||||
"homeTabsCustomizeDesc": "Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок",
|
||||
"reset": "Скинути",
|
||||
"availableTabs": "Доступні вкладки",
|
||||
"atLeastOneTab": "Потрібно вибрати принаймні одну вкладку"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "主页标签",
|
||||
"homeTabsCustomizeDesc": "自定义主页上显示的标签及其顺序",
|
||||
"reset": "重置",
|
||||
"availableTabs": "可用标签",
|
||||
"atLeastOneTab": "至少需要选择一个标签"
|
||||
}
|
||||
@@ -249,5 +249,37 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeTabs": "主頁標籤",
|
||||
"homeTabsCustomizeDesc": "自訂主頁上顯示的標籤及其順序",
|
||||
"reset": "重置",
|
||||
"availableTabs": "可用標籤",
|
||||
"atLeastOneTab": "至少需要選擇一個標籤"
|
||||
}
|
||||
@@ -188,7 +188,7 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
|
||||
Widget _buildPs(ContainerState containerState) {
|
||||
final items = containerState.items;
|
||||
if (items == null) return UIs.placeholder;
|
||||
final running = items.where((e) => e.running).length;
|
||||
final running = items.where((e) => e.status.isRunning).length;
|
||||
final stopped = items.length - running;
|
||||
final subtitle = stopped > 0
|
||||
? l10n.dockerStatusRunningAndStoppedFmt(running, stopped)
|
||||
@@ -219,8 +219,8 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
|
||||
),
|
||||
Text(
|
||||
'${item.image ?? l10n.unknown} - ${switch (item) {
|
||||
final PodmanPs ps => ps.running ? l10n.running : l10n.stopped,
|
||||
final DockerPs ps => ps.state,
|
||||
final PodmanPs ps => ps.status.displayName,
|
||||
final DockerPs ps => ps.state ?? ps.status.displayName,
|
||||
}}',
|
||||
style: UIs.text13Grey,
|
||||
),
|
||||
@@ -277,7 +277,7 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
|
||||
|
||||
Widget _buildMoreBtn(ContainerPs dItem) {
|
||||
return PopupMenu(
|
||||
items: ContainerMenu.items(dItem.running).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
||||
items: ContainerMenu.items(dItem.status.isRunning).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
||||
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
|
||||
late final _notifier = ref.read(serversNotifierProvider.notifier);
|
||||
late final _provider = ref.read(serversNotifierProvider);
|
||||
late List<AppTab> _tabs = Stores.setting.homeTabs.fetch();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -51,13 +52,30 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
SystemUIs.switchStatusBar(hide: false);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// avoid index out of range
|
||||
if (_selectIndex.value >= AppTab.values.length || _selectIndex.value < 0) {
|
||||
if (_selectIndex.value >= _tabs.length || _selectIndex.value < 0) {
|
||||
_selectIndex.value = 0;
|
||||
}
|
||||
_pageController = PageController(initialPage: _selectIndex.value);
|
||||
if (Stores.setting.generalWakeLock.fetch()) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
|
||||
// Listen to homeTabs changes
|
||||
Stores.setting.homeTabs.listenable().addListener(() {
|
||||
final newTabs = Stores.setting.homeTabs.fetch();
|
||||
if (mounted && newTabs != _tabs) {
|
||||
setState(() {
|
||||
_tabs = newTabs;
|
||||
// Ensure current page index is valid
|
||||
if (_selectIndex.value >= _tabs.length) {
|
||||
_selectIndex.value = _tabs.length - 1;
|
||||
}
|
||||
if (_selectIndex.value < 0 && _tabs.isNotEmpty) {
|
||||
_selectIndex.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -119,9 +137,9 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: AppTab.values.length,
|
||||
itemCount: _tabs.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (_, index) => AppTab.values[index].page,
|
||||
itemBuilder: (_, index) => _tabs[index].page,
|
||||
onPageChanged: (value) {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (!_switchingPage) {
|
||||
@@ -146,7 +164,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
animationDuration: const Duration(milliseconds: 250),
|
||||
onDestinationSelected: _onDestinationSelected,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
|
||||
destinations: AppTab.navDestinations,
|
||||
destinations: _tabs.map((tab) => tab.navDestination).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -165,7 +183,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
trailing: extended ? const SizedBox(height: 20) : null,
|
||||
labelType: extended ? NavigationRailLabelType.none : NavigationRailLabelType.all,
|
||||
selectedIndex: idx,
|
||||
destinations: AppTab.navRailDestinations,
|
||||
destinations: _tabs.map((tab) => tab.navRailDestination).toList(),
|
||||
onDestinationSelected: _onDestinationSelected,
|
||||
),
|
||||
),
|
||||
@@ -236,6 +254,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
|
||||
void _onDestinationSelected(int index) {
|
||||
if (_selectIndex.value == index) return;
|
||||
if (index < 0 || index >= _tabs.length) return;
|
||||
_selectIndex.value = index;
|
||||
_switchingPage = true;
|
||||
_pageController.animateToPage(
|
||||
|
||||
360
lib/view/page/server/connection_stats.dart
Normal file
360
lib/view/page/server/connection_stats.dart
Normal file
@@ -0,0 +1,360 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/connection_stat.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
class ConnectionStatsPage extends StatefulWidget {
|
||||
const ConnectionStatsPage({super.key});
|
||||
|
||||
@override
|
||||
State<ConnectionStatsPage> createState() => _ConnectionStatsPageState();
|
||||
}
|
||||
|
||||
class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
|
||||
List<ServerConnectionStats> _serverStats = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadStats();
|
||||
}
|
||||
|
||||
void _loadStats() {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
final stats = Stores.connectionStats.getAllServerStats();
|
||||
setState(() {
|
||||
_serverStats = stats;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(l10n.connectionStats),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _loadStats,
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: libL10n.refresh,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _showClearAllDialog,
|
||||
icon: const Icon(Icons.clear_all, color: Colors.red),
|
||||
tooltip: libL10n.clear,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildBody,
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildBody {
|
||||
if (_isLoading) {
|
||||
return const Center(child: SizedLoading.large);
|
||||
}
|
||||
if (_serverStats.isEmpty) {
|
||||
return Center(child: Text(l10n.noConnectionStatsData));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: _serverStats.length,
|
||||
itemBuilder: (context, index) {
|
||||
final stats = _serverStats[index];
|
||||
return _buildServerStatsCard(stats);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildServerStatsCard(ServerConnectionStats stats) {
|
||||
final successRate = stats.totalAttempts == 0
|
||||
? 'N/A'
|
||||
: '${(stats.successRate * 100).toStringAsFixed(1)}%';
|
||||
final lastSuccessTime = stats.lastSuccessTime;
|
||||
final lastFailureTime = stats.lastFailureTime;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
stats.serverName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${libL10n.success}: $successRate%',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: stats.successRate >= 0.8
|
||||
? Colors.green
|
||||
: stats.successRate >= 0.5
|
||||
? Colors.orange
|
||||
: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildStatItem(
|
||||
l10n.totalAttempts,
|
||||
stats.totalAttempts.toString(),
|
||||
Icons.all_inclusive,
|
||||
),
|
||||
_buildStatItem(
|
||||
libL10n.success,
|
||||
stats.successCount.toString(),
|
||||
Icons.check_circle,
|
||||
Colors.green,
|
||||
),
|
||||
_buildStatItem(
|
||||
libL10n.fail,
|
||||
stats.failureCount.toString(),
|
||||
Icons.error,
|
||||
Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (lastSuccessTime != null || lastFailureTime != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
if (lastSuccessTime != null)
|
||||
_buildTimeItem(
|
||||
l10n.lastSuccess,
|
||||
lastSuccessTime,
|
||||
Icons.check_circle,
|
||||
Colors.green,
|
||||
),
|
||||
if (lastFailureTime != null)
|
||||
_buildTimeItem(
|
||||
l10n.lastFailure,
|
||||
lastFailureTime,
|
||||
Icons.error,
|
||||
Colors.red,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
l10n.recentConnections,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _showServerDetailsDialog(stats),
|
||||
child: Text(l10n.viewDetails),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...stats.recentConnections.take(3).map(_buildConnectionItem),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(
|
||||
String label,
|
||||
String value,
|
||||
IconData icon, [
|
||||
Color? color,
|
||||
]) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(icon, size: 24, color: color ?? Colors.grey),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeItem(
|
||||
String label,
|
||||
DateTime time,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
final timeStr = time.simple();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
UIs.width7,
|
||||
Text(
|
||||
'$label: ',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
Text(timeStr, style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectionItem(ConnectionStat stat) {
|
||||
final timeStr = stat.timestamp.simple();
|
||||
final isSuccess = stat.result.isSuccess;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSuccess ? Icons.check_circle : Icons.error,
|
||||
size: 16,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
UIs.width7,
|
||||
Text(timeStr, style: const TextStyle(fontSize: 12)),
|
||||
UIs.width7,
|
||||
Expanded(
|
||||
child: Text(
|
||||
isSuccess
|
||||
? '${libL10n.success} (${stat.durationMs}ms)'
|
||||
: stat.result.displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on _ConnectionStatsPageState {
|
||||
void _showServerDetailsDialog(ServerConnectionStats stats) {
|
||||
context.showRoundDialog(
|
||||
title: '${stats.serverName} - ${l10n.connectionDetails}',
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: MediaQuery.sizeOf(context).height * 0.7,
|
||||
child: ListView.separated(
|
||||
itemCount: stats.recentConnections.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final stat = stats.recentConnections[index];
|
||||
final timeStr = stat.timestamp.simple();
|
||||
final isSuccess = stat.result.isSuccess;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
isSuccess ? Icons.check_circle : Icons.error,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
title: Text(timeStr),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isSuccess
|
||||
? '${libL10n.success} (${stat.durationMs}ms)'
|
||||
: '${libL10n.fail}: ${stat.result.displayName}',
|
||||
style: TextStyle(
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
if (!isSuccess && stat.errorMessage.isNotEmpty)
|
||||
Text(
|
||||
stat.errorMessage,
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: context.pop, child: Text(libL10n.close)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_showClearServerStatsDialog(stats);
|
||||
},
|
||||
child: Text(
|
||||
l10n.clearThisServerStats,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showClearAllDialog() {
|
||||
context.showRoundDialog(
|
||||
title: l10n.clearAllStatsTitle,
|
||||
child: Text(l10n.clearAllStatsContent),
|
||||
actions: [
|
||||
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
|
||||
CountDownBtn(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
Stores.connectionStats.clearAll();
|
||||
_loadStats();
|
||||
},
|
||||
text: libL10n.ok,
|
||||
afterColor: Colors.red,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showClearServerStatsDialog(ServerConnectionStats stats) {
|
||||
context.showRoundDialog(
|
||||
title: l10n.clearServerStatsTitle(stats.serverName),
|
||||
child: Text(l10n.clearServerStatsContent(stats.serverName)),
|
||||
actions: [
|
||||
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
|
||||
CountDownBtn(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
Stores.connectionStats.clearServerStats(stats.serverId);
|
||||
_loadStats();
|
||||
},
|
||||
text: libL10n.ok,
|
||||
afterColor: Colors.red,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/provider/server/single.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/pve.dart';
|
||||
import 'package:server_box/view/page/server/edit.dart';
|
||||
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||
|
||||
part 'misc.dart';
|
||||
|
||||
@@ -1,964 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:choice/choice.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/core/utils/server_dedup.dart';
|
||||
import 'package:server_box/core/utils/ssh_config.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
import 'package:server_box/data/provider/private_key.dart';
|
||||
import 'package:server_box/data/provider/server/all.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
import 'package:server_box/view/page/private_key/edit.dart';
|
||||
|
||||
class ServerEditPage extends ConsumerStatefulWidget {
|
||||
final SpiRequiredArgs? args;
|
||||
|
||||
const ServerEditPage({super.key, this.args});
|
||||
|
||||
static const route = AppRoute<bool, SpiRequiredArgs>(page: ServerEditPage.new, path: '/servers/edit');
|
||||
|
||||
@override
|
||||
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
|
||||
}
|
||||
|
||||
class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayoutMixin {
|
||||
late final spi = widget.args?.spi;
|
||||
final _nameController = TextEditingController();
|
||||
final _ipController = TextEditingController();
|
||||
final _altUrlController = TextEditingController();
|
||||
final _portController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _pveAddrCtrl = TextEditingController();
|
||||
final _preferTempDevCtrl = TextEditingController();
|
||||
final _logoUrlCtrl = TextEditingController();
|
||||
final _wolMacCtrl = TextEditingController();
|
||||
final _wolIpCtrl = TextEditingController();
|
||||
final _wolPwdCtrl = TextEditingController();
|
||||
final _netDevCtrl = TextEditingController();
|
||||
final _scriptDirCtrl = TextEditingController();
|
||||
|
||||
final _nameFocus = FocusNode();
|
||||
final _ipFocus = FocusNode();
|
||||
final _alterUrlFocus = FocusNode();
|
||||
final _portFocus = FocusNode();
|
||||
final _usernameFocus = FocusNode();
|
||||
|
||||
late FocusScopeNode _focusScope;
|
||||
|
||||
/// -1: non selected, null: password, others: index of private key
|
||||
final _keyIdx = ValueNotifier<int?>(null);
|
||||
final _autoConnect = ValueNotifier(true);
|
||||
final _jumpServer = nvn<String?>();
|
||||
final _pveIgnoreCert = ValueNotifier(false);
|
||||
final _env = <String, String>{}.vn;
|
||||
final _customCmds = <String, String>{}.vn;
|
||||
final _tags = <String>{}.vn;
|
||||
final _systemType = ValueNotifier<SystemType?>(null);
|
||||
final _disabledCmdTypes = <String>{}.vn;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_nameController.dispose();
|
||||
_ipController.dispose();
|
||||
_altUrlController.dispose();
|
||||
_portController.dispose();
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
_preferTempDevCtrl.dispose();
|
||||
_logoUrlCtrl.dispose();
|
||||
_wolMacCtrl.dispose();
|
||||
_wolIpCtrl.dispose();
|
||||
_wolPwdCtrl.dispose();
|
||||
_netDevCtrl.dispose();
|
||||
_scriptDirCtrl.dispose();
|
||||
|
||||
_nameFocus.dispose();
|
||||
_ipFocus.dispose();
|
||||
_alterUrlFocus.dispose();
|
||||
_portFocus.dispose();
|
||||
_usernameFocus.dispose();
|
||||
_pveAddrCtrl.dispose();
|
||||
|
||||
_keyIdx.dispose();
|
||||
_autoConnect.dispose();
|
||||
_jumpServer.dispose();
|
||||
_pveIgnoreCert.dispose();
|
||||
_env.dispose();
|
||||
_customCmds.dispose();
|
||||
_tags.dispose();
|
||||
_systemType.dispose();
|
||||
_disabledCmdTypes.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_focusScope = FocusScope.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actions = <Widget>[];
|
||||
if (spi != null) actions.add(_buildDelBtn());
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _focusScope.unfocus(),
|
||||
child: Scaffold(
|
||||
appBar: CustomAppBar(title: Text(libL10n.edit), actions: actions),
|
||||
body: _buildForm(),
|
||||
floatingActionButton: _buildFAB(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForm() {
|
||||
final topItems = [_buildWriteScriptTip(), if (isMobile) _buildQrScan(), if (isDesktop) _buildSSHImport()];
|
||||
final children = [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: topItems.joinWith(UIs.width13).toList()),
|
||||
Input(
|
||||
autoFocus: true,
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
node: _nameFocus,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),
|
||||
hint: libL10n.example,
|
||||
label: libL10n.name,
|
||||
icon: BoxIcons.bx_rename,
|
||||
obscureText: false,
|
||||
autoCorrect: true,
|
||||
suggestion: true,
|
||||
),
|
||||
Input(
|
||||
controller: _ipController,
|
||||
type: TextInputType.url,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_portFocus),
|
||||
node: _ipFocus,
|
||||
label: l10n.host,
|
||||
icon: BoxIcons.bx_server,
|
||||
hint: 'example.com',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _portController,
|
||||
type: TextInputType.number,
|
||||
node: _portFocus,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
|
||||
label: l10n.port,
|
||||
icon: Bootstrap.number_123,
|
||||
hint: '22',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _usernameController,
|
||||
type: TextInputType.text,
|
||||
node: _usernameFocus,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_alterUrlFocus),
|
||||
label: libL10n.user,
|
||||
icon: Icons.account_box,
|
||||
hint: 'root',
|
||||
suggestion: false,
|
||||
),
|
||||
TagTile(tags: _tags, allTags: ref.watch(serversNotifierProvider).tags).cardx,
|
||||
ListTile(
|
||||
title: Text(l10n.autoConnect),
|
||||
trailing: _autoConnect.listenVal(
|
||||
(val) => Switch(
|
||||
value: val,
|
||||
onChanged: (val) {
|
||||
_autoConnect.value = val;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildAuth(),
|
||||
_buildSystemType(),
|
||||
_buildJumpServer(),
|
||||
_buildMore(),
|
||||
];
|
||||
return AutoMultiList(children: children);
|
||||
}
|
||||
|
||||
Widget _buildAuth() {
|
||||
final switch_ = ListTile(
|
||||
title: Text(l10n.keyAuth),
|
||||
trailing: _keyIdx.listenVal(
|
||||
(v) => Switch(
|
||||
value: v != null,
|
||||
onChanged: (val) {
|
||||
if (val) {
|
||||
_keyIdx.value = -1;
|
||||
} else {
|
||||
_keyIdx.value = null;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// Put [switch_] out of [ValueBuilder] to avoid rebuild
|
||||
return _keyIdx.listenVal((v) {
|
||||
final children = <Widget>[switch_];
|
||||
if (v != null) {
|
||||
children.add(_buildKeyAuth());
|
||||
} else {
|
||||
children.add(
|
||||
Input(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
type: TextInputType.text,
|
||||
label: libL10n.pwd,
|
||||
icon: Icons.password,
|
||||
suggestion: false,
|
||||
onSubmitted: (_) => _onSave(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(children: children);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildKeyAuth() {
|
||||
final privateKeyState = ref.watch(privateKeyNotifierProvider);
|
||||
final pkis = privateKeyState.keys;
|
||||
|
||||
final tiles = List<Widget>.generate(pkis.length, (index) {
|
||||
final e = pkis[index];
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 10, right: 15),
|
||||
leading: Radio<int>(value: index),
|
||||
title: Text(e.id, textAlign: TextAlign.start),
|
||||
subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey),
|
||||
trailing: Btn.icon(
|
||||
icon: const Icon(Icons.edit),
|
||||
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: e)),
|
||||
),
|
||||
onTap: () => _keyIdx.value = index,
|
||||
);
|
||||
});
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(libL10n.add),
|
||||
contentPadding: const EdgeInsets.only(left: 23, right: 23),
|
||||
trailing: const Icon(Icons.add),
|
||||
onTap: () => PrivateKeyEditPage.route.go(context),
|
||||
),
|
||||
);
|
||||
return RadioGroup<int>(
|
||||
onChanged: (val) => _keyIdx.value = val,
|
||||
child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEnvs() {
|
||||
return _env.listenVal((val) {
|
||||
final subtitle = val.isEmpty ? null : Text(val.keys.join(','), style: UIs.textGrey);
|
||||
return ListTile(
|
||||
leading: const Icon(HeroIcons.variable),
|
||||
subtitle: subtitle,
|
||||
title: Text(l10n.envVars),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () async {
|
||||
final res = await KvEditor.route.go(context, KvEditorArgs(data: spi?.envs ?? {}));
|
||||
if (res == null) return;
|
||||
_env.value = res;
|
||||
},
|
||||
).cardx;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildMore() {
|
||||
return ExpandTile(
|
||||
title: Text(l10n.more),
|
||||
children: [
|
||||
Input(
|
||||
controller: _logoUrlCtrl,
|
||||
type: TextInputType.url,
|
||||
icon: Icons.image,
|
||||
label: 'Logo URL',
|
||||
hint: 'https://example.com/logo.png',
|
||||
suggestion: false,
|
||||
),
|
||||
_buildAltUrl(),
|
||||
_buildScriptDir(),
|
||||
_buildEnvs(),
|
||||
_buildPVEs(),
|
||||
_buildCustomCmds(),
|
||||
_buildDisabledCmdTypes(),
|
||||
_buildCustomDev(),
|
||||
_buildWOLs(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScriptDir() {
|
||||
return Input(
|
||||
controller: _scriptDirCtrl,
|
||||
type: TextInputType.text,
|
||||
label: '${l10n.remotePath} (Shell ${l10n.install})',
|
||||
icon: Icons.folder,
|
||||
hint: '~/.config/server_box',
|
||||
suggestion: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomDev() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CenterGreyTitle(l10n.specifyDev),
|
||||
ListTile(
|
||||
leading: const Icon(MingCute.question_line),
|
||||
title: TipText(libL10n.note, l10n.specifyDevTip),
|
||||
).cardx,
|
||||
Input(
|
||||
controller: _preferTempDevCtrl,
|
||||
type: TextInputType.text,
|
||||
label: l10n.temperature,
|
||||
icon: MingCute.low_temperature_line,
|
||||
hint: 'nvme-pci-0400',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _netDevCtrl,
|
||||
type: TextInputType.text,
|
||||
label: l10n.net,
|
||||
icon: ZondIcons.network,
|
||||
hint: 'eth0',
|
||||
suggestion: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSystemType() {
|
||||
return _systemType.listenVal((val) {
|
||||
return ListTile(
|
||||
leading: Icon(MingCute.laptop_2_line),
|
||||
title: Text(l10n.system),
|
||||
trailing: PopupMenu<SystemType?>(
|
||||
initialValue: val,
|
||||
items: [
|
||||
PopupMenuItem(value: null, child: Text(libL10n.auto)),
|
||||
PopupMenuItem(value: SystemType.linux, child: Text('Linux')),
|
||||
PopupMenuItem(value: SystemType.bsd, child: Text('BSD')),
|
||||
PopupMenuItem(value: SystemType.windows, child: Text('Windows')),
|
||||
],
|
||||
onSelected: (value) => _systemType.value = value,
|
||||
child: Text(val?.name ?? libL10n.auto, style: TextStyle(color: val == null ? Colors.grey : null)),
|
||||
),
|
||||
).cardx;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildAltUrl() {
|
||||
return Input(
|
||||
controller: _altUrlController,
|
||||
type: TextInputType.url,
|
||||
node: _alterUrlFocus,
|
||||
label: l10n.fallbackSshDest,
|
||||
icon: MingCute.link_line,
|
||||
hint: 'user@ip:port',
|
||||
suggestion: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPVEs() {
|
||||
const addr = 'https://127.0.0.1:8006';
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CenterGreyTitle('PVE'),
|
||||
Input(
|
||||
controller: _pveAddrCtrl,
|
||||
type: TextInputType.url,
|
||||
icon: MingCute.web_line,
|
||||
label: 'URL',
|
||||
hint: addr,
|
||||
suggestion: false,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(MingCute.certificate_line),
|
||||
title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip),
|
||||
trailing: _pveIgnoreCert.listenVal(
|
||||
(v) => Switch(
|
||||
value: v,
|
||||
onChanged: (val) {
|
||||
_pveIgnoreCert.value = val;
|
||||
},
|
||||
),
|
||||
),
|
||||
).cardx,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomCmds() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CenterGreyTitle(l10n.customCmd),
|
||||
_customCmds.listenVal((vals) {
|
||||
return ListTile(
|
||||
leading: const Icon(BoxIcons.bxs_file_json),
|
||||
title: const Text('JSON'),
|
||||
subtitle: vals.isEmpty ? null : Text(vals.keys.join(','), style: UIs.textGrey),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: _onTapCustomItem,
|
||||
);
|
||||
}).cardx,
|
||||
ListTile(
|
||||
leading: const Icon(MingCute.doc_line),
|
||||
title: Text(libL10n.doc),
|
||||
trailing: const Icon(Icons.open_in_new, size: 17),
|
||||
onTap: l10n.customCmdDocUrl.launchUrl,
|
||||
).cardx,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisabledCmdTypes() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CenterGreyTitle('${libL10n.disabled} ${l10n.cmd}'),
|
||||
_disabledCmdTypes.listenVal((disabled) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.disabled_by_default),
|
||||
title: Text('${libL10n.disabled} ${l10n.cmd}'),
|
||||
subtitle: disabled.isEmpty ? null : Text(disabled.join(', '), style: UIs.textGrey),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: _onTapDisabledCmdTypes,
|
||||
);
|
||||
}).cardx,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWOLs() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CenterGreyTitle('Wake On LAN (beta)'),
|
||||
ListTile(
|
||||
leading: const Icon(BoxIcons.bxs_help_circle),
|
||||
title: TipText(libL10n.about, l10n.wolTip),
|
||||
).cardx,
|
||||
Input(
|
||||
controller: _wolMacCtrl,
|
||||
type: TextInputType.text,
|
||||
label: 'MAC ${l10n.addr}',
|
||||
icon: Icons.computer,
|
||||
hint: '00:11:22:33:44:55',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _wolIpCtrl,
|
||||
type: TextInputType.text,
|
||||
label: 'IP ${l10n.addr}',
|
||||
icon: ZondIcons.network,
|
||||
hint: '192.168.1.x',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _wolPwdCtrl,
|
||||
type: TextInputType.text,
|
||||
obscureText: true,
|
||||
label: libL10n.pwd,
|
||||
icon: Icons.password,
|
||||
suggestion: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFAB() {
|
||||
return FloatingActionButton(onPressed: _onSave, child: const Icon(Icons.save));
|
||||
}
|
||||
|
||||
Widget _buildJumpServer() {
|
||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
||||
final srvs = ref
|
||||
.watch(serversNotifierProvider)
|
||||
.servers
|
||||
.values
|
||||
.where((e) => e.jumpId == null)
|
||||
.where((e) => e.id != spi?.id)
|
||||
.toList();
|
||||
final choice = _jumpServer.listenVal((val) {
|
||||
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
|
||||
return Choice<Spi>(
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
value: srv != null ? [srv] : [],
|
||||
builder: (state, _) => Wrap(
|
||||
children: List<Widget>.generate(srvs.length, (index) {
|
||||
final item = srvs[index];
|
||||
return ChoiceChipX<Spi>(
|
||||
label: item.name,
|
||||
state: state,
|
||||
value: item,
|
||||
onSelected: (srv, on) {
|
||||
if (on) {
|
||||
_jumpServer.value = srv.id;
|
||||
} else {
|
||||
_jumpServer.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
return ExpandTile(
|
||||
leading: const Icon(Icons.map),
|
||||
initiallyExpanded: _jumpServer.value != null,
|
||||
childrenPadding: padding,
|
||||
title: Text(l10n.jumpServer),
|
||||
children: [choice],
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildWriteScriptTip() {
|
||||
return Btn.tile(
|
||||
text: libL10n.attention,
|
||||
icon: const Icon(Icons.tips_and_updates, color: Colors.grey),
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: SimpleMarkdown(data: l10n.writeScriptTip),
|
||||
actions: Btnx.oks,
|
||||
);
|
||||
},
|
||||
textStyle: UIs.textGrey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQrScan() {
|
||||
return Btn.tile(
|
||||
text: libL10n.import,
|
||||
icon: const Icon(Icons.qr_code, color: Colors.grey),
|
||||
onTap: () async {
|
||||
final ret = await BarcodeScannerPage.route.go(context, args: const BarcodeScannerPageArgs());
|
||||
final code = ret?.text;
|
||||
if (code == null) return;
|
||||
try {
|
||||
final spi = Spi.fromJson(json.decode(code));
|
||||
_initWithSpi(spi);
|
||||
} catch (e, s) {
|
||||
context.showErrDialog(e, s);
|
||||
}
|
||||
},
|
||||
textStyle: UIs.textGrey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSSHImport() {
|
||||
return Btn.tile(
|
||||
text: l10n.sshConfigImport,
|
||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||
onTap: _onTapSSHImport,
|
||||
textStyle: UIs.textGrey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDelBtn() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.server}(${spi!.name})')),
|
||||
actions: Btn.ok(
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
ref.read(serversNotifierProvider.notifier).delServer(spi!.id);
|
||||
context.pop(true);
|
||||
},
|
||||
red: true,
|
||||
).toList,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
if (spi != null) {
|
||||
_initWithSpi(spi!);
|
||||
} else {
|
||||
// Only for new servers, check SSH config import on first time
|
||||
_checkSSHConfigImport();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _Actions on _ServerEditPageState {
|
||||
void _onTapSSHImport() async {
|
||||
try {
|
||||
final servers = await SSHConfig.parseConfig();
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
dprint('Parsed ${servers.length} servers from SSH config');
|
||||
await _processSSHServers(servers);
|
||||
dprint('Finished processing SSH config servers');
|
||||
} catch (e, s) {
|
||||
_handleImportSSHCfgPermissionIssue(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {
|
||||
dprint('Error importing SSH config: $e');
|
||||
// Check if it's a permission error and offer file picker as fallback
|
||||
if (e is PathAccessException || e.toString().contains('Operation not permitted')) {
|
||||
final useFilePicker = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigPermissionDenied),
|
||||
const SizedBox(height: 8),
|
||||
Text(l10n.sshConfigManualSelect),
|
||||
],
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
if (useFilePicker == true) {
|
||||
await _onTapSSHImportWithFilePicker();
|
||||
}
|
||||
} else {
|
||||
context.showErrDialog(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processSSHServers(List<Spi> servers) async {
|
||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||
|
||||
if (!summary.hasItemsToImport) {
|
||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldImport = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigFoundServers('${summary.total}')),
|
||||
if (summary.hasDuplicates)
|
||||
Text(l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'), style: UIs.textGrey),
|
||||
Text(l10n.sshConfigServersToImport('${summary.toImport}')),
|
||||
const SizedBox(height: 16),
|
||||
...resolved.map((s) => Text('• ${s.name} (${s.user}@${s.ip}:${s.port})')),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
if (shouldImport == true) {
|
||||
for (final server in resolved) {
|
||||
ref.read(serversNotifierProvider.notifier).addServer(server);
|
||||
}
|
||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onTapSSHImportWithFilePicker() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
allowMultiple: false,
|
||||
dialogTitle: 'SSH ${libL10n.select}',
|
||||
);
|
||||
|
||||
if (result?.files.single.path case final path?) {
|
||||
final servers = await SSHConfig.parseConfig(path);
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
await _processSSHServers(servers);
|
||||
}
|
||||
} catch (e, s) {
|
||||
context.showErrDialog(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCustomItem() async {
|
||||
final res = await KvEditor.route.go(context, KvEditorArgs(data: _customCmds.value));
|
||||
if (res == null) return;
|
||||
_customCmds.value = res;
|
||||
}
|
||||
|
||||
void _onTapDisabledCmdTypes() async {
|
||||
final allCmdTypes = ShellCmdType.all;
|
||||
|
||||
// [TimeSeq] depends on the `time` cmd type, so it should be removed from the list
|
||||
allCmdTypes.remove(StatusCmdType.time);
|
||||
|
||||
await _showCmdTypesDialog(allCmdTypes);
|
||||
}
|
||||
|
||||
void _onSave() async {
|
||||
if (_ipController.text.isEmpty) {
|
||||
context.showSnackBar('${libL10n.empty} ${l10n.host}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||
final ok = await context.showRoundDialog<bool>(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue(l10n.useNoPwd)),
|
||||
actions: Btnx.cancelRedOk,
|
||||
);
|
||||
if (ok != true) return;
|
||||
}
|
||||
|
||||
// If [_pubKeyIndex] is -1, it means that the user has not selected
|
||||
if (_keyIdx.value == -1) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
}
|
||||
if (_usernameController.text.isEmpty) {
|
||||
_usernameController.text = 'root';
|
||||
}
|
||||
if (_portController.text.isEmpty) {
|
||||
_portController.text = '22';
|
||||
}
|
||||
final customCmds = _customCmds.value;
|
||||
final custom = ServerCustom(
|
||||
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
|
||||
pveIgnoreCert: _pveIgnoreCert.value,
|
||||
cmds: customCmds.isEmpty ? null : customCmds,
|
||||
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
|
||||
logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,
|
||||
netDev: _netDevCtrl.text.selfNotEmptyOrNull,
|
||||
scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,
|
||||
);
|
||||
|
||||
final wolEmpty = _wolMacCtrl.text.isEmpty && _wolIpCtrl.text.isEmpty && _wolPwdCtrl.text.isEmpty;
|
||||
final wol = wolEmpty
|
||||
? null
|
||||
: WakeOnLanCfg(mac: _wolMacCtrl.text, ip: _wolIpCtrl.text, pwd: _wolPwdCtrl.text.selfNotEmptyOrNull);
|
||||
if (wol != null) {
|
||||
final wolValidation = wol.validate();
|
||||
if (!wolValidation.$2) {
|
||||
context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final spi = Spi(
|
||||
name: _nameController.text.isEmpty ? _ipController.text : _nameController.text,
|
||||
ip: _ipController.text,
|
||||
port: int.parse(_portController.text),
|
||||
user: _usernameController.text,
|
||||
pwd: _passwordController.text.selfNotEmptyOrNull,
|
||||
keyId: _keyIdx.value != null
|
||||
? ref.read(privateKeyNotifierProvider).keys.elementAt(_keyIdx.value!).id
|
||||
: null,
|
||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||
autoConnect: _autoConnect.value,
|
||||
jumpId: _jumpServer.value,
|
||||
custom: custom,
|
||||
wolCfg: wol,
|
||||
envs: _env.value.isEmpty ? null : _env.value,
|
||||
id: widget.args?.spi.id ?? ShortId.generate(),
|
||||
customSystemType: _systemType.value,
|
||||
disabledCmdTypes: _disabledCmdTypes.value.isEmpty ? null : _disabledCmdTypes.value.toList(),
|
||||
);
|
||||
|
||||
if (this.spi == null) {
|
||||
final existsIds = ServerStore.instance.box.keys;
|
||||
if (existsIds.contains(spi.id)) {
|
||||
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||
return;
|
||||
}
|
||||
ref.read(serversNotifierProvider.notifier).addServer(spi);
|
||||
} else {
|
||||
ref.read(serversNotifierProvider.notifier).updateServer(this.spi!, spi);
|
||||
}
|
||||
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
extension _Utils on _ServerEditPageState {
|
||||
void _checkSSHConfigImport() async {
|
||||
final prop = Stores.setting.firstTimeReadSSHCfg;
|
||||
// Only check if it's first time and user hasn't disabled it
|
||||
if (!prop.fetch()) return;
|
||||
|
||||
try {
|
||||
// Check if SSH config exists
|
||||
final (_, configExists) = SSHConfig.configExists();
|
||||
if (!configExists) return;
|
||||
|
||||
// Ask for permission
|
||||
final hasPermission = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigFound),
|
||||
UIs.height7,
|
||||
Text(l10n.sshConfigImportPermission),
|
||||
UIs.height7,
|
||||
Text(l10n.sshConfigImportHelp, style: UIs.textGrey),
|
||||
],
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
prop.put(false);
|
||||
|
||||
if (hasPermission == true) {
|
||||
// Parse and import SSH config
|
||||
final servers = await SSHConfig.parseConfig();
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||
|
||||
if (!summary.hasItemsToImport) {
|
||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Import without asking again since user already gave permission
|
||||
for (final server in resolved) {
|
||||
ref.read(serversNotifierProvider.notifier).addServer(server);
|
||||
}
|
||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||
}
|
||||
} catch (e, s) {
|
||||
_handleImportSSHCfgPermissionIssue(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
|
||||
return context.showRoundDialog(
|
||||
title: '${libL10n.disabled} ${l10n.cmd}',
|
||||
child: SizedBox(
|
||||
width: 270,
|
||||
child: _disabledCmdTypes.listenVal((disabled) {
|
||||
return ListView.builder(
|
||||
itemCount: allCmdTypes.length,
|
||||
itemExtent: 50,
|
||||
itemBuilder: (context, index) {
|
||||
final cmdType = allCmdTypes.elementAtOrNull(index);
|
||||
if (cmdType == null) return UIs.placeholder;
|
||||
final display = cmdType.displayName;
|
||||
return ListTile(
|
||||
leading: Icon(cmdType.sysType.icon, size: 20),
|
||||
title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),
|
||||
trailing: Checkbox(
|
||||
value: disabled.contains(display),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
_disabledCmdTypes.value.add(display);
|
||||
} else {
|
||||
_disabledCmdTypes.value.remove(display);
|
||||
}
|
||||
_disabledCmdTypes.notify();
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
final isDisabled = disabled.contains(display);
|
||||
if (isDisabled) {
|
||||
_disabledCmdTypes.value.remove(display);
|
||||
} else {
|
||||
_disabledCmdTypes.value.add(display);
|
||||
}
|
||||
_disabledCmdTypes.notify();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: Btnx.oks,
|
||||
);
|
||||
}
|
||||
|
||||
void _initWithSpi(Spi spi) {
|
||||
_nameController.text = spi.name;
|
||||
_ipController.text = spi.ip;
|
||||
_portController.text = spi.port.toString();
|
||||
_usernameController.text = spi.user;
|
||||
if (spi.keyId == null) {
|
||||
_passwordController.text = spi.pwd ?? '';
|
||||
} else {
|
||||
_keyIdx.value = ref.read(privateKeyNotifierProvider).keys.indexWhere((e) => e.id == spi.keyId);
|
||||
}
|
||||
|
||||
/// List in dart is passed by pointer, so you need to copy it here
|
||||
_tags.value = spi.tags?.toSet() ?? {};
|
||||
|
||||
_altUrlController.text = spi.alterUrl ?? '';
|
||||
_autoConnect.value = spi.autoConnect;
|
||||
_jumpServer.value = spi.jumpId;
|
||||
|
||||
final custom = spi.custom;
|
||||
if (custom != null) {
|
||||
_pveAddrCtrl.text = custom.pveAddr ?? '';
|
||||
_pveIgnoreCert.value = custom.pveIgnoreCert;
|
||||
_customCmds.value = custom.cmds ?? {};
|
||||
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
|
||||
_logoUrlCtrl.text = custom.logoUrl ?? '';
|
||||
}
|
||||
|
||||
final wol = spi.wolCfg;
|
||||
if (wol != null) {
|
||||
_wolMacCtrl.text = wol.mac;
|
||||
_wolIpCtrl.text = wol.ip;
|
||||
_wolPwdCtrl.text = wol.pwd ?? '';
|
||||
}
|
||||
|
||||
_env.value = spi.envs ?? {};
|
||||
|
||||
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
||||
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
||||
|
||||
_systemType.value = spi.customSystemType;
|
||||
|
||||
final disabledCmdTypes = spi.disabledCmdTypes?.toSet() ?? {};
|
||||
final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName);
|
||||
disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e));
|
||||
_disabledCmdTypes.value = disabledCmdTypes;
|
||||
}
|
||||
}
|
||||
382
lib/view/page/server/edit/actions.dart
Normal file
382
lib/view/page/server/edit/actions.dart
Normal file
@@ -0,0 +1,382 @@
|
||||
part of 'edit.dart';
|
||||
|
||||
extension _Actions on _ServerEditPageState {
|
||||
void _onTapSSHImport() async {
|
||||
try {
|
||||
final servers = await SSHConfig.parseConfig();
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
dprint('Parsed ${servers.length} servers from SSH config');
|
||||
await _processSSHServers(servers);
|
||||
dprint('Finished processing SSH config servers');
|
||||
} catch (e, s) {
|
||||
_handleImportSSHCfgPermissionIssue(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {
|
||||
dprint('Error importing SSH config: $e');
|
||||
// Check if it's a permission error and offer file picker as fallback
|
||||
if (e is PathAccessException ||
|
||||
e.toString().contains('Operation not permitted')) {
|
||||
final useFilePicker = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigPermissionDenied),
|
||||
const SizedBox(height: 8),
|
||||
Text(l10n.sshConfigManualSelect),
|
||||
],
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
if (useFilePicker == true) {
|
||||
await _onTapSSHImportWithFilePicker();
|
||||
}
|
||||
} else {
|
||||
context.showErrDialog(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processSSHServers(List<Spi> servers) async {
|
||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||
|
||||
if (!summary.hasItemsToImport) {
|
||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldImport = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigFoundServers('${summary.total}')),
|
||||
if (summary.hasDuplicates)
|
||||
Text(
|
||||
l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'),
|
||||
style: UIs.textGrey,
|
||||
),
|
||||
Text(l10n.sshConfigServersToImport('${summary.toImport}')),
|
||||
const SizedBox(height: 16),
|
||||
...resolved.map(
|
||||
(s) => Text('• ${s.name} (${s.user}@${s.ip}:${s.port})'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
if (shouldImport == true) {
|
||||
for (final server in resolved) {
|
||||
ref.read(serversNotifierProvider.notifier).addServer(server);
|
||||
}
|
||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onTapSSHImportWithFilePicker() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
allowMultiple: false,
|
||||
dialogTitle: 'SSH ${libL10n.select}',
|
||||
);
|
||||
|
||||
if (result?.files.single.path case final path?) {
|
||||
final servers = await SSHConfig.parseConfig(path);
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
await _processSSHServers(servers);
|
||||
}
|
||||
} catch (e, s) {
|
||||
context.showErrDialog(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCustomItem() async {
|
||||
final res = await KvEditor.route.go(
|
||||
context,
|
||||
KvEditorArgs(data: _customCmds.value),
|
||||
);
|
||||
if (res == null) return;
|
||||
_customCmds.value = res;
|
||||
}
|
||||
|
||||
void _onTapDisabledCmdTypes() async {
|
||||
final allCmdTypes = ShellCmdType.all;
|
||||
|
||||
// [TimeSeq] depends on the `time` cmd type, so it should be removed from the list
|
||||
allCmdTypes.remove(StatusCmdType.time);
|
||||
|
||||
await _showCmdTypesDialog(allCmdTypes);
|
||||
}
|
||||
|
||||
void _onSave() async {
|
||||
if (_ipController.text.isEmpty) {
|
||||
context.showSnackBar('${libL10n.empty} ${l10n.host}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||
final ok = await context.showRoundDialog<bool>(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue(l10n.useNoPwd)),
|
||||
actions: Btnx.cancelRedOk,
|
||||
);
|
||||
if (ok != true) return;
|
||||
}
|
||||
|
||||
// If [_pubKeyIndex] is -1, it means that the user has not selected
|
||||
if (_keyIdx.value == -1) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
}
|
||||
if (_usernameController.text.isEmpty) {
|
||||
_usernameController.text = 'root';
|
||||
}
|
||||
if (_portController.text.isEmpty) {
|
||||
_portController.text = '22';
|
||||
}
|
||||
final customCmds = _customCmds.value;
|
||||
final custom = ServerCustom(
|
||||
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
|
||||
pveIgnoreCert: _pveIgnoreCert.value,
|
||||
cmds: customCmds.isEmpty ? null : customCmds,
|
||||
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
|
||||
logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,
|
||||
netDev: _netDevCtrl.text.selfNotEmptyOrNull,
|
||||
scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,
|
||||
);
|
||||
|
||||
final wolEmpty =
|
||||
_wolMacCtrl.text.isEmpty &&
|
||||
_wolIpCtrl.text.isEmpty &&
|
||||
_wolPwdCtrl.text.isEmpty;
|
||||
final wol = wolEmpty
|
||||
? null
|
||||
: WakeOnLanCfg(
|
||||
mac: _wolMacCtrl.text,
|
||||
ip: _wolIpCtrl.text,
|
||||
pwd: _wolPwdCtrl.text.selfNotEmptyOrNull,
|
||||
);
|
||||
if (wol != null) {
|
||||
final wolValidation = wol.validate();
|
||||
if (!wolValidation.$2) {
|
||||
context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final spi = Spi(
|
||||
name: _nameController.text.isEmpty
|
||||
? _ipController.text
|
||||
: _nameController.text,
|
||||
ip: _ipController.text,
|
||||
port: int.parse(_portController.text),
|
||||
user: _usernameController.text,
|
||||
pwd: _passwordController.text.selfNotEmptyOrNull,
|
||||
keyId: _keyIdx.value != null
|
||||
? ref
|
||||
.read(privateKeyNotifierProvider)
|
||||
.keys
|
||||
.elementAt(_keyIdx.value!)
|
||||
.id
|
||||
: null,
|
||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||
autoConnect: _autoConnect.value,
|
||||
jumpId: _jumpServer.value,
|
||||
custom: custom,
|
||||
wolCfg: wol,
|
||||
envs: _env.value.isEmpty ? null : _env.value,
|
||||
id: widget.args?.spi.id ?? ShortId.generate(),
|
||||
customSystemType: _systemType.value,
|
||||
disabledCmdTypes: _disabledCmdTypes.value.isEmpty
|
||||
? null
|
||||
: _disabledCmdTypes.value.toList(),
|
||||
);
|
||||
|
||||
if (this.spi == null) {
|
||||
final existsIds = ServerStore.instance.box.keys;
|
||||
if (existsIds.contains(spi.id)) {
|
||||
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||
return;
|
||||
}
|
||||
ref.read(serversNotifierProvider.notifier).addServer(spi);
|
||||
} else {
|
||||
ref.read(serversNotifierProvider.notifier).updateServer(this.spi!, spi);
|
||||
}
|
||||
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
extension _Utils on _ServerEditPageState {
|
||||
void _checkSSHConfigImport() async {
|
||||
final prop = Stores.setting.firstTimeReadSSHCfg;
|
||||
// Only check if it's first time and user hasn't disabled it
|
||||
if (!prop.fetch()) return;
|
||||
|
||||
try {
|
||||
// Check if SSH config exists
|
||||
final (_, configExists) = SSHConfig.configExists();
|
||||
if (!configExists) return;
|
||||
|
||||
// Ask for permission
|
||||
final hasPermission = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigFound),
|
||||
UIs.height7,
|
||||
Text(l10n.sshConfigImportPermission),
|
||||
UIs.height7,
|
||||
Text(l10n.sshConfigImportHelp, style: UIs.textGrey),
|
||||
],
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
prop.put(false);
|
||||
|
||||
if (hasPermission == true) {
|
||||
// Parse and import SSH config
|
||||
final servers = await SSHConfig.parseConfig();
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||
|
||||
if (!summary.hasItemsToImport) {
|
||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Import without asking again since user already gave permission
|
||||
for (final server in resolved) {
|
||||
ref.read(serversNotifierProvider.notifier).addServer(server);
|
||||
}
|
||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||
}
|
||||
} catch (e, s) {
|
||||
_handleImportSSHCfgPermissionIssue(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
|
||||
return context.showRoundDialog(
|
||||
title: '${libL10n.disabled} ${l10n.cmd}',
|
||||
child: SizedBox(
|
||||
width: 270,
|
||||
child: _disabledCmdTypes.listenVal((disabled) {
|
||||
return ListView.builder(
|
||||
itemCount: allCmdTypes.length,
|
||||
itemExtent: 50,
|
||||
itemBuilder: (context, index) {
|
||||
final cmdType = allCmdTypes.elementAtOrNull(index);
|
||||
if (cmdType == null) return UIs.placeholder;
|
||||
final display = cmdType.displayName;
|
||||
return ListTile(
|
||||
leading: Icon(cmdType.sysType.icon, size: 20),
|
||||
title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),
|
||||
trailing: Checkbox(
|
||||
value: disabled.contains(display),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
_disabledCmdTypes.value.add(display);
|
||||
} else {
|
||||
_disabledCmdTypes.value.remove(display);
|
||||
}
|
||||
_disabledCmdTypes.notify();
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
final isDisabled = disabled.contains(display);
|
||||
if (isDisabled) {
|
||||
_disabledCmdTypes.value.remove(display);
|
||||
} else {
|
||||
_disabledCmdTypes.value.add(display);
|
||||
}
|
||||
_disabledCmdTypes.notify();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: Btnx.oks,
|
||||
);
|
||||
}
|
||||
|
||||
void _initWithSpi(Spi spi) {
|
||||
_nameController.text = spi.name;
|
||||
_ipController.text = spi.ip;
|
||||
_portController.text = spi.port.toString();
|
||||
_usernameController.text = spi.user;
|
||||
if (spi.keyId == null) {
|
||||
_passwordController.text = spi.pwd ?? '';
|
||||
} else {
|
||||
_keyIdx.value = ref
|
||||
.read(privateKeyNotifierProvider)
|
||||
.keys
|
||||
.indexWhere((e) => e.id == spi.keyId);
|
||||
}
|
||||
|
||||
/// List in dart is passed by pointer, so you need to copy it here
|
||||
_tags.value = spi.tags?.toSet() ?? {};
|
||||
|
||||
_altUrlController.text = spi.alterUrl ?? '';
|
||||
_autoConnect.value = spi.autoConnect;
|
||||
_jumpServer.value = spi.jumpId;
|
||||
|
||||
final custom = spi.custom;
|
||||
if (custom != null) {
|
||||
_pveAddrCtrl.text = custom.pveAddr ?? '';
|
||||
_pveIgnoreCert.value = custom.pveIgnoreCert;
|
||||
_customCmds.value = custom.cmds ?? {};
|
||||
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
|
||||
_logoUrlCtrl.text = custom.logoUrl ?? '';
|
||||
}
|
||||
|
||||
final wol = spi.wolCfg;
|
||||
if (wol != null) {
|
||||
_wolMacCtrl.text = wol.mac;
|
||||
_wolIpCtrl.text = wol.ip;
|
||||
_wolPwdCtrl.text = wol.pwd ?? '';
|
||||
}
|
||||
|
||||
_env.value = spi.envs ?? {};
|
||||
|
||||
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
||||
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
||||
|
||||
_systemType.value = spi.customSystemType;
|
||||
|
||||
final disabledCmdTypes = spi.disabledCmdTypes?.toSet() ?? {};
|
||||
final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName);
|
||||
disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e));
|
||||
_disabledCmdTypes.value = disabledCmdTypes;
|
||||
}
|
||||
}
|
||||
221
lib/view/page/server/edit/edit.dart
Normal file
221
lib/view/page/server/edit/edit.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:choice/choice.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/core/utils/server_dedup.dart';
|
||||
import 'package:server_box/core/utils/ssh_config.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
import 'package:server_box/data/provider/private_key.dart';
|
||||
import 'package:server_box/data/provider/server/all.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
import 'package:server_box/view/page/private_key/edit.dart';
|
||||
|
||||
part 'actions.dart';
|
||||
part 'widget.dart';
|
||||
|
||||
class ServerEditPage extends ConsumerStatefulWidget {
|
||||
final SpiRequiredArgs? args;
|
||||
|
||||
const ServerEditPage({super.key, this.args});
|
||||
|
||||
static const route = AppRoute<bool, SpiRequiredArgs>(
|
||||
page: ServerEditPage.new,
|
||||
path: '/servers/edit',
|
||||
);
|
||||
|
||||
@override
|
||||
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
|
||||
}
|
||||
|
||||
class _ServerEditPageState extends ConsumerState<ServerEditPage>
|
||||
with AfterLayoutMixin {
|
||||
late final spi = widget.args?.spi;
|
||||
final _nameController = TextEditingController();
|
||||
final _ipController = TextEditingController();
|
||||
final _altUrlController = TextEditingController();
|
||||
final _portController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _pveAddrCtrl = TextEditingController();
|
||||
final _preferTempDevCtrl = TextEditingController();
|
||||
final _logoUrlCtrl = TextEditingController();
|
||||
final _wolMacCtrl = TextEditingController();
|
||||
final _wolIpCtrl = TextEditingController();
|
||||
final _wolPwdCtrl = TextEditingController();
|
||||
final _netDevCtrl = TextEditingController();
|
||||
final _scriptDirCtrl = TextEditingController();
|
||||
|
||||
final _nameFocus = FocusNode();
|
||||
final _ipFocus = FocusNode();
|
||||
final _alterUrlFocus = FocusNode();
|
||||
final _portFocus = FocusNode();
|
||||
final _usernameFocus = FocusNode();
|
||||
|
||||
late FocusScopeNode _focusScope;
|
||||
|
||||
/// -1: non selected, null: password, others: index of private key
|
||||
final _keyIdx = ValueNotifier<int?>(null);
|
||||
final _autoConnect = ValueNotifier(true);
|
||||
final _jumpServer = nvn<String?>();
|
||||
final _pveIgnoreCert = ValueNotifier(false);
|
||||
final _env = <String, String>{}.vn;
|
||||
final _customCmds = <String, String>{}.vn;
|
||||
final _tags = <String>{}.vn;
|
||||
final _systemType = ValueNotifier<SystemType?>(null);
|
||||
final _disabledCmdTypes = <String>{}.vn;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_nameController.dispose();
|
||||
_ipController.dispose();
|
||||
_altUrlController.dispose();
|
||||
_portController.dispose();
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
_preferTempDevCtrl.dispose();
|
||||
_logoUrlCtrl.dispose();
|
||||
_wolMacCtrl.dispose();
|
||||
_wolIpCtrl.dispose();
|
||||
_wolPwdCtrl.dispose();
|
||||
_netDevCtrl.dispose();
|
||||
_scriptDirCtrl.dispose();
|
||||
|
||||
_nameFocus.dispose();
|
||||
_ipFocus.dispose();
|
||||
_alterUrlFocus.dispose();
|
||||
_portFocus.dispose();
|
||||
_usernameFocus.dispose();
|
||||
_pveAddrCtrl.dispose();
|
||||
|
||||
_keyIdx.dispose();
|
||||
_autoConnect.dispose();
|
||||
_jumpServer.dispose();
|
||||
_pveIgnoreCert.dispose();
|
||||
_env.dispose();
|
||||
_customCmds.dispose();
|
||||
_tags.dispose();
|
||||
_systemType.dispose();
|
||||
_disabledCmdTypes.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_focusScope = FocusScope.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actions = <Widget>[];
|
||||
if (spi != null) actions.add(_buildDelBtn());
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _focusScope.unfocus(),
|
||||
child: Scaffold(
|
||||
appBar: CustomAppBar(title: Text(libL10n.edit), actions: actions),
|
||||
body: _buildForm(),
|
||||
floatingActionButton: _buildFAB(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForm() {
|
||||
final topItems = [
|
||||
_buildWriteScriptTip(),
|
||||
if (isMobile) _buildQrScan(),
|
||||
if (isDesktop) _buildSSHImport(),
|
||||
];
|
||||
final children = [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: topItems.joinWith(UIs.width13).toList(),
|
||||
),
|
||||
Input(
|
||||
autoFocus: true,
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
node: _nameFocus,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),
|
||||
hint: libL10n.example,
|
||||
label: libL10n.name,
|
||||
icon: BoxIcons.bx_rename,
|
||||
obscureText: false,
|
||||
autoCorrect: true,
|
||||
suggestion: true,
|
||||
),
|
||||
Input(
|
||||
controller: _ipController,
|
||||
type: TextInputType.url,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_portFocus),
|
||||
node: _ipFocus,
|
||||
label: l10n.host,
|
||||
icon: BoxIcons.bx_server,
|
||||
hint: 'example.com',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _portController,
|
||||
type: TextInputType.number,
|
||||
node: _portFocus,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
|
||||
label: l10n.port,
|
||||
icon: Bootstrap.number_123,
|
||||
hint: '22',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _usernameController,
|
||||
type: TextInputType.text,
|
||||
node: _usernameFocus,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_alterUrlFocus),
|
||||
label: libL10n.user,
|
||||
icon: Icons.account_box,
|
||||
hint: 'root',
|
||||
suggestion: false,
|
||||
),
|
||||
TagTile(
|
||||
tags: _tags,
|
||||
allTags: ref.watch(serversNotifierProvider).tags,
|
||||
).cardx,
|
||||
ListTile(
|
||||
title: Text(l10n.autoConnect),
|
||||
trailing: _autoConnect.listenVal(
|
||||
(val) => Switch(
|
||||
value: val,
|
||||
onChanged: (val) {
|
||||
_autoConnect.value = val;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildAuth(),
|
||||
_buildSystemType(),
|
||||
_buildJumpServer(),
|
||||
_buildMore(),
|
||||
];
|
||||
return AutoMultiList(children: children);
|
||||
}
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
if (spi != null) {
|
||||
_initWithSpi(spi!);
|
||||
} else {
|
||||
// Only for new servers, check SSH config import on first time
|
||||
_checkSSHConfigImport();
|
||||
}
|
||||
}
|
||||
}
|
||||
465
lib/view/page/server/edit/widget.dart
Normal file
465
lib/view/page/server/edit/widget.dart
Normal file
@@ -0,0 +1,465 @@
|
||||
part of 'edit.dart';
|
||||
|
||||
extension _Widgets on _ServerEditPageState {
|
||||
Widget _buildAuth() {
|
||||
final switch_ = ListTile(
|
||||
title: Text(l10n.keyAuth),
|
||||
trailing: _keyIdx.listenVal(
|
||||
(v) => Switch(
|
||||
value: v != null,
|
||||
onChanged: (val) {
|
||||
if (val) {
|
||||
_keyIdx.value = -1;
|
||||
} else {
|
||||
_keyIdx.value = null;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// Put [switch_] out of [ValueBuilder] to avoid rebuild
|
||||
return _keyIdx.listenVal((v) {
|
||||
final children = <Widget>[switch_];
|
||||
if (v != null) {
|
||||
children.add(_buildKeyAuth());
|
||||
} else {
|
||||
children.add(
|
||||
Input(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
type: TextInputType.text,
|
||||
label: libL10n.pwd,
|
||||
icon: Icons.password,
|
||||
suggestion: false,
|
||||
onSubmitted: (_) => _onSave(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(children: children);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildKeyAuth() {
|
||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
||||
final privateKeyState = ref.watch(privateKeyNotifierProvider);
|
||||
final pkis = privateKeyState.keys;
|
||||
|
||||
final choice = _keyIdx.listenVal((val) {
|
||||
final selectedPki = val != null && val >= 0 && val < pkis.length
|
||||
? pkis[val]
|
||||
: null;
|
||||
return Choice<int>(
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
value: selectedPki != null ? [val!] : [],
|
||||
builder: (state, _) => Column(
|
||||
children: [
|
||||
Wrap(
|
||||
children: List<Widget>.generate(pkis.length, (index) {
|
||||
final item = pkis[index];
|
||||
return ChoiceChipX<int>(
|
||||
label: item.id,
|
||||
state: state,
|
||||
value: index,
|
||||
onSelected: (idx, on) {
|
||||
if (on) {
|
||||
_keyIdx.value = idx;
|
||||
} else {
|
||||
_keyIdx.value = -1;
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
UIs.height7,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (selectedPki != null)
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
text: libL10n.edit,
|
||||
onTap: () => PrivateKeyEditPage.route.go(
|
||||
context,
|
||||
args: PrivateKeyEditPageArgs(pki: selectedPki),
|
||||
),
|
||||
),
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.add, size: 20),
|
||||
text: libL10n.add,
|
||||
onTap: () => PrivateKeyEditPage.route.go(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return ExpandTile(
|
||||
leading: const Icon(Icons.key),
|
||||
initiallyExpanded: _keyIdx.value != null && _keyIdx.value! >= 0,
|
||||
childrenPadding: padding,
|
||||
title: Text(l10n.privateKey),
|
||||
children: [choice],
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildEnvs() {
|
||||
return _env.listenVal((val) {
|
||||
final subtitle = val.isEmpty
|
||||
? null
|
||||
: Text(val.keys.join(','), style: UIs.textGrey);
|
||||
return ListTile(
|
||||
leading: const Icon(HeroIcons.variable),
|
||||
subtitle: subtitle,
|
||||
title: Text(l10n.envVars),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () async {
|
||||
final res = await KvEditor.route.go(
|
||||
context,
|
||||
KvEditorArgs(data: spi?.envs ?? {}),
|
||||
);
|
||||
if (res == null) return;
|
||||
_env.value = res;
|
||||
},
|
||||
).cardx;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildMore() {
|
||||
return ExpandTile(
|
||||
title: Text(l10n.more),
|
||||
children: [
|
||||
Input(
|
||||
controller: _logoUrlCtrl,
|
||||
type: TextInputType.url,
|
||||
icon: Icons.image,
|
||||
label: 'Logo URL',
|
||||
hint: 'https://example.com/logo.png',
|
||||
suggestion: false,
|
||||
),
|
||||
_buildAltUrl(),
|
||||
_buildScriptDir(),
|
||||
_buildEnvs(),
|
||||
_buildPVEs(),
|
||||
_buildCustomCmds(),
|
||||
_buildDisabledCmdTypes(),
|
||||
_buildCustomDev(),
|
||||
_buildWOLs(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScriptDir() {
|
||||
return Input(
|
||||
controller: _scriptDirCtrl,
|
||||
type: TextInputType.text,
|
||||
label: '${l10n.remotePath} (Shell ${l10n.install})',
|
||||
icon: Icons.folder,
|
||||
hint: '~/.config/server_box',
|
||||
suggestion: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomDev() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CenterGreyTitle(l10n.specifyDev),
|
||||
ListTile(
|
||||
leading: const Icon(MingCute.question_line),
|
||||
title: TipText(libL10n.note, l10n.specifyDevTip),
|
||||
).cardx,
|
||||
Input(
|
||||
controller: _preferTempDevCtrl,
|
||||
type: TextInputType.text,
|
||||
label: l10n.temperature,
|
||||
icon: MingCute.low_temperature_line,
|
||||
hint: 'nvme-pci-0400',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _netDevCtrl,
|
||||
type: TextInputType.text,
|
||||
label: l10n.net,
|
||||
icon: ZondIcons.network,
|
||||
hint: 'eth0',
|
||||
suggestion: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSystemType() {
|
||||
return _systemType.listenVal((val) {
|
||||
return ListTile(
|
||||
leading: Icon(MingCute.laptop_2_line),
|
||||
title: Text(l10n.system),
|
||||
trailing: PopupMenu<SystemType?>(
|
||||
initialValue: val,
|
||||
items: [
|
||||
PopupMenuItem(value: null, child: Text(libL10n.auto)),
|
||||
PopupMenuItem(value: SystemType.linux, child: Text('Linux')),
|
||||
PopupMenuItem(value: SystemType.bsd, child: Text('BSD')),
|
||||
PopupMenuItem(value: SystemType.windows, child: Text('Windows')),
|
||||
],
|
||||
onSelected: (value) => _systemType.value = value,
|
||||
child: Text(
|
||||
val?.name ?? libL10n.auto,
|
||||
style: TextStyle(color: val == null ? Colors.grey : null),
|
||||
),
|
||||
),
|
||||
).cardx;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildAltUrl() {
|
||||
return Input(
|
||||
controller: _altUrlController,
|
||||
type: TextInputType.url,
|
||||
node: _alterUrlFocus,
|
||||
label: l10n.fallbackSshDest,
|
||||
icon: MingCute.link_line,
|
||||
hint: 'user@ip:port',
|
||||
suggestion: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPVEs() {
|
||||
const addr = 'https://127.0.0.1:8006';
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CenterGreyTitle('PVE'),
|
||||
Input(
|
||||
controller: _pveAddrCtrl,
|
||||
type: TextInputType.url,
|
||||
icon: MingCute.web_line,
|
||||
label: 'URL',
|
||||
hint: addr,
|
||||
suggestion: false,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(MingCute.certificate_line),
|
||||
title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip),
|
||||
trailing: _pveIgnoreCert.listenVal(
|
||||
(v) => Switch(
|
||||
value: v,
|
||||
onChanged: (val) {
|
||||
_pveIgnoreCert.value = val;
|
||||
},
|
||||
),
|
||||
),
|
||||
).cardx,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomCmds() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CenterGreyTitle(l10n.customCmd),
|
||||
_customCmds.listenVal((vals) {
|
||||
return ListTile(
|
||||
leading: const Icon(BoxIcons.bxs_file_json),
|
||||
title: const Text('JSON'),
|
||||
subtitle: vals.isEmpty
|
||||
? null
|
||||
: Text(vals.keys.join(','), style: UIs.textGrey),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: _onTapCustomItem,
|
||||
);
|
||||
}).cardx,
|
||||
ListTile(
|
||||
leading: const Icon(MingCute.doc_line),
|
||||
title: Text(libL10n.doc),
|
||||
trailing: const Icon(Icons.open_in_new, size: 17),
|
||||
onTap: l10n.customCmdDocUrl.launchUrl,
|
||||
).cardx,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisabledCmdTypes() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CenterGreyTitle('${libL10n.disabled} ${l10n.cmd}'),
|
||||
_disabledCmdTypes.listenVal((disabled) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.disabled_by_default),
|
||||
title: Text('${libL10n.disabled} ${l10n.cmd}'),
|
||||
subtitle: disabled.isEmpty
|
||||
? null
|
||||
: Text(disabled.join(', '), style: UIs.textGrey),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: _onTapDisabledCmdTypes,
|
||||
);
|
||||
}).cardx,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWOLs() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CenterGreyTitle('Wake On LAN (beta)'),
|
||||
ListTile(
|
||||
leading: const Icon(BoxIcons.bxs_help_circle),
|
||||
title: TipText(libL10n.about, l10n.wolTip),
|
||||
).cardx,
|
||||
Input(
|
||||
controller: _wolMacCtrl,
|
||||
type: TextInputType.text,
|
||||
label: 'MAC ${l10n.addr}',
|
||||
icon: Icons.computer,
|
||||
hint: '00:11:22:33:44:55',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _wolIpCtrl,
|
||||
type: TextInputType.text,
|
||||
label: 'IP ${l10n.addr}',
|
||||
icon: ZondIcons.network,
|
||||
hint: '192.168.1.x',
|
||||
suggestion: false,
|
||||
),
|
||||
Input(
|
||||
controller: _wolPwdCtrl,
|
||||
type: TextInputType.text,
|
||||
obscureText: true,
|
||||
label: libL10n.pwd,
|
||||
icon: Icons.password,
|
||||
suggestion: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFAB() {
|
||||
return FloatingActionButton(
|
||||
onPressed: _onSave,
|
||||
child: const Icon(Icons.save),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildJumpServer() {
|
||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
||||
final srvs = ref
|
||||
.watch(serversNotifierProvider)
|
||||
.servers
|
||||
.values
|
||||
.where((e) => e.jumpId == null)
|
||||
.where((e) => e.id != spi?.id)
|
||||
.toList();
|
||||
final choice = _jumpServer.listenVal((val) {
|
||||
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
|
||||
return Choice<Spi>(
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
value: srv != null ? [srv] : [],
|
||||
builder: (state, _) => Wrap(
|
||||
children: List<Widget>.generate(srvs.length, (index) {
|
||||
final item = srvs[index];
|
||||
return ChoiceChipX<Spi>(
|
||||
label: item.name,
|
||||
state: state,
|
||||
value: item,
|
||||
onSelected: (srv, on) {
|
||||
if (on) {
|
||||
_jumpServer.value = srv.id;
|
||||
} else {
|
||||
_jumpServer.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
return ExpandTile(
|
||||
leading: const Icon(Icons.map),
|
||||
initiallyExpanded: _jumpServer.value != null,
|
||||
childrenPadding: padding,
|
||||
title: Text(l10n.jumpServer),
|
||||
children: [choice],
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildWriteScriptTip() {
|
||||
return Btn.tile(
|
||||
text: libL10n.attention,
|
||||
icon: const Icon(Icons.tips_and_updates, color: Colors.grey),
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: SimpleMarkdown(data: l10n.writeScriptTip),
|
||||
actions: Btnx.oks,
|
||||
);
|
||||
},
|
||||
textStyle: UIs.textGrey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQrScan() {
|
||||
return Btn.tile(
|
||||
text: libL10n.import,
|
||||
icon: const Icon(Icons.qr_code, color: Colors.grey),
|
||||
onTap: () async {
|
||||
final ret = await BarcodeScannerPage.route.go(
|
||||
context,
|
||||
args: const BarcodeScannerPageArgs(),
|
||||
);
|
||||
final code = ret?.text;
|
||||
if (code == null) return;
|
||||
try {
|
||||
final spi = Spi.fromJson(json.decode(code));
|
||||
_initWithSpi(spi);
|
||||
} catch (e, s) {
|
||||
context.showErrDialog(e, s);
|
||||
}
|
||||
},
|
||||
textStyle: UIs.textGrey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSSHImport() {
|
||||
return Btn.tile(
|
||||
text: l10n.sshConfigImport,
|
||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||
onTap: _onTapSSHImport,
|
||||
textStyle: UIs.textGrey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDelBtn() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(
|
||||
libL10n.askContinue(
|
||||
'${libL10n.delete} ${l10n.server}(${spi!.name})',
|
||||
),
|
||||
),
|
||||
actions: Btn.ok(
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
ref.read(serversNotifierProvider.notifier).delServer(spi!.id);
|
||||
context.pop(true);
|
||||
},
|
||||
red: true,
|
||||
).toList,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import 'package:server_box/data/provider/server/single.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/server/detail/view.dart';
|
||||
import 'package:server_box/view/page/server/edit.dart';
|
||||
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||
import 'package:server_box/view/page/setting/entry.dart';
|
||||
import 'package:server_box/view/widget/percent_circle.dart';
|
||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||
|
||||
@@ -8,6 +8,7 @@ extension _App on _AppSettingsPageState {
|
||||
_buildThemeMode(),
|
||||
_buildAppColor(),
|
||||
_buildCheckUpdate(),
|
||||
_buildHomeTabs(),
|
||||
PlatformPublicSettings.buildBioAuth,
|
||||
if (specific != null) specific,
|
||||
_buildAppMore(),
|
||||
@@ -274,4 +275,15 @@ extension _App on _AppSettingsPageState {
|
||||
trailing: StoreSwitch(prop: _setting.hideTitleBar),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHomeTabs() {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.tab),
|
||||
title: Text(l10n.homeTabs),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () {
|
||||
HomeTabsConfigPage.route.go(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
130
lib/view/page/setting/entries/home_tabs.dart
Normal file
130
lib/view/page/setting/entries/home_tabs.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/app/tab.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
class HomeTabsConfigPage extends ConsumerStatefulWidget {
|
||||
const HomeTabsConfigPage({super.key});
|
||||
|
||||
static final route = AppRouteNoArg(page: HomeTabsConfigPage.new, path: '/settings/home-tabs');
|
||||
|
||||
@override
|
||||
ConsumerState<HomeTabsConfigPage> createState() => _HomeTabsConfigPageState();
|
||||
}
|
||||
|
||||
class _HomeTabsConfigPageState extends ConsumerState<HomeTabsConfigPage> {
|
||||
final _availableTabs = AppTab.values;
|
||||
var _selectedTabs = List<AppTab>.from(Stores.setting.homeTabs.fetch());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(l10n.homeTabs),
|
||||
actions: [
|
||||
TextButton(onPressed: _resetToDefault, child: Text(libL10n.reset)),
|
||||
TextButton(onPressed: _saveAndExit, child: Text(libL10n.save)),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(l10n.homeTabsCustomizeDesc, style: context.theme.textTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
child: ReorderableListView.builder(
|
||||
itemCount: _selectedTabs.length,
|
||||
onReorder: _onReorder,
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) {
|
||||
final tab = _selectedTabs[index];
|
||||
return _buildTabItem(tab, index, true);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(l10n.availableTabs, style: context.theme.textTheme.titleMedium),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _availableTabs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tab = _availableTabs[index];
|
||||
if (_selectedTabs.contains(tab)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return _buildTabItem(tab, index, false);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabItem(AppTab tab, int index, bool isSelected) {
|
||||
final canRemove = _selectedTabs.length > 1;
|
||||
final child = ListTile(
|
||||
leading: tab.navDestination.icon,
|
||||
title: Text(tab.navDestination.label),
|
||||
trailing: isSelected
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: canRemove ? () => _removeTab(tab) : null,
|
||||
color: canRemove ? null : Theme.of(context).disabledColor,
|
||||
tooltip: canRemove ? libL10n.delete : l10n.atLeastOneTab,
|
||||
)
|
||||
: IconButton(icon: const Icon(Icons.add), onPressed: () => _addTab(tab)),
|
||||
onTap: isSelected && canRemove ? () => _removeTab(tab) : null,
|
||||
);
|
||||
|
||||
return Card(
|
||||
key: ValueKey(tab.name),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: isSelected ? ReorderableDragStartListener(index: index, child: child) : child,
|
||||
);
|
||||
}
|
||||
|
||||
void _onReorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final tab = _selectedTabs.removeAt(oldIndex);
|
||||
_selectedTabs.insert(newIndex, tab);
|
||||
});
|
||||
}
|
||||
|
||||
void _addTab(AppTab tab) {
|
||||
setState(() {
|
||||
_selectedTabs.add(tab);
|
||||
});
|
||||
}
|
||||
|
||||
void _removeTab(AppTab tab) {
|
||||
if (_selectedTabs.length <= 1) {
|
||||
context.showSnackBar(l10n.atLeastOneTab);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedTabs.remove(tab);
|
||||
});
|
||||
}
|
||||
|
||||
void _saveAndExit() {
|
||||
Stores.setting.homeTabs.put(_selectedTabs);
|
||||
context.pop();
|
||||
}
|
||||
|
||||
void _resetToDefault() {
|
||||
setState(() {
|
||||
_selectedTabs = List<AppTab>.from(AppTab.values);
|
||||
});
|
||||
Stores.setting.homeTabs.put(_selectedTabs);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -17,6 +17,8 @@ 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/entries/home_tabs.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';
|
||||
|
||||
@@ -108,14 +108,13 @@ class _IosSettingsPageState extends State<IosSettingsPage> {
|
||||
}
|
||||
|
||||
void _onTapWatchApp(Map<String, dynamic> map) async {
|
||||
final urls = Map<String, String>.from(map['urls'] as Map? ?? {});
|
||||
final result = await KvEditor.route.go(context, KvEditorArgs(data: urls));
|
||||
final cfgs = List<String>.from(map['urls'] as List? ?? []);
|
||||
final result = await JsonListEditor.route.go(context, JsonListEditorArgs(data: cfgs));
|
||||
if (result == null) return;
|
||||
|
||||
final (_, err) = await context.showLoadingDialog(
|
||||
fn: () async {
|
||||
final data = {'urls': result};
|
||||
|
||||
// Try realtime update (app must be running foreground).
|
||||
try {
|
||||
if (await wc.isReachable) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/server/all.dart';
|
||||
import 'package:server_box/view/page/server/edit.dart';
|
||||
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||
|
||||
class SSHTabPage extends ConsumerStatefulWidget {
|
||||
|
||||
@@ -471,7 +471,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
||||
@@ -481,7 +481,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "Server Box";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -608,7 +608,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
||||
@@ -618,7 +618,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "Server Box";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -638,7 +638,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1231;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -649,7 +649,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MARKETING_VERSION = 1.0.1231;
|
||||
MARKETING_VERSION = 1.0.1241;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "Server Box";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
76
pubspec.lock
76
pubspec.lock
@@ -189,18 +189,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_android_camerax
|
||||
sha256: "58b8fe843a3c83fd1273c00cb35f5a8ae507f6cc9b2029bcf7e2abba499e28d8"
|
||||
sha256: "2d438248554f44766bf9ea34c117a5bb0074e241342ef7c22c768fb431335234"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.19+1"
|
||||
version: "0.6.21"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_avfoundation
|
||||
sha256: e4aca5bccaf897b70cac87e5fdd789393310985202442837922fd40325e2733b
|
||||
sha256: "951ef122d01ebba68b7a54bfe294e8b25585635a90465c311b2f875ae72c412f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.21+1"
|
||||
version: "0.9.21+2"
|
||||
camera_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -464,10 +464,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: ef7d2a085c1b1d69d17b6842d0734aad90156de08df6bd3c12496d0bd6ddf8e2
|
||||
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.1"
|
||||
version: "10.3.2"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -489,16 +489,16 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7"
|
||||
sha256: d3f82f4a38e33ba23d05a08ff304d7d8b22d2a59a5503f20bd802966e915db89
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
fl_lib:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "v1.0.345"
|
||||
resolved-ref: "1b797643ef7603dd825caf96a6c57b88dbd23c34"
|
||||
ref: "v1.0.346"
|
||||
resolved-ref: f277b7a4259e45889320ef6d80ab320662558784
|
||||
url: "https://github.com/lppcg/fl_lib"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
@@ -580,10 +580,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
|
||||
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.29"
|
||||
version: "2.0.30"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -644,10 +644,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: de82e6bf958cec7190fbc1c5298282c851228e35ae2b14e2b103e7f777818c64
|
||||
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
version: "2.2.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -682,6 +682,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
get_it:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: get_it
|
||||
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -727,10 +735,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive_ce_flutter
|
||||
sha256: a0989670652eab097b47544f1e5a4456e861b1b01b050098ea0b80a5fabe9909
|
||||
sha256: f5bd57fda84402bca7557fedb8c629c96c8ea10fab4a542968d7b60864ca02cc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
hive_ce_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -911,10 +919,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "316503f6772dea9c0c038bb7aac4f68ab00112d707d258c770f7fc3c250a2d88"
|
||||
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.51"
|
||||
version: "1.0.52"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1071,10 +1079,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||
sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.17"
|
||||
version: "2.2.18"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1184,10 +1192,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1224,10 +1232,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr_code_dart_scan
|
||||
sha256: b23879821242bc2c1b2d3a035d96d453a5554c86894a69e10726b559206bcafb
|
||||
sha256: "1b317b47f475f6995c19e0f41d790902a8cd158b23c435d936763d86ba44309c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.2"
|
||||
version: "0.11.3"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1368,10 +1376,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
|
||||
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.11"
|
||||
version: "2.4.12"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1613,10 +1621,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
|
||||
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.17"
|
||||
version: "6.3.18"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1685,18 +1693,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.11+1"
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
|
||||
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.11+1"
|
||||
version: "1.1.19"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1750,10 +1758,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.3"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: server_box
|
||||
description: server status & toolbox app.
|
||||
publish_to: "none"
|
||||
version: 1.0.1231+1231
|
||||
version: 1.0.1241+1241
|
||||
|
||||
environment:
|
||||
sdk: ">=3.9.0"
|
||||
@@ -63,8 +63,9 @@ dependencies:
|
||||
fl_lib:
|
||||
git:
|
||||
url: https://github.com/lppcg/fl_lib
|
||||
ref: v1.0.345
|
||||
ref: v1.0.346
|
||||
flutter_gbk2utf8: ^1.0.1
|
||||
get_it: ^8.2.0
|
||||
|
||||
dependency_overrides:
|
||||
# webdav_client_plus:
|
||||
@@ -77,7 +78,7 @@ dependency_overrides:
|
||||
# path: ../fl_lib
|
||||
# fl_build:
|
||||
# path: ../fl_build
|
||||
gtk:
|
||||
gtk: # TODO: remove it after fixed in upstream
|
||||
git:
|
||||
url: https://github.com/lollipopkit/gtk.dart
|
||||
ref: v0.0.36
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:server_box/data/model/container/ps.dart';
|
||||
import 'package:server_box/data/model/container/status.dart';
|
||||
|
||||
void main() {
|
||||
test('docker ps parse', () {
|
||||
@@ -26,7 +27,92 @@ fa1215b4be74 Up 12 hours firefly
|
||||
expect(ps.names, names[idx - 1]);
|
||||
expect(ps.image, images[idx - 1]);
|
||||
expect(ps.state, states[idx - 1]);
|
||||
expect(ps.running, true);
|
||||
expect(ps.status, ContainerStatus.running);
|
||||
expect(ps.status.isRunning, true);
|
||||
}
|
||||
});
|
||||
|
||||
test('docker ps status detection', () {
|
||||
// Test various Docker container states
|
||||
final testCases = [
|
||||
// Running states
|
||||
{'state': 'Up 2 minutes', 'status': ContainerStatus.running},
|
||||
{'state': 'Up 1 hour', 'status': ContainerStatus.running},
|
||||
{'state': 'UP 30 seconds', 'status': ContainerStatus.running}, // Case insensitive
|
||||
{'state': 'up 5 days', 'status': ContainerStatus.running}, // Case insensitive
|
||||
|
||||
// Non-running states
|
||||
{'state': 'Exited (0) 5 minutes ago', 'status': ContainerStatus.exited},
|
||||
{'state': 'Created', 'status': ContainerStatus.created},
|
||||
{'state': 'Paused', 'status': ContainerStatus.paused},
|
||||
{'state': 'Restarting', 'status': ContainerStatus.restarting},
|
||||
{'state': 'Removing', 'status': ContainerStatus.removing},
|
||||
{'state': 'Dead', 'status': ContainerStatus.dead},
|
||||
|
||||
// Edge cases
|
||||
{'state': null, 'status': ContainerStatus.unknown},
|
||||
{'state': '', 'status': ContainerStatus.unknown},
|
||||
{'state': 'Some Unknown Status', 'status': ContainerStatus.unknown},
|
||||
];
|
||||
|
||||
for (final testCase in testCases) {
|
||||
final ps = DockerPs(id: 'test', state: testCase['state'] as String?);
|
||||
final expectedStatus = testCase['status'] as ContainerStatus;
|
||||
expect(
|
||||
ps.status,
|
||||
expectedStatus,
|
||||
reason: 'State "${testCase['state']}" should be ${expectedStatus.name}'
|
||||
);
|
||||
|
||||
// Test status.isRunning method
|
||||
expect(
|
||||
ps.status.isRunning,
|
||||
expectedStatus.isRunning,
|
||||
reason: 'State "${testCase['state']}" isRunning should match status.isRunning'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('podman ps status detection', () {
|
||||
final testCases = [
|
||||
{'exited': false, 'status': ContainerStatus.running},
|
||||
{'exited': true, 'status': ContainerStatus.exited},
|
||||
{'exited': null, 'status': ContainerStatus.unknown},
|
||||
];
|
||||
|
||||
for (final testCase in testCases) {
|
||||
final ps = PodmanPs(id: 'test', exited: testCase['exited'] as bool?);
|
||||
final expectedStatus = testCase['status'] as ContainerStatus;
|
||||
expect(
|
||||
ps.status,
|
||||
expectedStatus,
|
||||
reason: 'Exited "${testCase['exited']}" should be ${expectedStatus.name}'
|
||||
);
|
||||
|
||||
// Test status.isRunning method
|
||||
expect(
|
||||
ps.status.isRunning,
|
||||
expectedStatus.isRunning,
|
||||
reason: 'Exited "${testCase['exited']}" isRunning should match status.isRunning'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('container status utility methods', () {
|
||||
expect(ContainerStatus.running.isRunning, true);
|
||||
expect(ContainerStatus.exited.isRunning, false);
|
||||
expect(ContainerStatus.created.isRunning, false);
|
||||
|
||||
expect(ContainerStatus.exited.canStart, true);
|
||||
expect(ContainerStatus.created.canStart, true);
|
||||
expect(ContainerStatus.running.canStart, false);
|
||||
|
||||
expect(ContainerStatus.running.canStop, true);
|
||||
expect(ContainerStatus.paused.canStop, true);
|
||||
expect(ContainerStatus.exited.canStop, false);
|
||||
|
||||
expect(ContainerStatus.running.canRestart, true);
|
||||
expect(ContainerStatus.removing.canRestart, false);
|
||||
expect(ContainerStatus.unknown.canRestart, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,6 +81,138 @@ void main() {
|
||||
expect(usage.usedPercent, 50);
|
||||
// This would use the "unknown" fallback for kname
|
||||
});
|
||||
|
||||
test('parse df -k output (fallback mode)', () {
|
||||
final disks = Disk.parse(_dfOutput);
|
||||
expect(disks, isNotEmpty);
|
||||
expect(disks.length, 3); // Should find 3 valid filesystems: udev, /dev/vda3, /dev/vda2
|
||||
|
||||
// Verify root filesystem
|
||||
final rootFs = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(rootFs.path, '/dev/vda3');
|
||||
expect(rootFs.usedPercent, 47);
|
||||
expect(rootFs.size, BigInt.from(40910528 ~/ 1024)); // df -k output divided by 1024 = MB
|
||||
expect(rootFs.used, BigInt.from(18067948 ~/ 1024));
|
||||
expect(rootFs.avail, BigInt.from(20951380 ~/ 1024));
|
||||
|
||||
// Verify boot/efi filesystem
|
||||
final efiFs = disks.firstWhere((disk) => disk.mount == '/boot/efi');
|
||||
expect(efiFs.path, '/dev/vda2');
|
||||
expect(efiFs.usedPercent, 7);
|
||||
expect(efiFs.size, BigInt.from(192559 ~/ 1024));
|
||||
|
||||
// Verify udev filesystem is included (virtual filesystem)
|
||||
final udevFs = disks.firstWhere((disk) => disk.path == 'udev');
|
||||
expect(udevFs.mount, '/dev');
|
||||
expect(udevFs.usedPercent, 0);
|
||||
expect(udevFs.size, BigInt.from(864088 ~/ 1024));
|
||||
});
|
||||
|
||||
test('handle empty input gracefully', () {
|
||||
final disks = Disk.parse('');
|
||||
expect(disks, isEmpty);
|
||||
});
|
||||
|
||||
test('handle whitespace-only input', () {
|
||||
final disks = Disk.parse(' \n\t \r\n ');
|
||||
expect(disks, isEmpty);
|
||||
});
|
||||
|
||||
test('handle JSON with null filesystem fields', () {
|
||||
final disks = Disk.parse(_jsonWithNullFields);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should handle null filesystem fields gracefully
|
||||
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(disk.size, BigInt.zero);
|
||||
expect(disk.used, BigInt.zero);
|
||||
expect(disk.avail, BigInt.zero);
|
||||
expect(disk.usedPercent, 0);
|
||||
});
|
||||
|
||||
test('handle JSON with string "null" values', () {
|
||||
final disks = Disk.parse(_jsonWithStringNulls);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should handle string "null" filesystem fields gracefully
|
||||
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(disk.size, BigInt.zero);
|
||||
expect(disk.used, BigInt.zero);
|
||||
expect(disk.avail, BigInt.zero);
|
||||
expect(disk.usedPercent, 0);
|
||||
});
|
||||
|
||||
test('handle JSON with empty string values', () {
|
||||
final disks = Disk.parse(_jsonWithEmptyStrings);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should handle empty string filesystem fields gracefully
|
||||
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(disk.size, BigInt.zero);
|
||||
expect(disk.used, BigInt.zero);
|
||||
expect(disk.avail, BigInt.zero);
|
||||
expect(disk.usedPercent, 0);
|
||||
});
|
||||
|
||||
test('handle JSON with invalid percentage format', () {
|
||||
final disks = Disk.parse(_jsonWithInvalidPercent);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should handle invalid percentage gracefully
|
||||
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(disk.usedPercent, 0);
|
||||
});
|
||||
|
||||
test('handle JSON with malformed numbers', () {
|
||||
final disks = Disk.parse(_jsonWithMalformedNumbers);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should handle malformed numbers gracefully
|
||||
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(disk.size, BigInt.zero);
|
||||
expect(disk.used, BigInt.zero);
|
||||
expect(disk.avail, BigInt.zero);
|
||||
});
|
||||
|
||||
test('handle JSON parsing errors gracefully', () {
|
||||
final disks = Disk.parse(_malformedJson);
|
||||
expect(disks, isEmpty); // Should fallback to legacy method, which also fails
|
||||
});
|
||||
|
||||
test('handle df output with missing fields', () {
|
||||
final disks = Disk.parse(_dfWithMissingFields);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should handle missing fields gracefully
|
||||
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(disk.usedPercent, 47);
|
||||
});
|
||||
|
||||
test('handle df output with inconsistent formatting', () {
|
||||
final disks = Disk.parse(_dfWithInconsistentFormatting);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should handle inconsistent formatting
|
||||
expect(disks.length, greaterThan(0));
|
||||
});
|
||||
|
||||
test('handle lsblk with success marker', () {
|
||||
final disks = Disk.parse(_lsblkWithSuccessMarker);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should parse JSON and ignore success marker
|
||||
final rootFs = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(rootFs.fsTyp, 'ext4');
|
||||
expect(rootFs.usedPercent, 56);
|
||||
});
|
||||
|
||||
test('handle malformed lsblk output fallback', () {
|
||||
final disks = Disk.parse(_malformedLsblkWithDfFallback);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
// Should fallback to df -k parsing when lsblk output is malformed
|
||||
expect(disks.length, 3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,3 +410,151 @@ overlay 1907116416 5470
|
||||
v2000pro/pve 1906694784 125440 1906569344 1% /mnt/v2000pro/pve
|
||||
v2000pro/download 1906569472 128 1906569344 1% /mnt/v2000pro/download''',
|
||||
];
|
||||
|
||||
const _dfOutput = '''
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
udev 864088 0 864088 0% /dev
|
||||
tmpfs 176724 688 176036 1% /run
|
||||
/dev/vda3 40910528 18067948 20951380 47% /
|
||||
tmpfs 883612 0 883612 0% /dev/shm
|
||||
tmpfs 5120 0 5120 0% /run/lock
|
||||
/dev/vda2 192559 11807 180752 7% /boot/efi
|
||||
tmpfs 176720 104 176616 1% /run/user/1000
|
||||
''';
|
||||
|
||||
// Test data for edge cases
|
||||
const _jsonWithNullFields = '''
|
||||
{
|
||||
"blockdevices": [
|
||||
{
|
||||
"fstype": "ext4",
|
||||
"mountpoint": "/",
|
||||
"fssize": null,
|
||||
"fsused": null,
|
||||
"fsavail": null,
|
||||
"fsuse%": null,
|
||||
"path": "/dev/sda1"
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
|
||||
const _jsonWithStringNulls = '''
|
||||
{
|
||||
"blockdevices": [
|
||||
{
|
||||
"fstype": "ext4",
|
||||
"mountpoint": "/",
|
||||
"fssize": "null",
|
||||
"fsused": "null",
|
||||
"fsavail": "null",
|
||||
"fsuse%": "null",
|
||||
"path": "/dev/sda1"
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
|
||||
const _jsonWithEmptyStrings = '''
|
||||
{
|
||||
"blockdevices": [
|
||||
{
|
||||
"fstype": "ext4",
|
||||
"mountpoint": "/",
|
||||
"fssize": "",
|
||||
"fsused": "",
|
||||
"fsavail": "",
|
||||
"fsuse%": "",
|
||||
"path": "/dev/sda1"
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
|
||||
const _jsonWithInvalidPercent = '''
|
||||
{
|
||||
"blockdevices": [
|
||||
{
|
||||
"fstype": "ext4",
|
||||
"mountpoint": "/",
|
||||
"fssize": "1000000",
|
||||
"fsused": "500000",
|
||||
"fsavail": "500000",
|
||||
"fsuse%": "invalid_percent",
|
||||
"path": "/dev/sda1"
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
|
||||
const _jsonWithMalformedNumbers = '''
|
||||
{
|
||||
"blockdevices": [
|
||||
{
|
||||
"fstype": "ext4",
|
||||
"mountpoint": "/",
|
||||
"fssize": "not_a_number",
|
||||
"fsused": "invalid",
|
||||
"fsavail": "broken",
|
||||
"fsuse%": "50%",
|
||||
"path": "/dev/sda1"
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
|
||||
const _malformedJson = '''
|
||||
{
|
||||
"blockdevices": [
|
||||
{
|
||||
"fstype": "ext4",
|
||||
"mountpoint": "/",
|
||||
"fssize": "1000000",
|
||||
"fsused": "500000",
|
||||
"fsavail": "500000",
|
||||
"fsuse%": "50%",
|
||||
"path": "/dev/sda1"
|
||||
}
|
||||
]
|
||||
// Missing closing brace and malformed structure
|
||||
''';
|
||||
|
||||
const _dfWithMissingFields = '''
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
/dev/vda3 40910528 18067948 20951380 47% /
|
||||
''';
|
||||
|
||||
const _dfWithInconsistentFormatting = '''
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
/dev/sda1 1000000 500000 500000 50% /
|
||||
/dev/sda2 2000000 1000000 1000000 50% /home
|
||||
udev 864088 0 864088 0% /dev
|
||||
''';
|
||||
|
||||
const _lsblkWithSuccessMarker = '''
|
||||
{
|
||||
"blockdevices": [
|
||||
{
|
||||
"fstype": "ext4",
|
||||
"mountpoint": "/",
|
||||
"fssize": 982141468672,
|
||||
"fsused": 552718364672,
|
||||
"fsavail": 379457622016,
|
||||
"fsuse%": "56%",
|
||||
"path": "/dev/sda1"
|
||||
}
|
||||
]
|
||||
}
|
||||
LSBLK_SUCCESS
|
||||
''';
|
||||
|
||||
const _malformedLsblkWithDfFallback = '''
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
udev 864088 0 864088 0% /dev
|
||||
tmpfs 176724 688 176036 1% /run
|
||||
/dev/vda3 40910528 18067948 20951380 47% /
|
||||
tmpfs 883612 0 883612 0% /dev/shm
|
||||
tmpfs 5120 0 5120 0% /run/lock
|
||||
/dev/vda2 192559 11807 180752 7% /boot/efi
|
||||
tmpfs 176720 104 176616 1% /run/user/1000
|
||||
''';
|
||||
|
||||
Reference in New Issue
Block a user