mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 14:54:34 +01:00
feat: Windows compatibility (#836)
* feat: win compatibility * fix * fix: uptime parse * opt.: linux uptime accuracy * fix: windows temperature fetching * opt. * opt.: powershell exec * refactor: address PR review feedback and improve code quality ### Major Improvements: - **Refactored Windows status parsing**: Broke down large `_getWindowsStatus` method into 13 smaller, focused helper methods for better maintainability and readability - **Extracted system detection logic**: Created dedicated `SystemDetector` helper class to separate OS detection concerns from ServerProvider - **Improved concurrency handling**: Implemented proper synchronization for server updates using Future-based locks to prevent race conditions ### Bug Fixes: - **Fixed CPU percentage parsing**: Removed incorrect '*100' multiplication in BSD CPU parsing (values were already percentages) - **Enhanced memory parsing**: Added validation and error handling to BSD memory fallback parsing with proper logging - **Improved uptime parsing**: Added support for multiple Windows date formats and robust error handling with validation - **Fixed division by zero**: Added safety checks in Swap.usedPercent getter ### Code Quality Enhancements: - **Added comprehensive documentation**: Documented Windows CPU counter limitations and approach - **Strengthened error handling**: Added detailed logging and validation throughout parsing methods - **Improved robustness**: Enhanced BSD CPU parsing with percentage validation and warnings - **Better separation of concerns**: Each parsing method now has single responsibility ### Files Changed: - `lib/data/helper/system_detector.dart` (new): System detection helper - `lib/data/model/server/cpu.dart`: Fixed percentage parsing and added validation - `lib/data/model/server/memory.dart`: Enhanced fallback parsing and division-by-zero protection - `lib/data/model/server/server_status_update_req.dart`: Refactored into 13 focused parsing methods - `lib/data/provider/server.dart`: Improved synchronization and extracted system detection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: parse & shell fn struct --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ English | [简体中文](README_zh.md)
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
A Flutter project which provide charts to display <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> server status and tools to manage server.
|
||||
A Flutter project which provides charts to display Linux, Unix and Windows server status and tools to manage servers.
|
||||
<br>
|
||||
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
|
||||
</p>
|
||||
@@ -26,7 +26,7 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 📥 Install
|
||||
## 📥 Installation
|
||||
|
||||
|Platform| From|
|
||||
|--|--|
|
||||
@@ -36,7 +36,7 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
|
||||
|
||||
Please only download pkgs from the source that **you trust**!
|
||||
|
||||
## 🔖 Feature
|
||||
## 🔖 Features
|
||||
|
||||
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`, `S.M.A.R.T`...
|
||||
- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...
|
||||
@@ -61,7 +61,7 @@ Before you open an issue, please read the following:
|
||||
|
||||
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
|
||||
|
||||
## 🧱 Contribution
|
||||
## 🧱 Contributions
|
||||
|
||||
Any positive contribution is welcome.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
使用 Flutter 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
|
||||
使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱,提供服务器状态图表和管理工具。
|
||||
<br>
|
||||
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>。
|
||||
</p>
|
||||
|
||||
6505
coverage/lcov.info
Normal file
6505
coverage/lcov.info
Normal file
File diff suppressed because it is too large
Load Diff
35
lib/app.dart
35
lib/app.dart
@@ -40,17 +40,13 @@ class MyApp extends StatelessWidget {
|
||||
light: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: UIs.colorSeed,
|
||||
appBarTheme: AppBarTheme(
|
||||
scrolledUnderElevation: 0.0,
|
||||
),
|
||||
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||
),
|
||||
dark: ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorSchemeSeed: UIs.colorSeed,
|
||||
appBarTheme: AppBarTheme(
|
||||
scrolledUnderElevation: 0.0,
|
||||
),
|
||||
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -58,15 +54,8 @@ class MyApp extends StatelessWidget {
|
||||
Widget _buildDynamicColor(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (light, dark) {
|
||||
final lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: light,
|
||||
);
|
||||
final darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: dark,
|
||||
);
|
||||
final lightTheme = ThemeData(useMaterial3: true, colorScheme: light);
|
||||
final darkTheme = ThemeData(useMaterial3: true, brightness: Brightness.dark, colorScheme: dark);
|
||||
if (context.isDark && dark != null) {
|
||||
UIs.primaryColor = dark.primary;
|
||||
} else if (!context.isDark && light != null) {
|
||||
@@ -78,11 +67,7 @@ class MyApp extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildApp(
|
||||
BuildContext ctx, {
|
||||
required ThemeData light,
|
||||
required ThemeData dark,
|
||||
}) {
|
||||
Widget _buildApp(BuildContext ctx, {required ThemeData light, required ThemeData dark}) {
|
||||
final tMode = Stores.setting.themeMode.fetch();
|
||||
// Issue #57
|
||||
final themeMode = switch (tMode) {
|
||||
@@ -103,10 +88,7 @@ class MyApp extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
locale: locale,
|
||||
localizationsDelegates: const [
|
||||
LibLocalizations.delegate,
|
||||
...AppLocalizations.localizationsDelegates,
|
||||
],
|
||||
localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeListResolutionCallback: LocaleUtil.resolve,
|
||||
navigatorObservers: [AppRouteObserver.instance],
|
||||
@@ -128,10 +110,7 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
child = const HomePage();
|
||||
|
||||
return VirtualWindowFrame(
|
||||
title: BuildData.name,
|
||||
child: child,
|
||||
);
|
||||
return VirtualWindowFrame(title: BuildData.name, child: child);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -12,21 +12,9 @@ extension SftpFileX on SftpFileMode {
|
||||
|
||||
UnixPerm toUnixPerm() {
|
||||
return UnixPerm(
|
||||
user: UnixPermOp(
|
||||
r: userRead,
|
||||
w: userWrite,
|
||||
x: userExecute,
|
||||
),
|
||||
group: UnixPermOp(
|
||||
r: groupRead,
|
||||
w: groupWrite,
|
||||
x: groupExecute,
|
||||
),
|
||||
other: UnixPermOp(
|
||||
r: otherRead,
|
||||
w: otherWrite,
|
||||
x: otherExecute,
|
||||
),
|
||||
user: UnixPermOp(r: userRead, w: userWrite, x: userExecute),
|
||||
group: UnixPermOp(r: groupRead, w: groupWrite, x: groupExecute),
|
||||
other: UnixPermOp(r: otherRead, w: otherWrite, x: otherExecute),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
|
||||
@@ -13,6 +14,52 @@ typedef OnStdin = void Function(SSHSession session);
|
||||
typedef PwdRequestFunc = Future<String?> Function(String? user);
|
||||
|
||||
extension SSHClientX on SSHClient {
|
||||
/// Create a persistent PowerShell session for Windows commands
|
||||
Future<(SSHSession, String)> execPowerShell(
|
||||
OnStdin onStdin, {
|
||||
SSHPtyConfig? pty,
|
||||
OnStdout? onStdout,
|
||||
OnStdout? onStderr,
|
||||
bool stdout = true,
|
||||
bool stderr = true,
|
||||
Map<String, String>? env,
|
||||
}) async {
|
||||
final session = await execute(
|
||||
'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
|
||||
pty: pty,
|
||||
environment: env,
|
||||
);
|
||||
|
||||
final result = BytesBuilder(copy: false);
|
||||
final stdoutDone = Completer<void>();
|
||||
final stderrDone = Completer<void>();
|
||||
|
||||
session.stdout.listen(
|
||||
(e) {
|
||||
onStdout?.call(e.string, session);
|
||||
if (stdout) result.add(e);
|
||||
},
|
||||
onDone: stdoutDone.complete,
|
||||
onError: stderrDone.completeError,
|
||||
);
|
||||
|
||||
session.stderr.listen(
|
||||
(e) {
|
||||
onStderr?.call(e.string, session);
|
||||
if (stderr) result.add(e);
|
||||
},
|
||||
onDone: stderrDone.complete,
|
||||
onError: stderrDone.completeError,
|
||||
);
|
||||
|
||||
onStdin(session);
|
||||
|
||||
await stdoutDone.future;
|
||||
await stderrDone.future;
|
||||
|
||||
return (session, result.takeBytes().string);
|
||||
}
|
||||
|
||||
Future<(SSHSession, String)> exec(
|
||||
OnStdin onStdin, {
|
||||
String? entry,
|
||||
@@ -22,9 +69,14 @@ extension SSHClientX on SSHClient {
|
||||
bool stdout = true,
|
||||
bool stderr = true,
|
||||
Map<String, String>? env,
|
||||
SystemType? systemType,
|
||||
}) async {
|
||||
final session = await execute(
|
||||
entry ?? 'cat | sh',
|
||||
entry ??
|
||||
switch (systemType) {
|
||||
SystemType.windows => 'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
|
||||
_ => 'cat | sh',
|
||||
},
|
||||
pty: pty,
|
||||
environment: env,
|
||||
);
|
||||
@@ -81,9 +133,7 @@ extension SSHClientX on SSHClient {
|
||||
isRequestingPwd = true;
|
||||
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
||||
if (context == null) return;
|
||||
final pwd = context.mounted
|
||||
? await context.showPwdDialog(title: user, id: id)
|
||||
: null;
|
||||
final pwd = context.mounted ? await context.showPwdDialog(title: user, id: id) : null;
|
||||
if (pwd == null || pwd.isEmpty) {
|
||||
session.stdin.close();
|
||||
} else {
|
||||
|
||||
@@ -6,10 +6,8 @@ class ChainComparator<T> {
|
||||
ChainComparator.empty() : this._create(null, (a, b) => 0);
|
||||
ChainComparator.create() : this._create(null, (a, b) => 0);
|
||||
|
||||
static ChainComparator<T> comparing<T, F extends Comparable<F>>(
|
||||
F Function(T) extractor) {
|
||||
return ChainComparator._create(
|
||||
null, (a, b) => extractor(a).compareTo(extractor(b)));
|
||||
static ChainComparator<T> comparing<T, F extends Comparable<F>>(F Function(T) extractor) {
|
||||
return ChainComparator._create(null, (a, b) => extractor(a).compareTo(extractor(b)));
|
||||
}
|
||||
|
||||
int compare(T a, T b) {
|
||||
@@ -26,8 +24,9 @@ class ChainComparator<T> {
|
||||
}
|
||||
|
||||
ChainComparator<T> thenCompareBy<F extends Comparable<F>>(
|
||||
F Function(T) extractor,
|
||||
{bool reversed = false}) {
|
||||
F Function(T) extractor, {
|
||||
bool reversed = false,
|
||||
}) {
|
||||
return ChainComparator._create(
|
||||
this,
|
||||
reversed
|
||||
@@ -36,18 +35,12 @@ class ChainComparator<T> {
|
||||
);
|
||||
}
|
||||
|
||||
ChainComparator<T> thenWithComparator(Comparator<T> comparator,
|
||||
{bool reversed = false}) {
|
||||
return ChainComparator._create(
|
||||
this,
|
||||
!reversed ? comparator : (a, b) => comparator(b, a),
|
||||
);
|
||||
ChainComparator<T> thenWithComparator(Comparator<T> comparator, {bool reversed = false}) {
|
||||
return ChainComparator._create(this, !reversed ? comparator : (a, b) => comparator(b, a));
|
||||
}
|
||||
|
||||
ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(
|
||||
F Function(T) extractor) {
|
||||
return ChainComparator._create(
|
||||
this, (a, b) => -extractor(a).compareTo(extractor(b)));
|
||||
ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(F Function(T) extractor) {
|
||||
return ChainComparator._create(this, (a, b) => -extractor(a).compareTo(extractor(b)));
|
||||
}
|
||||
|
||||
ChainComparator<T> thenTrueFirst(bool Function(T) f) {
|
||||
@@ -63,8 +56,7 @@ class ChainComparator<T> {
|
||||
}
|
||||
|
||||
class Comparators {
|
||||
static Comparator<String> compareStringCaseInsensitive(
|
||||
{bool uppercaseFirst = false}) {
|
||||
static Comparator<String> compareStringCaseInsensitive({bool uppercaseFirst = false}) {
|
||||
return (String a, String b) {
|
||||
final r = a.toLowerCase().compareTo(b.toLowerCase());
|
||||
if (r != 0) return r;
|
||||
|
||||
@@ -24,19 +24,12 @@ String decyptPem(List<String> args) {
|
||||
return sshKey.first.toPem();
|
||||
}
|
||||
|
||||
enum GenSSHClientStatus {
|
||||
socket,
|
||||
key,
|
||||
pwd,
|
||||
}
|
||||
enum GenSSHClientStatus { socket, key, pwd }
|
||||
|
||||
String getPrivateKey(String id) {
|
||||
final pki = Stores.key.fetchOne(id);
|
||||
if (pki == null) {
|
||||
throw SSHErr(
|
||||
type: SSHErrType.noPrivateKey,
|
||||
message: 'key [$id] not found',
|
||||
);
|
||||
throw SSHErr(type: SSHErrType.noPrivateKey, message: 'key [$id] not found');
|
||||
}
|
||||
return pki.key;
|
||||
}
|
||||
@@ -73,36 +66,21 @@ Future<SSHClient> genClient(
|
||||
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
||||
}();
|
||||
if (jumpSpi_ != null) {
|
||||
final jumpClient = await genClient(
|
||||
jumpSpi_,
|
||||
privateKey: jumpPrivateKey,
|
||||
timeout: timeout,
|
||||
);
|
||||
final jumpClient = await genClient(jumpSpi_, privateKey: jumpPrivateKey, timeout: timeout);
|
||||
|
||||
return await jumpClient.forwardLocal(
|
||||
spi.ip,
|
||||
spi.port,
|
||||
);
|
||||
return await jumpClient.forwardLocal(spi.ip, spi.port);
|
||||
}
|
||||
|
||||
// Direct
|
||||
try {
|
||||
return await SSHSocket.connect(
|
||||
spi.ip,
|
||||
spi.port,
|
||||
timeout: timeout,
|
||||
);
|
||||
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
|
||||
} catch (e) {
|
||||
Loggers.app.warning('genClient', e);
|
||||
if (spi.alterUrl == null) rethrow;
|
||||
try {
|
||||
final res = spi.fromStringUrl();
|
||||
alterUser = res.$2;
|
||||
return await SSHSocket.connect(
|
||||
res.$1,
|
||||
res.$3,
|
||||
timeout: timeout,
|
||||
);
|
||||
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
||||
} catch (e) {
|
||||
Loggers.app.warning('genClient alterUrl', e);
|
||||
rethrow;
|
||||
|
||||
57
lib/data/helper/system_detector.dart
Normal file
57
lib/data/helper/system_detector.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
|
||||
/// Helper class for detecting remote system types
|
||||
class SystemDetector {
|
||||
/// Detects the system type of a remote server
|
||||
///
|
||||
/// First checks if a custom system type is configured in [spi].
|
||||
/// If not, attempts to detect the system by running commands:
|
||||
/// 1. 'ver' command to detect Windows
|
||||
/// 2. 'uname -a' command to detect Linux/BSD/Darwin
|
||||
///
|
||||
/// Returns [SystemType.linux] as default if detection fails.
|
||||
static Future<SystemType> detect(
|
||||
SSHClient client,
|
||||
Spi spi,
|
||||
) async {
|
||||
// First, check if custom system type is defined
|
||||
SystemType? detectedSystemType = spi.customSystemType;
|
||||
if (detectedSystemType != null) {
|
||||
dprint('Using custom system type ${detectedSystemType.name} for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to detect Windows systems first (more reliable detection)
|
||||
final powershellResult = await client.run('ver 2>nul').string;
|
||||
if (powershellResult.isNotEmpty &&
|
||||
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
|
||||
detectedSystemType = SystemType.windows;
|
||||
dprint('Detected Windows system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
|
||||
// Try to detect Unix/Linux/BSD systems
|
||||
final unixResult = await client.run('uname -a').string;
|
||||
if (unixResult.contains('Linux')) {
|
||||
detectedSystemType = SystemType.linux;
|
||||
dprint('Detected Linux system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
} else if (unixResult.contains('Darwin') || unixResult.contains('BSD')) {
|
||||
detectedSystemType = SystemType.bsd;
|
||||
dprint('Detected BSD system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
} catch (e) {
|
||||
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
detectedSystemType = SystemType.linux;
|
||||
dprint('Defaulting to Linux system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
}
|
||||
@@ -213,13 +213,10 @@ class Backup implements Mergeable {
|
||||
_logger.info('Restore success');
|
||||
}
|
||||
|
||||
factory Backup.fromJsonString(String raw) =>
|
||||
Backup.fromJson(json.decode(_diyDecrypt(raw)));
|
||||
factory Backup.fromJsonString(String raw) => Backup.fromJson(json.decode(_diyDecrypt(raw)));
|
||||
}
|
||||
|
||||
String _diyEncrypt(String raw) => json.encode(
|
||||
raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false),
|
||||
);
|
||||
String _diyEncrypt(String raw) => json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
|
||||
|
||||
String _diyDecrypt(String raw) {
|
||||
try {
|
||||
@@ -234,4 +231,3 @@ String _diyDecrypt(String raw) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
|
||||
if (password != null && password.isNotEmpty) {
|
||||
result = Cryptor.encrypt(result, password);
|
||||
}
|
||||
|
||||
|
||||
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
|
||||
await File(path).writeAsString(result);
|
||||
return path;
|
||||
@@ -94,7 +94,7 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
|
||||
}
|
||||
jsonString = Cryptor.decrypt(jsonString, password);
|
||||
}
|
||||
|
||||
|
||||
final map = json.decode(jsonString) as Map<String, dynamic>;
|
||||
return BackupV2.fromJson(map);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import 'package:flutter/material.dart';
|
||||
abstract class BackupSource {
|
||||
/// Get content from this source for restore
|
||||
Future<String?> getContent();
|
||||
|
||||
|
||||
/// Save content to this source for backup
|
||||
Future<void> saveContent(String filePath);
|
||||
|
||||
|
||||
/// Display name for this source
|
||||
String get displayName;
|
||||
|
||||
|
||||
/// Icon for this source
|
||||
IconData get icon;
|
||||
}
|
||||
@@ -59,4 +59,4 @@ class ClipboardBackupSource implements BackupSource {
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.content_paste;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
|
||||
enum SSHErrType {
|
||||
unknown,
|
||||
connect,
|
||||
auth,
|
||||
noPrivateKey,
|
||||
chdir,
|
||||
segements,
|
||||
writeScript,
|
||||
getStatus,
|
||||
;
|
||||
}
|
||||
enum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus }
|
||||
|
||||
class SSHErr extends Err<SSHErrType> {
|
||||
SSHErr({required super.type, super.message});
|
||||
|
||||
@override
|
||||
String? get solution => switch (type) {
|
||||
SSHErrType.chdir => l10n.needHomeDir,
|
||||
SSHErrType.auth => l10n.authFailTip,
|
||||
SSHErrType.writeScript => l10n.writeScriptFailTip,
|
||||
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
|
||||
_ => null,
|
||||
};
|
||||
SSHErrType.chdir => l10n.needHomeDir,
|
||||
SSHErrType.auth => l10n.authFailTip,
|
||||
SSHErrType.writeScript => l10n.writeScriptFailTip,
|
||||
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
enum ContainerErrType {
|
||||
@@ -45,11 +35,7 @@ class ContainerErr extends Err<ContainerErrType> {
|
||||
String? get solution => null;
|
||||
}
|
||||
|
||||
enum ICloudErrType {
|
||||
generic,
|
||||
notFound,
|
||||
multipleFiles,
|
||||
}
|
||||
enum ICloudErrType { generic, notFound, multipleFiles }
|
||||
|
||||
class ICloudErr extends Err<ICloudErrType> {
|
||||
ICloudErr({required super.type, super.message});
|
||||
@@ -58,11 +44,7 @@ class ICloudErr extends Err<ICloudErrType> {
|
||||
String? get solution => null;
|
||||
}
|
||||
|
||||
enum WebdavErrType {
|
||||
generic,
|
||||
notFound,
|
||||
;
|
||||
}
|
||||
enum WebdavErrType { generic, notFound }
|
||||
|
||||
class WebdavErr extends Err<WebdavErrType> {
|
||||
WebdavErr({required super.type, super.message});
|
||||
@@ -71,12 +53,7 @@ class WebdavErr extends Err<WebdavErrType> {
|
||||
String? get solution => null;
|
||||
}
|
||||
|
||||
enum PveErrType {
|
||||
unknown,
|
||||
net,
|
||||
loginFailed,
|
||||
;
|
||||
}
|
||||
enum PveErrType { unknown, net, loginFailed }
|
||||
|
||||
class PveErr extends Err<PveErrType> {
|
||||
PveErr({required super.type, super.message});
|
||||
|
||||
@@ -8,7 +8,7 @@ enum ContainerMenu {
|
||||
restart,
|
||||
rm,
|
||||
logs,
|
||||
terminal,
|
||||
terminal
|
||||
//stats,
|
||||
;
|
||||
|
||||
@@ -27,22 +27,22 @@ enum ContainerMenu {
|
||||
}
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
ContainerMenu.start => Icons.play_arrow,
|
||||
ContainerMenu.stop => Icons.stop,
|
||||
ContainerMenu.restart => Icons.restart_alt,
|
||||
ContainerMenu.rm => Icons.delete,
|
||||
ContainerMenu.logs => Icons.logo_dev,
|
||||
ContainerMenu.terminal => Icons.terminal,
|
||||
// DockerMenuType.stats => Icons.bar_chart,
|
||||
};
|
||||
ContainerMenu.start => Icons.play_arrow,
|
||||
ContainerMenu.stop => Icons.stop,
|
||||
ContainerMenu.restart => Icons.restart_alt,
|
||||
ContainerMenu.rm => Icons.delete,
|
||||
ContainerMenu.logs => Icons.logo_dev,
|
||||
ContainerMenu.terminal => Icons.terminal,
|
||||
// DockerMenuType.stats => Icons.bar_chart,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
ContainerMenu.start => l10n.start,
|
||||
ContainerMenu.stop => l10n.stop,
|
||||
ContainerMenu.restart => l10n.restart,
|
||||
ContainerMenu.rm => libL10n.delete,
|
||||
ContainerMenu.logs => libL10n.log,
|
||||
ContainerMenu.terminal => l10n.terminal,
|
||||
// DockerMenuType.stats => s.stats,
|
||||
};
|
||||
ContainerMenu.start => l10n.start,
|
||||
ContainerMenu.stop => l10n.stop,
|
||||
ContainerMenu.restart => l10n.restart,
|
||||
ContainerMenu.rm => libL10n.delete,
|
||||
ContainerMenu.logs => libL10n.log,
|
||||
ContainerMenu.terminal => l10n.terminal,
|
||||
// DockerMenuType.stats => s.stats,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ enum ServerFuncBtn {
|
||||
snippet(),
|
||||
iperf(),
|
||||
// pve(),
|
||||
systemd(1058),
|
||||
;
|
||||
systemd(1058);
|
||||
|
||||
final int? addedVersion;
|
||||
|
||||
@@ -41,24 +40,24 @@ enum ServerFuncBtn {
|
||||
].map((e) => e.index).toList();
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
sftp => Icons.insert_drive_file,
|
||||
snippet => Icons.code,
|
||||
//pkg => Icons.system_security_update,
|
||||
container => FontAwesome.docker_brand,
|
||||
process => Icons.list_alt_outlined,
|
||||
terminal => Icons.terminal,
|
||||
iperf => Icons.speed,
|
||||
systemd => MingCute.plugin_2_fill,
|
||||
};
|
||||
sftp => Icons.insert_drive_file,
|
||||
snippet => Icons.code,
|
||||
//pkg => Icons.system_security_update,
|
||||
container => FontAwesome.docker_brand,
|
||||
process => Icons.list_alt_outlined,
|
||||
terminal => Icons.terminal,
|
||||
iperf => Icons.speed,
|
||||
systemd => MingCute.plugin_2_fill,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
sftp => 'SFTP',
|
||||
snippet => l10n.snippet,
|
||||
//pkg => l10n.pkg,
|
||||
container => l10n.container,
|
||||
process => l10n.process,
|
||||
terminal => l10n.terminal,
|
||||
iperf => 'iperf',
|
||||
systemd => 'Systemd',
|
||||
};
|
||||
sftp => 'SFTP',
|
||||
snippet => l10n.snippet,
|
||||
//pkg => l10n.pkg,
|
||||
container => l10n.container,
|
||||
process => l10n.process,
|
||||
terminal => l10n.terminal,
|
||||
iperf => 'iperf',
|
||||
systemd => 'Systemd',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@ enum NetViewType {
|
||||
traffic;
|
||||
|
||||
NetViewType get next => switch (this) {
|
||||
conn => speed,
|
||||
speed => traffic,
|
||||
traffic => conn,
|
||||
};
|
||||
conn => speed,
|
||||
speed => traffic,
|
||||
traffic => conn,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
NetViewType.conn => l10n.conn,
|
||||
NetViewType.traffic => l10n.traffic,
|
||||
NetViewType.speed => l10n.speed,
|
||||
};
|
||||
NetViewType.conn => l10n.conn,
|
||||
NetViewType.traffic => l10n.traffic,
|
||||
NetViewType.speed => l10n.speed,
|
||||
};
|
||||
|
||||
/// If no device is specified, return the cached value (only real devices,
|
||||
/// such as ethX, wlanX...).
|
||||
@@ -26,32 +26,17 @@ enum NetViewType {
|
||||
try {
|
||||
switch (this) {
|
||||
case NetViewType.conn:
|
||||
return (
|
||||
'${l10n.conn}:\n${ss.tcp.maxConn}',
|
||||
'${libL10n.fail}:\n${ss.tcp.fail}',
|
||||
);
|
||||
return ('${l10n.conn}:\n${ss.tcp.maxConn}', '${libL10n.fail}:\n${ss.tcp.fail}');
|
||||
case NetViewType.speed:
|
||||
if (notSepcifyDev) {
|
||||
return (
|
||||
'↓:\n${ss.netSpeed.cachedVals.speedIn}',
|
||||
'↑:\n${ss.netSpeed.cachedVals.speedOut}',
|
||||
);
|
||||
return ('↓:\n${ss.netSpeed.cachedVals.speedIn}', '↑:\n${ss.netSpeed.cachedVals.speedOut}');
|
||||
}
|
||||
return (
|
||||
'↓:\n${ss.netSpeed.speedIn(device: dev)}',
|
||||
'↑:\n${ss.netSpeed.speedOut(device: dev)}',
|
||||
);
|
||||
return ('↓:\n${ss.netSpeed.speedIn(device: dev)}', '↑:\n${ss.netSpeed.speedOut(device: dev)}');
|
||||
case NetViewType.traffic:
|
||||
if (notSepcifyDev) {
|
||||
return (
|
||||
'↓:\n${ss.netSpeed.cachedVals.sizeIn}',
|
||||
'↑:\n${ss.netSpeed.cachedVals.sizeOut}',
|
||||
);
|
||||
return ('↓:\n${ss.netSpeed.cachedVals.sizeIn}', '↑:\n${ss.netSpeed.cachedVals.sizeOut}');
|
||||
}
|
||||
return (
|
||||
'↓:\n${ss.netSpeed.sizeIn(device: dev)}',
|
||||
'↑:\n${ss.netSpeed.sizeOut(device: dev)}',
|
||||
);
|
||||
return ('↓:\n${ss.netSpeed.sizeIn(device: dev)}', '↑:\n${ss.netSpeed.sizeOut(device: dev)}');
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('NetViewType.build', e, s);
|
||||
@@ -60,14 +45,14 @@ enum NetViewType {
|
||||
}
|
||||
|
||||
int toJson() => switch (this) {
|
||||
NetViewType.conn => 0,
|
||||
NetViewType.speed => 1,
|
||||
NetViewType.traffic => 2,
|
||||
};
|
||||
NetViewType.conn => 0,
|
||||
NetViewType.speed => 1,
|
||||
NetViewType.traffic => 2,
|
||||
};
|
||||
|
||||
static NetViewType fromJson(int json) => switch (json) {
|
||||
0 => NetViewType.conn,
|
||||
1 => NetViewType.speed,
|
||||
_ => NetViewType.traffic,
|
||||
};
|
||||
0 => NetViewType.conn,
|
||||
1 => NetViewType.speed,
|
||||
_ => NetViewType.traffic,
|
||||
};
|
||||
}
|
||||
|
||||
242
lib/data/model/app/script_builders.dart
Normal file
242
lib/data/model/app/script_builders.dart
Normal file
@@ -0,0 +1,242 @@
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
|
||||
/// Abstract base class for platform-specific script builders
|
||||
abstract class ScriptBuilder {
|
||||
const ScriptBuilder();
|
||||
|
||||
/// Generate a complete script for all shell functions
|
||||
String buildScript(Map<String, String>? customCmds);
|
||||
|
||||
/// Get the script file name for this platform
|
||||
String get scriptFileName;
|
||||
|
||||
/// Get the command to install the script
|
||||
String getInstallCommand(String scriptDir, String scriptPath);
|
||||
|
||||
/// Get the execution command for a specific function
|
||||
String getExecCommand(String scriptPath, ShellFunc func);
|
||||
|
||||
/// Get custom commands string for this platform
|
||||
String getCustomCmdsString(
|
||||
ShellFunc func,
|
||||
Map<String, String>? customCmds,
|
||||
);
|
||||
}
|
||||
|
||||
/// Windows PowerShell script builder
|
||||
class WindowsScriptBuilder extends ScriptBuilder {
|
||||
const WindowsScriptBuilder();
|
||||
|
||||
@override
|
||||
String get scriptFileName => 'srvboxm_v${BuildData.script}.ps1';
|
||||
|
||||
@override
|
||||
String getInstallCommand(String scriptDir, String scriptPath) {
|
||||
return 'New-Item -ItemType Directory -Force -Path \'$scriptDir\' | Out-Null; '
|
||||
'\$content = [System.Console]::In.ReadToEnd(); '
|
||||
'Set-Content -Path \'$scriptPath\' -Value \$content -Encoding UTF8';
|
||||
}
|
||||
|
||||
@override
|
||||
String getExecCommand(String scriptPath, ShellFunc func) {
|
||||
return 'powershell -ExecutionPolicy Bypass -File "$scriptPath" -${func.flag}';
|
||||
}
|
||||
|
||||
@override
|
||||
String getCustomCmdsString(
|
||||
ShellFunc func,
|
||||
Map<String, String>? customCmds,
|
||||
) {
|
||||
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
||||
return '\n${customCmds.values.map((cmd) => '\t$cmd').join('\n')}';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
String buildScript(Map<String, String>? customCmds) {
|
||||
final sb = StringBuffer();
|
||||
sb.write('''
|
||||
# PowerShell script for ServerBox app v1.0.${BuildData.build}
|
||||
# DO NOT delete this file while app is running
|
||||
|
||||
\$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
''');
|
||||
|
||||
// Write each function
|
||||
for (final func in ShellFunc.values) {
|
||||
final customCmdsStr = getCustomCmdsString(func, customCmds);
|
||||
|
||||
sb.write('''
|
||||
function ${func.name} {
|
||||
${_getWindowsCommand(func).split('\n').map((e) => e.isEmpty ? '' : ' $e').join('\n')}$customCmdsStr
|
||||
}
|
||||
|
||||
''');
|
||||
}
|
||||
|
||||
// Write switch case
|
||||
sb.write('''
|
||||
switch (\$args[0]) {
|
||||
''');
|
||||
for (final func in ShellFunc.values) {
|
||||
sb.write('''
|
||||
"-${func.flag}" { ${func.name} }
|
||||
''');
|
||||
}
|
||||
sb.write('''
|
||||
default { Write-Host "Invalid argument \$(\$args[0])" }
|
||||
}
|
||||
''');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
String _getWindowsCommand(ShellFunc func) => switch (func) {
|
||||
ShellFunc.status => WindowsStatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider),
|
||||
ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',
|
||||
ShellFunc.shutdown => 'Stop-Computer -Force',
|
||||
ShellFunc.reboot => 'Restart-Computer -Force',
|
||||
ShellFunc.suspend =>
|
||||
'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState(\'Suspend\', \$false, \$false)',
|
||||
};
|
||||
}
|
||||
|
||||
/// Unix shell script builder
|
||||
class UnixScriptBuilder extends ScriptBuilder {
|
||||
const UnixScriptBuilder();
|
||||
|
||||
@override
|
||||
String get scriptFileName => 'srvboxm_v${BuildData.script}.sh';
|
||||
|
||||
@override
|
||||
String getInstallCommand(String scriptDir, String scriptPath) {
|
||||
return '''
|
||||
mkdir -p $scriptDir
|
||||
cat > $scriptPath
|
||||
chmod 755 $scriptPath
|
||||
''';
|
||||
}
|
||||
|
||||
@override
|
||||
String getExecCommand(String scriptPath, ShellFunc func) {
|
||||
return 'sh $scriptPath -${func.flag}';
|
||||
}
|
||||
|
||||
@override
|
||||
String getCustomCmdsString(
|
||||
ShellFunc func,
|
||||
Map<String, String>? customCmds,
|
||||
) {
|
||||
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
||||
return '${ShellFunc.cmdDivider}\n\t${customCmds.values.join(ShellFunc.cmdDivider)}';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
String buildScript(Map<String, String>? customCmds) {
|
||||
final sb = StringBuffer();
|
||||
sb.write('''
|
||||
#!/bin/sh
|
||||
# Script for ServerBox app v1.0.${BuildData.build}
|
||||
# DO NOT delete this file while app is running
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
|
||||
# If macSign & bsdSign are both empty, then it's linux
|
||||
macSign=\$(uname -a 2>&1 | grep "Darwin")
|
||||
bsdSign=\$(uname -a 2>&1 | grep "BSD")
|
||||
|
||||
# Link /bin/sh to busybox?
|
||||
isBusybox=\$(ls -l /bin/sh | grep "busybox")
|
||||
|
||||
userId=\$(id -u)
|
||||
|
||||
exec 2>/dev/null
|
||||
|
||||
''');
|
||||
// Write each function
|
||||
for (final func in ShellFunc.values) {
|
||||
final customCmdsStr = getCustomCmdsString(func, customCmds);
|
||||
sb.write('''
|
||||
${func.name}() {
|
||||
${_getUnixCommand(func).split('\n').map((e) => '\t$e').join('\n')}
|
||||
$customCmdsStr
|
||||
}
|
||||
|
||||
''');
|
||||
}
|
||||
|
||||
// Write switch case
|
||||
sb.write('case \$1 in\n');
|
||||
for (final func in ShellFunc.values) {
|
||||
sb.write('''
|
||||
'-${func.flag}')
|
||||
${func.name}
|
||||
;;
|
||||
''');
|
||||
}
|
||||
sb.write('''
|
||||
*)
|
||||
echo "Invalid argument \$1"
|
||||
;;
|
||||
esac''');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
String _getUnixCommand(ShellFunc func) {
|
||||
switch (func) {
|
||||
case ShellFunc.status:
|
||||
return '''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\t${StatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider)}
|
||||
else
|
||||
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider)}
|
||||
fi''';
|
||||
case ShellFunc.process:
|
||||
return '''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\tif [ "\$isBusybox" != "" ]; then
|
||||
\t\tps w
|
||||
\telse
|
||||
\t\tps -aux
|
||||
\tfi
|
||||
else
|
||||
\tps -ax
|
||||
fi
|
||||
''';
|
||||
case ShellFunc.shutdown:
|
||||
return '''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\tshutdown -h now
|
||||
else
|
||||
\tsudo -S shutdown -h now
|
||||
fi''';
|
||||
case ShellFunc.reboot:
|
||||
return '''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\treboot
|
||||
else
|
||||
\tsudo -S reboot
|
||||
fi''';
|
||||
case ShellFunc.suspend:
|
||||
return '''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\tsystemctl suspend
|
||||
else
|
||||
\tsudo -S systemctl suspend
|
||||
fi''';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory class to get appropriate script builder for platform
|
||||
class ScriptBuilderFactory {
|
||||
const ScriptBuilderFactory._();
|
||||
|
||||
static ScriptBuilder getBuilder(bool isWindows) {
|
||||
return isWindows ? const WindowsScriptBuilder() : const UnixScriptBuilder();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,33 @@
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/app/script_builders.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
|
||||
enum ShellFunc {
|
||||
status,
|
||||
status('SbStatus'),
|
||||
//docker,
|
||||
process,
|
||||
shutdown,
|
||||
reboot,
|
||||
suspend;
|
||||
process('SbProcess'),
|
||||
shutdown('SbShutdown'),
|
||||
reboot('SbReboot'),
|
||||
suspend('SbSuspend');
|
||||
|
||||
final String name;
|
||||
|
||||
const ShellFunc(this.name);
|
||||
|
||||
static const seperator = 'SrvBoxSep';
|
||||
|
||||
/// The suffix `\t` is for formatting
|
||||
static const cmdDivider = '\necho $seperator\n\t';
|
||||
|
||||
/// Cached Linux status commands string
|
||||
static final _linuxStatusCmds = StatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
|
||||
|
||||
/// Cached BSD status commands string
|
||||
static final _bsdStatusCmds = BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
|
||||
|
||||
/// srvboxm -> ServerBox Mobile
|
||||
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
||||
static const scriptFileWindows = 'srvboxm_v${BuildData.script}.ps1';
|
||||
static const scriptDirHome = '~/.config/server_box';
|
||||
static const scriptDirTmp = '/tmp/server_box';
|
||||
static const scriptDirHomeWindows = '%USERPROFILE%/.config/server_box';
|
||||
static const scriptDirTmpWindows = '%TEMP%/server_box';
|
||||
|
||||
static final _scriptDirMap = <String, String>{};
|
||||
|
||||
@@ -33,31 +35,38 @@ enum ShellFunc {
|
||||
///
|
||||
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
||||
/// it will be changed to [scriptDirHome]/[scriptFile].
|
||||
static String getScriptDir(String id) {
|
||||
static String getScriptDir(String id, {SystemType? systemType}) {
|
||||
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||
if (customScriptDir != null) return customScriptDir;
|
||||
_scriptDirMap[id] ??= scriptDirTmp;
|
||||
|
||||
final defaultTmpDir = systemType == SystemType.windows ? scriptDirTmpWindows : scriptDirTmp;
|
||||
_scriptDirMap[id] ??= defaultTmpDir;
|
||||
return _scriptDirMap[id]!;
|
||||
}
|
||||
|
||||
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
|
||||
static void switchScriptDir(String id, {SystemType? systemType}) => switch (_scriptDirMap[id]) {
|
||||
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
|
||||
scriptDirTmpWindows => _scriptDirMap[id] = scriptDirHomeWindows,
|
||||
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
|
||||
_ => _scriptDirMap[id] = scriptDirHome,
|
||||
scriptDirHomeWindows => _scriptDirMap[id] = scriptDirTmpWindows,
|
||||
_ => _scriptDirMap[id] = systemType == SystemType.windows ? scriptDirHomeWindows : scriptDirHome,
|
||||
};
|
||||
|
||||
static String getScriptPath(String id) {
|
||||
return '${getScriptDir(id)}/$scriptFile';
|
||||
static String getScriptPath(String id, {SystemType? systemType}) {
|
||||
final dir = getScriptDir(id, systemType: systemType);
|
||||
final fileName = systemType == SystemType.windows ? scriptFileWindows : scriptFile;
|
||||
final separator = systemType == SystemType.windows ? '\\' : '/';
|
||||
return '$dir$separator$fileName';
|
||||
}
|
||||
|
||||
static String getInstallShellCmd(String id) {
|
||||
final scriptDir = getScriptDir(id);
|
||||
final scriptPath = '$scriptDir/$scriptFile';
|
||||
return '''
|
||||
mkdir -p $scriptDir
|
||||
cat > $scriptPath
|
||||
chmod 755 $scriptPath
|
||||
''';
|
||||
static String getInstallShellCmd(String id, {SystemType? systemType}) {
|
||||
final scriptDir = getScriptDir(id, systemType: systemType);
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||
final separator = isWindows ? '\\' : '/';
|
||||
final scriptPath = '$scriptDir$separator${builder.scriptFileName}';
|
||||
|
||||
return builder.getInstallCommand(scriptDir, scriptPath);
|
||||
}
|
||||
|
||||
String get flag => switch (this) {
|
||||
@@ -69,120 +78,24 @@ chmod 755 $scriptPath
|
||||
// ShellFunc.docker=> 'd',
|
||||
};
|
||||
|
||||
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
|
||||
|
||||
String get name => switch (this) {
|
||||
ShellFunc.status => 'status',
|
||||
ShellFunc.process => 'process',
|
||||
ShellFunc.shutdown => 'ShutDown',
|
||||
ShellFunc.reboot => 'Reboot',
|
||||
ShellFunc.suspend => 'Suspend',
|
||||
};
|
||||
|
||||
String get _cmd => switch (this) {
|
||||
ShellFunc.status =>
|
||||
'''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\t$_linuxStatusCmds
|
||||
else
|
||||
\t$_bsdStatusCmds
|
||||
fi''',
|
||||
ShellFunc.process =>
|
||||
'''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\tif [ "\$isBusybox" != "" ]; then
|
||||
\t\tps w
|
||||
\telse
|
||||
\t\tps -aux
|
||||
\tfi
|
||||
else
|
||||
\tps -ax
|
||||
fi
|
||||
''',
|
||||
ShellFunc.shutdown =>
|
||||
'''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\tshutdown -h now
|
||||
else
|
||||
\tsudo -S shutdown -h now
|
||||
fi''',
|
||||
ShellFunc.reboot =>
|
||||
'''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\treboot
|
||||
else
|
||||
\tsudo -S reboot
|
||||
fi''',
|
||||
ShellFunc.suspend =>
|
||||
'''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\tsystemctl suspend
|
||||
else
|
||||
\tsudo -S systemctl suspend
|
||||
fi''',
|
||||
};
|
||||
|
||||
static String allScript(Map<String, String>? customCmds) {
|
||||
final sb = StringBuffer();
|
||||
sb.write('''
|
||||
#!/bin/sh
|
||||
# Script for ServerBox app v1.0.${BuildData.build}
|
||||
# DO NOT delete this file while app is running
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
|
||||
# If macSign & bsdSign are both empty, then it's linux
|
||||
macSign=\$(uname -a 2>&1 | grep "Darwin")
|
||||
bsdSign=\$(uname -a 2>&1 | grep "BSD")
|
||||
|
||||
# Link /bin/sh to busybox?
|
||||
isBusybox=\$(ls -l /bin/sh | grep "busybox")
|
||||
|
||||
userId=\$(id -u)
|
||||
|
||||
exec 2>/dev/null
|
||||
|
||||
''');
|
||||
// Write each func
|
||||
for (final func in values) {
|
||||
final customCmdsStr = () {
|
||||
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
||||
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
|
||||
}
|
||||
return '';
|
||||
}();
|
||||
sb.write('''
|
||||
${func.name}() {
|
||||
${func._cmd.split('\n').map((e) => '\t$e').join('\n')}
|
||||
$customCmdsStr
|
||||
}
|
||||
|
||||
''');
|
||||
}
|
||||
|
||||
// Write switch case
|
||||
sb.write('case \$1 in\n');
|
||||
for (final func in values) {
|
||||
sb.write('''
|
||||
'-${func.flag}')
|
||||
${func.name}
|
||||
;;
|
||||
''');
|
||||
}
|
||||
sb.write('''
|
||||
*)
|
||||
echo "Invalid argument \$1"
|
||||
;;
|
||||
esac''');
|
||||
return sb.toString();
|
||||
String exec(String id, {SystemType? systemType}) {
|
||||
final scriptPath = getScriptPath(id, systemType: systemType);
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||
|
||||
return builder.getExecCommand(scriptPath, this);
|
||||
}
|
||||
}
|
||||
|
||||
extension EnumX on Enum {
|
||||
/// Find out the required segment from [segments]
|
||||
String find(List<String> segments) {
|
||||
return segments[index];
|
||||
|
||||
|
||||
/// Generate script based on system type
|
||||
static String allScript(Map<String, String>? customCmds, {SystemType? systemType}) {
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||
|
||||
return builder.buildScript(customCmds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum StatusCmdType {
|
||||
@@ -193,7 +106,10 @@ enum StatusCmdType {
|
||||
cpu._('cat /proc/stat | grep cpu'),
|
||||
uptime._('uptime'),
|
||||
conn._('cat /proc/net/snmp'),
|
||||
disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'),
|
||||
disk._(
|
||||
'lsblk --bytes --json --output '
|
||||
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
||||
),
|
||||
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
||||
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||
@@ -201,7 +117,16 @@ enum StatusCmdType {
|
||||
diskio._('cat /proc/diskstats'),
|
||||
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
|
||||
nvidia._('nvidia-smi -q -x'),
|
||||
amd._('if command -v amd-smi >/dev/null 2>&1; then amd-smi list --json && amd-smi metric --json; elif command -v rocm-smi >/dev/null 2>&1; then rocm-smi --json || rocm-smi --showunique --showuse --showtemp --showfan --showclocks --showmemuse --showpower; elif command -v radeontop >/dev/null 2>&1; then timeout 2s radeontop -d - -l 1 | tail -n +2; else echo "No AMD GPU monitoring tools found"; fi'),
|
||||
amd._(
|
||||
'if command -v amd-smi >/dev/null 2>&1; then '
|
||||
'amd-smi list --json && amd-smi metric --json; '
|
||||
'elif command -v rocm-smi >/dev/null 2>&1; then '
|
||||
'rocm-smi --json || rocm-smi --showunique --showuse --showtemp '
|
||||
'--showfan --showclocks --showmemuse --showpower; '
|
||||
'elif command -v radeontop >/dev/null 2>&1; then '
|
||||
'timeout 2s radeontop -d - -l 1 | tail -n +2; '
|
||||
'else echo "No AMD GPU monitoring tools found"; fi',
|
||||
),
|
||||
sensors._('sensors'),
|
||||
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
||||
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
||||
@@ -241,3 +166,77 @@ extension StatusCmdTypeX on StatusCmdType {
|
||||
final val => val.name,
|
||||
};
|
||||
}
|
||||
|
||||
enum WindowsStatusCmdType {
|
||||
echo._('echo ${SystemType.windowsSign}'),
|
||||
time._('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
|
||||
net._(
|
||||
r'Get-Counter -Counter '
|
||||
r'"\\NetworkInterface(*)\\Bytes Received/sec", '
|
||||
r'"\\NetworkInterface(*)\\Bytes Sent/sec" '
|
||||
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
|
||||
),
|
||||
sys._('(Get-ComputerInfo).OsName'),
|
||||
cpu._(
|
||||
'Get-WmiObject -Class Win32_Processor | '
|
||||
'Select-Object Name, LoadPercentage | ConvertTo-Json',
|
||||
),
|
||||
uptime._('(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime'),
|
||||
conn._('(netstat -an | findstr ESTABLISHED | Measure-Object -Line).Count'),
|
||||
disk._(
|
||||
'Get-WmiObject -Class Win32_LogicalDisk | '
|
||||
'Select-Object DeviceID, Size, FreeSpace, FileSystem | ConvertTo-Json',
|
||||
),
|
||||
mem._(
|
||||
'Get-WmiObject -Class Win32_OperatingSystem | '
|
||||
'Select-Object TotalVisibleMemorySize, FreePhysicalMemory | ConvertTo-Json',
|
||||
),
|
||||
temp._(
|
||||
'Get-CimInstance -ClassName MSAcpi_ThermalZoneTemperature '
|
||||
'-Namespace root/wmi -ErrorAction SilentlyContinue | '
|
||||
'Select-Object InstanceName, @{Name=\'Temperature\';'
|
||||
'Expression={[math]::Round((\$_.CurrentTemperature - 2732) / 10, 1)}} | '
|
||||
'ConvertTo-Json',
|
||||
),
|
||||
host._(r'Write-Output $env:COMPUTERNAME'),
|
||||
diskio._(
|
||||
r'Get-Counter -Counter '
|
||||
r'"\\PhysicalDisk(*)\\Disk Read Bytes/sec", '
|
||||
r'"\\PhysicalDisk(*)\\Disk Write Bytes/sec" '
|
||||
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
|
||||
),
|
||||
battery._(
|
||||
'Get-WmiObject -Class Win32_Battery | '
|
||||
'Select-Object EstimatedChargeRemaining, BatteryStatus | ConvertTo-Json',
|
||||
),
|
||||
nvidia._(
|
||||
'if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { '
|
||||
'nvidia-smi -q -x } else { echo "NVIDIA driver not found" }',
|
||||
),
|
||||
amd._(
|
||||
'if (Get-Command amd-smi -ErrorAction SilentlyContinue) { '
|
||||
'amd-smi list --json } else { echo "AMD driver not found" }',
|
||||
),
|
||||
sensors._(
|
||||
'Get-CimInstance -ClassName Win32_TemperatureProbe '
|
||||
'-ErrorAction SilentlyContinue | '
|
||||
'Select-Object Name, CurrentReading | ConvertTo-Json',
|
||||
),
|
||||
diskSmart._(
|
||||
'Get-PhysicalDisk | Get-StorageReliabilityCounter | '
|
||||
'Select-Object DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours | '
|
||||
'ConvertTo-Json',
|
||||
),
|
||||
cpuBrand._('(Get-WmiObject -Class Win32_Processor).Name');
|
||||
|
||||
final String cmd;
|
||||
|
||||
const WindowsStatusCmdType._(this.cmd);
|
||||
}
|
||||
|
||||
extension EnumX on Enum {
|
||||
/// Find out the required segment from [segments]
|
||||
String find(List<String> segments) {
|
||||
return segments[index];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ enum AppTab {
|
||||
server,
|
||||
ssh,
|
||||
file,
|
||||
snippet,
|
||||
snippet
|
||||
//settings,
|
||||
;
|
||||
|
||||
@@ -29,60 +29,60 @@ enum AppTab {
|
||||
NavigationDestination get navDestination {
|
||||
return switch (this) {
|
||||
server => NavigationDestination(
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: l10n.server,
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: l10n.server,
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
// settings => NavigationDestination(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// label: libL10n.setting,
|
||||
// selectedIcon: const Icon(Icons.settings),
|
||||
// ),
|
||||
ssh => const NavigationDestination(
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: 'SSH',
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: 'SSH',
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
snippet => NavigationDestination(
|
||||
icon: const Icon(Icons.code),
|
||||
label: l10n.snippet,
|
||||
selectedIcon: const Icon(Icons.code),
|
||||
),
|
||||
icon: const Icon(Icons.code),
|
||||
label: l10n.snippet,
|
||||
selectedIcon: const Icon(Icons.code),
|
||||
),
|
||||
file => NavigationDestination(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: libL10n.file,
|
||||
selectedIcon: const Icon(Icons.folder),
|
||||
),
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: libL10n.file,
|
||||
selectedIcon: const Icon(Icons.folder),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
NavigationRailDestination get navRailDestination {
|
||||
return switch (this) {
|
||||
server => NavigationRailDestination(
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: Text(l10n.server),
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: Text(l10n.server),
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
// settings => NavigationRailDestination(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// label: libL10n.setting,
|
||||
// selectedIcon: const Icon(Icons.settings),
|
||||
// ),
|
||||
ssh => const NavigationRailDestination(
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: Text('SSH'),
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: Text('SSH'),
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
snippet => NavigationRailDestination(
|
||||
icon: const Icon(Icons.code),
|
||||
label: Text(l10n.snippet),
|
||||
selectedIcon: const Icon(Icons.code),
|
||||
),
|
||||
icon: const Icon(Icons.code),
|
||||
label: Text(l10n.snippet),
|
||||
selectedIcon: const Icon(Icons.code),
|
||||
),
|
||||
file => NavigationRailDestination(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: Text(libL10n.file),
|
||||
selectedIcon: const Icon(Icons.folder),
|
||||
),
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: Text(libL10n.file),
|
||||
selectedIcon: const Icon(Icons.folder),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,14 +24,7 @@ final class PodmanImg implements ContainerImg {
|
||||
final int? size;
|
||||
final int? containers;
|
||||
|
||||
PodmanImg({
|
||||
this.repository,
|
||||
this.tag,
|
||||
this.id,
|
||||
this.created,
|
||||
this.size,
|
||||
this.containers,
|
||||
});
|
||||
PodmanImg({this.repository, this.tag, this.id, this.created, this.size, this.containers});
|
||||
|
||||
@override
|
||||
String? get sizeMB => size?.bytes2Str;
|
||||
@@ -39,28 +32,27 @@ final class PodmanImg implements ContainerImg {
|
||||
@override
|
||||
int? get containersCount => containers;
|
||||
|
||||
factory PodmanImg.fromRawJson(String str) =>
|
||||
PodmanImg.fromJson(json.decode(str));
|
||||
factory PodmanImg.fromRawJson(String str) => PodmanImg.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
|
||||
repository: json['repository'],
|
||||
tag: json['tag'],
|
||||
id: json['Id'],
|
||||
created: json['Created'],
|
||||
size: json['Size'],
|
||||
containers: json['Containers'],
|
||||
);
|
||||
repository: json['repository'],
|
||||
tag: json['tag'],
|
||||
id: json['Id'],
|
||||
created: json['Created'],
|
||||
size: json['Size'],
|
||||
containers: json['Containers'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'repository': repository,
|
||||
'tag': tag,
|
||||
'Id': id,
|
||||
'Created': created,
|
||||
'Size': size,
|
||||
'Containers': containers,
|
||||
};
|
||||
'repository': repository,
|
||||
'tag': tag,
|
||||
'Id': id,
|
||||
'Created': created,
|
||||
'Size': size,
|
||||
'Containers': containers,
|
||||
};
|
||||
}
|
||||
|
||||
final class DockerImg implements ContainerImg {
|
||||
@@ -87,11 +79,9 @@ final class DockerImg implements ContainerImg {
|
||||
String? get sizeMB => size;
|
||||
|
||||
@override
|
||||
int? get containersCount =>
|
||||
containers == 'N/A' ? 0 : int.tryParse(containers);
|
||||
int? get containersCount => containers == 'N/A' ? 0 : int.tryParse(containers);
|
||||
|
||||
factory DockerImg.fromRawJson(String str) =>
|
||||
DockerImg.fromJson(json.decode(str));
|
||||
factory DockerImg.fromRawJson(String str) => DockerImg.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
@@ -121,11 +111,11 @@ final class DockerImg implements ContainerImg {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'Containers': containers,
|
||||
'CreatedAt': createdAt,
|
||||
'ID': id,
|
||||
'Repository': repository,
|
||||
'Size': size,
|
||||
'Tag': tag,
|
||||
};
|
||||
'Containers': containers,
|
||||
'CreatedAt': createdAt,
|
||||
'ID': id,
|
||||
'Repository': repository,
|
||||
'Size': size,
|
||||
'Tag': tag,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,15 +42,7 @@ final class PodmanPs implements ContainerPs {
|
||||
@override
|
||||
String? disk;
|
||||
|
||||
PodmanPs({
|
||||
this.command,
|
||||
this.created,
|
||||
this.exited,
|
||||
this.id,
|
||||
this.image,
|
||||
this.names,
|
||||
this.startedAt,
|
||||
});
|
||||
PodmanPs({this.command, this.created, this.exited, this.id, this.image, this.names, this.startedAt});
|
||||
|
||||
@override
|
||||
String? get name => names?.firstOrNull;
|
||||
@@ -78,36 +70,29 @@ final class PodmanPs implements ContainerPs {
|
||||
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
|
||||
}
|
||||
|
||||
factory PodmanPs.fromRawJson(String str) =>
|
||||
PodmanPs.fromJson(json.decode(str));
|
||||
factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
|
||||
command: json['Command'] == null
|
||||
? []
|
||||
: List<String>.from(json['Command']!.map((x) => x)),
|
||||
created:
|
||||
json['Created'] == null ? null : DateTime.parse(json['Created']),
|
||||
exited: json['Exited'],
|
||||
id: json['Id'],
|
||||
image: json['Image'],
|
||||
names: json['Names'] == null
|
||||
? []
|
||||
: List<String>.from(json['Names']!.map((x) => x)),
|
||||
startedAt: json['StartedAt'],
|
||||
);
|
||||
command: json['Command'] == null ? [] : List<String>.from(json['Command']!.map((x) => x)),
|
||||
created: json['Created'] == null ? null : DateTime.parse(json['Created']),
|
||||
exited: json['Exited'],
|
||||
id: json['Id'],
|
||||
image: json['Image'],
|
||||
names: json['Names'] == null ? [] : List<String>.from(json['Names']!.map((x) => x)),
|
||||
startedAt: json['StartedAt'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'Command':
|
||||
command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
||||
'Created': created?.toIso8601String(),
|
||||
'Exited': exited,
|
||||
'Id': id,
|
||||
'Image': image,
|
||||
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
|
||||
'StartedAt': startedAt,
|
||||
};
|
||||
'Command': command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
||||
'Created': created?.toIso8601String(),
|
||||
'Exited': exited,
|
||||
'Id': id,
|
||||
'Image': image,
|
||||
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
|
||||
'StartedAt': startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
final class DockerPs implements ContainerPs {
|
||||
@@ -127,12 +112,7 @@ final class DockerPs implements ContainerPs {
|
||||
@override
|
||||
String? disk;
|
||||
|
||||
DockerPs({
|
||||
this.id,
|
||||
this.image,
|
||||
this.names,
|
||||
this.state,
|
||||
});
|
||||
DockerPs({this.id, this.image, this.names, this.state});
|
||||
|
||||
@override
|
||||
String? get name => names;
|
||||
@@ -159,11 +139,6 @@ final class DockerPs implements ContainerPs {
|
||||
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
|
||||
factory DockerPs.parse(String raw) {
|
||||
final parts = raw.split(Miscs.multiBlankreg);
|
||||
return DockerPs(
|
||||
id: parts[0],
|
||||
state: parts[1],
|
||||
names: parts[2],
|
||||
image: parts[3].trim(),
|
||||
);
|
||||
return DockerPs(id: parts[0], state: parts[1], names: parts[2], image: parts[3].trim());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,15 @@ import 'package:server_box/data/model/container/ps.dart';
|
||||
|
||||
enum ContainerType {
|
||||
docker,
|
||||
podman,
|
||||
;
|
||||
podman;
|
||||
|
||||
ContainerPs Function(String str) get ps => switch (this) {
|
||||
ContainerType.docker => DockerPs.parse,
|
||||
ContainerType.podman => PodmanPs.fromRawJson,
|
||||
};
|
||||
ContainerType.docker => DockerPs.parse,
|
||||
ContainerType.podman => PodmanPs.fromRawJson,
|
||||
};
|
||||
|
||||
ContainerImg Function(String str) get img => switch (this) {
|
||||
ContainerType.docker => DockerImg.fromRawJson,
|
||||
ContainerType.podman => PodmanImg.fromRawJson,
|
||||
};
|
||||
ContainerType.docker => DockerImg.fromRawJson,
|
||||
ContainerType.podman => PodmanImg.fromRawJson,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,8 +62,7 @@ enum PkgManager {
|
||||
case PkgManager.yum:
|
||||
list = list.sublist(2);
|
||||
list.removeWhere((element) => element.isEmpty);
|
||||
final endLine = list.lastIndexWhere(
|
||||
(element) => element.contains('Obsoleting Packages'));
|
||||
final endLine = list.lastIndexWhere((element) => element.contains('Obsoleting Packages'));
|
||||
if (endLine != -1 && list.isNotEmpty) {
|
||||
list = list.sublist(0, endLine);
|
||||
}
|
||||
@@ -71,8 +70,7 @@ enum PkgManager {
|
||||
case PkgManager.apt:
|
||||
// avoid other outputs
|
||||
// such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...]
|
||||
final idx =
|
||||
list.indexWhere((element) => element.contains('[upgradable from:'));
|
||||
final idx = list.indexWhere((element) => element.contains('[upgradable from:'));
|
||||
if (idx == -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class AmdSmi {
|
||||
try {
|
||||
final jsonData = json.decode(raw);
|
||||
if (jsonData is! List) return [];
|
||||
|
||||
|
||||
return jsonData
|
||||
.map((gpu) => _parseGpuItem(gpu))
|
||||
.where((item) => item != null)
|
||||
@@ -47,28 +47,28 @@ class AmdSmi {
|
||||
try {
|
||||
final name = gpu['name'] ?? gpu['card_model'] ?? gpu['device_name'] ?? 'Unknown AMD GPU';
|
||||
final deviceId = gpu['device_id']?.toString() ?? gpu['gpu_id']?.toString() ?? '0';
|
||||
|
||||
|
||||
// Temperature parsing
|
||||
final tempRaw = gpu['temperature'] ?? gpu['temp'] ?? gpu['gpu_temp'];
|
||||
final temp = _parseIntValue(tempRaw);
|
||||
|
||||
|
||||
// Power parsing
|
||||
final powerDraw = gpu['power_draw'] ?? gpu['current_power'];
|
||||
final powerCap = gpu['power_cap'] ?? gpu['power_limit'] ?? gpu['max_power'];
|
||||
final power = _formatPower(powerDraw, powerCap);
|
||||
|
||||
|
||||
// Memory parsing
|
||||
final memory = _parseMemory(gpu['memory'] ?? gpu['vram'] ?? {});
|
||||
|
||||
|
||||
// Utilization parsing
|
||||
final utilization = _parseIntValue(gpu['utilization'] ?? gpu['gpu_util'] ?? gpu['activity']);
|
||||
|
||||
|
||||
// Fan speed parsing
|
||||
final fanSpeed = _parseIntValue(gpu['fan_speed'] ?? gpu['fan_rpm']);
|
||||
|
||||
|
||||
// Clock speed parsing
|
||||
final clockSpeed = _parseIntValue(gpu['clock_speed'] ?? gpu['gpu_clock'] ?? gpu['sclk']);
|
||||
|
||||
|
||||
return AmdSmiItem(
|
||||
deviceId: deviceId,
|
||||
name: name,
|
||||
@@ -98,7 +98,7 @@ class AmdSmi {
|
||||
static String _formatPower(dynamic draw, dynamic cap) {
|
||||
final drawValue = _parseIntValue(draw);
|
||||
final capValue = _parseIntValue(cap);
|
||||
|
||||
|
||||
if (drawValue == 0 && capValue == 0) return 'N/A';
|
||||
if (capValue == 0) return '${drawValue}W';
|
||||
return '${drawValue}W / ${capValue}W';
|
||||
@@ -108,7 +108,7 @@ class AmdSmi {
|
||||
final total = _parseIntValue(memData['total'] ?? memData['total_memory']);
|
||||
final used = _parseIntValue(memData['used'] ?? memData['used_memory']);
|
||||
final unit = memData['unit']?.toString() ?? 'MB';
|
||||
|
||||
|
||||
final processes = <AmdSmiMemProcess>[];
|
||||
final processesData = memData['processes'];
|
||||
if (processesData is List) {
|
||||
@@ -119,7 +119,7 @@ class AmdSmi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return AmdSmiMem(total, used, unit, processes);
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ class AmdSmi {
|
||||
final pid = _parseIntValue(procData['pid']);
|
||||
final name = procData['name']?.toString() ?? procData['process_name']?.toString() ?? 'Unknown';
|
||||
final memory = _parseIntValue(procData['memory'] ?? procData['used_memory']);
|
||||
|
||||
|
||||
if (pid == 0) return null;
|
||||
return AmdSmiMemProcess(pid, name, memory);
|
||||
}
|
||||
@@ -185,4 +185,4 @@ class AmdSmiMemProcess {
|
||||
String toString() {
|
||||
return 'AmdSmiMemProcess{pid: $pid, name: $name, memory: $memory}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,7 @@ class Battery {
|
||||
final int? cycle;
|
||||
final String? tech;
|
||||
|
||||
const Battery({
|
||||
required this.status,
|
||||
this.percent,
|
||||
this.name,
|
||||
this.cycle,
|
||||
this.tech,
|
||||
});
|
||||
const Battery({required this.status, this.percent, this.name, this.cycle, this.tech});
|
||||
|
||||
factory Battery.fromRaw(String raw) {
|
||||
final lines = raw.split('\n');
|
||||
@@ -63,8 +57,7 @@ enum BatteryStatus {
|
||||
charging,
|
||||
discharging,
|
||||
full,
|
||||
unknown,
|
||||
;
|
||||
unknown;
|
||||
|
||||
static BatteryStatus parse(String? status) {
|
||||
switch (status) {
|
||||
|
||||
@@ -6,17 +6,11 @@ class Conn {
|
||||
final int passive;
|
||||
final int fail;
|
||||
|
||||
const Conn({
|
||||
required this.maxConn,
|
||||
required this.active,
|
||||
required this.passive,
|
||||
required this.fail,
|
||||
});
|
||||
const Conn({required this.maxConn, required this.active, required this.passive, required this.fail});
|
||||
|
||||
static Conn? parse(String raw) {
|
||||
final lines = raw.split('\n');
|
||||
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
|
||||
orElse: () => '');
|
||||
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'), orElse: () => '');
|
||||
if (idx != '') {
|
||||
final vals = idx.split(Miscs.blankReg);
|
||||
return Conn(
|
||||
|
||||
@@ -200,22 +200,98 @@ final class CpuBrand {
|
||||
}
|
||||
|
||||
final _bsdCpuPercentReg = RegExp(r'(\d+\.\d+)%');
|
||||
final _macCpuPercentReg = RegExp(
|
||||
r'CPU usage: ([\d.]+)% user, ([\d.]+)% sys, ([\d.]+)% idle');
|
||||
final _freebsdCpuPercentReg = RegExp(
|
||||
r'CPU: ([\d.]+)% user, ([\d.]+)% nice, ([\d.]+)% system, '
|
||||
r'([\d.]+)% interrupt, ([\d.]+)% idle');
|
||||
|
||||
/// TODO: Change this implementation to parse cpu status on BSD system
|
||||
/// Parse CPU status on BSD system with support for different BSD variants
|
||||
///
|
||||
/// [raw]:
|
||||
/// CPU usage: 14.70% user, 12.76% sys, 72.52% idle
|
||||
/// Supports multiple formats:
|
||||
/// - macOS: "CPU usage: 14.70% user, 12.76% sys, 72.52% idle"
|
||||
/// - FreeBSD: "CPU: 5.2% user, 0.0% nice, 3.1% system, 0.1% interrupt, 91.6% idle"
|
||||
/// - Generic BSD: fallback to percentage extraction
|
||||
Cpus parseBsdCpu(String raw) {
|
||||
final init = InitStatus.cpus;
|
||||
|
||||
// Try macOS format first
|
||||
final macMatch = _macCpuPercentReg.firstMatch(raw);
|
||||
if (macMatch != null) {
|
||||
final userPercent = double.parse(macMatch.group(1)!).toInt();
|
||||
final sysPercent = double.parse(macMatch.group(2)!).toInt();
|
||||
final idlePercent = double.parse(macMatch.group(3)!).toInt();
|
||||
|
||||
init.add([
|
||||
SingleCpuCore(
|
||||
'cpu0',
|
||||
userPercent,
|
||||
sysPercent,
|
||||
0, // nice
|
||||
idlePercent,
|
||||
0, // iowait
|
||||
0, // irq
|
||||
0, // softirq
|
||||
),
|
||||
]);
|
||||
return init;
|
||||
}
|
||||
|
||||
// Try FreeBSD format
|
||||
final freebsdMatch = _freebsdCpuPercentReg.firstMatch(raw);
|
||||
if (freebsdMatch != null) {
|
||||
final userPercent = double.parse(freebsdMatch.group(1)!).toInt();
|
||||
final nicePercent = double.parse(freebsdMatch.group(2)!).toInt();
|
||||
final sysPercent = double.parse(freebsdMatch.group(3)!).toInt();
|
||||
final irqPercent = double.parse(freebsdMatch.group(4)!).toInt();
|
||||
final idlePercent = double.parse(freebsdMatch.group(5)!).toInt();
|
||||
|
||||
init.add([
|
||||
SingleCpuCore(
|
||||
'cpu0',
|
||||
userPercent,
|
||||
sysPercent,
|
||||
nicePercent,
|
||||
idlePercent,
|
||||
0, // iowait
|
||||
irqPercent,
|
||||
0, // softirq
|
||||
),
|
||||
]);
|
||||
return init;
|
||||
}
|
||||
|
||||
// Fallback to generic percentage extraction
|
||||
final percents = _bsdCpuPercentReg
|
||||
.allMatches(raw)
|
||||
.map((e) => double.parse(e.group(1) ?? '0') * 100)
|
||||
.map((e) => double.parse(e.group(1) ?? '0'))
|
||||
.toList();
|
||||
if (percents.length != 3) return InitStatus.cpus;
|
||||
|
||||
final init = InitStatus.cpus;
|
||||
init.add([
|
||||
SingleCpuCore('cpu', percents[0].toInt(), 0, 0,
|
||||
percents[2].toInt() + percents[1].toInt(), 0, 0, 0),
|
||||
]);
|
||||
|
||||
if (percents.length >= 3) {
|
||||
// Validate that percentages are reasonable (0-100 range)
|
||||
final validPercents = percents.where((p) => p >= 0 && p <= 100).toList();
|
||||
if (validPercents.length != percents.length) {
|
||||
Loggers.app.warning('BSD CPU fallback parsing found invalid percentages in: $raw');
|
||||
}
|
||||
|
||||
init.add([
|
||||
SingleCpuCore(
|
||||
'cpu0',
|
||||
percents[0].toInt(), // user
|
||||
percents.length > 1 ? percents[1].toInt() : 0, // sys
|
||||
0, // nice
|
||||
percents.length > 2 ? percents[2].toInt() : 0, // idle
|
||||
0, // iowait
|
||||
0, // irq
|
||||
0, // softirq
|
||||
),
|
||||
]);
|
||||
return init;
|
||||
} else if (percents.isNotEmpty) {
|
||||
Loggers.app.warning('BSD CPU fallback parsing found ${percents.length} percentages (expected at least 3) in: $raw');
|
||||
} else {
|
||||
Loggers.app.warning('BSD CPU fallback parsing found no percentages in: $raw');
|
||||
}
|
||||
|
||||
return init;
|
||||
}
|
||||
|
||||
@@ -70,14 +70,14 @@ class Disk with EquatableMixin {
|
||||
if (disk != null) {
|
||||
list.add(disk);
|
||||
}
|
||||
|
||||
|
||||
// For devices with children (like physical disks with partitions),
|
||||
// also process each child individually to ensure BTRFS RAID disks are properly handled
|
||||
final List<dynamic> childDevices = device['children'] ?? [];
|
||||
for (final childDevice in childDevices) {
|
||||
final String childPath = childDevice['path']?.toString() ?? '';
|
||||
final String childFsType = childDevice['fstype']?.toString() ?? '';
|
||||
|
||||
|
||||
// If this is a BTRFS partition, add it directly to ensure it's properly represented
|
||||
if (childFsType == 'btrfs' && childPath.isNotEmpty) {
|
||||
final childDisk = _processSingleDevice(childDevice);
|
||||
@@ -93,11 +93,11 @@ class Disk with EquatableMixin {
|
||||
final fstype = device['fstype']?.toString();
|
||||
final String mountpoint = device['mountpoint']?.toString() ?? '';
|
||||
final String path = device['path']?.toString() ?? '';
|
||||
|
||||
|
||||
if (path.isEmpty || (fstype == null && mountpoint.isEmpty)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (!_shouldCalc(fstype ?? '', mountpoint)) {
|
||||
return null;
|
||||
}
|
||||
@@ -154,8 +154,7 @@ class Disk with EquatableMixin {
|
||||
}
|
||||
|
||||
// Handle common filesystem cases or parent devices with children
|
||||
if ((fstype != null && _shouldCalc(fstype, mount)) ||
|
||||
(childDisks.isNotEmpty && path.isNotEmpty)) {
|
||||
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);
|
||||
|
||||
@@ -221,14 +220,16 @@ class Disk with EquatableMixin {
|
||||
final fs = vals[0];
|
||||
final mount = vals[5];
|
||||
if (!_shouldCalc(fs, mount)) continue;
|
||||
list.add(Disk(
|
||||
path: fs,
|
||||
mount: mount,
|
||||
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
|
||||
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
|
||||
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
|
||||
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
|
||||
));
|
||||
list.add(
|
||||
Disk(
|
||||
path: fs,
|
||||
mount: mount,
|
||||
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
|
||||
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
|
||||
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
|
||||
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
@@ -237,8 +238,19 @@ class Disk with EquatableMixin {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[path, name, kname, fsTyp, mount, usedPercent, used, size, avail, uuid, children];
|
||||
List<Object?> get props => [
|
||||
path,
|
||||
name,
|
||||
kname,
|
||||
fsTyp,
|
||||
mount,
|
||||
usedPercent,
|
||||
used,
|
||||
size,
|
||||
avail,
|
||||
uuid,
|
||||
children,
|
||||
];
|
||||
}
|
||||
|
||||
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
||||
@@ -314,12 +326,14 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
||||
try {
|
||||
final dev = vals[2];
|
||||
if (dev.startsWith('loop')) continue;
|
||||
items.add(DiskIOPiece(
|
||||
dev: dev,
|
||||
sectorsRead: int.parse(vals[5]),
|
||||
sectorsWrite: int.parse(vals[9]),
|
||||
time: time,
|
||||
));
|
||||
items.add(
|
||||
DiskIOPiece(
|
||||
dev: dev,
|
||||
sectorsRead: int.parse(vals[5]),
|
||||
sectorsWrite: int.parse(vals[9]),
|
||||
time: time,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
@@ -334,12 +348,7 @@ class DiskIOPiece extends TimeSeqIface<DiskIOPiece> {
|
||||
final int sectorsWrite;
|
||||
final int time;
|
||||
|
||||
DiskIOPiece({
|
||||
required this.dev,
|
||||
required this.sectorsRead,
|
||||
required this.sectorsWrite,
|
||||
required this.time,
|
||||
});
|
||||
DiskIOPiece({required this.dev, required this.sectorsRead, required this.sectorsWrite, required this.time});
|
||||
|
||||
@override
|
||||
bool same(DiskIOPiece other) => dev == other.dev;
|
||||
@@ -349,10 +358,7 @@ class DiskUsage {
|
||||
final BigInt used;
|
||||
final BigInt size;
|
||||
|
||||
DiskUsage({
|
||||
required this.used,
|
||||
required this.size,
|
||||
});
|
||||
DiskUsage({required this.used, required this.size});
|
||||
|
||||
double get usedPercent {
|
||||
// Avoid division by zero
|
||||
|
||||
@@ -12,7 +12,6 @@ enum Dist {
|
||||
rocky,
|
||||
deepin,
|
||||
coreelec,
|
||||
;
|
||||
}
|
||||
|
||||
extension StringX on String {
|
||||
@@ -34,6 +33,4 @@ extension StringX on String {
|
||||
|
||||
// Special rules
|
||||
|
||||
const _wrts = [
|
||||
'istoreos',
|
||||
];
|
||||
const _wrts = ['istoreos'];
|
||||
|
||||
@@ -5,11 +5,7 @@ class Memory {
|
||||
final int free;
|
||||
final int avail;
|
||||
|
||||
const Memory({
|
||||
required this.total,
|
||||
required this.free,
|
||||
required this.avail,
|
||||
});
|
||||
const Memory({required this.total, required this.free, required this.avail});
|
||||
|
||||
double get availPercent {
|
||||
if (avail == 0) {
|
||||
@@ -23,46 +19,99 @@ class Memory {
|
||||
static Memory parse(String raw) {
|
||||
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
||||
|
||||
final total = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
|
||||
?.group(2) ??
|
||||
'1') ??
|
||||
1;
|
||||
final free = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
|
||||
?.group(2) ??
|
||||
'0') ??
|
||||
0;
|
||||
final available = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
|
||||
?.group(2) ??
|
||||
'0') ??
|
||||
0;
|
||||
final total = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
|
||||
?.group(2) ?? '1') ?? 1;
|
||||
final free = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
|
||||
?.group(2) ?? '0') ?? 0;
|
||||
final available = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
|
||||
?.group(2) ?? '0') ?? 0;
|
||||
|
||||
return Memory(
|
||||
total: total,
|
||||
free: free,
|
||||
avail: available,
|
||||
);
|
||||
return Memory(total: total, free: free, avail: available);
|
||||
}
|
||||
}
|
||||
|
||||
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
|
||||
|
||||
/// Parse BSD/macOS memory from top output
|
||||
///
|
||||
/// Supports formats like:
|
||||
/// - macOS: "PhysMem: 32G used (1536M wired), 64G unused."
|
||||
/// - FreeBSD: "Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free"
|
||||
Memory parseBsdMemory(String raw) {
|
||||
// Try macOS format first: "PhysMem: 32G used (1536M wired), 64G unused."
|
||||
final macMemReg = RegExp(
|
||||
r'PhysMem:\s*([\d.]+)([KMGT])\s*used.*?,\s*([\d.]+)([KMGT])\s*unused');
|
||||
final macMatch = macMemReg.firstMatch(raw);
|
||||
|
||||
if (macMatch != null) {
|
||||
final usedAmount = double.parse(macMatch.group(1)!);
|
||||
final usedUnit = macMatch.group(2)!;
|
||||
final freeAmount = double.parse(macMatch.group(3)!);
|
||||
final freeUnit = macMatch.group(4)!;
|
||||
|
||||
final usedKB = _convertToKB(usedAmount, usedUnit);
|
||||
final freeKB = _convertToKB(freeAmount, freeUnit);
|
||||
return Memory(total: usedKB + freeKB, free: freeKB, avail: freeKB);
|
||||
}
|
||||
|
||||
// Try FreeBSD format: "Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free"
|
||||
final freeBsdReg = RegExp(
|
||||
r'(\d+)([KMGT])\s+(Active|Inact|Wired|Cache|Buf|Free)', caseSensitive: false);
|
||||
final matches = freeBsdReg.allMatches(raw);
|
||||
|
||||
if (matches.isNotEmpty) {
|
||||
double usedKB = 0;
|
||||
double freeKB = 0;
|
||||
for (final match in matches) {
|
||||
final amount = double.parse(match.group(1)!);
|
||||
final unit = match.group(2)!;
|
||||
final keyword = match.group(3)!.toLowerCase();
|
||||
final kb = _convertToKB(amount, unit);
|
||||
|
||||
// Only sum known keywords
|
||||
if (keyword == 'active' || keyword == 'inact' || keyword == 'wired' || keyword == 'cache' || keyword == 'buf') {
|
||||
usedKB += kb;
|
||||
} else if (keyword == 'free') {
|
||||
freeKB += kb;
|
||||
}
|
||||
}
|
||||
return Memory(total: (usedKB + freeKB).round(), free: freeKB.round(), avail: freeKB.round());
|
||||
}
|
||||
|
||||
// If neither format matches, throw an error to avoid misinterpretation
|
||||
throw FormatException('Unrecognized BSD/macOS memory format: $raw');
|
||||
}
|
||||
|
||||
/// Convert memory size to KB based on unit
|
||||
int _convertToKB(double amount, String unit) {
|
||||
switch (unit.toUpperCase()) {
|
||||
case 'T':
|
||||
return (amount * 1024 * 1024 * 1024).round();
|
||||
case 'G':
|
||||
return (amount * 1024 * 1024).round();
|
||||
case 'M':
|
||||
return (amount * 1024).round();
|
||||
case 'K':
|
||||
case '':
|
||||
return amount.round();
|
||||
default:
|
||||
return amount.round();
|
||||
}
|
||||
}
|
||||
|
||||
class Swap {
|
||||
final int total;
|
||||
final int free;
|
||||
final int cached;
|
||||
|
||||
const Swap({
|
||||
required this.total,
|
||||
required this.free,
|
||||
required this.cached,
|
||||
});
|
||||
const Swap({required this.total, required this.free, required this.cached});
|
||||
|
||||
double get usedPercent => 1 - free / total;
|
||||
double get usedPercent => total == 0 ? 0.0 : 1 - free / total;
|
||||
|
||||
double get freePercent => free / total;
|
||||
double get freePercent => total == 0 ? 0.0 : free / total;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -72,26 +121,16 @@ class Swap {
|
||||
static Swap parse(String raw) {
|
||||
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
||||
|
||||
final total = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
|
||||
?.group(2) ??
|
||||
'1') ??
|
||||
0;
|
||||
final free = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
|
||||
?.group(2) ??
|
||||
'1') ??
|
||||
0;
|
||||
final cached = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
|
||||
?.group(2) ??
|
||||
'0') ??
|
||||
0;
|
||||
final total = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
|
||||
?.group(2) ?? '1') ?? 0;
|
||||
final free = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
|
||||
?.group(2) ?? '1') ?? 0;
|
||||
final cached = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
|
||||
?.group(2) ?? '0') ?? 0;
|
||||
|
||||
return Swap(
|
||||
total: total,
|
||||
free: free,
|
||||
cached: cached,
|
||||
);
|
||||
return Swap(total: total, free: free, cached: cached);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,7 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
|
||||
bool same(NetSpeedPart other) => device == other.device;
|
||||
}
|
||||
|
||||
typedef CachedNetVals = ({
|
||||
String sizeIn,
|
||||
String sizeOut,
|
||||
String speedIn,
|
||||
String speedOut,
|
||||
});
|
||||
typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});
|
||||
|
||||
class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
||||
NetSpeed(super.init1, super.init2);
|
||||
@@ -32,20 +27,14 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
||||
devices.addAll(now.map((e) => e.device).toList());
|
||||
|
||||
realIfaces.clear();
|
||||
realIfaces.addAll(devices
|
||||
.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
|
||||
realIfaces.addAll(devices.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
|
||||
|
||||
final sizeIn = this.sizeIn();
|
||||
final sizeOut = this.sizeOut();
|
||||
final speedIn = this.speedIn();
|
||||
final speedOut = this.speedOut();
|
||||
|
||||
cachedVals = (
|
||||
sizeIn: sizeIn,
|
||||
sizeOut: sizeOut,
|
||||
speedIn: speedIn,
|
||||
speedOut: speedOut,
|
||||
);
|
||||
cachedVals = (sizeIn: sizeIn, sizeOut: sizeOut, speedIn: speedIn, speedOut: speedOut);
|
||||
}
|
||||
|
||||
/// Cached network device list
|
||||
@@ -58,15 +47,13 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
||||
/// Cached non-virtual network device prefix
|
||||
final realIfaces = <String>[];
|
||||
|
||||
CachedNetVals cachedVals =
|
||||
(sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
|
||||
CachedNetVals cachedVals = (sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
|
||||
|
||||
/// Time diff between [pre] and [now]
|
||||
BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);
|
||||
|
||||
double speedInBytes(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
|
||||
double speedOutBytes(int i) =>
|
||||
(now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
|
||||
double speedOutBytes(int i) => (now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
|
||||
BigInt sizeInBytes(int i) => now[i].bytesIn;
|
||||
BigInt sizeOutBytes(int i) => now[i].bytesOut;
|
||||
|
||||
|
||||
@@ -35,25 +35,17 @@ class NvidiaSmi {
|
||||
.firstOrNull
|
||||
?.innerText;
|
||||
final power = gpu.findElements('gpu_power_readings').firstOrNull;
|
||||
final powerDraw =
|
||||
power?.findElements('power_draw').firstOrNull?.innerText;
|
||||
final powerLimit =
|
||||
power?.findElements('current_power_limit').firstOrNull?.innerText;
|
||||
final powerDraw = power?.findElements('power_draw').firstOrNull?.innerText;
|
||||
final powerLimit = power?.findElements('current_power_limit').firstOrNull?.innerText;
|
||||
final memory = gpu.findElements('fb_memory_usage').firstOrNull;
|
||||
final memoryUsed = memory?.findElements('used').firstOrNull?.innerText;
|
||||
final memoryTotal = memory?.findElements('total').firstOrNull?.innerText;
|
||||
final processes = gpu
|
||||
.findElements('processes')
|
||||
.firstOrNull
|
||||
?.findElements('process_info');
|
||||
final memoryProcesses =
|
||||
List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) {
|
||||
final processes = gpu.findElements('processes').firstOrNull?.findElements('process_info');
|
||||
final memoryProcesses = List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) {
|
||||
final process = processes?.elementAt(index);
|
||||
final pid = process?.findElements('pid').firstOrNull?.innerText;
|
||||
final name =
|
||||
process?.findElements('process_name').firstOrNull?.innerText;
|
||||
final memory =
|
||||
process?.findElements('used_memory').firstOrNull?.innerText;
|
||||
final name = process?.findElements('process_name').firstOrNull?.innerText;
|
||||
final memory = process?.findElements('used_memory').firstOrNull?.innerText;
|
||||
if (pid != null && name != null && memory != null) {
|
||||
return NvidiaSmiMemProcess(
|
||||
int.tryParse(pid) ?? 0,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
final parseFailed = Exception('Parse failed');
|
||||
final seqReg = RegExp(r'seq=(.+) ttl=(.+) time=(.+) ms');
|
||||
final packetReg =
|
||||
RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
|
||||
final packetReg = RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
|
||||
final timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms');
|
||||
final timeAlpineReg = RegExp(r'round-trip min/avg/max = (.+)/(.+)/(.+) ms');
|
||||
final ipReg = RegExp(r' \((\S+)\)');
|
||||
@@ -15,17 +14,13 @@ class PingResult {
|
||||
PingResult.parse(this.serverName, String raw) {
|
||||
final lines = raw.split('\n');
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
final statisticIndex =
|
||||
lines.indexWhere((element) => element.startsWith('---'));
|
||||
final statisticIndex = lines.indexWhere((element) => element.startsWith('---'));
|
||||
if (statisticIndex == -1) {
|
||||
throw parseFailed;
|
||||
}
|
||||
final statisticRaw = lines.sublist(statisticIndex + 1);
|
||||
statistic = PingStatistics.parse(statisticRaw);
|
||||
results = lines
|
||||
.sublist(1, statisticIndex)
|
||||
.map((e) => PingSeqResult.parse(e))
|
||||
.toList();
|
||||
results = lines.sublist(1, statisticIndex).map((e) => PingSeqResult.parse(e)).toList();
|
||||
ip = ipReg.firstMatch(lines[0])?.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ class PrivateKeyInfo {
|
||||
@JsonKey(name: 'private_key')
|
||||
final String key;
|
||||
|
||||
const PrivateKeyInfo({
|
||||
required this.id,
|
||||
required this.key,
|
||||
});
|
||||
const PrivateKeyInfo({required this.id, required this.key});
|
||||
|
||||
factory PrivateKeyInfo.fromJson(Map<String, dynamic> json) => _$PrivateKeyInfoFromJson(json);
|
||||
|
||||
|
||||
@@ -107,10 +107,7 @@ class PsResult {
|
||||
final List<Proc> procs;
|
||||
final String? error;
|
||||
|
||||
const PsResult({
|
||||
required this.procs,
|
||||
this.error,
|
||||
});
|
||||
const PsResult({required this.procs, this.error});
|
||||
|
||||
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
|
||||
final lines = raw.split('\n').map((e) => e.trim()).toList();
|
||||
@@ -167,14 +164,7 @@ class PsResult {
|
||||
}
|
||||
}
|
||||
|
||||
enum ProcSortMode {
|
||||
cpu,
|
||||
mem,
|
||||
pid,
|
||||
user,
|
||||
name,
|
||||
;
|
||||
}
|
||||
enum ProcSortMode { cpu, mem, pid, user, name }
|
||||
|
||||
extension _StrIndex on List<String> {
|
||||
int? indexOfOrNull(String val) {
|
||||
|
||||
@@ -6,25 +6,24 @@ enum PveResType {
|
||||
qemu,
|
||||
node,
|
||||
storage,
|
||||
sdn,
|
||||
;
|
||||
sdn;
|
||||
|
||||
static PveResType? fromString(String type) => switch (type.toLowerCase()) {
|
||||
'lxc' => PveResType.lxc,
|
||||
'qemu' => PveResType.qemu,
|
||||
'node' => PveResType.node,
|
||||
'storage' => PveResType.storage,
|
||||
'sdn' => PveResType.sdn,
|
||||
_ => null,
|
||||
};
|
||||
'lxc' => PveResType.lxc,
|
||||
'qemu' => PveResType.qemu,
|
||||
'node' => PveResType.node,
|
||||
'storage' => PveResType.storage,
|
||||
'sdn' => PveResType.sdn,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
PveResType.node => l10n.node,
|
||||
PveResType.qemu => 'QEMU',
|
||||
PveResType.lxc => 'LXC',
|
||||
PveResType.storage => l10n.storage,
|
||||
PveResType.sdn => 'SDN',
|
||||
};
|
||||
PveResType.node => l10n.node,
|
||||
PveResType.qemu => 'QEMU',
|
||||
PveResType.lxc => 'LXC',
|
||||
PveResType.storage => l10n.storage,
|
||||
PveResType.sdn => 'SDN',
|
||||
};
|
||||
}
|
||||
|
||||
sealed class PveResIface {
|
||||
@@ -334,13 +333,7 @@ final class PveSdn extends PveResIface implements PveCtrlIface {
|
||||
@override
|
||||
final String status;
|
||||
|
||||
PveSdn({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.sdn,
|
||||
required this.node,
|
||||
required this.status,
|
||||
});
|
||||
PveSdn({required this.id, required this.type, required this.sdn, required this.node, required this.status});
|
||||
|
||||
static PveSdn fromJson(Map<String, dynamic> json) {
|
||||
return PveSdn(
|
||||
@@ -379,8 +372,7 @@ final class PveRes {
|
||||
|
||||
bool get onlyOneNode => nodes.length == 1;
|
||||
|
||||
int get length =>
|
||||
qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
|
||||
int get length => qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
|
||||
|
||||
PveResIface operator [](int index) {
|
||||
if (index < nodes.length) {
|
||||
@@ -432,29 +424,13 @@ final class PveRes {
|
||||
}
|
||||
|
||||
if (old != null) {
|
||||
qemus.reorder(
|
||||
order: old.qemus.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
lxcs.reorder(
|
||||
order: old.lxcs.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
nodes.reorder(
|
||||
order: old.nodes.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
storages.reorder(
|
||||
order: old.storages.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
sdns.reorder(
|
||||
order: old.sdns.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
qemus.reorder(order: old.qemus.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
lxcs.reorder(order: old.lxcs.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
nodes.reorder(order: old.nodes.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
storages.reorder(order: old.storages.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
sdns.reorder(order: old.sdns.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
}
|
||||
|
||||
return PveRes(
|
||||
qemus: qemus,
|
||||
lxcs: lxcs,
|
||||
nodes: nodes,
|
||||
storages: storages,
|
||||
sdns: sdns,
|
||||
);
|
||||
return PveRes(qemus: qemus, lxcs: lxcs, nodes: nodes, storages: storages, sdns: sdns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ final class SensorAdaptor {
|
||||
static const isa = SensorAdaptor(isaRaw);
|
||||
|
||||
static SensorAdaptor parse(String raw) => switch (raw) {
|
||||
acpiRaw => acpi,
|
||||
pciRaw => pci,
|
||||
virtualRaw => virtual,
|
||||
isaRaw => isa,
|
||||
_ => SensorAdaptor(raw),
|
||||
};
|
||||
acpiRaw => acpi,
|
||||
pciRaw => pci,
|
||||
virtualRaw => virtual,
|
||||
isaRaw => isa,
|
||||
_ => SensorAdaptor(raw),
|
||||
};
|
||||
}
|
||||
|
||||
final class SensorItem {
|
||||
@@ -28,11 +28,7 @@ final class SensorItem {
|
||||
final SensorAdaptor adapter;
|
||||
final Map<String, String> details;
|
||||
|
||||
const SensorItem({
|
||||
required this.device,
|
||||
required this.adapter,
|
||||
required this.details,
|
||||
});
|
||||
const SensorItem({required this.device, required this.adapter, required this.details});
|
||||
|
||||
String get toMarkdown {
|
||||
final sb = StringBuffer();
|
||||
@@ -72,8 +68,7 @@ final class SensorItem {
|
||||
final len = sensorLines.length;
|
||||
if (len < 3) continue;
|
||||
final device = sensorLines.first;
|
||||
final adapter =
|
||||
SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
|
||||
final adapter = SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
|
||||
|
||||
final details = <String, String>{};
|
||||
for (var idx = 2; idx < len; idx++) {
|
||||
@@ -84,11 +79,7 @@ final class SensorItem {
|
||||
final value = detailParts[1].trim();
|
||||
details[key] = value;
|
||||
}
|
||||
sensors.add(SensorItem(
|
||||
device: device,
|
||||
adapter: adapter,
|
||||
details: details,
|
||||
));
|
||||
sensors.add(SensorItem(device: device, adapter: adapter, details: details));
|
||||
}
|
||||
|
||||
return sensors;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/server.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/server.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
@@ -44,6 +45,9 @@ abstract class Spi with _$Spi {
|
||||
/// It only applies to SSH terminal.
|
||||
Map<String, String>? envs,
|
||||
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
|
||||
|
||||
/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||
@JsonKey(includeIfNull: false) SystemType? customSystemType,
|
||||
}) = _Spi;
|
||||
|
||||
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||
@@ -119,26 +123,25 @@ extension Spix on Spi {
|
||||
///
|
||||
/// **NOT** the default value.
|
||||
static final example = Spi(
|
||||
name: 'name',
|
||||
ip: 'ip',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
pwd: 'pwd',
|
||||
keyId: 'private_key_id',
|
||||
tags: ['tag1', 'tag2'],
|
||||
alterUrl: 'user@ip:port',
|
||||
autoConnect: true,
|
||||
jumpId: 'jump_server_id',
|
||||
custom: ServerCustom(
|
||||
pveAddr: 'http://localhost:8006',
|
||||
pveIgnoreCert: false,
|
||||
cmds: {
|
||||
'echo': 'echo hello',
|
||||
},
|
||||
preferTempDev: 'nvme-pci-0400',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
),
|
||||
id: 'id');
|
||||
name: 'name',
|
||||
ip: 'ip',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
pwd: 'pwd',
|
||||
keyId: 'private_key_id',
|
||||
tags: ['tag1', 'tag2'],
|
||||
alterUrl: 'user@ip:port',
|
||||
autoConnect: true,
|
||||
jumpId: 'jump_server_id',
|
||||
custom: ServerCustom(
|
||||
pveAddr: 'http://localhost:8006',
|
||||
pveIgnoreCert: false,
|
||||
cmds: {'echo': 'echo hello'},
|
||||
preferTempDev: 'nvme-pci-0400',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
),
|
||||
id: 'id',
|
||||
);
|
||||
|
||||
bool get isRoot => user == 'root';
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ mixin _$Spi {
|
||||
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
|
||||
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
|
||||
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
||||
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;
|
||||
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||
@JsonKey(includeIfNull: false) SystemType? get customSystemType;
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -32,12 +33,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id);
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType);
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +49,7 @@ abstract mixin class $SpiCopyWith<$Res> {
|
||||
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType
|
||||
});
|
||||
|
||||
|
||||
@@ -65,7 +66,7 @@ class _$SpiCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||
@@ -81,7 +82,8 @@ as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nul
|
||||
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
||||
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
|
||||
as SystemType?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -92,7 +94,7 @@ as String,
|
||||
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class _Spi extends Spi {
|
||||
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = ''}): _tags = tags,_envs = envs,super._();
|
||||
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType}): _tags = tags,_envs = envs,super._();
|
||||
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||
|
||||
@override final String name;
|
||||
@@ -129,6 +131,8 @@ class _Spi extends Spi {
|
||||
}
|
||||
|
||||
@override@JsonKey(fromJson: Spi.parseId) final String id;
|
||||
/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||
@override@JsonKey(includeIfNull: false) final SystemType? customSystemType;
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -143,12 +147,12 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id);
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType);
|
||||
|
||||
|
||||
|
||||
@@ -159,7 +163,7 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
|
||||
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType
|
||||
});
|
||||
|
||||
|
||||
@@ -176,7 +180,7 @@ class __$SpiCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,}) {
|
||||
return _then(_Spi(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||
@@ -192,7 +196,8 @@ as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nul
|
||||
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
||||
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
|
||||
as SystemType?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ _Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
id: json['id'] == null ? '' : Spi.parseId(json['id']),
|
||||
customSystemType: $enumDecodeNullable(
|
||||
_$SystemTypeEnumMap,
|
||||
json['customSystemType'],
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
||||
@@ -44,4 +48,12 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
||||
if (instance.wolCfg case final value?) 'wolCfg': value,
|
||||
if (instance.envs case final value?) 'envs': value,
|
||||
'id': instance.id,
|
||||
if (_$SystemTypeEnumMap[instance.customSystemType] case final value?)
|
||||
'customSystemType': value,
|
||||
};
|
||||
|
||||
const _$SystemTypeEnumMap = {
|
||||
SystemType.linux: 'linux',
|
||||
SystemType.bsd: 'bsd',
|
||||
SystemType.windows: 'windows',
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/amd.dart';
|
||||
@@ -12,6 +14,8 @@ import 'package:server_box/data/model/server/nvdia.dart';
|
||||
import 'package:server_box/data/model/server/sensors.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/temp.dart';
|
||||
import 'package:server_box/data/model/server/windows_parser.dart';
|
||||
|
||||
class ServerStatusUpdateReq {
|
||||
final ServerStatus ss;
|
||||
@@ -31,6 +35,7 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
|
||||
return switch (req.system) {
|
||||
SystemType.linux => _getLinuxStatus(req),
|
||||
SystemType.bsd => _getBsdStatus(req),
|
||||
SystemType.windows => _getWindowsStatus(req),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,8 +44,7 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
|
||||
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
final segments = req.segments;
|
||||
|
||||
final time =
|
||||
int.tryParse(StatusCmdType.time.find(segments)) ??
|
||||
final time = int.tryParse(StatusCmdType.time.find(segments)) ??
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
try {
|
||||
@@ -210,11 +214,11 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
// try {
|
||||
// req.ss.mem = parseBsdMem(BSDStatusCmdType.mem.find(segments));
|
||||
// } catch (e, s) {
|
||||
// Loggers.app.warning(e, s);
|
||||
// }
|
||||
try {
|
||||
req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.find(segments));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final uptime = _parseUpTime(BSDStatusCmdType.uptime.find(segments));
|
||||
@@ -235,13 +239,48 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
|
||||
|
||||
// raw:
|
||||
// 19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00
|
||||
// 19:39:15 up 1 day, 2:34, 1 user, load average: 0.00, 0.00, 0.00
|
||||
// 19:39:15 up 2:34, 1 user, load average: 0.00, 0.00, 0.00
|
||||
// 19:39:15 up 34 min, 1 user, load average: 0.00, 0.00, 0.00
|
||||
String? _parseUpTime(String raw) {
|
||||
final splitedUp = raw.split('up ');
|
||||
if (splitedUp.length == 2) {
|
||||
final splitedComma = splitedUp[1].split(', ');
|
||||
if (splitedComma.length >= 2) {
|
||||
return splitedComma[0];
|
||||
final uptimePart = splitedUp[1];
|
||||
final splitedComma = uptimePart.split(', ');
|
||||
|
||||
if (splitedComma.isEmpty) return null;
|
||||
|
||||
// Handle different uptime formats
|
||||
final firstPart = splitedComma[0].trim();
|
||||
|
||||
// Case 1: "61 days" or "1 day" - need to get the time part from next segment
|
||||
if (firstPart.contains('day')) {
|
||||
if (splitedComma.length >= 2) {
|
||||
final timePart = splitedComma[1].trim();
|
||||
// Check if it's in HH:MM format
|
||||
if (timePart.contains(':') &&
|
||||
!timePart.contains('user') &&
|
||||
!timePart.contains('load')) {
|
||||
return '$firstPart, $timePart';
|
||||
}
|
||||
}
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Case 2: "2:34" (hours:minutes) - already in good format
|
||||
if (firstPart.contains(':') &&
|
||||
!firstPart.contains('user') &&
|
||||
!firstPart.contains('load')) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Case 3: "34 min" - already in good format
|
||||
if (firstPart.contains('min')) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Fallback: return first part
|
||||
return firstPart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -259,3 +298,406 @@ String? _parseHostName(String raw) {
|
||||
if (raw.contains(ShellFunc.scriptFile)) return null;
|
||||
return raw;
|
||||
}
|
||||
|
||||
// Windows status parsing implementation
|
||||
Future<ServerStatus> _getWindowsStatus(ServerStatusUpdateReq req) async {
|
||||
final segments = req.segments;
|
||||
final time = int.tryParse(WindowsStatusCmdType.time.find(segments)) ??
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
// Parse all different resource types using helper methods
|
||||
_parseWindowsNetworkData(req, segments, time);
|
||||
_parseWindowsSystemData(req, segments);
|
||||
_parseWindowsHostData(req, segments);
|
||||
_parseWindowsCpuData(req, segments);
|
||||
_parseWindowsMemoryData(req, segments);
|
||||
_parseWindowsDiskData(req, segments);
|
||||
_parseWindowsUptimeData(req, segments);
|
||||
_parseWindowsDiskIOData(req, segments, time);
|
||||
_parseWindowsConnectionData(req, segments);
|
||||
_parseWindowsBatteryData(req, segments);
|
||||
_parseWindowsTemperatureData(req, segments);
|
||||
_parseWindowsGpuData(req, segments);
|
||||
WindowsParser.parseCustomCommands(req.ss, segments, req.customCmds, req.system.segmentsLen);
|
||||
|
||||
return req.ss;
|
||||
}
|
||||
|
||||
/// Parse Windows network data
|
||||
void _parseWindowsNetworkData(ServerStatusUpdateReq req, List<String> segments, int time) {
|
||||
try {
|
||||
final netRaw = WindowsStatusCmdType.net.find(segments);
|
||||
if (netRaw.isNotEmpty &&
|
||||
netRaw != 'null' &&
|
||||
!netRaw.contains('network_error') &&
|
||||
!netRaw.contains('error') &&
|
||||
!netRaw.contains('Exception')) {
|
||||
final netParts = _parseWindowsNetwork(netRaw, time);
|
||||
if (netParts.isNotEmpty) {
|
||||
req.ss.netSpeed.update(netParts);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows network parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows system information
|
||||
void _parseWindowsSystemData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
final sys = WindowsStatusCmdType.sys.find(segments);
|
||||
if (sys.isNotEmpty) {
|
||||
req.ss.more[StatusCmdType.sys] = sys;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows system parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows host information
|
||||
void _parseWindowsHostData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
final host = _parseHostName(WindowsStatusCmdType.host.find(segments));
|
||||
if (host != null) {
|
||||
req.ss.more[StatusCmdType.host] = host;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows host parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows CPU data and brand information
|
||||
void _parseWindowsCpuData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
// Windows CPU parsing - JSON format from PowerShell
|
||||
final cpuRaw = WindowsStatusCmdType.cpu.find(segments);
|
||||
if (cpuRaw.isNotEmpty &&
|
||||
cpuRaw != 'null' &&
|
||||
!cpuRaw.contains('error') &&
|
||||
!cpuRaw.contains('Exception')) {
|
||||
final cpus = WindowsParser.parseCpu(cpuRaw, req.ss);
|
||||
if (cpus.isNotEmpty) {
|
||||
req.ss.cpu.update(cpus);
|
||||
}
|
||||
}
|
||||
|
||||
// Windows CPU brand parsing
|
||||
final brandRaw = WindowsStatusCmdType.cpuBrand.find(segments);
|
||||
if (brandRaw.isNotEmpty && brandRaw != 'null') {
|
||||
req.ss.cpu.brand.clear();
|
||||
req.ss.cpu.brand[brandRaw.trim()] = 1;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows CPU parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows memory data
|
||||
void _parseWindowsMemoryData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
final memRaw = WindowsStatusCmdType.mem.find(segments);
|
||||
if (memRaw.isNotEmpty &&
|
||||
memRaw != 'null' &&
|
||||
!memRaw.contains('error') &&
|
||||
!memRaw.contains('Exception')) {
|
||||
final memory = WindowsParser.parseMemory(memRaw);
|
||||
if (memory != null) {
|
||||
req.ss.mem = memory;
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows memory parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows disk data
|
||||
void _parseWindowsDiskData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
final diskRaw = WindowsStatusCmdType.disk.find(segments);
|
||||
if (diskRaw.isNotEmpty && diskRaw != 'null') {
|
||||
final disks = WindowsParser.parseDisks(diskRaw);
|
||||
req.ss.disk = disks;
|
||||
req.ss.diskUsage = DiskUsage.parse(disks);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows disk parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows uptime data
|
||||
void _parseWindowsUptimeData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.find(segments));
|
||||
if (uptime != null) {
|
||||
req.ss.more[StatusCmdType.uptime] = uptime;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows uptime parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows disk I/O data
|
||||
void _parseWindowsDiskIOData(ServerStatusUpdateReq req, List<String> segments, int time) {
|
||||
try {
|
||||
final diskIOraw = WindowsStatusCmdType.diskio.find(segments);
|
||||
if (diskIOraw.isNotEmpty && diskIOraw != 'null') {
|
||||
final diskio = _parseWindowsDiskIO(diskIOraw, time);
|
||||
req.ss.diskIO.update(diskio);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows disk I/O parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows connection data
|
||||
void _parseWindowsConnectionData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
final connStr = WindowsStatusCmdType.conn.find(segments);
|
||||
final connCount = int.tryParse(connStr.trim());
|
||||
if (connCount != null) {
|
||||
req.ss.tcp = Conn(maxConn: 0, active: connCount, passive: 0, fail: 0);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows connection parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows battery data
|
||||
void _parseWindowsBatteryData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
final batteryRaw = WindowsStatusCmdType.battery.find(segments);
|
||||
if (batteryRaw.isNotEmpty && batteryRaw != 'null') {
|
||||
final batteries = _parseWindowsBatteries(batteryRaw);
|
||||
req.ss.batteries.clear();
|
||||
if (batteries.isNotEmpty) {
|
||||
req.ss.batteries.addAll(batteries);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows battery parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows temperature data
|
||||
void _parseWindowsTemperatureData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
final tempRaw = WindowsStatusCmdType.temp.find(segments);
|
||||
if (tempRaw.isNotEmpty && tempRaw != 'null') {
|
||||
_parseWindowsTemperatures(req.ss.temps, tempRaw);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows temperature parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows GPU data (NVIDIA/AMD)
|
||||
void _parseWindowsGpuData(ServerStatusUpdateReq req, List<String> segments) {
|
||||
try {
|
||||
req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.find(segments));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows NVIDIA GPU parsing failed: $e', s);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.find(segments));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows AMD GPU parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<Battery> _parseWindowsBatteries(String raw) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<Battery> batteries = [];
|
||||
|
||||
final batteryList = jsonData is List ? jsonData : [jsonData];
|
||||
|
||||
for (final batteryData in batteryList) {
|
||||
final chargeRemaining =
|
||||
batteryData['EstimatedChargeRemaining'] as int? ?? 0;
|
||||
final batteryStatus = batteryData['BatteryStatus'] as int? ?? 0;
|
||||
|
||||
// Windows battery status: 1=Other, 2=Unknown, 3=Full, 4=Low,
|
||||
// 5=Critical, 6=Charging, 7=ChargingAndLow, 8=ChargingAndCritical,
|
||||
// 9=Undefined, 10=PartiallyCharged
|
||||
final isCharging = batteryStatus == 6 ||
|
||||
batteryStatus == 7 ||
|
||||
batteryStatus == 8;
|
||||
|
||||
batteries.add(
|
||||
Battery(
|
||||
name: 'Battery',
|
||||
percent: chargeRemaining,
|
||||
status: isCharging
|
||||
? BatteryStatus.charging
|
||||
: BatteryStatus.discharging,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return batteries;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<NetSpeedPart> netParts = [];
|
||||
|
||||
// PowerShell Get-Counter returns a structure with CounterSamples
|
||||
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
|
||||
final samples = jsonData['CounterSamples'] as List?;
|
||||
if (samples != null && samples.length >= 2) {
|
||||
// We need 2 samples to calculate speed (interval between them)
|
||||
final Map<String, double> interfaceRx = {};
|
||||
final Map<String, double> interfaceTx = {};
|
||||
|
||||
for (final sample in samples) {
|
||||
final path = sample['Path']?.toString() ?? '';
|
||||
final cookedValue = sample['CookedValue'] as num? ?? 0;
|
||||
|
||||
if (path.contains('Bytes Received/sec')) {
|
||||
final interfaceName = _extractInterfaceName(path);
|
||||
if (interfaceName.isNotEmpty) {
|
||||
interfaceRx[interfaceName] = cookedValue.toDouble();
|
||||
}
|
||||
} else if (path.contains('Bytes Sent/sec')) {
|
||||
final interfaceName = _extractInterfaceName(path);
|
||||
if (interfaceName.isNotEmpty) {
|
||||
interfaceTx[interfaceName] = cookedValue.toDouble();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create NetSpeedPart for each interface
|
||||
for (final interfaceName in interfaceRx.keys) {
|
||||
final rx = interfaceRx[interfaceName] ?? 0;
|
||||
final tx = interfaceTx[interfaceName] ?? 0;
|
||||
|
||||
netParts.add(
|
||||
NetSpeedPart(
|
||||
interfaceName,
|
||||
BigInt.from(rx.toInt()),
|
||||
BigInt.from(tx.toInt()),
|
||||
currentTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return netParts;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
String _extractInterfaceName(String path) {
|
||||
// Extract interface name from path like
|
||||
// "\\Computer\\NetworkInterface(Interface Name)\\..."
|
||||
final match = RegExp(r'\\NetworkInterface\(([^)]+)\)\\').firstMatch(path);
|
||||
return match?.group(1) ?? '';
|
||||
}
|
||||
|
||||
List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<DiskIOPiece> diskParts = [];
|
||||
|
||||
// PowerShell Get-Counter returns a structure with CounterSamples
|
||||
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
|
||||
final samples = jsonData['CounterSamples'] as List?;
|
||||
if (samples != null) {
|
||||
final Map<String, double> diskReads = {};
|
||||
final Map<String, double> diskWrites = {};
|
||||
|
||||
for (final sample in samples) {
|
||||
final path = sample['Path']?.toString() ?? '';
|
||||
final cookedValue = sample['CookedValue'] as num? ?? 0;
|
||||
|
||||
if (path.contains('Disk Read Bytes/sec')) {
|
||||
final diskName = _extractDiskName(path);
|
||||
if (diskName.isNotEmpty) {
|
||||
diskReads[diskName] = cookedValue.toDouble();
|
||||
}
|
||||
} else if (path.contains('Disk Write Bytes/sec')) {
|
||||
final diskName = _extractDiskName(path);
|
||||
if (diskName.isNotEmpty) {
|
||||
diskWrites[diskName] = cookedValue.toDouble();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create DiskIOPiece for each disk - convert bytes to sectors
|
||||
// (assuming 512 bytes per sector)
|
||||
for (final diskName in diskReads.keys) {
|
||||
final readBytes = diskReads[diskName] ?? 0;
|
||||
final writeBytes = diskWrites[diskName] ?? 0;
|
||||
final sectorsRead = (readBytes / 512).round();
|
||||
final sectorsWrite = (writeBytes / 512).round();
|
||||
|
||||
diskParts.add(
|
||||
DiskIOPiece(
|
||||
dev: diskName,
|
||||
sectorsRead: sectorsRead,
|
||||
sectorsWrite: sectorsWrite,
|
||||
time: currentTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diskParts;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
String _extractDiskName(String path) {
|
||||
// Extract disk name from path like
|
||||
// "\\Computer\\PhysicalDisk(Disk Name)\\..."
|
||||
final match = RegExp(r'\\PhysicalDisk\(([^)]+)\)\\').firstMatch(path);
|
||||
return match?.group(1) ?? '';
|
||||
}
|
||||
|
||||
void _parseWindowsTemperatures(Temperatures temps, String raw) {
|
||||
try {
|
||||
// Handle error output
|
||||
if (raw.contains('Error') ||
|
||||
raw.contains('Exception') ||
|
||||
raw.contains('The term')) {
|
||||
return;
|
||||
}
|
||||
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final tempList = jsonData is List ? jsonData : [jsonData];
|
||||
|
||||
// Create fake type and value strings that the existing parse method can handle
|
||||
final typeLines = <String>[];
|
||||
final valueLines = <String>[];
|
||||
|
||||
for (int i = 0; i < tempList.length; i++) {
|
||||
final item = tempList[i];
|
||||
final typeName = item['InstanceName']?.toString() ?? 'Unknown';
|
||||
final temperature = item['Temperature'] as num?;
|
||||
|
||||
if (temperature != null) {
|
||||
// Convert to the format expected by the existing parse method
|
||||
typeLines.add('/sys/class/thermal/thermal_zone$i/$typeName');
|
||||
// Convert to millicelsius (multiply by 1000)
|
||||
// as expected by Linux parsing
|
||||
valueLines.add((temperature * 1000).round().toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (typeLines.isNotEmpty && valueLines.isNotEmpty) {
|
||||
temps.parse(typeLines.join('\n'), valueLines.join('\n'));
|
||||
}
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, ignore temperature data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,23 +35,16 @@ extension SnippetX on Snippet {
|
||||
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
||||
|
||||
String fmtWithSpi(Spi spi) {
|
||||
return script.replaceAllMapped(
|
||||
fmtFinder,
|
||||
(match) {
|
||||
final key = match.group(0);
|
||||
final func = fmtArgs[key];
|
||||
if (func != null) return func(spi);
|
||||
// If not found, return the original content for further processing
|
||||
return key ?? '';
|
||||
},
|
||||
);
|
||||
return script.replaceAllMapped(fmtFinder, (match) {
|
||||
final key = match.group(0);
|
||||
final func = fmtArgs[key];
|
||||
if (func != null) return func(spi);
|
||||
// If not found, return the original content for further processing
|
||||
return key ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> runInTerm(
|
||||
Terminal terminal,
|
||||
Spi spi, {
|
||||
bool autoEnter = false,
|
||||
}) async {
|
||||
Future<void> runInTerm(Terminal terminal, Spi spi, {bool autoEnter = false}) async {
|
||||
final argsFmted = fmtWithSpi(spi);
|
||||
final matches = fmtFinder.allMatches(argsFmted);
|
||||
|
||||
@@ -119,11 +112,7 @@ extension SnippetX on Snippet {
|
||||
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
||||
}
|
||||
|
||||
Future<void> _doTermKeys(
|
||||
Terminal terminal,
|
||||
MapEntry<String, TerminalKey> termKey,
|
||||
String key,
|
||||
) async {
|
||||
Future<void> _doTermKeys(Terminal terminal, MapEntry<String, TerminalKey> termKey, String key) async {
|
||||
// if (termKey.value == TerminalKey.enter) {
|
||||
// terminal.keyInput(TerminalKey.enter);
|
||||
// return;
|
||||
@@ -140,11 +129,7 @@ extension SnippetX on Snippet {
|
||||
// `${ctrl+ad}` -> `ctrla + d`
|
||||
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
||||
if (chars.isEmpty) return;
|
||||
final ok = terminal.charInput(
|
||||
chars.codeUnitAt(0),
|
||||
ctrl: ctrlAlt.ctrl,
|
||||
alt: ctrlAlt.alt,
|
||||
);
|
||||
final ok = terminal.charInput(chars.codeUnitAt(0), ctrl: ctrlAlt.ctrl, alt: ctrlAlt.alt);
|
||||
if (!ok) {
|
||||
Loggers.app.warning('Failed to input: $key');
|
||||
}
|
||||
@@ -166,10 +151,7 @@ extension SnippetX on Snippet {
|
||||
};
|
||||
|
||||
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
|
||||
static final fmtTermKeys = {
|
||||
r'${ctrl': TerminalKey.control,
|
||||
r'${alt': TerminalKey.alt,
|
||||
};
|
||||
static final fmtTermKeys = {r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt};
|
||||
}
|
||||
|
||||
class SnippetResult {
|
||||
@@ -177,11 +159,7 @@ class SnippetResult {
|
||||
final String result;
|
||||
final Duration time;
|
||||
|
||||
SnippetResult({
|
||||
required this.dest,
|
||||
required this.result,
|
||||
required this.time,
|
||||
});
|
||||
SnippetResult({required this.dest, required this.result, required this.time});
|
||||
}
|
||||
|
||||
typedef SnippetFuncCtx = ({Terminal term, String raw});
|
||||
@@ -193,10 +171,7 @@ abstract final class SnippetFuncs {
|
||||
r'${enter': SnippetFuncs.enter,
|
||||
};
|
||||
|
||||
static const help = {
|
||||
'sleep': 'Sleep for a few seconds',
|
||||
'enter': 'Enter a few times',
|
||||
};
|
||||
static const help = {'sleep': 'Sleep for a few seconds', 'enter': 'Enter a few times'};
|
||||
|
||||
static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
|
||||
final seconds = int.tryParse(ctx.raw);
|
||||
|
||||
@@ -1,21 +1,55 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
|
||||
enum SystemType {
|
||||
linux._(linuxSign),
|
||||
bsd._(bsdSign),
|
||||
;
|
||||
linux(linuxSign),
|
||||
bsd(bsdSign),
|
||||
windows(windowsSign);
|
||||
|
||||
final String value;
|
||||
final String? value;
|
||||
|
||||
const SystemType._(this.value);
|
||||
const SystemType([this.value]);
|
||||
|
||||
static const linuxSign = '__linux';
|
||||
static const bsdSign = '__bsd';
|
||||
static const windowsSign = '__windows';
|
||||
|
||||
/// Used for parsing system types from shell output.
|
||||
///
|
||||
/// This method looks for specific system signatures in the shell output
|
||||
/// and returns the corresponding SystemType. If no signature is found,
|
||||
/// it defaults to Linux but logs the detection failure for debugging.
|
||||
static SystemType parse(String value) {
|
||||
// Log the raw value for debugging purposes (truncated to avoid spam)
|
||||
final truncatedValue = value.length > 100
|
||||
? '${value.substring(0, 100)}...'
|
||||
: value;
|
||||
|
||||
if (value.contains(windowsSign)) {
|
||||
Loggers.app.info('System detected as Windows from signature in: $truncatedValue');
|
||||
return SystemType.windows;
|
||||
}
|
||||
if (value.contains(bsdSign)) {
|
||||
Loggers.app.info('System detected as BSD from signature in: $truncatedValue');
|
||||
return SystemType.bsd;
|
||||
}
|
||||
|
||||
// Log when falling back to Linux detection
|
||||
if (value.trim().isEmpty) {
|
||||
Loggers.app.warning(
|
||||
'System detection received empty input, defaulting to Linux. '
|
||||
'This may indicate a script execution issue.'
|
||||
);
|
||||
} else if (!value.contains(linuxSign)) {
|
||||
Loggers.app.warning(
|
||||
'System detection could not find any known signatures (Windows: $windowsSign, '
|
||||
'BSD: $bsdSign, Linux: $linuxSign) in output: "$truncatedValue". '
|
||||
'Defaulting to Linux, but this may cause incorrect parsing.'
|
||||
);
|
||||
} else {
|
||||
Loggers.app.info('System detected as Linux from signature in: $truncatedValue');
|
||||
}
|
||||
|
||||
return SystemType.linux;
|
||||
}
|
||||
|
||||
@@ -27,6 +61,8 @@ enum SystemType {
|
||||
return StatusCmdType.values.length;
|
||||
case SystemType.bsd:
|
||||
return BSDStatusCmdType.values.length;
|
||||
case SystemType.windows:
|
||||
return WindowsStatusCmdType.values.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,24 @@ enum SystemdUnitFunc {
|
||||
reload,
|
||||
enable,
|
||||
disable,
|
||||
status,
|
||||
;
|
||||
status;
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
start => Icons.play_arrow,
|
||||
stop => Icons.stop,
|
||||
restart => Icons.refresh,
|
||||
reload => Icons.refresh,
|
||||
enable => Icons.check,
|
||||
disable => Icons.close,
|
||||
status => Icons.info,
|
||||
};
|
||||
start => Icons.play_arrow,
|
||||
stop => Icons.stop,
|
||||
restart => Icons.refresh,
|
||||
reload => Icons.refresh,
|
||||
enable => Icons.check,
|
||||
disable => Icons.close,
|
||||
status => Icons.info,
|
||||
};
|
||||
}
|
||||
|
||||
enum SystemdUnitType {
|
||||
service,
|
||||
socket,
|
||||
mount,
|
||||
timer,
|
||||
;
|
||||
timer;
|
||||
|
||||
static SystemdUnitType? fromString(String? value) {
|
||||
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
|
||||
@@ -36,13 +34,12 @@ enum SystemdUnitType {
|
||||
|
||||
enum SystemdUnitScope {
|
||||
system,
|
||||
user,
|
||||
;
|
||||
user;
|
||||
|
||||
Color? get color => switch (this) {
|
||||
system => Colors.red,
|
||||
_ => null,
|
||||
};
|
||||
system => Colors.red,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
String getCmdPrefix(bool isRoot) {
|
||||
if (this == system) {
|
||||
@@ -57,17 +54,16 @@ enum SystemdUnitState {
|
||||
inactive,
|
||||
failed,
|
||||
activating,
|
||||
deactivating,
|
||||
;
|
||||
deactivating;
|
||||
|
||||
static SystemdUnitState? fromString(String? value) {
|
||||
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
|
||||
}
|
||||
|
||||
Color? get color => switch (this) {
|
||||
failed => Colors.red,
|
||||
_ => null,
|
||||
};
|
||||
failed => Colors.red,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
final class SystemdUnit {
|
||||
@@ -85,10 +81,7 @@ final class SystemdUnit {
|
||||
required this.state,
|
||||
});
|
||||
|
||||
String getCmd({
|
||||
required SystemdUnitFunc func,
|
||||
required bool isRoot,
|
||||
}) {
|
||||
String getCmd({required SystemdUnitFunc func, required bool isRoot}) {
|
||||
final prefix = scope.getCmdPrefix(isRoot);
|
||||
return '$prefix ${func.name} $name';
|
||||
}
|
||||
|
||||
@@ -40,11 +40,7 @@ class Fifo<T> extends ListBase<T> {
|
||||
abstract class TimeSeq<T extends List<TimeSeqIface>> extends Fifo<T> {
|
||||
/// Due to the design, at least two elements are required, otherwise [pre] /
|
||||
/// [now] will throw.
|
||||
TimeSeq(
|
||||
T init1,
|
||||
T init2, {
|
||||
super.capacity,
|
||||
}) : super(list: [init1, init2]);
|
||||
TimeSeq(T init1, T init2, {super.capacity}) : super(list: [init1, init2]);
|
||||
|
||||
T get pre {
|
||||
return _list[length - 2];
|
||||
|
||||
258
lib/data/model/server/windows_parser.dart
Normal file
258
lib/data/model/server/windows_parser.dart
Normal file
@@ -0,0 +1,258 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:server_box/data/model/server/cpu.dart';
|
||||
import 'package:server_box/data/model/server/disk.dart';
|
||||
import 'package:server_box/data/model/server/memory.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
|
||||
/// Windows-specific status parsing utilities
|
||||
///
|
||||
/// This module handles parsing of Windows PowerShell command outputs
|
||||
/// for server monitoring. It extracts the Windows parsing logic
|
||||
/// to improve maintainability and readability.
|
||||
class WindowsParser {
|
||||
const WindowsParser._();
|
||||
|
||||
/// Parse Windows custom commands from segments
|
||||
static void parseCustomCommands(
|
||||
ServerStatus serverStatus,
|
||||
List<String> segments,
|
||||
Map<String, String> customCmds,
|
||||
int systemSegmentsLength,
|
||||
) {
|
||||
try {
|
||||
for (int idx = 0; idx < customCmds.length; idx++) {
|
||||
final key = customCmds.keys.elementAt(idx);
|
||||
// Ensure we don't go out of bounds when accessing segments
|
||||
final segmentIndex = idx + systemSegmentsLength;
|
||||
if (segmentIndex < segments.length) {
|
||||
final value = segments[segmentIndex];
|
||||
serverStatus.customCmds[key] = value;
|
||||
} else {
|
||||
Loggers.app.warning(
|
||||
'Windows custom commands: segment index $segmentIndex out of bounds '
|
||||
'(segments length: ${segments.length}, systemSegmentsLength: $systemSegmentsLength)'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows custom commands parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows uptime from PowerShell output
|
||||
static String? parseUpTime(String raw) {
|
||||
try {
|
||||
// Clean the input - trim whitespace and get the first non-empty line
|
||||
final cleanedInput = raw.trim().split('\n')
|
||||
.where((line) => line.trim().isNotEmpty)
|
||||
.firstOrNull;
|
||||
|
||||
if (cleanedInput == null || cleanedInput.isEmpty) {
|
||||
Loggers.app.warning('Windows uptime parsing: empty or null input');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try multiple date formats to handle different Windows locale/version outputs
|
||||
final formatters = [
|
||||
DateFormat('EEEE, MMMM d, yyyy h:mm:ss a', 'en_US'), // Original format
|
||||
DateFormat('EEEE, MMMM dd, yyyy h:mm:ss a', 'en_US'), // Double-digit day
|
||||
DateFormat('EEE, MMM d, yyyy h:mm:ss a', 'en_US'), // Shortened format
|
||||
DateFormat('EEE, MMM dd, yyyy h:mm:ss a', 'en_US'), // Shortened with double-digit day
|
||||
DateFormat('M/d/yyyy h:mm:ss a', 'en_US'), // Short US format
|
||||
DateFormat('MM/dd/yyyy h:mm:ss a', 'en_US'), // Short US format with zero padding
|
||||
DateFormat('d/M/yyyy h:mm:ss a', 'en_US'), // Short European format
|
||||
DateFormat('dd/MM/yyyy h:mm:ss a', 'en_US'), // Short European format with zero padding
|
||||
];
|
||||
|
||||
DateTime? dateTime;
|
||||
for (final formatter in formatters) {
|
||||
dateTime = formatter.tryParseLoose(cleanedInput);
|
||||
if (dateTime != null) break;
|
||||
}
|
||||
|
||||
if (dateTime == null) {
|
||||
Loggers.app.warning('Windows uptime parsing: could not parse date format for: $cleanedInput');
|
||||
return null;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final uptime = now.difference(dateTime);
|
||||
|
||||
// Validate that the uptime is reasonable (not negative, not too far in the future)
|
||||
if (uptime.isNegative || uptime.inDays > 3650) { // More than 10 years seems unreasonable
|
||||
Loggers.app.warning('Windows uptime parsing: unreasonable uptime calculated: ${uptime.inDays} days for date: $cleanedInput');
|
||||
return null;
|
||||
}
|
||||
|
||||
final days = uptime.inDays;
|
||||
final hours = uptime.inHours % 24;
|
||||
final minutes = uptime.inMinutes % 60;
|
||||
|
||||
if (days > 0) {
|
||||
return '$days days, $hours:${minutes.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '$hours:${minutes.toString().padLeft(2, '0')}';
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows uptime parsing failed: $e for input: $raw', s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows CPU information from PowerShell output
|
||||
static List<SingleCpuCore> parseCpu(String raw, ServerStatus serverStatus) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<SingleCpuCore> cpus = [];
|
||||
|
||||
if (jsonData is List) {
|
||||
for (int i = 0; i < jsonData.length; i++) {
|
||||
final cpu = jsonData[i];
|
||||
final loadPercentage = cpu['LoadPercentage'] ?? 0;
|
||||
final usage = loadPercentage as int;
|
||||
final idle = 100 - usage;
|
||||
|
||||
// Get previous CPU data to calculate cumulative values
|
||||
final prevCpus = serverStatus.cpu.now;
|
||||
final prevCpu = i < prevCpus.length ? prevCpus[i] : null;
|
||||
|
||||
// LIMITATION: Windows CPU counters approach
|
||||
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
|
||||
// We simulate cumulative counters by adding current percentages to previous totals.
|
||||
// This approach has limitations:
|
||||
// 1. Not as accurate as true cumulative time counters (Linux /proc/stat)
|
||||
// 2. May drift over time with variable polling intervals
|
||||
// 3. Results depend on consistent polling frequency
|
||||
// However, this allows compatibility with existing delta-based CPU calculation logic.
|
||||
final newUser = (prevCpu?.user ?? 0) + usage;
|
||||
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
||||
|
||||
cpus.add(
|
||||
SingleCpuCore(
|
||||
'cpu$i',
|
||||
newUser, // cumulative user time
|
||||
0, // sys (not available)
|
||||
0, // nice (not available)
|
||||
newIdle, // cumulative idle time
|
||||
0, // iowait (not available)
|
||||
0, // irq (not available)
|
||||
0, // softirq (not available)
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (jsonData is Map) {
|
||||
// Single CPU core
|
||||
final loadPercentage = jsonData['LoadPercentage'] ?? 0;
|
||||
final usage = loadPercentage as int;
|
||||
final idle = 100 - usage;
|
||||
|
||||
// Get previous CPU data to calculate cumulative values
|
||||
final prevCpus = serverStatus.cpu.now;
|
||||
final prevCpu = prevCpus.isNotEmpty ? prevCpus[0] : null;
|
||||
|
||||
// LIMITATION: See comment above for Windows CPU counter limitations
|
||||
final newUser = (prevCpu?.user ?? 0) + usage;
|
||||
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
||||
|
||||
cpus.add(
|
||||
SingleCpuCore(
|
||||
'cpu0',
|
||||
newUser, // cumulative user time
|
||||
0, // sys
|
||||
0, // nice
|
||||
newIdle, // cumulative idle time
|
||||
0, // iowait
|
||||
0, // irq
|
||||
0, // softirq
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return cpus;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows memory information from PowerShell output
|
||||
///
|
||||
/// NOTE: Windows Win32_OperatingSystem properties TotalVisibleMemorySize
|
||||
/// and FreePhysicalMemory are returned in KB units.
|
||||
static Memory? parseMemory(String raw) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final data = jsonData is List ? jsonData.first : jsonData;
|
||||
|
||||
// Win32_OperatingSystem properties are in KB
|
||||
final totalKB = data['TotalVisibleMemorySize'] as int? ?? 0;
|
||||
final freeKB = data['FreePhysicalMemory'] as int? ?? 0;
|
||||
|
||||
return Memory(
|
||||
total: totalKB,
|
||||
free: freeKB,
|
||||
avail: freeKB, // Windows doesn't distinguish between free and available
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows disk information from PowerShell output
|
||||
static List<Disk> parseDisks(String raw) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<Disk> disks = [];
|
||||
|
||||
final diskList = jsonData is List ? jsonData : [jsonData];
|
||||
|
||||
for (final diskData in diskList) {
|
||||
final deviceId = diskData['DeviceID']?.toString() ?? '';
|
||||
final size =
|
||||
BigInt.tryParse(diskData['Size']?.toString() ?? '0') ?? BigInt.zero;
|
||||
final freeSpace =
|
||||
BigInt.tryParse(diskData['FreeSpace']?.toString() ?? '0') ??
|
||||
BigInt.zero;
|
||||
final fileSystem = diskData['FileSystem']?.toString() ?? '';
|
||||
|
||||
// Validate all required fields
|
||||
final hasRequiredFields = deviceId.isNotEmpty &&
|
||||
size != BigInt.zero &&
|
||||
freeSpace != BigInt.zero &&
|
||||
fileSystem.isNotEmpty;
|
||||
|
||||
if (!hasRequiredFields) {
|
||||
Loggers.app.warning('Windows disk parsing: skipping disk with missing required fields. '
|
||||
'DeviceID: $deviceId, Size: $size, FreeSpace: $freeSpace, FileSystem: $fileSystem');
|
||||
continue;
|
||||
}
|
||||
|
||||
final sizeKB = size ~/ BigInt.from(1024);
|
||||
final freeKB = freeSpace ~/ BigInt.from(1024);
|
||||
final usedKB = sizeKB - freeKB;
|
||||
final usedPercent = sizeKB > BigInt.zero
|
||||
? ((usedKB * BigInt.from(100)) ~/ sizeKB).toInt()
|
||||
: 0;
|
||||
|
||||
disks.add(
|
||||
Disk(
|
||||
path: deviceId,
|
||||
fsTyp: fileSystem,
|
||||
size: sizeKB,
|
||||
avail: freeKB,
|
||||
used: usedKB,
|
||||
usedPercent: usedPercent,
|
||||
mount: deviceId, // Windows uses drive letters as mount points
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return disks;
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Windows disk parsing failed: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,7 @@ final class WakeOnLanCfg {
|
||||
final String ip;
|
||||
final String? pwd;
|
||||
|
||||
const WakeOnLanCfg({
|
||||
required this.mac,
|
||||
required this.ip,
|
||||
this.pwd,
|
||||
});
|
||||
const WakeOnLanCfg({required this.mac, required this.ip, this.pwd});
|
||||
|
||||
(Object?, bool) validate() {
|
||||
final macValidation = MACAddress.validate(mac);
|
||||
@@ -39,10 +35,7 @@ final class WakeOnLanCfg {
|
||||
final mac_ = MACAddress(mac);
|
||||
final pwd_ = pwd != null ? SecureONPassword(pwd!) : null;
|
||||
final obj = WakeOnLAN(ip_, mac_, password: pwd_);
|
||||
return obj.wake(
|
||||
repeat: 3,
|
||||
repeatDelay: const Duration(milliseconds: 500),
|
||||
);
|
||||
return obj.wake(repeat: 3, repeatDelay: const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) => _$WakeOnLanCfgFromJson(json);
|
||||
|
||||
@@ -9,12 +9,7 @@ class SftpReq {
|
||||
Spi? jumpSpi;
|
||||
String? jumpPrivateKey;
|
||||
|
||||
SftpReq(
|
||||
this.spi,
|
||||
this.remotePath,
|
||||
this.localPath,
|
||||
this.type,
|
||||
) {
|
||||
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
||||
final keyId = spi.keyId;
|
||||
if (keyId != null) {
|
||||
privateKey = getPrivateKey(keyId);
|
||||
@@ -44,15 +39,9 @@ class SftpReqStatus {
|
||||
Exception? error;
|
||||
Duration? spentTime;
|
||||
|
||||
SftpReqStatus({
|
||||
required this.req,
|
||||
required this.notifyListeners,
|
||||
this.completer,
|
||||
}) : id = DateTime.now().microsecondsSinceEpoch {
|
||||
worker = SftpWorker(
|
||||
onNotify: onNotify,
|
||||
req: req,
|
||||
)..init();
|
||||
SftpReqStatus({required this.req, required this.notifyListeners, this.completer})
|
||||
: id = DateTime.now().microsecondsSinceEpoch {
|
||||
worker = SftpWorker(onNotify: onNotify, req: req)..init();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -18,10 +18,7 @@ class SftpWorker {
|
||||
|
||||
final worker = Worker();
|
||||
|
||||
SftpWorker({
|
||||
required this.onNotify,
|
||||
required this.req,
|
||||
});
|
||||
SftpWorker({required this.onNotify, required this.req});
|
||||
|
||||
void _dispose() {
|
||||
worker.dispose();
|
||||
@@ -31,11 +28,7 @@ class SftpWorker {
|
||||
/// the threads
|
||||
Future<void> init() async {
|
||||
if (worker.isInitialized) worker.dispose();
|
||||
await worker.init(
|
||||
mainMessageHandler,
|
||||
isolateMessageHandler,
|
||||
errorHandler: print,
|
||||
);
|
||||
await worker.init(mainMessageHandler, isolateMessageHandler, errorHandler: print);
|
||||
worker.sendMessage(req);
|
||||
}
|
||||
|
||||
@@ -46,11 +39,7 @@ class SftpWorker {
|
||||
}
|
||||
|
||||
/// Handle the messages coming from the main
|
||||
Future<void> isolateMessageHandler(
|
||||
dynamic data,
|
||||
SendPort mainSendPort,
|
||||
SendErrorFunction sendError,
|
||||
) async {
|
||||
Future<void> isolateMessageHandler(dynamic data, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||
switch (data) {
|
||||
case final SftpReq val:
|
||||
switch (val.type) {
|
||||
@@ -67,11 +56,7 @@ Future<void> isolateMessageHandler(
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _download(
|
||||
SftpReq req,
|
||||
SendPort mainSendPort,
|
||||
SendErrorFunction sendError,
|
||||
) async {
|
||||
Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||
try {
|
||||
mainSendPort.send(SftpWorkerStatus.preparing);
|
||||
final watch = Stopwatch()..start();
|
||||
@@ -103,12 +88,12 @@ Future<void> _download(
|
||||
// Due to single core performance, limit the chunk size
|
||||
const defaultChunkSize = 1024 * 1024 * 5;
|
||||
var totalRead = 0;
|
||||
|
||||
|
||||
while (totalRead < size) {
|
||||
final remaining = size - totalRead;
|
||||
final chunkSize = remaining > defaultChunkSize ? defaultChunkSize : remaining;
|
||||
dprint('Size: $size, Total Read: $totalRead, Chunk Size: $chunkSize');
|
||||
|
||||
|
||||
final fileData = file.read(offset: totalRead, length: chunkSize);
|
||||
await for (var chunk in fileData) {
|
||||
localFile.add(chunk);
|
||||
@@ -127,11 +112,7 @@ Future<void> _download(
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _upload(
|
||||
SftpReq req,
|
||||
SendPort mainSendPort,
|
||||
SendErrorFunction sendError,
|
||||
) async {
|
||||
Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||
try {
|
||||
mainSendPort.send(SftpWorkerStatus.preparing);
|
||||
final watch = Stopwatch()..start();
|
||||
@@ -156,9 +137,7 @@ Future<void> _upload(
|
||||
// If remote exists, overwrite it
|
||||
final file = await sftp.open(
|
||||
req.remotePath,
|
||||
mode: SftpFileOpenMode.truncate |
|
||||
SftpFileOpenMode.create |
|
||||
SftpFileOpenMode.write,
|
||||
mode: SftpFileOpenMode.truncate | SftpFileOpenMode.create | SftpFileOpenMode.write,
|
||||
);
|
||||
final writer = file.write(
|
||||
localFile,
|
||||
|
||||
@@ -51,30 +51,30 @@ enum VirtKey {
|
||||
f9,
|
||||
f10,
|
||||
f11,
|
||||
f12;
|
||||
f12,
|
||||
}
|
||||
|
||||
extension VirtKeyX on VirtKey {
|
||||
/// Used for input to terminal
|
||||
String? get inputRaw => switch (this) {
|
||||
VirtKey.slash => '/',
|
||||
VirtKey.backSlash => '\\',
|
||||
VirtKey.underscore => '_',
|
||||
VirtKey.plus => '+',
|
||||
VirtKey.equal => '=',
|
||||
VirtKey.minus => '-',
|
||||
VirtKey.parenLeft => '(',
|
||||
VirtKey.parenRight => ')',
|
||||
VirtKey.bracketLeft => '[',
|
||||
VirtKey.bracketRight => ']',
|
||||
VirtKey.braceLeft => '{',
|
||||
VirtKey.braceRight => '}',
|
||||
VirtKey.chevronLeft => '<',
|
||||
VirtKey.chevronRight => '>',
|
||||
VirtKey.colon => ':',
|
||||
VirtKey.semicolon => ';',
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.slash => '/',
|
||||
VirtKey.backSlash => '\\',
|
||||
VirtKey.underscore => '_',
|
||||
VirtKey.plus => '+',
|
||||
VirtKey.equal => '=',
|
||||
VirtKey.minus => '-',
|
||||
VirtKey.parenLeft => '(',
|
||||
VirtKey.parenRight => ')',
|
||||
VirtKey.bracketLeft => '[',
|
||||
VirtKey.bracketRight => ']',
|
||||
VirtKey.braceLeft => '{',
|
||||
VirtKey.braceRight => '}',
|
||||
VirtKey.chevronLeft => '<',
|
||||
VirtKey.chevronRight => '>',
|
||||
VirtKey.colon => ':',
|
||||
VirtKey.semicolon => ';',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// Used for displaying on UI
|
||||
String get text {
|
||||
@@ -111,74 +111,74 @@ extension VirtKeyX on VirtKey {
|
||||
|
||||
/// Corresponding [TerminalKey]
|
||||
TerminalKey? get key => switch (this) {
|
||||
VirtKey.esc => TerminalKey.escape,
|
||||
VirtKey.alt => TerminalKey.alt,
|
||||
VirtKey.home => TerminalKey.home,
|
||||
VirtKey.up => TerminalKey.arrowUp,
|
||||
VirtKey.end => TerminalKey.end,
|
||||
VirtKey.tab => TerminalKey.tab,
|
||||
VirtKey.ctrl => TerminalKey.control,
|
||||
VirtKey.left => TerminalKey.arrowLeft,
|
||||
VirtKey.down => TerminalKey.arrowDown,
|
||||
VirtKey.right => TerminalKey.arrowRight,
|
||||
VirtKey.shift => TerminalKey.shift,
|
||||
VirtKey.pgup => TerminalKey.pageUp,
|
||||
VirtKey.pgdn => TerminalKey.pageDown,
|
||||
VirtKey.f1 => TerminalKey.f1,
|
||||
VirtKey.f2 => TerminalKey.f2,
|
||||
VirtKey.f3 => TerminalKey.f3,
|
||||
VirtKey.f4 => TerminalKey.f4,
|
||||
VirtKey.f5 => TerminalKey.f5,
|
||||
VirtKey.f6 => TerminalKey.f6,
|
||||
VirtKey.f7 => TerminalKey.f7,
|
||||
VirtKey.f8 => TerminalKey.f8,
|
||||
VirtKey.f9 => TerminalKey.f9,
|
||||
VirtKey.f10 => TerminalKey.f10,
|
||||
VirtKey.f11 => TerminalKey.f11,
|
||||
VirtKey.f12 => TerminalKey.f12,
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.esc => TerminalKey.escape,
|
||||
VirtKey.alt => TerminalKey.alt,
|
||||
VirtKey.home => TerminalKey.home,
|
||||
VirtKey.up => TerminalKey.arrowUp,
|
||||
VirtKey.end => TerminalKey.end,
|
||||
VirtKey.tab => TerminalKey.tab,
|
||||
VirtKey.ctrl => TerminalKey.control,
|
||||
VirtKey.left => TerminalKey.arrowLeft,
|
||||
VirtKey.down => TerminalKey.arrowDown,
|
||||
VirtKey.right => TerminalKey.arrowRight,
|
||||
VirtKey.shift => TerminalKey.shift,
|
||||
VirtKey.pgup => TerminalKey.pageUp,
|
||||
VirtKey.pgdn => TerminalKey.pageDown,
|
||||
VirtKey.f1 => TerminalKey.f1,
|
||||
VirtKey.f2 => TerminalKey.f2,
|
||||
VirtKey.f3 => TerminalKey.f3,
|
||||
VirtKey.f4 => TerminalKey.f4,
|
||||
VirtKey.f5 => TerminalKey.f5,
|
||||
VirtKey.f6 => TerminalKey.f6,
|
||||
VirtKey.f7 => TerminalKey.f7,
|
||||
VirtKey.f8 => TerminalKey.f8,
|
||||
VirtKey.f9 => TerminalKey.f9,
|
||||
VirtKey.f10 => TerminalKey.f10,
|
||||
VirtKey.f11 => TerminalKey.f11,
|
||||
VirtKey.f12 => TerminalKey.f12,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// Icons for virtual keys
|
||||
IconData? get icon => switch (this) {
|
||||
VirtKey.up => Icons.arrow_upward,
|
||||
VirtKey.left => Icons.arrow_back,
|
||||
VirtKey.down => Icons.arrow_downward,
|
||||
VirtKey.right => Icons.arrow_forward,
|
||||
VirtKey.sftp => Icons.file_open,
|
||||
VirtKey.snippet => Icons.code,
|
||||
VirtKey.clipboard => Icons.paste,
|
||||
VirtKey.ime => Icons.keyboard,
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.up => Icons.arrow_upward,
|
||||
VirtKey.left => Icons.arrow_back,
|
||||
VirtKey.down => Icons.arrow_downward,
|
||||
VirtKey.right => Icons.arrow_forward,
|
||||
VirtKey.sftp => Icons.file_open,
|
||||
VirtKey.snippet => Icons.code,
|
||||
VirtKey.clipboard => Icons.paste,
|
||||
VirtKey.ime => Icons.keyboard,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
// Use [VirtualKeyFunc] instead of [VirtKey]
|
||||
// This can help linter to enum all [VirtualKeyFunc]
|
||||
// and make sure all [VirtualKeyFunc] are handled
|
||||
VirtualKeyFunc? get func => switch (this) {
|
||||
VirtKey.sftp => VirtualKeyFunc.file,
|
||||
VirtKey.snippet => VirtualKeyFunc.snippet,
|
||||
VirtKey.clipboard => VirtualKeyFunc.clipboard,
|
||||
VirtKey.ime => VirtualKeyFunc.toggleIME,
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.sftp => VirtualKeyFunc.file,
|
||||
VirtKey.snippet => VirtualKeyFunc.snippet,
|
||||
VirtKey.clipboard => VirtualKeyFunc.clipboard,
|
||||
VirtKey.ime => VirtualKeyFunc.toggleIME,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
bool get toggleable => switch (this) {
|
||||
VirtKey.alt || VirtKey.ctrl || VirtKey.shift => true,
|
||||
_ => false,
|
||||
};
|
||||
VirtKey.alt || VirtKey.ctrl || VirtKey.shift => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
bool get canLongPress => switch (this) {
|
||||
VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,
|
||||
_ => false,
|
||||
};
|
||||
VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
String? get help => switch (this) {
|
||||
VirtKey.sftp => l10n.virtKeyHelpSFTP,
|
||||
VirtKey.clipboard => l10n.virtKeyHelpClipboard,
|
||||
VirtKey.ime => l10n.virtKeyHelpIME,
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.sftp => l10n.virtKeyHelpSFTP,
|
||||
VirtKey.clipboard => l10n.virtKeyHelpClipboard,
|
||||
VirtKey.ime => l10n.virtKeyHelpIME,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// - [saveDefaultIfErr] if the stored raw values is invalid, save default order to store
|
||||
static List<VirtKey> loadFromStore({bool saveDefaultIfErr = true}) {
|
||||
|
||||
@@ -7,9 +7,7 @@ part 'app.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class AppState with _$AppState {
|
||||
const factory AppState({
|
||||
@Default(false) bool desktopMode,
|
||||
}) = _AppState;
|
||||
const factory AppState({@Default(false) bool desktopMode}) = _AppState;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
|
||||
@@ -12,8 +12,7 @@ import 'package:server_box/data/model/container/ps.dart';
|
||||
import 'package:server_box/data/model/container/type.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
final _dockerNotFound =
|
||||
RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
||||
final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
||||
|
||||
class ContainerProvider extends ChangeNotifier {
|
||||
final SSHClient? client;
|
||||
@@ -90,11 +89,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
final includeStats = Stores.setting.containerParseStat.fetch();
|
||||
|
||||
var raw = '';
|
||||
final cmd = _wrap(ContainerCmdType.execAll(
|
||||
type,
|
||||
sudo: sudo,
|
||||
includeStats: includeStats,
|
||||
));
|
||||
final cmd = _wrap(ContainerCmdType.execAll(type, sudo: sudo, includeStats: includeStats));
|
||||
final code = await client?.execWithPwd(
|
||||
cmd,
|
||||
context: context,
|
||||
@@ -130,10 +125,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
try {
|
||||
version = json.decode(verRaw)['Client']['Version'];
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.invalidVersion,
|
||||
message: '$e',
|
||||
);
|
||||
error = ContainerErr(type: ContainerErrType.invalidVersion, message: '$e');
|
||||
Loggers.app.warning('Container version failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -150,10 +142,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parsePs,
|
||||
message: '$e',
|
||||
);
|
||||
error = ContainerErr(type: ContainerErrType.parsePs, message: '$e');
|
||||
Loggers.app.warning('Container ps failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -173,10 +162,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
|
||||
}
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parseImages,
|
||||
message: '$e',
|
||||
);
|
||||
error = ContainerErr(type: ContainerErrType.parseImages, message: '$e');
|
||||
Loggers.app.warning('Container images failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -199,10 +185,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
item.parseStats(statsLine);
|
||||
}
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parseStats,
|
||||
message: '$e',
|
||||
);
|
||||
error = ContainerErr(type: ContainerErrType.parseStats, message: '$e');
|
||||
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -261,10 +244,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
if (code != 0) {
|
||||
return ContainerErr(
|
||||
type: ContainerErrType.unknown,
|
||||
message: errs.join('\n').trim(),
|
||||
);
|
||||
return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim());
|
||||
}
|
||||
if (autoRefresh) await refresh();
|
||||
return null;
|
||||
@@ -288,40 +268,32 @@ enum ContainerCmdType {
|
||||
version,
|
||||
ps,
|
||||
stats,
|
||||
images,
|
||||
images
|
||||
// No specific commands needed for prune actions as they are simple
|
||||
// and don't require splitting output with ShellFunc.seperator
|
||||
;
|
||||
|
||||
String exec(
|
||||
ContainerType type, {
|
||||
bool sudo = false,
|
||||
bool includeStats = false,
|
||||
}) {
|
||||
String exec(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
||||
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
|
||||
return switch (this) {
|
||||
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
||||
ContainerCmdType.ps => switch (type) {
|
||||
/// TODO: Rollback to json format when permformance recovers.
|
||||
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
||||
ContainerType.docker => '$prefix ps -a --format "table {{printf \\"'
|
||||
/// TODO: Rollback to json format when permformance recovers.
|
||||
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
||||
ContainerType.docker =>
|
||||
'$prefix ps -a --format "table {{printf \\"'
|
||||
'%-15.15s '
|
||||
'%-30.30s '
|
||||
'${"%-50.50s " * 2}\\"'
|
||||
' .ID .Status .Names .Image}}"',
|
||||
ContainerType.podman => '$prefix ps -a $_jsonFmt',
|
||||
},
|
||||
ContainerCmdType.stats =>
|
||||
includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
|
||||
ContainerType.podman => '$prefix ps -a $_jsonFmt',
|
||||
},
|
||||
ContainerCmdType.stats => includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
|
||||
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
|
||||
};
|
||||
}
|
||||
|
||||
static String execAll(
|
||||
ContainerType type, {
|
||||
bool sudo = false,
|
||||
bool includeStats = false,
|
||||
}) {
|
||||
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
||||
return ContainerCmdType.values
|
||||
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
|
||||
.join('\necho ${ShellFunc.seperator}\n');
|
||||
|
||||
@@ -86,15 +86,18 @@ final class PveProvider extends ChangeNotifier {
|
||||
forward.stream.cast<List<int>>().pipe(socket);
|
||||
socket.cast<List<int>>().pipe(forward.sink);
|
||||
});
|
||||
final newUrl = Uri.parse(addr)
|
||||
.replace(host: 'localhost', port: _localPort)
|
||||
.toString();
|
||||
final newUrl = Uri.parse(
|
||||
addr,
|
||||
).replace(host: 'localhost', port: _localPort).toString();
|
||||
debugPrint('Forwarding $newUrl to $addr');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConnectionTask<Socket>> cf(
|
||||
Uri url, String? proxyHost, int? proxyPort) async {
|
||||
Uri url,
|
||||
String? proxyHost,
|
||||
int? proxyPort,
|
||||
) async {
|
||||
/* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
final _localPort = serverSocket.port;
|
||||
serverSocket.listen((socket) async {
|
||||
@@ -105,8 +108,11 @@ final class PveProvider extends ChangeNotifier {
|
||||
});*/
|
||||
|
||||
if (url.isScheme('https')) {
|
||||
return SecureSocket.startConnect('localhost', _localPort,
|
||||
onBadCertificate: (_) => true);
|
||||
return SecureSocket.startConnect(
|
||||
'localhost',
|
||||
_localPort,
|
||||
onBadCertificate: (_) => true,
|
||||
);
|
||||
} else {
|
||||
return Socket.startConnect('localhost', _localPort);
|
||||
}
|
||||
@@ -119,7 +125,7 @@ final class PveProvider extends ChangeNotifier {
|
||||
'username': spi.user,
|
||||
'password': spi.pwd,
|
||||
'realm': 'pam',
|
||||
'new-format': '1'
|
||||
'new-format': '1',
|
||||
},
|
||||
options: Options(
|
||||
headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType},
|
||||
@@ -151,8 +157,10 @@ final class PveProvider extends ChangeNotifier {
|
||||
try {
|
||||
final resp = await session.get('$addr/api2/json/cluster/resources');
|
||||
final res = resp.data['data'] as List;
|
||||
final result =
|
||||
await Computer.shared.start(PveRes.parse, (res, data.value));
|
||||
final result = await Computer.shared.start(PveRes.parse, (
|
||||
res,
|
||||
data.value,
|
||||
));
|
||||
data.value = result;
|
||||
} catch (e) {
|
||||
Loggers.app.warning('PVE list failed', e);
|
||||
@@ -164,29 +172,33 @@ final class PveProvider extends ChangeNotifier {
|
||||
|
||||
Future<bool> reboot(String node, String id) async {
|
||||
await connected.future;
|
||||
final resp =
|
||||
await session.post('$addr/api2/json/nodes/$node/$id/status/reboot');
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/reboot',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
}
|
||||
|
||||
Future<bool> start(String node, String id) async {
|
||||
await connected.future;
|
||||
final resp =
|
||||
await session.post('$addr/api2/json/nodes/$node/$id/status/start');
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/start',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
}
|
||||
|
||||
Future<bool> stop(String node, String id) async {
|
||||
await connected.future;
|
||||
final resp =
|
||||
await session.post('$addr/api2/json/nodes/$node/$id/status/stop');
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/stop',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
}
|
||||
|
||||
Future<bool> shutdown(String node, String id) async {
|
||||
await connected.future;
|
||||
final resp =
|
||||
await session.post('$addr/api2/json/nodes/$node/$id/status/shutdown');
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/shutdown',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/core/sync.dart';
|
||||
import 'package:server_box/core/utils/server.dart';
|
||||
import 'package:server_box/core/utils/ssh_auth.dart';
|
||||
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/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
@@ -32,6 +33,8 @@ class ServerProvider extends Provider {
|
||||
|
||||
static final _manualDisconnectedIds = <String>{};
|
||||
|
||||
static final _serverIdsUpdating = <String, Future<void>?>{};
|
||||
|
||||
@override
|
||||
Future<void> load() async {
|
||||
super.load();
|
||||
@@ -124,11 +127,35 @@ class ServerProvider extends Provider {
|
||||
return;
|
||||
}
|
||||
|
||||
return await _getData(s.spi);
|
||||
// Check if already updating, and if so, wait for it to complete
|
||||
final existingUpdate = _serverIdsUpdating[s.spi.id];
|
||||
if (existingUpdate != null) {
|
||||
// Already updating, wait for the existing update to complete
|
||||
try {
|
||||
await existingUpdate;
|
||||
} catch (e) {
|
||||
// Ignore errors from the existing update, we'll try our own
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Start a new update operation
|
||||
final updateFuture = _updateServer(s.spi);
|
||||
_serverIdsUpdating[s.spi.id] = updateFuture;
|
||||
|
||||
try {
|
||||
await updateFuture;
|
||||
} finally {
|
||||
_serverIdsUpdating.remove(s.spi.id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _updateServer(Spi spi) async {
|
||||
await _getData(spi);
|
||||
}
|
||||
|
||||
static Future<void> startAutoRefresh() async {
|
||||
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
||||
stopAutoRefresh();
|
||||
@@ -305,13 +332,17 @@ class ServerProvider extends Provider {
|
||||
_setServerState(s, ServerConn.connected);
|
||||
|
||||
try {
|
||||
// Detect system type using helper
|
||||
final detectedSystemType = await SystemDetector.detect(sv.client!, spi);
|
||||
sv.status.system = detectedSystemType;
|
||||
|
||||
final (_, writeScriptResult) = await sv.client!.exec((session) async {
|
||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds, systemType: detectedSystemType).uint8List;
|
||||
session.stdin.add(scriptRaw);
|
||||
session.stdin.close();
|
||||
}, entry: ShellFunc.getInstallShellCmd(spi.id));
|
||||
if (writeScriptResult.isNotEmpty) {
|
||||
ShellFunc.switchScriptDir(spi.id);
|
||||
}, entry: ShellFunc.getInstallShellCmd(spi.id, systemType: detectedSystemType));
|
||||
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
|
||||
ShellFunc.switchScriptDir(spi.id, systemType: detectedSystemType);
|
||||
throw writeScriptResult;
|
||||
}
|
||||
} on SSHAuthAbortError catch (e) {
|
||||
@@ -351,7 +382,8 @@ class ServerProvider extends Provider {
|
||||
String? raw;
|
||||
|
||||
try {
|
||||
raw = await sv.client?.run(ShellFunc.status.exec(spi.id)).string;
|
||||
raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string;
|
||||
dprint('Get status from ${spi.name}:\n$raw');
|
||||
segments = raw?.split(ShellFunc.seperator).map((e) => e.trim()).toList();
|
||||
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
|
||||
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
||||
|
||||
@@ -44,10 +44,8 @@ final class SystemdProvider {
|
||||
}
|
||||
}
|
||||
|
||||
final parsedUserUnits =
|
||||
await _parseUnitObj(userUnits, SystemdUnitScope.user);
|
||||
final parsedSystemUnits =
|
||||
await _parseUnitObj(systemUnits, SystemdUnitScope.system);
|
||||
final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);
|
||||
final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);
|
||||
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Parse systemd', e, s);
|
||||
@@ -56,14 +54,10 @@ final class SystemdProvider {
|
||||
isBusy.value = false;
|
||||
}
|
||||
|
||||
Future<List<SystemdUnit>> _parseUnitObj(
|
||||
List<String> unitNames,
|
||||
SystemdUnitScope scope,
|
||||
) async {
|
||||
final unitNames_ = unitNames
|
||||
.map((e) => e.trim().split('/').last.split('.').first)
|
||||
.toList();
|
||||
final script = '''
|
||||
Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {
|
||||
final unitNames_ = unitNames.map((e) => e.trim().split('/').last.split('.').first).toList();
|
||||
final script =
|
||||
'''
|
||||
for unit in ${unitNames_.join(' ')}; do
|
||||
state=\$(systemctl show --no-pager \$unit)
|
||||
echo -n "${ShellFunc.seperator}\n\$state"
|
||||
@@ -108,13 +102,9 @@ done
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedUnits.add(SystemdUnit(
|
||||
name: name,
|
||||
type: unitType,
|
||||
scope: scope,
|
||||
state: unitState,
|
||||
description: description,
|
||||
));
|
||||
parsedUnits.add(
|
||||
SystemdUnit(name: name, type: unitType, scope: scope, state: unitState, description: description),
|
||||
);
|
||||
}
|
||||
|
||||
parsedUnits.sort((a, b) {
|
||||
@@ -131,7 +121,8 @@ done
|
||||
return parsedUnits;
|
||||
}
|
||||
|
||||
late final _getUnitsCmd = '''
|
||||
late final _getUnitsCmd =
|
||||
'''
|
||||
get_files() {
|
||||
unit_type=\$1
|
||||
base_dir=\$2
|
||||
|
||||
@@ -20,8 +20,7 @@ class ContainerStore extends HiveStore {
|
||||
ContainerType getType([String id = '']) {
|
||||
final cfg = box.get(_keyConfig + id);
|
||||
if (cfg != null) {
|
||||
final type =
|
||||
ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);
|
||||
final type = ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);
|
||||
if (type != null) return type;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,10 @@ class _ListHistory {
|
||||
final String _name;
|
||||
final Box _box;
|
||||
|
||||
_ListHistory({
|
||||
required Box box,
|
||||
required String name,
|
||||
}) : _box = box,
|
||||
_name = name,
|
||||
_history = box.get(name, defaultValue: [])!;
|
||||
_ListHistory({required Box box, required String name})
|
||||
: _box = box,
|
||||
_name = name,
|
||||
_history = box.get(name, defaultValue: [])!;
|
||||
|
||||
void add(String path) {
|
||||
_history.remove(path);
|
||||
@@ -28,12 +26,10 @@ class _MapHistory {
|
||||
final String _name;
|
||||
final Box _box;
|
||||
|
||||
_MapHistory({
|
||||
required Box box,
|
||||
required String name,
|
||||
}) : _box = box,
|
||||
_name = name,
|
||||
_history = box.get(name, defaultValue: <dynamic, dynamic>{})!;
|
||||
_MapHistory({required Box box, required String name})
|
||||
: _box = box,
|
||||
_name = name,
|
||||
_history = box.get(name, defaultValue: <dynamic, dynamic>{})!;
|
||||
|
||||
void put(String id, String val) {
|
||||
_history[id] = val;
|
||||
@@ -56,6 +52,5 @@ class HistoryStore extends HiveStore {
|
||||
late final sshCmds = _ListHistory(box: box, name: 'sshCmds');
|
||||
|
||||
/// Notify users that this app will write script to server to works properly
|
||||
late final writeScriptTipShown =
|
||||
propertyDefault('writeScriptTipShown', false);
|
||||
late final writeScriptTipShown = propertyDefault('writeScriptTipShown', false);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get acceptBeta => '接受测试版更新推送';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => '当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?';
|
||||
String get addSystemPrivateKeyTip => '检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => '已添加至任务列表';
|
||||
@@ -24,13 +24,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get addr => '地址';
|
||||
|
||||
@override
|
||||
String get alreadyLastDir => '已经是最上层目录了';
|
||||
String get alreadyLastDir => '已是顶级目录';
|
||||
|
||||
@override
|
||||
String get authFailTip => '认证失败,请检查密码/密钥/主机/用户等是否错误';
|
||||
String get authFailTip => '认证失败,请检查连接信息是否正确';
|
||||
|
||||
@override
|
||||
String get autoBackupConflict => '只能同时开启一个自动备份';
|
||||
String get autoBackupConflict => '仅可启用一个自动备份任务';
|
||||
|
||||
@override
|
||||
String get autoConnect => '自动连接';
|
||||
@@ -42,10 +42,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => '自动更新桌面小部件';
|
||||
|
||||
@override
|
||||
String get backupTip => '导出的数据可以使用密码加密,请妥善保管。';
|
||||
String get backupTip => '导出数据可通过密码加密,请妥善保管。';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => '备份版本不匹配,无法恢复';
|
||||
String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
|
||||
|
||||
@override
|
||||
String get backupPassword => '备份密码';
|
||||
@@ -76,7 +76,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get bgRunTip =>
|
||||
'此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请修改省电策略为“无限制”。';
|
||||
'此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请将省电策略改为“无限制”。';
|
||||
|
||||
@override
|
||||
String get closeAfterSave => '保存后关闭';
|
||||
@@ -132,10 +132,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get desktopTerminalTip => '启动 SSH 连接所用的终端模拟器命令';
|
||||
|
||||
@override
|
||||
String get dirEmpty => '请确保文件夹为空';
|
||||
String get dirEmpty => '请确保目录为空';
|
||||
|
||||
@override
|
||||
String get disconnected => '连接断开';
|
||||
String get disconnected => '已断开连接';
|
||||
|
||||
@override
|
||||
String get disk => '磁盘';
|
||||
@@ -160,11 +160,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String dockerImagesFmt(Object count) {
|
||||
return '共 $count 个镜像';
|
||||
return '$count 个镜像';
|
||||
}
|
||||
|
||||
@override
|
||||
String get dockerNotInstalled => 'Docker 未安装';
|
||||
String get dockerNotInstalled => '未安装 Docker';
|
||||
|
||||
@override
|
||||
String dockerStatusRunningAndStoppedFmt(
|
||||
@@ -183,7 +183,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get doubleColumnMode => '双列模式';
|
||||
|
||||
@override
|
||||
String get doubleColumnTip => '此选项仅开启功能,实际是否能开启还取决于设备的宽度';
|
||||
String get doubleColumnTip => '此选项仅用于启用该功能,是否生效取决于设备宽度';
|
||||
|
||||
@override
|
||||
String get editVirtKeys => '编辑虚拟按键';
|
||||
@@ -192,7 +192,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get editor => '编辑器';
|
||||
|
||||
@override
|
||||
String get editorHighlightTip => '目前的代码高亮性能较为糟糕,可以选择关闭以改善。';
|
||||
String get editorHighlightTip => '代码高亮功能可能影响性能,可选择关闭。';
|
||||
|
||||
@override
|
||||
String get emulator => '模拟器';
|
||||
@@ -246,7 +246,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get fullScreenJitter => '全屏模式抖动';
|
||||
|
||||
@override
|
||||
String get fullScreenJitterHelp => '防止烧屏';
|
||||
String get fullScreenJitterHelp => '用于防止屏幕烧屏';
|
||||
|
||||
@override
|
||||
String get fullScreenTip => '当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。';
|
||||
@@ -271,7 +271,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String httpFailedWithCode(Object code) {
|
||||
return '请求失败, 状态码: $code';
|
||||
return '请求失败,状态码: $code';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -294,7 +294,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get installDockerWithUrl =>
|
||||
'请先 https://docs.docker.com/engine/install docker';
|
||||
'请先前往 https://docs.docker.com/engine/install 安装 Docker';
|
||||
|
||||
@override
|
||||
String get invalid => '无效';
|
||||
@@ -303,7 +303,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get jumpServer => '跳板服务器';
|
||||
|
||||
@override
|
||||
String get keepForeground => '请保持应用处于前台!';
|
||||
String get keepForeground => '请将应用保持在前台运行';
|
||||
|
||||
@override
|
||||
String get keepStatusWhenErr => '保留上次的服务器状态';
|
||||
@@ -344,7 +344,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get maxRetryCount => '服务器尝试重连次数';
|
||||
|
||||
@override
|
||||
String get maxRetryCountEqual0 => '会无限重试';
|
||||
String get maxRetryCountEqual0 => '将无限次重试';
|
||||
|
||||
@override
|
||||
String get min => '最小';
|
||||
@@ -409,7 +409,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get openLastPath => '打开上次的路径';
|
||||
|
||||
@override
|
||||
String get openLastPathTip => '不同的服务器会有不同的记录,且记录的是退出时的路径';
|
||||
String get openLastPathTip => '将为每台服务器记录其最后访问路径';
|
||||
|
||||
@override
|
||||
String get parseContainerStatsTip => 'Docker 解析占用状态较为缓慢';
|
||||
@@ -462,7 +462,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get pveIgnoreCertTip => '不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项';
|
||||
|
||||
@override
|
||||
String get pveLoginFailed => '登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。';
|
||||
String get pveLoginFailed => '登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。';
|
||||
|
||||
@override
|
||||
String get pveVersionLow => '当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用';
|
||||
@@ -544,7 +544,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'SFTP 已连接...';
|
||||
String get sftpSSHConnected => 'SFTP 已连接';
|
||||
|
||||
@override
|
||||
String get sftpShowFoldersFirst => '文件夹显示在前';
|
||||
@@ -575,7 +575,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String spentTime(Object time) {
|
||||
return '耗时: $time';
|
||||
return '耗时:$time';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -683,7 +683,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get update => '更新';
|
||||
|
||||
@override
|
||||
String get updateIntervalEqual0 => '你设置为 0,服务器状态不会自动刷新。\n且不能计算 CPU 使用情况。';
|
||||
String get updateIntervalEqual0 => '设置为 0 将不自动刷新服务器状态。\n且无法计算 CPU 使用率。';
|
||||
|
||||
@override
|
||||
String get updateServerStatusInterval => '服务器状态刷新间隔';
|
||||
@@ -743,7 +743,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get whenOpenApp => '当打开 App 时';
|
||||
|
||||
@override
|
||||
String get wolTip => '在配置 WOL 后,每次连接服务器都会先发送一次 WOL 请求';
|
||||
String get wolTip => '配置 WOL 后,每次连接服务器时将自动发送唤醒请求';
|
||||
|
||||
@override
|
||||
String get write => '写';
|
||||
@@ -767,7 +767,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get acceptBeta => '接受測試版更新推送';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => '目前沒有任何私鑰,是否新增系統原有的 (~/.ssh/id_rsa)?';
|
||||
String get addSystemPrivateKeyTip => '偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => '已新增至任務清單';
|
||||
@@ -776,13 +776,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get addr => '位址';
|
||||
|
||||
@override
|
||||
String get alreadyLastDir => '已經是最上層目錄了';
|
||||
String get alreadyLastDir => '已是頂層目錄';
|
||||
|
||||
@override
|
||||
String get authFailTip => '認證失敗,請檢查密碼/金鑰/主機/使用者等是否錯誤。';
|
||||
String get authFailTip => '認證失敗,請檢查連線資訊是否正確';
|
||||
|
||||
@override
|
||||
String get autoBackupConflict => '只能同時開啓一個自動備份';
|
||||
String get autoBackupConflict => '僅能啟用一項自動備份任務';
|
||||
|
||||
@override
|
||||
String get autoConnect => '自動連線';
|
||||
@@ -794,10 +794,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get autoUpdateHomeWidget => '自動更新桌面小工具';
|
||||
|
||||
@override
|
||||
String get backupTip => '匯出的資料可以使用密碼加密。 \n請妥善保管。';
|
||||
String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => '備份版本不相符,無法還原';
|
||||
String get backupVersionNotMatch => '備份版本不相容,無法還原';
|
||||
|
||||
@override
|
||||
String get backupPassword => '備份密碼';
|
||||
@@ -828,7 +828,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get bgRunTip =>
|
||||
'此開關只代表程式會嘗試在背景執行,具體能否在後臺執行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池最佳化”,MIUI / HyperOS 請修改省電策略為“無限制”。';
|
||||
'此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。';
|
||||
|
||||
@override
|
||||
String get closeAfterSave => '儲存後關閉';
|
||||
@@ -884,10 +884,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get desktopTerminalTip => '啟動 SSH 連線時用於打開終端機模擬器的指令。';
|
||||
|
||||
@override
|
||||
String get dirEmpty => '請確保資料夾為空';
|
||||
String get dirEmpty => '請確保目錄為空';
|
||||
|
||||
@override
|
||||
String get disconnected => '連線中斷';
|
||||
String get disconnected => '已中斷連線';
|
||||
|
||||
@override
|
||||
String get disk => '磁碟';
|
||||
@@ -912,11 +912,11 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String dockerImagesFmt(Object count) {
|
||||
return '共 $count 個映像檔';
|
||||
return '$count 個映像檔';
|
||||
}
|
||||
|
||||
@override
|
||||
String get dockerNotInstalled => 'Docker 未安裝';
|
||||
String get dockerNotInstalled => '未安裝 Docker';
|
||||
|
||||
@override
|
||||
String dockerStatusRunningAndStoppedFmt(
|
||||
@@ -935,7 +935,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get doubleColumnMode => '雙列模式';
|
||||
|
||||
@override
|
||||
String get doubleColumnTip => '此選項僅開啟功能,實際是否能開啟還取決於設備的頻寬';
|
||||
String get doubleColumnTip => '此選項僅用於啟用此功能,是否生效取決於裝置寬度';
|
||||
|
||||
@override
|
||||
String get editVirtKeys => '編輯虛擬按鍵';
|
||||
@@ -944,7 +944,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get editor => '編輯器';
|
||||
|
||||
@override
|
||||
String get editorHighlightTip => '目前的程式碼標記效能較為糟糕,可以選擇關閉以改善。';
|
||||
String get editorHighlightTip => '程式碼高亮功能可能影響效能,可選擇性關閉。';
|
||||
|
||||
@override
|
||||
String get emulator => '模擬器';
|
||||
@@ -998,7 +998,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get fullScreenJitter => '全螢幕模式抖動';
|
||||
|
||||
@override
|
||||
String get fullScreenJitterHelp => '防止烙印';
|
||||
String get fullScreenJitterHelp => '防止螢幕烙印';
|
||||
|
||||
@override
|
||||
String get fullScreenTip => '當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。';
|
||||
@@ -1023,7 +1023,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String httpFailedWithCode(Object code) {
|
||||
return '請求失敗, 狀態碼: $code';
|
||||
return '請求失敗,狀態碼:$code';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1046,7 +1046,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get installDockerWithUrl =>
|
||||
'請先 https://docs.docker.com/engine/install docker';
|
||||
'請先前往 https://docs.docker.com/engine/install 安裝 Docker';
|
||||
|
||||
@override
|
||||
String get invalid => '無效';
|
||||
@@ -1055,7 +1055,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get jumpServer => '跳板伺服器';
|
||||
|
||||
@override
|
||||
String get keepForeground => '請保持App處於前端!';
|
||||
String get keepForeground => '請讓 App 保持在前景執行';
|
||||
|
||||
@override
|
||||
String get keepStatusWhenErr => '保留上次的伺服器狀態';
|
||||
@@ -1096,7 +1096,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get maxRetryCount => '伺服器嘗試重連次數';
|
||||
|
||||
@override
|
||||
String get maxRetryCountEqual0 => '會無限重試';
|
||||
String get maxRetryCountEqual0 => '將無限次重試';
|
||||
|
||||
@override
|
||||
String get min => '最小';
|
||||
@@ -1161,7 +1161,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get openLastPath => '打開上次的路徑';
|
||||
|
||||
@override
|
||||
String get openLastPathTip => '不同的伺服器會有不同的記錄,且記錄的是退出時的路徑';
|
||||
String get openLastPathTip => '將為每台伺服器紀錄其最後存取路徑';
|
||||
|
||||
@override
|
||||
String get parseContainerStatsTip => 'Docker 解析消耗狀態較為緩慢';
|
||||
@@ -1214,7 +1214,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get pveIgnoreCertTip => '不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。';
|
||||
|
||||
@override
|
||||
String get pveLoginFailed => '登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。';
|
||||
String get pveLoginFailed => '登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。';
|
||||
|
||||
@override
|
||||
String get pveVersionLow => '此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。';
|
||||
@@ -1296,7 +1296,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除檔案夾';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'SFTP 已連線...';
|
||||
String get sftpSSHConnected => 'SFTP 已連線';
|
||||
|
||||
@override
|
||||
String get sftpShowFoldersFirst => '資料夾顯示在前';
|
||||
@@ -1327,7 +1327,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String spentTime(Object time) {
|
||||
return '耗時: $time';
|
||||
return '耗時:$time';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1435,7 +1435,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get update => '更新';
|
||||
|
||||
@override
|
||||
String get updateIntervalEqual0 => '你設定為 0,伺服器狀態不會自動更新。\n且不能計算CPU使用情況。';
|
||||
String get updateIntervalEqual0 => '設定為 0 將不自動刷新伺服器狀態,\n也無法計算 CPU 使用率。';
|
||||
|
||||
@override
|
||||
String get updateServerStatusInterval => '伺服器狀態更新間隔';
|
||||
@@ -1495,7 +1495,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get whenOpenApp => '當打開 App 時';
|
||||
|
||||
@override
|
||||
String get wolTip => '在配置 WOL(網絡喚醒)後,每次連線伺服器都會先發送一次 WOL 請求。';
|
||||
String get wolTip => '設定 WOL 後,每次連線伺服器時將自動發送喚醒請求';
|
||||
|
||||
@override
|
||||
String get write => '寫入';
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/snippet.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/model/ssh/virtual_key.dart';
|
||||
|
||||
@@ -17,5 +18,6 @@ import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||
AdapterSpec<ServerFuncBtn>(),
|
||||
AdapterSpec<ServerCustom>(),
|
||||
AdapterSpec<WakeOnLanCfg>(),
|
||||
AdapterSpec<SystemType>(),
|
||||
])
|
||||
part 'hive_adapters.g.dart';
|
||||
|
||||
@@ -111,13 +111,14 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
||||
wolCfg: fields[11] as WakeOnLanCfg?,
|
||||
envs: (fields[12] as Map?)?.cast<String, String>(),
|
||||
id: fields[13] == null ? '' : fields[13] as String,
|
||||
customSystemType: fields[14] as SystemType?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Spi obj) {
|
||||
writer
|
||||
..writeByte(14)
|
||||
..writeByte(15)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
@@ -145,7 +146,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
||||
..writeByte(12)
|
||||
..write(obj.envs)
|
||||
..writeByte(13)
|
||||
..write(obj.id);
|
||||
..write(obj.id)
|
||||
..writeByte(14)
|
||||
..write(obj.customSystemType);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -557,3 +560,44 @@ class WakeOnLanCfgAdapter extends TypeAdapter<WakeOnLanCfg> {
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class SystemTypeAdapter extends TypeAdapter<SystemType> {
|
||||
@override
|
||||
final typeId = 9;
|
||||
|
||||
@override
|
||||
SystemType read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return SystemType.linux;
|
||||
case 1:
|
||||
return SystemType.bsd;
|
||||
case 2:
|
||||
return SystemType.windows;
|
||||
default:
|
||||
return SystemType.linux;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SystemType obj) {
|
||||
switch (obj) {
|
||||
case SystemType.linux:
|
||||
writer.writeByte(0);
|
||||
case SystemType.bsd:
|
||||
writer.writeByte(1);
|
||||
case SystemType.windows:
|
||||
writer.writeByte(2);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SystemTypeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Hive CE
|
||||
# Manual modifications may be necessary for certain migrations
|
||||
# Check in to version control
|
||||
nextTypeId: 9
|
||||
nextTypeId: 10
|
||||
types:
|
||||
PrivateKeyInfo:
|
||||
typeId: 1
|
||||
@@ -27,7 +27,7 @@ types:
|
||||
index: 4
|
||||
Spi:
|
||||
typeId: 3
|
||||
nextIndex: 14
|
||||
nextIndex: 15
|
||||
fields:
|
||||
name:
|
||||
index: 0
|
||||
@@ -57,6 +57,8 @@ types:
|
||||
index: 12
|
||||
id:
|
||||
index: 13
|
||||
customSystemType:
|
||||
index: 14
|
||||
VirtKey:
|
||||
typeId: 4
|
||||
nextIndex: 45
|
||||
@@ -207,3 +209,13 @@ types:
|
||||
index: 1
|
||||
pwd:
|
||||
index: 2
|
||||
SystemType:
|
||||
typeId: 9
|
||||
nextIndex: 3
|
||||
fields:
|
||||
linux:
|
||||
index: 0
|
||||
bsd:
|
||||
index: 1
|
||||
windows:
|
||||
index: 2
|
||||
|
||||
@@ -13,6 +13,7 @@ extension HiveRegistrar on HiveInterface {
|
||||
registerAdapter(ServerFuncBtnAdapter());
|
||||
registerAdapter(SnippetAdapter());
|
||||
registerAdapter(SpiAdapter());
|
||||
registerAdapter(SystemTypeAdapter());
|
||||
registerAdapter(VirtKeyAdapter());
|
||||
registerAdapter(WakeOnLanCfgAdapter());
|
||||
}
|
||||
@@ -26,6 +27,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
registerAdapter(ServerFuncBtnAdapter());
|
||||
registerAdapter(SnippetAdapter());
|
||||
registerAdapter(SpiAdapter());
|
||||
registerAdapter(SystemTypeAdapter());
|
||||
registerAdapter(VirtKeyAdapter());
|
||||
registerAdapter(WakeOnLanCfgAdapter());
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ final class _IntroPage extends StatelessWidget {
|
||||
|
||||
const _IntroPage(this.pages);
|
||||
|
||||
static const _builders = {
|
||||
1: _buildAppSettings,
|
||||
};
|
||||
static const _builders = {1: _buildAppSettings};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -20,9 +18,7 @@ final class _IntroPage extends StatelessWidget {
|
||||
pages: pages_,
|
||||
onDone: (ctx) {
|
||||
Stores.setting.introVer.put(BuildData.build);
|
||||
Navigator.of(ctx).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const HomePage()),
|
||||
);
|
||||
Navigator.of(ctx).pushReplacement(MaterialPageRoute(builder: (_) => const HomePage()));
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -52,17 +48,12 @@ final class _IntroPage extends StatelessWidget {
|
||||
RNodes.app.notify();
|
||||
}
|
||||
},
|
||||
trailing: Text(
|
||||
ctx.localeNativeName,
|
||||
style: const TextStyle(fontSize: 15, color: Colors.grey),
|
||||
),
|
||||
trailing: Text(ctx.localeNativeName, style: const TextStyle(fontSize: 15, color: Colors.grey)),
|
||||
).cardx,
|
||||
ListTile(
|
||||
leading: const Icon(Icons.update),
|
||||
title: Text(libL10n.checkUpdate),
|
||||
subtitle: isAndroid
|
||||
? Text(l10n.fdroidReleaseTip, style: UIs.textGrey)
|
||||
: null,
|
||||
subtitle: isAndroid ? Text(l10n.fdroidReleaseTip, style: UIs.textGrey) : null,
|
||||
trailing: StoreSwitch(prop: _setting.autoCheckAppUpdate),
|
||||
).cardx,
|
||||
ListTile(
|
||||
@@ -87,10 +78,7 @@ final class _IntroPage extends StatelessWidget {
|
||||
|
||||
static List<IntroPageBuilder> get builders {
|
||||
final storedVer = _setting.introVer.fetch();
|
||||
return _builders.entries
|
||||
.where((e) => e.key > storedVer)
|
||||
.map((e) => e.value)
|
||||
.toList();
|
||||
return _builders.entries.where((e) => e.key > storedVer).map((e) => e.value).toList();
|
||||
}
|
||||
|
||||
static final _setting = Stores.setting;
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"@@locale": "zh",
|
||||
"aboutThanks": "感谢以下参与的各位。",
|
||||
"acceptBeta": "接受测试版更新推送",
|
||||
"addSystemPrivateKeyTip": "当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?",
|
||||
"addSystemPrivateKeyTip": "检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?",
|
||||
"added2List": "已添加至任务列表",
|
||||
"addr": "地址",
|
||||
"alreadyLastDir": "已经是最上层目录了",
|
||||
"authFailTip": "认证失败,请检查密码/密钥/主机/用户等是否错误",
|
||||
"autoBackupConflict": "只能同时开启一个自动备份",
|
||||
"alreadyLastDir": "已是顶级目录",
|
||||
"authFailTip": "认证失败,请检查连接信息是否正确",
|
||||
"autoBackupConflict": "仅可启用一个自动备份任务",
|
||||
"autoConnect": "自动连接",
|
||||
"autoRun": "自动运行",
|
||||
"autoUpdateHomeWidget": "自动更新桌面小部件",
|
||||
"backupTip": "导出的数据可以使用密码加密,请妥善保管。",
|
||||
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
|
||||
"backupTip": "导出数据可通过密码加密,请妥善保管。",
|
||||
"backupVersionNotMatch": "备份版本不兼容,无法恢复",
|
||||
"backupPassword": "备份密码",
|
||||
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
|
||||
"backupPasswordWrong": "备份密码错误",
|
||||
@@ -22,7 +22,7 @@
|
||||
"backupPasswordRemoved": "备份密码已移除",
|
||||
"battery": "电池",
|
||||
"bgRun": "后台运行",
|
||||
"bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请修改省电策略为“无限制”。",
|
||||
"bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请将省电策略改为“无限制”。",
|
||||
"closeAfterSave": "保存后关闭",
|
||||
"cmd": "命令",
|
||||
"collapseUITip": "是否默认折叠 UI 中的长列表",
|
||||
@@ -40,23 +40,23 @@
|
||||
"decompress": "解压缩",
|
||||
"deleteServers": "批量删除服务器",
|
||||
"desktopTerminalTip": "启动 SSH 连接所用的终端模拟器命令",
|
||||
"dirEmpty": "请确保文件夹为空",
|
||||
"disconnected": "连接断开",
|
||||
"dirEmpty": "请确保目录为空",
|
||||
"disconnected": "已断开连接",
|
||||
"disk": "磁盘",
|
||||
"diskHealth": "磁盘健康",
|
||||
"diskIgnorePath": "忽略的磁盘路径",
|
||||
"displayCpuIndex": "显示 CPU 索引",
|
||||
"dl2Local": "下载 {fileName} 到本地?",
|
||||
"dockerEmptyRunningItems": "没有正在运行的容器。\n这可能是因为:\n- Docker 安装用户与 App 内配置的用户名不同\n- 环境变量 DOCKER_HOST 没有被正确读取。可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。",
|
||||
"dockerImagesFmt": "共 {count} 个镜像",
|
||||
"dockerNotInstalled": "Docker 未安装",
|
||||
"dockerImagesFmt": "{count} 个镜像",
|
||||
"dockerNotInstalled": "未安装 Docker",
|
||||
"dockerStatusRunningAndStoppedFmt": "{runningCount} 个正在运行, {stoppedCount} 个已停止",
|
||||
"dockerStatusRunningFmt": "{count} 个容器正在运行",
|
||||
"doubleColumnMode": "双列模式",
|
||||
"doubleColumnTip": "此选项仅开启功能,实际是否能开启还取决于设备的宽度",
|
||||
"doubleColumnTip": "此选项仅用于启用该功能,是否生效取决于设备宽度",
|
||||
"editVirtKeys": "编辑虚拟按键",
|
||||
"editor": "编辑器",
|
||||
"editorHighlightTip": "目前的代码高亮性能较为糟糕,可以选择关闭以改善。",
|
||||
"editorHighlightTip": "代码高亮功能可能影响性能,可选择关闭。",
|
||||
"emulator": "模拟器",
|
||||
"encode": "编码",
|
||||
"envVars": "环境变量",
|
||||
@@ -73,7 +73,7 @@
|
||||
"force": "强制",
|
||||
"fullScreen": "全屏模式",
|
||||
"fullScreenJitter": "全屏模式抖动",
|
||||
"fullScreenJitterHelp": "防止烧屏",
|
||||
"fullScreenJitterHelp": "用于防止屏幕烧屏",
|
||||
"fullScreenTip": "当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。",
|
||||
"goBackQ": "返回?",
|
||||
"goto": "前往",
|
||||
@@ -81,17 +81,17 @@
|
||||
"highlight": "代码高亮",
|
||||
"homeWidgetUrlConfig": "桌面部件链接配置",
|
||||
"host": "主机",
|
||||
"httpFailedWithCode": "请求失败, 状态码: {code}",
|
||||
"httpFailedWithCode": "请求失败,状态码: {code}",
|
||||
"ignoreCert": "忽略证书",
|
||||
"image": "镜像",
|
||||
"imagesList": "镜像列表",
|
||||
"init": "初始化",
|
||||
"inner": "内置",
|
||||
"install": "安装",
|
||||
"installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
|
||||
"installDockerWithUrl": "请先前往 https://docs.docker.com/engine/install 安装 Docker",
|
||||
"invalid": "无效",
|
||||
"jumpServer": "跳板服务器",
|
||||
"keepForeground": "请保持应用处于前台!",
|
||||
"keepForeground": "请将应用保持在前台运行",
|
||||
"keepStatusWhenErr": "保留上次的服务器状态",
|
||||
"keepStatusWhenErrTip": "仅限于执行脚本出错",
|
||||
"keyAuth": "密钥认证",
|
||||
@@ -104,7 +104,7 @@
|
||||
"manual": "手动",
|
||||
"max": "最大",
|
||||
"maxRetryCount": "服务器尝试重连次数",
|
||||
"maxRetryCountEqual0": "会无限重试",
|
||||
"maxRetryCountEqual0": "将无限次重试",
|
||||
"min": "最小",
|
||||
"mission": "任务",
|
||||
"more": "更多",
|
||||
@@ -125,7 +125,7 @@
|
||||
"onlyOneLine": "仅显示为一行(可滚动)",
|
||||
"onlyWhenCoreBiggerThan8": "仅当核心数大于 8 时生效",
|
||||
"openLastPath": "打开上次的路径",
|
||||
"openLastPathTip": "不同的服务器会有不同的记录,且记录的是退出时的路径",
|
||||
"openLastPathTip": "将为每台服务器记录其最后访问路径",
|
||||
"parseContainerStatsTip": "Docker 解析占用状态较为缓慢",
|
||||
"percentOfSize": "{size} 的 {percent}%",
|
||||
"permission": "权限",
|
||||
@@ -142,7 +142,7 @@
|
||||
"prune": "修剪",
|
||||
"pushToken": "消息推送 Token",
|
||||
"pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项",
|
||||
"pveLoginFailed": "登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。",
|
||||
"pveLoginFailed": "登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。",
|
||||
"pveVersionLow": "当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用",
|
||||
"read": "读",
|
||||
"reboot": "重启",
|
||||
@@ -169,7 +169,7 @@
|
||||
"sftpDlPrepare": "准备连接至服务器...",
|
||||
"sftpEditorTip": "如果为空, 使用App内置的文件编辑器. 如果有值, 这是用远程服务器的编辑器, 例如 `vim` (建议根据 `EDITOR` 自动获取).",
|
||||
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 来删除文件夹",
|
||||
"sftpSSHConnected": "SFTP 已连接...",
|
||||
"sftpSSHConnected": "SFTP 已连接",
|
||||
"sftpShowFoldersFirst": "文件夹显示在前",
|
||||
"showDistLogo": "显示发行版 Logo",
|
||||
"shutdown": "关机",
|
||||
@@ -179,7 +179,7 @@
|
||||
"specifyDev": "指定设备",
|
||||
"specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备",
|
||||
"speed": "速度",
|
||||
"spentTime": "耗时: {time}",
|
||||
"spentTime": "耗时:{time}",
|
||||
"sshTermHelp": "在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容,在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。",
|
||||
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
|
||||
"sshVirtualKeyAutoOff": "虚拟按键自动切换",
|
||||
@@ -213,7 +213,7 @@
|
||||
"unknown": "未知",
|
||||
"unkownConvertMode": "未知转换模式",
|
||||
"update": "更新",
|
||||
"updateIntervalEqual0": "你设置为 0,服务器状态不会自动刷新。\n且不能计算 CPU 使用情况。",
|
||||
"updateIntervalEqual0": "设置为 0 将不自动刷新服务器状态。\n且无法计算 CPU 使用率。",
|
||||
"updateServerStatusInterval": "服务器状态刷新间隔",
|
||||
"upload": "上传",
|
||||
"upsideDown": "上下交换",
|
||||
@@ -233,7 +233,7 @@
|
||||
"watchNotPaired": "没有已配对的 Apple Watch",
|
||||
"webdavSettingEmpty": "WebDav 设置项为空",
|
||||
"whenOpenApp": "当打开 App 时",
|
||||
"wolTip": "在配置 WOL 后,每次连接服务器都会先发送一次 WOL 请求",
|
||||
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
||||
"write": "写",
|
||||
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
||||
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"@@locale": "zh_TW",
|
||||
"aboutThanks": "感謝以下參與的各位。",
|
||||
"acceptBeta": "接受測試版更新推送",
|
||||
"addSystemPrivateKeyTip": "目前沒有任何私鑰,是否新增系統原有的 (~/.ssh/id_rsa)?",
|
||||
"addSystemPrivateKeyTip": "偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?",
|
||||
"added2List": "已新增至任務清單",
|
||||
"addr": "位址",
|
||||
"alreadyLastDir": "已經是最上層目錄了",
|
||||
"authFailTip": "認證失敗,請檢查密碼/金鑰/主機/使用者等是否錯誤。",
|
||||
"autoBackupConflict": "只能同時開啓一個自動備份",
|
||||
"alreadyLastDir": "已是頂層目錄",
|
||||
"authFailTip": "認證失敗,請檢查連線資訊是否正確",
|
||||
"autoBackupConflict": "僅能啟用一項自動備份任務",
|
||||
"autoConnect": "自動連線",
|
||||
"autoRun": "自動執行",
|
||||
"autoUpdateHomeWidget": "自動更新桌面小工具",
|
||||
"backupTip": "匯出的資料可以使用密碼加密。 \n請妥善保管。",
|
||||
"backupVersionNotMatch": "備份版本不相符,無法還原",
|
||||
"backupTip": "匯出的資料可透過密碼加密,請妥善保管。",
|
||||
"backupVersionNotMatch": "備份版本不相容,無法還原",
|
||||
"backupPassword": "備份密碼",
|
||||
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
|
||||
"backupPasswordWrong": "備份密碼錯誤",
|
||||
@@ -22,7 +22,7 @@
|
||||
"backupPasswordRemoved": "備份密碼已移除",
|
||||
"battery": "電池",
|
||||
"bgRun": "背景執行",
|
||||
"bgRunTip": "此開關只代表程式會嘗試在背景執行,具體能否在後臺執行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池最佳化”,MIUI / HyperOS 請修改省電策略為“無限制”。",
|
||||
"bgRunTip": "此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。",
|
||||
"closeAfterSave": "儲存後關閉",
|
||||
"cmd": "指令",
|
||||
"collapseUITip": "是否預設折疊 UI 中存在的長列表",
|
||||
@@ -40,23 +40,23 @@
|
||||
"decompress": "解壓縮",
|
||||
"deleteServers": "大量刪除伺服器",
|
||||
"desktopTerminalTip": "啟動 SSH 連線時用於打開終端機模擬器的指令。",
|
||||
"dirEmpty": "請確保資料夾為空",
|
||||
"disconnected": "連線中斷",
|
||||
"dirEmpty": "請確保目錄為空",
|
||||
"disconnected": "已中斷連線",
|
||||
"disk": "磁碟",
|
||||
"diskHealth": "磁碟健康",
|
||||
"diskIgnorePath": "忽略的磁碟路徑",
|
||||
"displayCpuIndex": "顯示 CPU 索引",
|
||||
"dl2Local": "下載 {fileName} 到本地?",
|
||||
"dockerEmptyRunningItems": "沒有正在執行的容器。\n這可能是因為:\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變數 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo $DOCKER_HOST` 來獲取。",
|
||||
"dockerImagesFmt": "共 {count} 個映像檔",
|
||||
"dockerNotInstalled": "Docker 未安裝",
|
||||
"dockerImagesFmt": "{count} 個映像檔",
|
||||
"dockerNotInstalled": "未安裝 Docker",
|
||||
"dockerStatusRunningAndStoppedFmt": "{runningCount} 個正在執行, {stoppedCount} 個已停止",
|
||||
"dockerStatusRunningFmt": "{count} 個容器正在執行",
|
||||
"doubleColumnMode": "雙列模式",
|
||||
"doubleColumnTip": "此選項僅開啟功能,實際是否能開啟還取決於設備的頻寬",
|
||||
"doubleColumnTip": "此選項僅用於啟用此功能,是否生效取決於裝置寬度",
|
||||
"editVirtKeys": "編輯虛擬按鍵",
|
||||
"editor": "編輯器",
|
||||
"editorHighlightTip": "目前的程式碼標記效能較為糟糕,可以選擇關閉以改善。",
|
||||
"editorHighlightTip": "程式碼高亮功能可能影響效能,可選擇性關閉。",
|
||||
"emulator": "模擬器",
|
||||
"encode": "編碼",
|
||||
"envVars": "環境變數",
|
||||
@@ -73,7 +73,7 @@
|
||||
"force": "強制",
|
||||
"fullScreen": "全螢幕模式",
|
||||
"fullScreenJitter": "全螢幕模式抖動",
|
||||
"fullScreenJitterHelp": "防止烙印",
|
||||
"fullScreenJitterHelp": "防止螢幕烙印",
|
||||
"fullScreenTip": "當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。",
|
||||
"goBackQ": "返回?",
|
||||
"goto": "前往",
|
||||
@@ -81,17 +81,17 @@
|
||||
"highlight": "程式碼標記",
|
||||
"homeWidgetUrlConfig": "桌面小工具連結配置",
|
||||
"host": "主機",
|
||||
"httpFailedWithCode": "請求失敗, 狀態碼: {code}",
|
||||
"httpFailedWithCode": "請求失敗,狀態碼:{code}",
|
||||
"ignoreCert": "忽略憑證",
|
||||
"image": "映像檔",
|
||||
"imagesList": "映像檔列表",
|
||||
"init": "初始化",
|
||||
"inner": "內建",
|
||||
"install": "安裝",
|
||||
"installDockerWithUrl": "請先 https://docs.docker.com/engine/install docker",
|
||||
"installDockerWithUrl": "請先前往 https://docs.docker.com/engine/install 安裝 Docker",
|
||||
"invalid": "無效",
|
||||
"jumpServer": "跳板伺服器",
|
||||
"keepForeground": "請保持App處於前端!",
|
||||
"keepForeground": "請讓 App 保持在前景執行",
|
||||
"keepStatusWhenErr": "保留上次的伺服器狀態",
|
||||
"keepStatusWhenErrTip": "僅在執行腳本出錯時",
|
||||
"keyAuth": "金鑰認證",
|
||||
@@ -104,7 +104,7 @@
|
||||
"manual": "手動",
|
||||
"max": "最大",
|
||||
"maxRetryCount": "伺服器嘗試重連次數",
|
||||
"maxRetryCountEqual0": "會無限重試",
|
||||
"maxRetryCountEqual0": "將無限次重試",
|
||||
"min": "最小",
|
||||
"mission": "任務",
|
||||
"more": "更多",
|
||||
@@ -125,7 +125,7 @@
|
||||
"onlyOneLine": "僅顯示為一行(可捲動)",
|
||||
"onlyWhenCoreBiggerThan8": "僅當核心數大於 8 時生效",
|
||||
"openLastPath": "打開上次的路徑",
|
||||
"openLastPathTip": "不同的伺服器會有不同的記錄,且記錄的是退出時的路徑",
|
||||
"openLastPathTip": "將為每台伺服器紀錄其最後存取路徑",
|
||||
"parseContainerStatsTip": "Docker 解析消耗狀態較為緩慢",
|
||||
"percentOfSize": "{size} 的 {percent}%",
|
||||
"permission": "權限",
|
||||
@@ -142,7 +142,7 @@
|
||||
"prune": "修剪",
|
||||
"pushToken": "消息推送 Token",
|
||||
"pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。",
|
||||
"pveLoginFailed": "登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。",
|
||||
"pveLoginFailed": "登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。",
|
||||
"pveVersionLow": "此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。",
|
||||
"read": "讀取",
|
||||
"reboot": "重開",
|
||||
@@ -169,7 +169,7 @@
|
||||
"sftpDlPrepare": "準備連線至伺服器...",
|
||||
"sftpEditorTip": "如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠端伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。",
|
||||
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除檔案夾",
|
||||
"sftpSSHConnected": "SFTP 已連線...",
|
||||
"sftpSSHConnected": "SFTP 已連線",
|
||||
"sftpShowFoldersFirst": "資料夾顯示在前",
|
||||
"showDistLogo": "顯示發行版 Logo",
|
||||
"shutdown": "關機",
|
||||
@@ -179,7 +179,7 @@
|
||||
"specifyDev": "指定裝置",
|
||||
"specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。",
|
||||
"speed": "速度",
|
||||
"spentTime": "耗時: {time}",
|
||||
"spentTime": "耗時:{time}",
|
||||
"sshTermHelp": "在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容,在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。",
|
||||
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 回饋問題,或者加入我們開發。",
|
||||
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換",
|
||||
@@ -213,7 +213,7 @@
|
||||
"unknown": "未知",
|
||||
"unkownConvertMode": "未知轉換模式",
|
||||
"update": "更新",
|
||||
"updateIntervalEqual0": "你設定為 0,伺服器狀態不會自動更新。\n且不能計算CPU使用情況。",
|
||||
"updateIntervalEqual0": "設定為 0 將不自動刷新伺服器狀態,\n也無法計算 CPU 使用率。",
|
||||
"updateServerStatusInterval": "伺服器狀態更新間隔",
|
||||
"upload": "上傳",
|
||||
"upsideDown": "上下交換",
|
||||
@@ -233,7 +233,7 @@
|
||||
"watchNotPaired": "沒有已配對的 Apple Watch",
|
||||
"webdavSettingEmpty": "WebDav 設定項爲空",
|
||||
"whenOpenApp": "當打開 App 時",
|
||||
"wolTip": "在配置 WOL(網絡喚醒)後,每次連線伺服器都會先發送一次 WOL 請求。",
|
||||
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
||||
"write": "寫入",
|
||||
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
||||
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
|
||||
|
||||
@@ -76,9 +76,9 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
initiallyExpanded: false,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(libL10n.backup),
|
||||
trailing: const Icon(Icons.save),
|
||||
onTap: () => BackupService.backup(context, FileBackupSource())
|
||||
title: Text(libL10n.backup),
|
||||
trailing: const Icon(Icons.save),
|
||||
onTap: () => BackupService.backup(context, FileBackupSource()),
|
||||
),
|
||||
ListTile(
|
||||
trailing: const Icon(Icons.restore),
|
||||
@@ -264,7 +264,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
).cardx;
|
||||
}
|
||||
|
||||
|
||||
Future<void> _onTapWebdavDl(BuildContext context) async {
|
||||
webdavLoading.value = true;
|
||||
try {
|
||||
@@ -357,7 +356,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _onBulkImportServers(BuildContext context) async {
|
||||
final data = await context.showImportDialog(title: l10n.server, modelDef: Spix.example.toJson());
|
||||
if (data == null) return;
|
||||
@@ -394,11 +392,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ enum _PruneTypes {
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@ class HomePage extends StatefulWidget {
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: HomePage.new,
|
||||
path: '/',
|
||||
);
|
||||
static const route = AppRouteNoArg(page: HomePage.new, path: '/');
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage>
|
||||
@@ -181,11 +178,7 @@ class _HomePageState extends State<HomePage>
|
||||
//_reqNotiPerm();
|
||||
|
||||
if (Stores.setting.autoCheckAppUpdate.fetch()) {
|
||||
AppUpdateIface.doUpdate(
|
||||
build: BuildData.build,
|
||||
url: Urls.updateCfg,
|
||||
context: context,
|
||||
);
|
||||
AppUpdateIface.doUpdate(build: BuildData.build, url: Urls.updateCfg, context: context);
|
||||
}
|
||||
MethodChans.updateHomeWidget();
|
||||
await ServerProvider.refresh();
|
||||
@@ -216,10 +209,7 @@ class _HomePageState extends State<HomePage>
|
||||
void _goAuth() {
|
||||
if (Stores.setting.useBioAuth.fetch()) {
|
||||
if (LocalAuthPage.route.alreadyIn) return;
|
||||
LocalAuthPage.route.go(
|
||||
context,
|
||||
args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false),
|
||||
);
|
||||
LocalAuthPage.route.go(context, args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,9 +235,7 @@ final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: preferredSize.height,
|
||||
);
|
||||
return SizedBox(height: preferredSize.height);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||
|
||||
|
||||
class IPerfPage extends StatefulWidget {
|
||||
final SpiRequiredArgs args;
|
||||
|
||||
@@ -13,10 +12,7 @@ class IPerfPage extends StatefulWidget {
|
||||
@override
|
||||
State<IPerfPage> createState() => _IPerfPageState();
|
||||
|
||||
static const route = AppRouteArg<void, SpiRequiredArgs>(
|
||||
page: IPerfPage.new,
|
||||
path: '/iperf',
|
||||
);
|
||||
static const route = AppRouteArg<void, SpiRequiredArgs>(page: IPerfPage.new, path: '/iperf');
|
||||
}
|
||||
|
||||
class _IPerfPageState extends State<IPerfPage> {
|
||||
@@ -33,9 +29,7 @@ class _IPerfPageState extends State<IPerfPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: const Text('iperf'),
|
||||
),
|
||||
appBar: CustomAppBar(title: const Text('iperf')),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: _buildFAB(),
|
||||
);
|
||||
@@ -63,12 +57,7 @@ class _IPerfPageState extends State<IPerfPage> {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
children: [
|
||||
Input(
|
||||
controller: _hostCtrl,
|
||||
label: l10n.host,
|
||||
icon: Icons.computer,
|
||||
suggestion: false,
|
||||
),
|
||||
Input(controller: _hostCtrl, label: l10n.host, icon: Icons.computer, suggestion: false),
|
||||
Input(
|
||||
controller: _portCtrl,
|
||||
label: l10n.port,
|
||||
|
||||
@@ -24,10 +24,7 @@ class PrivateKeyEditPage extends StatefulWidget {
|
||||
@override
|
||||
State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
|
||||
|
||||
static const route = AppRoute(
|
||||
page: PrivateKeyEditPage.new,
|
||||
path: '/private_key/edit',
|
||||
);
|
||||
static const route = AppRoute(page: PrivateKeyEditPage.new, path: '/private_key/edit');
|
||||
}
|
||||
|
||||
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
@@ -82,11 +79,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: _buildFAB(),
|
||||
);
|
||||
return Scaffold(appBar: _buildAppBar(), body: _buildBody(), floatingActionButton: _buildFAB());
|
||||
}
|
||||
|
||||
CustomAppBar _buildAppBar() {
|
||||
@@ -98,9 +91,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
onPressed: () {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue(
|
||||
'${libL10n.delete} ${l10n.privateKey}(${pki.id})',
|
||||
)),
|
||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.privateKey}(${pki.id})')),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
PrivateKeyProvider.delete(pki);
|
||||
@@ -112,13 +103,10 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
)
|
||||
),
|
||||
]
|
||||
: null;
|
||||
return CustomAppBar(
|
||||
title: Text(libL10n.edit),
|
||||
actions: actions,
|
||||
);
|
||||
return CustomAppBar(title: Text(libL10n.edit), actions: actions);
|
||||
}
|
||||
|
||||
String _standardizeLineSeparators(String value) {
|
||||
@@ -126,11 +114,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
}
|
||||
|
||||
Widget _buildFAB() {
|
||||
return FloatingActionButton(
|
||||
tooltip: l10n.save,
|
||||
onPressed: _onTapSave,
|
||||
child: const Icon(Icons.save),
|
||||
);
|
||||
return FloatingActionButton(tooltip: l10n.save, onPressed: _onTapSave, child: const Icon(Icons.save));
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
@@ -170,11 +154,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
final size = (await file.stat()).size;
|
||||
if (size > Miscs.privateKeyMaxSize) {
|
||||
context.showSnackBar(
|
||||
l10n.fileTooLarge(
|
||||
path,
|
||||
size.bytes2Str,
|
||||
Miscs.privateKeyMaxSize.bytes2Str,
|
||||
),
|
||||
l10n.fileTooLarge(path, size.bytes2Str, Miscs.privateKeyMaxSize.bytes2Str),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -196,10 +176,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
onSubmitted: (_) => _onTapSave(),
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
|
||||
ValBuilder(
|
||||
listenable: _loading,
|
||||
builder: (val) => val ?? UIs.placeholder,
|
||||
),
|
||||
ValBuilder(listenable: _loading, builder: (val) => val ?? UIs.placeholder),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,7 @@ class PrivateKeysListPage extends StatefulWidget {
|
||||
@override
|
||||
State<PrivateKeysListPage> createState() => _PrivateKeyListState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: PrivateKeysListPage.new,
|
||||
path: '/private_key',
|
||||
);
|
||||
static const route = AppRouteNoArg(page: PrivateKeysListPage.new, path: '/private_key');
|
||||
}
|
||||
|
||||
class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMixin {
|
||||
@@ -34,26 +31,21 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMi
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return PrivateKeyProvider.pkis.listenVal(
|
||||
(pkis) {
|
||||
if (pkis.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty));
|
||||
}
|
||||
return PrivateKeyProvider.pkis.listenVal((pkis) {
|
||||
if (pkis.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty));
|
||||
}
|
||||
|
||||
final children = pkis.map(_buildKeyItem).toList();
|
||||
return AutoMultiList(children: children);
|
||||
},
|
||||
);
|
||||
final children = pkis.map(_buildKeyItem).toList();
|
||||
return AutoMultiList(children: children);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildKeyItem(PrivateKeyInfo item) {
|
||||
return ListTile(
|
||||
title: Text(item.id),
|
||||
subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),
|
||||
onTap: () => PrivateKeyEditPage.route.go(
|
||||
context,
|
||||
args: PrivateKeyEditPageArgs(pki: item),
|
||||
),
|
||||
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: item)),
|
||||
trailing: const Icon(Icons.edit),
|
||||
).cardx;
|
||||
}
|
||||
@@ -72,20 +64,16 @@ extension on _PrivateKeyListState {
|
||||
if (home == null) return;
|
||||
final idRsaFile = File(home.joinPath('.ssh/id_rsa'));
|
||||
if (!idRsaFile.existsSync()) return;
|
||||
final sysPk = PrivateKeyInfo(
|
||||
id: 'system',
|
||||
key: await idRsaFile.readAsString(),
|
||||
);
|
||||
final sysPk = PrivateKeyInfo(id: 'system', key: await idRsaFile.readAsString());
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(l10n.addSystemPrivateKeyTip),
|
||||
actions: Btn.ok(onTap: () {
|
||||
context.pop();
|
||||
PrivateKeyEditPage.route.go(
|
||||
context,
|
||||
args: PrivateKeyEditPageArgs(pki: sysPk),
|
||||
);
|
||||
}).toList,
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: sysPk));
|
||||
},
|
||||
).toList,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,13 @@ import 'package:server_box/data/res/store.dart';
|
||||
|
||||
class ProcessPage extends StatefulWidget {
|
||||
final SpiRequiredArgs args;
|
||||
|
||||
|
||||
const ProcessPage({super.key, required this.args});
|
||||
|
||||
@override
|
||||
State<ProcessPage> createState() => _ProcessPageState();
|
||||
|
||||
static const route = AppRouteArg(
|
||||
page: ProcessPage.new,
|
||||
path: '/process',
|
||||
);
|
||||
static const route = AppRouteArg(page: ProcessPage.new, path: '/process');
|
||||
}
|
||||
|
||||
class _ProcessPageState extends State<ProcessPage> {
|
||||
@@ -49,8 +46,7 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_client = widget.args.spi.server?.value.client;
|
||||
final duration =
|
||||
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
|
||||
final duration = Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
|
||||
_timer = Timer.periodic(duration, (_) => _refresh());
|
||||
}
|
||||
|
||||
@@ -62,8 +58,10 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
|
||||
Future<void> _refresh() async {
|
||||
if (mounted) {
|
||||
final result =
|
||||
await _client?.run(ShellFunc.process.exec(widget.args.spi.id)).string;
|
||||
final systemType = widget.args.spi.server?.value.status.system;
|
||||
final result = await _client
|
||||
?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType))
|
||||
.string;
|
||||
if (result == null || result.isEmpty) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
@@ -72,8 +70,7 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
|
||||
// If there are any [Proc]'s data is not complete,
|
||||
// the option to sort by cpu/mem will not be available.
|
||||
final isAnyProcDataNotComplete =
|
||||
_result.procs.any((e) => e.cpu == null || e.mem == null);
|
||||
final isAnyProcDataNotComplete = _result.procs.any((e) => e.cpu == null || e.mem == null);
|
||||
if (isAnyProcDataNotComplete) {
|
||||
_sortModes.removeWhere((e) => e == ProcSortMode.cpu);
|
||||
_sortModes.removeWhere((e) => e == ProcSortMode.mem);
|
||||
@@ -97,25 +94,20 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
},
|
||||
icon: const Icon(Icons.sort),
|
||||
initialValue: _procSortMode,
|
||||
itemBuilder: (_) => _sortModes
|
||||
.map((e) => PopupMenuItem(value: e, child: Text(e.name)))
|
||||
.toList(),
|
||||
itemBuilder: (_) => _sortModes.map((e) => PopupMenuItem(value: e, child: Text(e.name))).toList(),
|
||||
),
|
||||
];
|
||||
if (_result.error != null) {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(Icons.error),
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: SingleChildScrollView(child: Text(_result.error!)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Pfs.copy(_result.error!),
|
||||
child: Text(libL10n.copy),
|
||||
),
|
||||
],
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: const Icon(Icons.error),
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: SingleChildScrollView(child: Text(_result.error!)),
|
||||
actions: [TextButton(onPressed: () => Pfs.copy(_result.error!), child: Text(libL10n.copy))],
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
Widget child;
|
||||
if (_result.procs.isEmpty) {
|
||||
@@ -144,32 +136,26 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
return CardX(
|
||||
key: ValueKey(proc.pid),
|
||||
child: ListTile(
|
||||
leading: SizedBox(
|
||||
width: _media.size.width / 6,
|
||||
child: leading,
|
||||
),
|
||||
leading: SizedBox(width: _media.size.width / 6, child: leading),
|
||||
title: Text(proc.binary),
|
||||
subtitle: Text(
|
||||
proc.command,
|
||||
style: UIs.textGrey,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
subtitle: Text(proc.command, style: UIs.textGrey, maxLines: 3, overflow: TextOverflow.fade),
|
||||
trailing: _buildItemTrail(proc),
|
||||
onTap: () => _lastFocusId = proc.pid,
|
||||
onLongPress: () {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue(
|
||||
'${l10n.stop} ${l10n.process}(${proc.pid})',
|
||||
)),
|
||||
actions: Btn.ok(onTap: () async {
|
||||
context.pop();
|
||||
await context.showLoadingDialog(fn: () async {
|
||||
await _client?.run('kill ${proc.pid}');
|
||||
await _refresh();
|
||||
});
|
||||
}).toList,
|
||||
child: Text(libL10n.askContinue('${l10n.stop} ${l10n.process}(${proc.pid})')),
|
||||
actions: Btn.ok(
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
await context.showLoadingDialog(
|
||||
fn: () async {
|
||||
await _client?.run('kill ${proc.pid}');
|
||||
await _refresh();
|
||||
},
|
||||
);
|
||||
},
|
||||
).toList,
|
||||
);
|
||||
},
|
||||
selected: _lastFocusId == proc.pid,
|
||||
@@ -185,17 +171,9 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (proc.cpu != null)
|
||||
TwoLineText(
|
||||
up: proc.cpu!.toStringAsFixed(1),
|
||||
down: 'cpu',
|
||||
),
|
||||
if (proc.cpu != null) TwoLineText(up: proc.cpu!.toStringAsFixed(1), down: 'cpu'),
|
||||
UIs.width13,
|
||||
if (proc.mem != null)
|
||||
TwoLineText(
|
||||
up: proc.mem!.toStringAsFixed(1),
|
||||
down: 'mem',
|
||||
),
|
||||
if (proc.mem != null) TwoLineText(up: proc.mem!.toStringAsFixed(1), down: 'mem'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,18 +18,12 @@ final class PvePageArgs {
|
||||
final class PvePage extends StatefulWidget {
|
||||
final PvePageArgs args;
|
||||
|
||||
const PvePage({
|
||||
super.key,
|
||||
required this.args,
|
||||
});
|
||||
const PvePage({super.key, required this.args});
|
||||
|
||||
@override
|
||||
State<PvePage> createState() => _PvePageState();
|
||||
|
||||
static const route = AppRouteArg<void, PvePageArgs>(
|
||||
page: PvePage.new,
|
||||
path: '/pve',
|
||||
);
|
||||
static const route = AppRouteArg<void, PvePageArgs>(page: PvePage.new, path: '/pve');
|
||||
}
|
||||
|
||||
const _kHorziPadding = 11.0;
|
||||
@@ -87,9 +81,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
_timer?.cancel();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: Center(
|
||||
child: Text(val),
|
||||
),
|
||||
child: Center(child: Text(val)),
|
||||
);
|
||||
}
|
||||
return ValBuilder(
|
||||
@@ -110,10 +102,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
|
||||
PveResType? lastType;
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: _kHorziPadding,
|
||||
vertical: 7,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: _kHorziPadding, vertical: 7),
|
||||
itemCount: data.length * 2,
|
||||
itemBuilder: (context, index) {
|
||||
final item = data[index ~/ 2];
|
||||
@@ -135,10 +124,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
type.toStr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
@@ -183,18 +169,11 @@ final class _PvePageState extends State<PvePage> {
|
||||
UIs.width7,
|
||||
const Text('CPU', style: UIs.text12Grey),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${(item.cpu * 100).toStringAsFixed(1)} %',
|
||||
style: UIs.text12Grey,
|
||||
),
|
||||
Text('${(item.cpu * 100).toStringAsFixed(1)} %', style: UIs.text12Grey),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
LinearProgressIndicator(
|
||||
value: item.cpu / item.maxcpu,
|
||||
minHeight: 7,
|
||||
valueColor: valueAnim,
|
||||
),
|
||||
LinearProgressIndicator(value: item.cpu / item.maxcpu, minHeight: 7, valueColor: valueAnim),
|
||||
UIs.height7,
|
||||
Row(
|
||||
children: [
|
||||
@@ -202,18 +181,11 @@ final class _PvePageState extends State<PvePage> {
|
||||
UIs.width7,
|
||||
const Text('RAM', style: UIs.text12Grey),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}',
|
||||
style: UIs.text12Grey,
|
||||
),
|
||||
Text('${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}', style: UIs.text12Grey),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
LinearProgressIndicator(
|
||||
value: item.mem / item.maxmem,
|
||||
minHeight: 7,
|
||||
valueColor: valueAnim,
|
||||
),
|
||||
LinearProgressIndicator(value: item.mem / item.maxmem, minHeight: 7, valueColor: valueAnim),
|
||||
],
|
||||
),
|
||||
).cardx;
|
||||
@@ -232,14 +204,8 @@ final class _PvePageState extends State<PvePage> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const SizedBox(width: 15),
|
||||
Text(
|
||||
_wrapNodeName(item),
|
||||
style: UIs.text13Bold,
|
||||
),
|
||||
Text(
|
||||
' / ${item.summary}',
|
||||
style: UIs.text12Grey,
|
||||
),
|
||||
Text(_wrapNodeName(item), style: UIs.text13Bold),
|
||||
Text(' / ${item.summary}', style: UIs.text12Grey),
|
||||
const Spacer(),
|
||||
_buildCtrlBtns(item),
|
||||
UIs.width13,
|
||||
@@ -266,34 +232,23 @@ final class _PvePageState extends State<PvePage> {
|
||||
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
|
||||
style: UIs.text11Grey,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'↓:\n${item.netin.bytes2Str}',
|
||||
style: UIs.text11Grey,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text('↓:\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
'↑:\n${item.netout.bytes2Str}',
|
||||
style: UIs.text11Grey,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
Text('↑:\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 21)
|
||||
const SizedBox(height: 21),
|
||||
];
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
).cardx;
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;
|
||||
}
|
||||
|
||||
Widget _buildLxc(PveLxc item) {
|
||||
@@ -309,14 +264,8 @@ final class _PvePageState extends State<PvePage> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const SizedBox(width: 15),
|
||||
Text(
|
||||
_wrapNodeName(item),
|
||||
style: UIs.text13Bold,
|
||||
),
|
||||
Text(
|
||||
' / ${item.summary}',
|
||||
style: UIs.text12Grey,
|
||||
),
|
||||
Text(_wrapNodeName(item), style: UIs.text13Bold),
|
||||
Text(' / ${item.summary}', style: UIs.text12Grey),
|
||||
const Spacer(),
|
||||
_buildCtrlBtns(item),
|
||||
UIs.width13,
|
||||
@@ -343,34 +292,23 @@ final class _PvePageState extends State<PvePage> {
|
||||
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
|
||||
style: UIs.text11Grey,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'↓:\n${item.netin.bytes2Str}',
|
||||
style: UIs.text11Grey,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text('↓:\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
'↑:\n${item.netout.bytes2Str}',
|
||||
style: UIs.text11Grey,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
Text('↑:\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 21)
|
||||
const SizedBox(height: 21),
|
||||
];
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
).cardx;
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;
|
||||
}
|
||||
|
||||
Widget _buildStorage(PveStorage item) {
|
||||
@@ -396,33 +334,34 @@ final class _PvePageState extends State<PvePage> {
|
||||
}
|
||||
|
||||
Widget _buildSdn(PveSdn item) {
|
||||
return ListTile(
|
||||
title: Text(_wrapNodeName(item)),
|
||||
trailing: Text(item.summary),
|
||||
).cardx;
|
||||
return ListTile(title: Text(_wrapNodeName(item)), trailing: Text(item.summary)).cardx;
|
||||
}
|
||||
|
||||
Widget _buildCtrlBtns(PveCtrlIface item) {
|
||||
const pad = EdgeInsets.symmetric(horizontal: 7, vertical: 5);
|
||||
if (!item.available) {
|
||||
return Btn.icon(
|
||||
icon: const Icon(Icons.play_arrow, color: Colors.grey),
|
||||
onTap: () => _onCtrl(_pve.start, l10n.start, item));
|
||||
icon: const Icon(Icons.play_arrow, color: Colors.grey),
|
||||
onTap: () => _onCtrl(_pve.start, l10n.start, item),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.stop, l10n.stop, item)),
|
||||
icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.stop, l10n.stop, item),
|
||||
),
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item)),
|
||||
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item),
|
||||
),
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item)),
|
||||
icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -437,9 +376,7 @@ extension on _PvePageState {
|
||||
);
|
||||
if (sure != true) return;
|
||||
|
||||
final (suc, err) = await context.showLoadingDialog(
|
||||
fn: () => func(item.node, item.id),
|
||||
);
|
||||
final (suc, err) = await context.showLoadingDialog(fn: () => func(item.node, item.id));
|
||||
if (suc == true) {
|
||||
context.showSnackBar(libL10n.success);
|
||||
} else {
|
||||
|
||||
@@ -61,7 +61,7 @@ extension on _ServerDetailPageState {
|
||||
titleMaxLines: 1,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UIs.height13,
|
||||
Text('Memory: ${process.memory} ${process.memory > 1024 ? 'MB' : 'KB'}'),
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/server/custom.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/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.dart';
|
||||
@@ -59,6 +60,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
final _env = <String, String>{}.vn;
|
||||
final _customCmds = <String, String>{}.vn;
|
||||
final _tags = <String>{}.vn;
|
||||
final _systemType = ValueNotifier<SystemType?>(null);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -91,6 +93,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
_env.dispose();
|
||||
_customCmds.dispose();
|
||||
_tags.dispose();
|
||||
_systemType.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -174,6 +177,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
),
|
||||
),
|
||||
_buildAuth(),
|
||||
_buildSystemType(),
|
||||
_buildJumpServer(),
|
||||
_buildMore(),
|
||||
];
|
||||
@@ -331,6 +335,26 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -614,6 +638,7 @@ extension on _ServerEditPageState {
|
||||
wolCfg: wol,
|
||||
envs: _env.value.isEmpty ? null : _env.value,
|
||||
id: widget.args?.spi.id ?? ShortId.generate(),
|
||||
customSystemType: _systemType.value,
|
||||
);
|
||||
|
||||
if (this.spi == null) {
|
||||
@@ -668,5 +693,7 @@ extension on _ServerEditPageState {
|
||||
|
||||
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
||||
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
||||
|
||||
_systemType.value = spi.customSystemType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,9 @@ class _CardStatus {
|
||||
final bool? diskIO;
|
||||
final NetViewType? net;
|
||||
|
||||
const _CardStatus({
|
||||
this.flip = false,
|
||||
this.diskIO,
|
||||
this.net,
|
||||
});
|
||||
const _CardStatus({this.flip = false, this.diskIO, this.net});
|
||||
|
||||
_CardStatus copyWith({
|
||||
bool? flip,
|
||||
bool? diskIO,
|
||||
NetViewType? net,
|
||||
}) {
|
||||
return _CardStatus(
|
||||
flip: flip ?? this.flip,
|
||||
diskIO: diskIO ?? this.diskIO,
|
||||
net: net ?? this.net,
|
||||
);
|
||||
_CardStatus copyWith({bool? flip, bool? diskIO, NetViewType? net}) {
|
||||
return _CardStatus(flip: flip ?? this.flip, diskIO: diskIO ?? this.diskIO, net: net ?? this.net);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,8 +319,7 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
],
|
||||
),
|
||||
UIs.height13,
|
||||
if (Stores.setting.moveServerFuncs.fetch())
|
||||
SizedBox(height: 27, child: ServerFuncBtns(spi: spi)),
|
||||
if (Stores.setting.moveServerFuncs.fetch()) SizedBox(height: 27, child: ServerFuncBtns(spi: spi)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -5,11 +5,7 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final void Function(String) onTagChanged;
|
||||
final String initTag;
|
||||
|
||||
const _TopBar({
|
||||
required this.initTag,
|
||||
required this.onTagChanged,
|
||||
required this.tags,
|
||||
});
|
||||
const _TopBar({required this.initTag, required this.onTagChanged, required this.tags});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -31,15 +27,9 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
BuildData.name,
|
||||
style: TextStyle(fontSize: 19),
|
||||
),
|
||||
Text(BuildData.name, style: TextStyle(fontSize: 19)),
|
||||
SizedBox(width: 5),
|
||||
Icon(
|
||||
Icons.settings,
|
||||
size: 17,
|
||||
),
|
||||
Icon(Icons.settings, size: 17),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -49,7 +49,11 @@ extension _Operation on _ServerPageState {
|
||||
await context.showRoundDialog(title: libL10n.attention, child: Text(l10n.suspendTip));
|
||||
Stores.setting.showSuspendTip.put(false);
|
||||
}
|
||||
srv.client?.execWithPwd(ShellFunc.suspend.exec(srv.spi.id), context: context, id: srv.id);
|
||||
srv.client?.execWithPwd(
|
||||
ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
);
|
||||
},
|
||||
typ: l10n.suspend,
|
||||
name: srv.spi.name,
|
||||
@@ -58,7 +62,11 @@ extension _Operation on _ServerPageState {
|
||||
|
||||
void _onTapShutdown(Server srv) {
|
||||
_askFor(
|
||||
func: () => srv.client?.execWithPwd(ShellFunc.shutdown.exec(srv.spi.id), context: context, id: srv.id),
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
typ: l10n.shutdown,
|
||||
name: srv.spi.name,
|
||||
);
|
||||
@@ -66,7 +74,11 @@ extension _Operation on _ServerPageState {
|
||||
|
||||
void _onTapReboot(Server srv) {
|
||||
_askFor(
|
||||
func: () => srv.client?.execWithPwd(ShellFunc.reboot.exec(srv.spi.id), context: context, id: srv.id),
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
typ: l10n.reboot,
|
||||
name: srv.spi.name,
|
||||
);
|
||||
|
||||
@@ -7,8 +7,7 @@ final class _AppAboutPage extends StatefulWidget {
|
||||
State<_AppAboutPage> createState() => _AppAboutPageState();
|
||||
}
|
||||
|
||||
final class _AppAboutPageState extends State<_AppAboutPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final class _AppAboutPageState extends State<_AppAboutPage> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@@ -16,15 +15,8 @@ final class _AppAboutPageState extends State<_AppAboutPage>
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
UIs.height13,
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47),
|
||||
child: UIs.appIcon,
|
||||
),
|
||||
const Text(
|
||||
'${BuildData.name}\nv${BuildData.build}',
|
||||
textAlign: TextAlign.center,
|
||||
style: UIs.text15,
|
||||
),
|
||||
ConstrainedBox(constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47), child: UIs.appIcon),
|
||||
const Text('${BuildData.name}\nv${BuildData.build}', textAlign: TextAlign.center, style: UIs.text15),
|
||||
UIs.height13,
|
||||
SizedBox(
|
||||
height: 77,
|
||||
@@ -52,7 +44,8 @@ final class _AppAboutPageState extends State<_AppAboutPage>
|
||||
),
|
||||
UIs.height13,
|
||||
SimpleMarkdown(
|
||||
data: '''
|
||||
data:
|
||||
'''
|
||||
#### Contributors
|
||||
${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')}
|
||||
|
||||
|
||||
@@ -10,10 +10,7 @@ class ServerDetailOrderPage extends StatefulWidget {
|
||||
@override
|
||||
State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: ServerDetailOrderPage.new,
|
||||
path: '/settings/order/server_detail',
|
||||
);
|
||||
static const route = AppRouteNoArg(page: ServerDetailOrderPage.new, path: '/settings/order/server_detail');
|
||||
}
|
||||
|
||||
class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
|
||||
@@ -31,8 +28,7 @@ class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
|
||||
return ValBuilder(
|
||||
listenable: prop.listenable(),
|
||||
builder: (keys) {
|
||||
final disabled =
|
||||
ServerDetailCards.names.where((e) => !keys.contains(e)).toList();
|
||||
final disabled = ServerDetailCards.names.where((e) => !keys.contains(e)).toList();
|
||||
final allKeys = [...keys, ...disabled];
|
||||
return ReorderableListView.builder(
|
||||
padding: const EdgeInsets.all(7),
|
||||
|
||||
@@ -10,10 +10,7 @@ class ServerFuncBtnsOrderPage extends StatefulWidget {
|
||||
@override
|
||||
State<ServerFuncBtnsOrderPage> createState() => _ServerDetailOrderPageState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: ServerFuncBtnsOrderPage.new,
|
||||
path: '/setting/seq/srv_func',
|
||||
);
|
||||
static const route = AppRouteNoArg(page: ServerFuncBtnsOrderPage.new, path: '/setting/seq/srv_func');
|
||||
}
|
||||
|
||||
class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
||||
@@ -67,12 +64,7 @@ class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckBox(
|
||||
List<int> keys,
|
||||
int key,
|
||||
int idx,
|
||||
bool value,
|
||||
) {
|
||||
Widget _buildCheckBox(List<int> keys, int key, int idx, bool value) {
|
||||
return Checkbox(
|
||||
value: value,
|
||||
onChanged: (val) {
|
||||
|
||||
@@ -12,10 +12,7 @@ class ServerOrderPage extends StatefulWidget {
|
||||
@override
|
||||
State<ServerOrderPage> createState() => _ServerOrderPageState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: ServerOrderPage.new,
|
||||
path: '/settings/order/server',
|
||||
);
|
||||
static const route = AppRouteNoArg(page: ServerOrderPage.new, path: '/settings/order/server');
|
||||
}
|
||||
|
||||
class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||
@@ -36,10 +33,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||
final double scale = lerpDouble(1, 1.02, animValue)!;
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Card(
|
||||
elevation: elevation,
|
||||
child: child,
|
||||
),
|
||||
child: Card(elevation: elevation, child: child),
|
||||
);
|
||||
},
|
||||
child: _buildCardTile(index),
|
||||
@@ -56,11 +50,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||
footer: const SizedBox(height: 77),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
orders.value.move(
|
||||
oldIndex,
|
||||
newIndex,
|
||||
property: Stores.setting.serverOrder,
|
||||
);
|
||||
orders.value.move(oldIndex, newIndex, property: Stores.setting.serverOrder);
|
||||
});
|
||||
},
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -78,9 +68,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||
index: index,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: CardX(
|
||||
child: _buildCardTile(index),
|
||||
),
|
||||
child: CardX(child: _buildCardTile(index)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -93,20 +81,14 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
spi.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
title: Text(spi.name, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text(spi.oldId, style: UIs.textGrey),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
child: Text(spi.name[0]),
|
||||
),
|
||||
trailing: ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: const Icon(Icons.drag_handle),
|
||||
),
|
||||
trailing: ReorderableDragStartListener(index: index, child: const Icon(Icons.drag_handle)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,7 @@ class SnippetEditPage extends StatefulWidget {
|
||||
@override
|
||||
State<SnippetEditPage> createState() => _SnippetEditPageState();
|
||||
|
||||
static const route = AppRoute(
|
||||
page: SnippetEditPage.new,
|
||||
path: '/snippets/edit',
|
||||
);
|
||||
static const route = AppRoute(page: SnippetEditPage.new, path: '/snippets/edit');
|
||||
}
|
||||
|
||||
class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin {
|
||||
@@ -47,10 +44,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(libL10n.edit),
|
||||
actions: _buildAppBarActions(),
|
||||
),
|
||||
appBar: CustomAppBar(title: Text(libL10n.edit), actions: _buildAppBarActions()),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: _buildFAB(),
|
||||
);
|
||||
@@ -64,9 +58,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
onPressed: () {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue(
|
||||
'${libL10n.delete} ${l10n.snippet}(${snippet.name})',
|
||||
)),
|
||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.snippet}(${snippet.name})')),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
SnippetProvider.del(snippet);
|
||||
@@ -79,7 +71,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
},
|
||||
tooltip: libL10n.delete,
|
||||
icon: const Icon(Icons.delete),
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -168,12 +160,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
subtitle: subtitle == null
|
||||
? null
|
||||
: Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
style: UIs.textGrey,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
: Text(subtitle, maxLines: 1, style: UIs.textGrey, overflow: TextOverflow.ellipsis),
|
||||
onTap: () async {
|
||||
vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e));
|
||||
final serverIds = await context.showPickDialog(
|
||||
@@ -198,7 +185,8 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: SimpleMarkdown(
|
||||
data: '''
|
||||
data:
|
||||
'''
|
||||
📌 ${l10n.supportFmtArgs}\n
|
||||
${SnippetX.fmtArgs.keys.map((e) => '`$e`').join(', ')}\n
|
||||
|
||||
@@ -207,11 +195,7 @@ ${libL10n.example}:
|
||||
- `\${ctrl+c}` (Control + C)
|
||||
- `\${ctrl+b}d` (Tmux Detach)
|
||||
''',
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
codeblockDecoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
styleSheet: MarkdownStyleSheet(codeblockDecoration: const BoxDecoration(color: Colors.transparent)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -11,10 +11,7 @@ class SnippetListPage extends StatefulWidget {
|
||||
@override
|
||||
State<SnippetListPage> createState() => _SnippetListPageState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: SnippetListPage.new,
|
||||
path: '/snippets',
|
||||
);
|
||||
static const route = AppRouteNoArg(page: SnippetListPage.new, path: '/snippets');
|
||||
}
|
||||
|
||||
class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAliveClientMixin {
|
||||
@@ -38,24 +35,22 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
||||
|
||||
Widget _buildBody() {
|
||||
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||
return SnippetProvider.snippets.listenVal(
|
||||
(snippets) {
|
||||
return _tag.listenVal((tag) {
|
||||
final child = _buildScaffold(snippets, tag);
|
||||
// if (isMobile) {
|
||||
return child;
|
||||
// }
|
||||
return SnippetProvider.snippets.listenVal((snippets) {
|
||||
return _tag.listenVal((tag) {
|
||||
final child = _buildScaffold(snippets, tag);
|
||||
// if (isMobile) {
|
||||
return child;
|
||||
// }
|
||||
|
||||
// return SplitView(
|
||||
// controller: _splitViewCtrl,
|
||||
// leftWeight: 1,
|
||||
// rightWeight: 1.3,
|
||||
// initialRight: Center(child: Text(libL10n.empty)),
|
||||
// leftBuilder: (_, __) => child,
|
||||
// );
|
||||
});
|
||||
},
|
||||
);
|
||||
// return SplitView(
|
||||
// controller: _splitViewCtrl,
|
||||
// leftWeight: 1,
|
||||
// rightWeight: 1.3,
|
||||
// initialRight: Center(child: Text(libL10n.empty)),
|
||||
// leftBuilder: (_, __) => child,
|
||||
// );
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildScaffold(List<Snippet> snippets, String tag) {
|
||||
@@ -104,11 +99,7 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
||||
Widget _buildSnippetItem(Snippet snippet) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 23, right: 17),
|
||||
title: Text(
|
||||
snippet.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
title: Text(snippet.name, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
subtitle: Text(
|
||||
snippet.note ?? snippet.script,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -119,10 +110,7 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
||||
onTap: () {
|
||||
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||
// if (isMobile) {
|
||||
SnippetEditPage.route.go(
|
||||
context,
|
||||
args: SnippetEditPageArgs(snippet: snippet),
|
||||
);
|
||||
SnippetEditPage.route.go(context, args: SnippetEditPageArgs(snippet: snippet));
|
||||
// } else {
|
||||
// _splitViewCtrl.replace(SnippetEditPage(
|
||||
// args: SnippetEditPageArgs(snippet: snippet),
|
||||
|
||||
@@ -8,10 +8,7 @@ class SnippetResultPage extends StatelessWidget {
|
||||
|
||||
const SnippetResultPage({super.key, required this.args});
|
||||
|
||||
static const route = AppRouteArg(
|
||||
page: SnippetResultPage.new,
|
||||
path: '/snippets/result',
|
||||
);
|
||||
static const route = AppRouteArg(page: SnippetResultPage.new, path: '/snippets/result');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -37,10 +34,7 @@ class SnippetResultPage extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Text(
|
||||
item.result,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
child: Text(item.result, textAlign: TextAlign.start),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ extension _Keyboard on SSHPageState {
|
||||
_handleEscKeyOrBackButton();
|
||||
return true; // Mark as handled so it doesn't propagate
|
||||
}
|
||||
if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||
|
||||
if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||
|
||||
event.logicalKey == LogicalKeyboardKey.shiftRight) {
|
||||
// Handle shift key press
|
||||
_terminal.keyInput(TerminalKey.shift);
|
||||
|
||||
@@ -88,10 +88,7 @@ extension _VirtKey on SSHPageState {
|
||||
while (initPath == null) {
|
||||
// Check if we've exceeded timeout
|
||||
if (DateTime.now().difference(startTime) > timeout) {
|
||||
contextSafe?.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: Text(libL10n.empty),
|
||||
);
|
||||
contextSafe?.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -119,10 +116,7 @@ extension _VirtKey on SSHPageState {
|
||||
}
|
||||
|
||||
if (!initPath.startsWith('/')) {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: Text('${l10n.remotePath}: $initPath'),
|
||||
);
|
||||
context.showRoundDialog(title: libL10n.error, child: Text('${l10n.remotePath}: $initPath'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,10 +132,7 @@ extension _VirtKey on SSHPageState {
|
||||
if (text != null) {
|
||||
_terminal.textInput(text);
|
||||
} else {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: Text(libL10n.empty),
|
||||
);
|
||||
context.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,19 +16,14 @@ class SSHTabPage extends StatefulWidget {
|
||||
@override
|
||||
State<SSHTabPage> createState() => _SSHTabPageState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: SSHTabPage.new,
|
||||
path: '/ssh',
|
||||
);
|
||||
static const route = AppRouteNoArg(page: SSHTabPage.new, path: '/ssh');
|
||||
}
|
||||
|
||||
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
|
||||
|
||||
class _SSHTabPageState extends State<SSHTabPage>
|
||||
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||
late final _TabMap _tabMap = {
|
||||
libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null),
|
||||
};
|
||||
late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null)};
|
||||
final _pageCtrl = PageController();
|
||||
final _fabVN = 0.vn;
|
||||
final _tabRN = RNode();
|
||||
@@ -48,12 +43,7 @@ class _SSHTabPageState extends State<SSHTabPage>
|
||||
appBar: PreferredSizeListenBuilder(
|
||||
listenable: _tabRN,
|
||||
builder: () {
|
||||
return _TabBar(
|
||||
idxVN: _fabVN,
|
||||
map: _tabMap,
|
||||
onTap: _onTapTab,
|
||||
onClose: _onTapClose,
|
||||
);
|
||||
return _TabBar(idxVN: _fabVN, map: _tabMap, onTap: _onTapTab, onClose: _onTapClose);
|
||||
},
|
||||
),
|
||||
body: _buildBody(),
|
||||
@@ -159,12 +149,7 @@ extension on _SSHTabPageState {
|
||||
}
|
||||
|
||||
final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
const _TabBar({
|
||||
required this.idxVN,
|
||||
required this.map,
|
||||
required this.onTap,
|
||||
required this.onClose,
|
||||
});
|
||||
const _TabBar({required this.idxVN, required this.map, required this.onTap, required this.onClose});
|
||||
|
||||
final ValueListenable<int> idxVN;
|
||||
final _TabMap map;
|
||||
@@ -188,10 +173,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
itemBuilder: (_, idx) => _buildItem(idx),
|
||||
separatorBuilder: (_, _) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 17),
|
||||
child: Container(
|
||||
color: const Color.fromARGB(61, 158, 158, 158),
|
||||
width: 3,
|
||||
),
|
||||
child: Container(color: const Color.fromARGB(61, 158, 158, 158), width: 3),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -242,10 +224,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
width: selected ? kWideWidth : kNarrowWidth,
|
||||
duration: Durations.medium3,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
child: OverflowBox(
|
||||
maxWidth: selected ? kWideWidth : null,
|
||||
child: btn,
|
||||
),
|
||||
child: OverflowBox(maxWidth: selected ? kWideWidth : null, child: btn),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -280,9 +259,7 @@ class _AddPage extends StatelessWidget {
|
||||
|
||||
return ServerProvider.serverOrder.listenVal((order) {
|
||||
if (order.isEmpty) {
|
||||
return Center(
|
||||
child: Text(libL10n.empty, textAlign: TextAlign.center),
|
||||
);
|
||||
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
|
||||
}
|
||||
|
||||
// Custom grid
|
||||
@@ -316,7 +293,7 @@ class _AddPage extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right)
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,10 +16,7 @@ import 'package:server_box/view/page/storage/sftp_mission.dart';
|
||||
final class LocalFilePageArgs {
|
||||
final bool? isPickFile;
|
||||
final String? initDir;
|
||||
const LocalFilePageArgs({
|
||||
this.isPickFile,
|
||||
this.initDir,
|
||||
});
|
||||
const LocalFilePageArgs({this.isPickFile, this.initDir});
|
||||
}
|
||||
|
||||
class LocalFilePage extends StatefulWidget {
|
||||
@@ -27,10 +24,7 @@ class LocalFilePage extends StatefulWidget {
|
||||
|
||||
const LocalFilePage({super.key, this.args});
|
||||
|
||||
static const route = AppRoute<String, LocalFilePageArgs>(
|
||||
page: LocalFilePage.new,
|
||||
path: '/files/local',
|
||||
);
|
||||
static const route = AppRoute<String, LocalFilePageArgs>(page: LocalFilePage.new, path: '/files/local');
|
||||
|
||||
@override
|
||||
State<LocalFilePage> createState() => _LocalFilePageState();
|
||||
@@ -98,9 +92,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
|
||||
Future<List<(FileSystemEntity, FileStat)>> getEntities() async {
|
||||
final files = await Directory(_path.path).list().toList();
|
||||
final sorted = _sortType.value.sort(files);
|
||||
final stats = await Future.wait(
|
||||
sorted.map((e) async => (e, await e.stat())),
|
||||
);
|
||||
final stats = await Future.wait(sorted.map((e) async => (e, await e.stat())));
|
||||
return stats;
|
||||
}
|
||||
|
||||
@@ -133,12 +125,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
|
||||
final stat = item.$2;
|
||||
final isDir = stat.type == FileSystemEntityType.directory;
|
||||
|
||||
return _buildItem(
|
||||
file: file,
|
||||
fileName: fileName,
|
||||
stat: stat,
|
||||
isDir: isDir,
|
||||
);
|
||||
return _buildItem(file: file, fileName: fileName, stat: stat, isDir: isDir);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -156,10 +143,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
|
||||
leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file),
|
||||
title: Text(fileName),
|
||||
subtitle: isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
|
||||
trailing: Text(
|
||||
stat.modified.ymdhms(),
|
||||
style: UIs.textGrey,
|
||||
),
|
||||
trailing: Text(stat.modified.ymdhms(), style: UIs.textGrey),
|
||||
onLongPress: () {
|
||||
if (isDir) {
|
||||
_showDirActionDialog(file);
|
||||
@@ -187,17 +171,15 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
|
||||
}
|
||||
|
||||
Widget _buildSortBtn() {
|
||||
return _sortType.listenVal(
|
||||
(value) {
|
||||
return PopupMenuButton<_SortType>(
|
||||
icon: const Icon(Icons.sort),
|
||||
itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(),
|
||||
onSelected: (value) {
|
||||
_sortType.value = value;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
return _sortType.listenVal((value) {
|
||||
return PopupMenuButton<_SortType>(
|
||||
icon: const Icon(Icons.sort),
|
||||
itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(),
|
||||
onSelected: (value) {
|
||||
_sortType.value = value;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -238,10 +220,12 @@ extension _Actions on _LocalFilePageState {
|
||||
title: libL10n.file,
|
||||
child: Text(fileName),
|
||||
actions: [
|
||||
Btn.ok(onTap: () {
|
||||
context.pop();
|
||||
context.pop(file.path);
|
||||
}),
|
||||
Btn.ok(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
context.pop(file.path);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
return;
|
||||
@@ -382,21 +366,13 @@ extension _OnTapFile on _LocalFilePageState {
|
||||
);
|
||||
if (spi == null) return;
|
||||
|
||||
final args = SftpPageArgs(
|
||||
spi: spi,
|
||||
isSelect: true,
|
||||
);
|
||||
final args = SftpPageArgs(spi: spi, isSelect: true);
|
||||
final remotePath = await SftpPage.route.go(context, args);
|
||||
if (remotePath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SftpProvider.add(SftpReq(
|
||||
spi,
|
||||
'$remotePath/$fileName',
|
||||
file.absolute.path,
|
||||
SftpReqType.upload,
|
||||
));
|
||||
SftpProvider.add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload));
|
||||
context.showSnackBar(l10n.added2List);
|
||||
}
|
||||
}
|
||||
@@ -404,8 +380,7 @@ extension _OnTapFile on _LocalFilePageState {
|
||||
enum _SortType {
|
||||
name,
|
||||
size,
|
||||
time,
|
||||
;
|
||||
time;
|
||||
|
||||
List<FileSystemEntity> sort(List<FileSystemEntity> files) {
|
||||
switch (this) {
|
||||
@@ -423,27 +398,21 @@ enum _SortType {
|
||||
}
|
||||
|
||||
String get i18n => switch (this) {
|
||||
name => libL10n.name,
|
||||
size => l10n.size,
|
||||
time => l10n.time,
|
||||
};
|
||||
name => libL10n.name,
|
||||
size => l10n.size,
|
||||
time => l10n.time,
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
name => Icons.sort_by_alpha,
|
||||
size => Icons.sort,
|
||||
time => Icons.access_time,
|
||||
};
|
||||
name => Icons.sort_by_alpha,
|
||||
size => Icons.sort,
|
||||
time => Icons.access_time,
|
||||
};
|
||||
|
||||
PopupMenuItem<_SortType> get menuItem {
|
||||
return PopupMenuItem(
|
||||
value: this,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Icon(icon),
|
||||
Text(i18n),
|
||||
],
|
||||
),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [Icon(icon), Text(i18n)]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,14 @@ class SftpMissionPage extends StatefulWidget {
|
||||
@override
|
||||
State<SftpMissionPage> createState() => _SftpMissionPageState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: SftpMissionPage.new,
|
||||
path: '/sftp/mission',
|
||||
);
|
||||
static const route = AppRouteNoArg(page: SftpMissionPage.new, path: '/sftp/mission');
|
||||
}
|
||||
|
||||
class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(l10n.mission, style: UIs.text18),
|
||||
),
|
||||
appBar: CustomAppBar(title: Text(l10n.mission, style: UIs.text18)),
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
@@ -50,10 +45,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
status: status,
|
||||
subtitle: libL10n.error,
|
||||
trailing: IconButton(
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: Text(err.toString()),
|
||||
),
|
||||
onPressed: () => context.showRoundDialog(title: libL10n.error, child: Text(err.toString())),
|
||||
icon: const Icon(Icons.error),
|
||||
),
|
||||
);
|
||||
@@ -109,9 +101,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
|
||||
Widget _buildFinished(SftpReqStatus status) {
|
||||
final time = status.spentTime.toString();
|
||||
final str = l10n.spentTime(
|
||||
time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7)),
|
||||
);
|
||||
final str = l10n.spentTime(time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7)));
|
||||
|
||||
final btns = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -120,41 +110,26 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
onPressed: () {
|
||||
final idx = status.req.localPath.lastIndexOf(Pfs.seperator);
|
||||
final dir = status.req.localPath.substring(0, idx);
|
||||
LocalFilePage.route.go(
|
||||
context,
|
||||
args: LocalFilePageArgs(initDir: dir),
|
||||
);
|
||||
LocalFilePage.route.go(context, args: LocalFilePageArgs(initDir: dir));
|
||||
},
|
||||
icon: const Icon(Icons.file_open),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Pfs.sharePaths(paths: [status.req.localPath]),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return _wrapInCard(
|
||||
status: status,
|
||||
subtitle: str,
|
||||
trailing: btns,
|
||||
);
|
||||
return _wrapInCard(status: status, subtitle: str, trailing: btns);
|
||||
}
|
||||
|
||||
Widget _wrapInCard({
|
||||
required SftpReqStatus status,
|
||||
String? subtitle,
|
||||
Widget? trailing,
|
||||
}) {
|
||||
Widget _wrapInCard({required SftpReqStatus status, String? subtitle, Widget? trailing}) {
|
||||
final time = DateTime.fromMicrosecondsSinceEpoch(status.id);
|
||||
return CardX(
|
||||
child: ListTile(
|
||||
leading: Text(time.hourMinute),
|
||||
title: Text(
|
||||
status.fileName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
title: Text(status.fileName, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
subtitle: subtitle == null ? null : Text(subtitle, style: UIs.textGrey),
|
||||
trailing: trailing,
|
||||
),
|
||||
@@ -165,9 +140,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
return IconButton(
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue(
|
||||
'${libL10n.delete} ${l10n.mission}($name)',
|
||||
)),
|
||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.mission}($name)')),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
SftpProvider.cancel(id);
|
||||
|
||||
@@ -9,15 +9,9 @@ import 'package:server_box/view/page/ssh/page/page.dart';
|
||||
final class SystemdPage extends StatefulWidget {
|
||||
final SpiRequiredArgs args;
|
||||
|
||||
const SystemdPage({
|
||||
super.key,
|
||||
required this.args,
|
||||
});
|
||||
const SystemdPage({super.key, required this.args});
|
||||
|
||||
static const route = AppRouteArg<void, SpiRequiredArgs>(
|
||||
page: SystemdPage.new,
|
||||
path: '/systemd',
|
||||
);
|
||||
static const route = AppRouteArg<void, SpiRequiredArgs>(page: SystemdPage.new, path: '/systemd');
|
||||
|
||||
@override
|
||||
State<SystemdPage> createState() => _SystemdPageState();
|
||||
@@ -37,9 +31,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: const Text('Systemd'),
|
||||
actions: isDesktop
|
||||
? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)]
|
||||
: null,
|
||||
actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)] : null,
|
||||
),
|
||||
body: RefreshIndicator(onRefresh: _pro.getUnits, child: _buildBody()),
|
||||
);
|
||||
@@ -54,9 +46,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
duration: Durations.medium1,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: isBusy ? SizedLoading.medium.size : 0,
|
||||
child: isBusy
|
||||
? SizedLoading.medium
|
||||
: const SizedBox.shrink(),
|
||||
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -66,35 +56,24 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
}
|
||||
|
||||
Widget _buildUnitList(VNode<List<SystemdUnit>> units) {
|
||||
return units.listenVal(
|
||||
(units) {
|
||||
if (units.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child:
|
||||
CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13),
|
||||
);
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final unit = units[index];
|
||||
return ListTile(
|
||||
leading: _buildScopeTag(unit.scope),
|
||||
title: unit.description != null
|
||||
? TipText(unit.name, unit.description!)
|
||||
: Text(unit.name),
|
||||
subtitle: Wrap(children: [
|
||||
_buildStateTag(unit.state),
|
||||
_buildTypeTag(unit.type),
|
||||
]).paddingOnly(top: 7),
|
||||
trailing: _buildUnitFuncs(unit),
|
||||
).cardx.paddingSymmetric(horizontal: 13);
|
||||
},
|
||||
childCount: units.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return units.listenVal((units) {
|
||||
if (units.isEmpty) {
|
||||
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final unit = units[index];
|
||||
return ListTile(
|
||||
leading: _buildScopeTag(unit.scope),
|
||||
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
|
||||
subtitle: Wrap(
|
||||
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
|
||||
).paddingOnly(top: 7),
|
||||
trailing: _buildUnitFuncs(unit),
|
||||
).cardx.paddingSymmetric(horizontal: 13);
|
||||
}, childCount: units.length),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildUnitFuncs(SystemdUnit unit) {
|
||||
@@ -128,11 +107,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Icon(func.icon, size: 19),
|
||||
const SizedBox(width: 10),
|
||||
Text(func.name.capitalize),
|
||||
],
|
||||
children: [Icon(func.icon, size: 19), const SizedBox(width: 10), Text(func.name.capitalize)],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -155,8 +130,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
color: color?.withValues(alpha: 0.7) ?? UIs.halfAlpha,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Text(tag, style: UIs.text11)
|
||||
.paddingSymmetric(horizontal: 5, vertical: 1),
|
||||
child: Text(tag, style: UIs.text11).paddingSymmetric(horizontal: 5, vertical: 1),
|
||||
).paddingOnly(right: noPad ? 0 : 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,47 +6,39 @@ class OmitStartText extends StatelessWidget {
|
||||
final TextStyle? style;
|
||||
final TextOverflow? overflow;
|
||||
|
||||
const OmitStartText(
|
||||
this.text, {
|
||||
super.key,
|
||||
this.maxLines,
|
||||
this.style,
|
||||
this.overflow,
|
||||
});
|
||||
const OmitStartText(this.text, {super.key, this.maxLines, this.style, this.overflow});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, size) {
|
||||
bool exceeded = false;
|
||||
int len = 0;
|
||||
for (; !exceeded && len < text.length; len++) {
|
||||
// Build the textspan
|
||||
final span = TextSpan(
|
||||
text: 'A' * 7 + text.substring(text.length - len),
|
||||
style: style ?? Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
return LayoutBuilder(
|
||||
builder: (context, size) {
|
||||
bool exceeded = false;
|
||||
int len = 0;
|
||||
for (; !exceeded && len < text.length; len++) {
|
||||
// Build the textspan
|
||||
final span = TextSpan(
|
||||
text: 'A' * 7 + text.substring(text.length - len),
|
||||
style: style ?? Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
|
||||
// Use a textpainter to determine if it will exceed max lines
|
||||
final tp = TextPainter(
|
||||
// Use a textpainter to determine if it will exceed max lines
|
||||
final tp = TextPainter(maxLines: maxLines ?? 1, textDirection: TextDirection.ltr, text: span);
|
||||
|
||||
// trigger it to layout
|
||||
tp.layout(maxWidth: size.maxWidth);
|
||||
|
||||
// whether the text overflowed or not
|
||||
exceeded = tp.didExceedMaxLines;
|
||||
}
|
||||
|
||||
return Text(
|
||||
(exceeded ? '...' : '') + text.substring(text.length - len),
|
||||
overflow: overflow ?? TextOverflow.fade,
|
||||
softWrap: false,
|
||||
maxLines: maxLines ?? 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
text: span,
|
||||
style: style,
|
||||
);
|
||||
|
||||
// trigger it to layout
|
||||
tp.layout(maxWidth: size.maxWidth);
|
||||
|
||||
// whether the text overflowed or not
|
||||
exceeded = tp.didExceedMaxLines;
|
||||
}
|
||||
|
||||
return Text(
|
||||
(exceeded ? '...' : '') + text.substring(text.length - len),
|
||||
overflow: overflow ?? TextOverflow.fade,
|
||||
softWrap: false,
|
||||
maxLines: maxLines ?? 1,
|
||||
style: style,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,13 @@ import 'package:flutter/material.dart';
|
||||
final class PercentCircle extends StatelessWidget {
|
||||
final double percent;
|
||||
|
||||
const PercentCircle({
|
||||
super.key,
|
||||
required this.percent,
|
||||
});
|
||||
const PercentCircle({super.key, required this.percent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final percent = switch (this.percent) {
|
||||
0 => 0.01,
|
||||
100 => 99.9,
|
||||
// NaN
|
||||
final val when val.isNaN => 0.01,
|
||||
<= 0.01 => 0.01,
|
||||
>= 99.9 => 99.9,
|
||||
_ => this.percent,
|
||||
};
|
||||
return Stack(
|
||||
|
||||
@@ -6,11 +6,7 @@ final class UnixPermOp {
|
||||
final bool w;
|
||||
final bool x;
|
||||
|
||||
const UnixPermOp({
|
||||
required this.r,
|
||||
required this.w,
|
||||
required this.x,
|
||||
});
|
||||
const UnixPermOp({required this.r, required this.w, required this.x});
|
||||
|
||||
UnixPermOp copyWith({bool? r, bool? w, bool? x}) {
|
||||
return UnixPermOp(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x);
|
||||
@@ -24,8 +20,7 @@ final class UnixPermOp {
|
||||
enum UnixPermScope {
|
||||
user,
|
||||
group,
|
||||
other,
|
||||
;
|
||||
other;
|
||||
|
||||
String get title {
|
||||
return switch (this) {
|
||||
@@ -72,10 +67,10 @@ final class UnixPerm {
|
||||
}
|
||||
|
||||
static UnixPerm get empty => const UnixPerm(
|
||||
user: UnixPermOp(r: false, w: false, x: false),
|
||||
group: UnixPermOp(r: false, w: false, x: false),
|
||||
other: UnixPermOp(r: false, w: false, x: false),
|
||||
);
|
||||
user: UnixPermOp(r: false, w: false, x: false),
|
||||
group: UnixPermOp(r: false, w: false, x: false),
|
||||
other: UnixPermOp(r: false, w: false, x: false),
|
||||
);
|
||||
}
|
||||
|
||||
final class UnixPermEditor extends StatefulWidget {
|
||||
@@ -150,9 +145,6 @@ final class _UnixPermEditorState extends State<UnixPermEditor> {
|
||||
}
|
||||
|
||||
Widget _buildSwitch(bool value, void Function(bool) onChanged) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
return Switch(value: value, onChanged: onChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +96,12 @@ include(flutter/generated_plugins.cmake)
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
# if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
# set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
# endif()
|
||||
# Always set the install prefix to the build bundle directory, even if
|
||||
# CMAKE_INSTALL_PREFIX was set to something else before.
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
|
||||
@@ -313,7 +313,7 @@ void main() {
|
||||
}
|
||||
]
|
||||
''';
|
||||
|
||||
|
||||
final gpu = AmdSmi.fromJson(jsonWithInvalidProcess)[0];
|
||||
expect(gpu.memory.processes.length, 1);
|
||||
expect(gpu.memory.processes[0].pid, 1234);
|
||||
@@ -409,4 +409,4 @@ void main() {
|
||||
expect(gpus[0].clockSpeed, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,25 +9,25 @@ void main() {
|
||||
final disks = Disk.parse(_btrfsRaidJsonOutput);
|
||||
expect(disks, isNotEmpty);
|
||||
expect(disks.length, 4); // Should have 2 parent disks + 2 BTRFS partitions
|
||||
|
||||
|
||||
// We should get two distinct disks with the same UUID but different paths
|
||||
final nvme1Disk = disks.firstWhere((disk) => disk.path == '/dev/nvme1n1p1');
|
||||
final nvme2Disk = disks.firstWhere((disk) => disk.path == '/dev/nvme2n1p1');
|
||||
|
||||
|
||||
// Both should exist
|
||||
expect(nvme1Disk, isNotNull);
|
||||
expect(nvme2Disk, isNotNull);
|
||||
|
||||
|
||||
// They should have the same UUID (since they're part of the same BTRFS volume)
|
||||
expect(nvme1Disk.uuid, nvme2Disk.uuid);
|
||||
|
||||
|
||||
// But they should be treated as distinct disks
|
||||
expect(identical(nvme1Disk, nvme2Disk), isFalse);
|
||||
|
||||
|
||||
// Verify DiskUsage counts physical disks correctly
|
||||
final usage = DiskUsage.parse(disks);
|
||||
// With our unique path+kname identifier, both disks should be counted
|
||||
expect(usage.size, nvme1Disk.size + nvme2Disk.size);
|
||||
expect(usage.size, nvme1Disk.size + nvme2Disk.size);
|
||||
expect(usage.used, nvme1Disk.used + nvme2Disk.used);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ fa1215b4be74 Up 12 hours firefly
|
||||
const images = [
|
||||
'rustdesk/rustdesk-server:latest',
|
||||
'rustdesk/rustdesk-server:latest',
|
||||
'uusec/firefly:latest'
|
||||
'uusec/firefly:latest',
|
||||
];
|
||||
const states = ['Up 2 hours', 'Up 41 minutes', 'Up 12 hours'];
|
||||
for (var idx = 1; idx < lines.length; idx++) {
|
||||
|
||||
@@ -11,12 +11,12 @@ void main() {
|
||||
expect(disks, isNotEmpty);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('parse lsblk JSON output', () {
|
||||
final disks = Disk.parse(_jsonLsblkOutput);
|
||||
expect(disks, isNotEmpty);
|
||||
expect(disks.length, 6); // Should find ext4 root, vfat efi, and ext2 boot
|
||||
|
||||
expect(disks.length, 6); // Should find ext4 root, vfat efi, and ext2 boot
|
||||
|
||||
// Verify root filesystem
|
||||
final rootFs = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(rootFs.fsTyp, 'ext4');
|
||||
@@ -24,44 +24,44 @@ void main() {
|
||||
expect(rootFs.used, BigInt.parse('552718364672') ~/ BigInt.from(1024));
|
||||
expect(rootFs.avail, BigInt.parse('379457622016') ~/ BigInt.from(1024));
|
||||
expect(rootFs.usedPercent, 56);
|
||||
|
||||
|
||||
// Verify boot/efi filesystem
|
||||
final efiFs = disks.firstWhere((disk) => disk.mount == '/boot/efi');
|
||||
expect(efiFs.fsTyp, 'vfat');
|
||||
expect(efiFs.size, BigInt.parse('535805952') ~/ BigInt.from(1024));
|
||||
expect(efiFs.usedPercent, 1);
|
||||
|
||||
|
||||
// Verify boot filesystem
|
||||
final bootFs = disks.firstWhere((disk) => disk.mount == '/boot');
|
||||
expect(bootFs.fsTyp, 'ext2');
|
||||
expect(bootFs.usedPercent, 34);
|
||||
});
|
||||
|
||||
|
||||
test('parse nested lsblk JSON output with parent/child relationships', () {
|
||||
final disks = Disk.parse(_nestedJsonLsblkOutput);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
|
||||
// Check parent device with children
|
||||
final parentDisk = disks.firstWhere((disk) => disk.path == '/dev/nvme0n1');
|
||||
expect(parentDisk.children, isNotEmpty);
|
||||
expect(parentDisk.children.length, 3);
|
||||
|
||||
|
||||
// Check one of the children
|
||||
final rootPartition = parentDisk.children.firstWhere((disk) => disk.mount == '/');
|
||||
expect(rootPartition.fsTyp, 'ext4');
|
||||
expect(rootPartition.path, '/dev/nvme0n1p2');
|
||||
expect(rootPartition.usedPercent, 45);
|
||||
|
||||
|
||||
// Verify we have a child partition with UUID
|
||||
final bootPartition = parentDisk.children.firstWhere((disk) => disk.mount == '/boot');
|
||||
expect(bootPartition.uuid, '12345678-abcd-1234-abcd-1234567890ab');
|
||||
});
|
||||
|
||||
|
||||
test('DiskUsage handles zero size correctly', () {
|
||||
final usage = DiskUsage(used: BigInt.from(1000), size: BigInt.zero);
|
||||
expect(usage.usedPercent, 0); // Should return 0 instead of throwing
|
||||
});
|
||||
|
||||
|
||||
test('DiskUsage handles null kname', () {
|
||||
final disks = [
|
||||
Disk(
|
||||
@@ -74,7 +74,7 @@ void main() {
|
||||
kname: null, // Explicitly null kname
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
final usage = DiskUsage.parse(disks);
|
||||
expect(usage.used, BigInt.from(5000));
|
||||
expect(usage.size, BigInt.from(10000));
|
||||
@@ -198,16 +198,16 @@ const _nestedJsonLsblkOutput = '''
|
||||
''';
|
||||
|
||||
const _raws = [
|
||||
// '''
|
||||
// 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
|
||||
// ''',
|
||||
// '''
|
||||
// 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
|
||||
// ''',
|
||||
'''
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
udev 16181648 0 16181648 0% /dev
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user