mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +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>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
<br>
|
||||||
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
|
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
|
||||||
</p>
|
</p>
|
||||||
@@ -26,7 +26,7 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## 📥 Install
|
## 📥 Installation
|
||||||
|
|
||||||
|Platform| From|
|
|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**!
|
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`...
|
- `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`...
|
- 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).
|
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.
|
Any positive contribution is welcome.
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
使用 Flutter 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
|
使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱,提供服务器状态图表和管理工具。
|
||||||
<br>
|
<br>
|
||||||
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>。
|
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>。
|
||||||
</p>
|
</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(
|
light: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorSchemeSeed: UIs.colorSeed,
|
colorSchemeSeed: UIs.colorSeed,
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||||
scrolledUnderElevation: 0.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
dark: ThemeData(
|
dark: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
colorSchemeSeed: UIs.colorSeed,
|
colorSchemeSeed: UIs.colorSeed,
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||||
scrolledUnderElevation: 0.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -58,15 +54,8 @@ class MyApp extends StatelessWidget {
|
|||||||
Widget _buildDynamicColor(BuildContext context) {
|
Widget _buildDynamicColor(BuildContext context) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (light, dark) {
|
builder: (light, dark) {
|
||||||
final lightTheme = ThemeData(
|
final lightTheme = ThemeData(useMaterial3: true, colorScheme: light);
|
||||||
useMaterial3: true,
|
final darkTheme = ThemeData(useMaterial3: true, brightness: Brightness.dark, colorScheme: dark);
|
||||||
colorScheme: light,
|
|
||||||
);
|
|
||||||
final darkTheme = ThemeData(
|
|
||||||
useMaterial3: true,
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
colorScheme: dark,
|
|
||||||
);
|
|
||||||
if (context.isDark && dark != null) {
|
if (context.isDark && dark != null) {
|
||||||
UIs.primaryColor = dark.primary;
|
UIs.primaryColor = dark.primary;
|
||||||
} else if (!context.isDark && light != null) {
|
} else if (!context.isDark && light != null) {
|
||||||
@@ -78,11 +67,7 @@ class MyApp extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildApp(
|
Widget _buildApp(BuildContext ctx, {required ThemeData light, required ThemeData dark}) {
|
||||||
BuildContext ctx, {
|
|
||||||
required ThemeData light,
|
|
||||||
required ThemeData dark,
|
|
||||||
}) {
|
|
||||||
final tMode = Stores.setting.themeMode.fetch();
|
final tMode = Stores.setting.themeMode.fetch();
|
||||||
// Issue #57
|
// Issue #57
|
||||||
final themeMode = switch (tMode) {
|
final themeMode = switch (tMode) {
|
||||||
@@ -103,10 +88,7 @@ class MyApp extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
locale: locale,
|
locale: locale,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates],
|
||||||
LibLocalizations.delegate,
|
|
||||||
...AppLocalizations.localizationsDelegates,
|
|
||||||
],
|
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
localeListResolutionCallback: LocaleUtil.resolve,
|
localeListResolutionCallback: LocaleUtil.resolve,
|
||||||
navigatorObservers: [AppRouteObserver.instance],
|
navigatorObservers: [AppRouteObserver.instance],
|
||||||
@@ -128,10 +110,7 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
child = const HomePage();
|
child = const HomePage();
|
||||||
|
|
||||||
return VirtualWindowFrame(
|
return VirtualWindowFrame(title: BuildData.name, child: child);
|
||||||
title: BuildData.name,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,21 +12,9 @@ extension SftpFileX on SftpFileMode {
|
|||||||
|
|
||||||
UnixPerm toUnixPerm() {
|
UnixPerm toUnixPerm() {
|
||||||
return UnixPerm(
|
return UnixPerm(
|
||||||
user: UnixPermOp(
|
user: UnixPermOp(r: userRead, w: userWrite, x: userExecute),
|
||||||
r: userRead,
|
group: UnixPermOp(r: groupRead, w: groupWrite, x: groupExecute),
|
||||||
w: userWrite,
|
other: UnixPermOp(r: otherRead, w: otherWrite, x: otherExecute),
|
||||||
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:dartssh2/dartssh2.dart';
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
|
|
||||||
import 'package:server_box/data/res/misc.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);
|
typedef PwdRequestFunc = Future<String?> Function(String? user);
|
||||||
|
|
||||||
extension SSHClientX on SSHClient {
|
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(
|
Future<(SSHSession, String)> exec(
|
||||||
OnStdin onStdin, {
|
OnStdin onStdin, {
|
||||||
String? entry,
|
String? entry,
|
||||||
@@ -22,9 +69,14 @@ extension SSHClientX on SSHClient {
|
|||||||
bool stdout = true,
|
bool stdout = true,
|
||||||
bool stderr = true,
|
bool stderr = true,
|
||||||
Map<String, String>? env,
|
Map<String, String>? env,
|
||||||
|
SystemType? systemType,
|
||||||
}) async {
|
}) async {
|
||||||
final session = await execute(
|
final session = await execute(
|
||||||
entry ?? 'cat | sh',
|
entry ??
|
||||||
|
switch (systemType) {
|
||||||
|
SystemType.windows => 'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
|
||||||
|
_ => 'cat | sh',
|
||||||
|
},
|
||||||
pty: pty,
|
pty: pty,
|
||||||
environment: env,
|
environment: env,
|
||||||
);
|
);
|
||||||
@@ -81,9 +133,7 @@ extension SSHClientX on SSHClient {
|
|||||||
isRequestingPwd = true;
|
isRequestingPwd = true;
|
||||||
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
||||||
if (context == null) return;
|
if (context == null) return;
|
||||||
final pwd = context.mounted
|
final pwd = context.mounted ? await context.showPwdDialog(title: user, id: id) : null;
|
||||||
? await context.showPwdDialog(title: user, id: id)
|
|
||||||
: null;
|
|
||||||
if (pwd == null || pwd.isEmpty) {
|
if (pwd == null || pwd.isEmpty) {
|
||||||
session.stdin.close();
|
session.stdin.close();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ class ChainComparator<T> {
|
|||||||
ChainComparator.empty() : this._create(null, (a, b) => 0);
|
ChainComparator.empty() : this._create(null, (a, b) => 0);
|
||||||
ChainComparator.create() : this._create(null, (a, b) => 0);
|
ChainComparator.create() : this._create(null, (a, b) => 0);
|
||||||
|
|
||||||
static ChainComparator<T> comparing<T, F extends Comparable<F>>(
|
static ChainComparator<T> comparing<T, F extends Comparable<F>>(F Function(T) extractor) {
|
||||||
F Function(T) extractor) {
|
return ChainComparator._create(null, (a, b) => extractor(a).compareTo(extractor(b)));
|
||||||
return ChainComparator._create(
|
|
||||||
null, (a, b) => extractor(a).compareTo(extractor(b)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int compare(T a, T b) {
|
int compare(T a, T b) {
|
||||||
@@ -26,8 +24,9 @@ class ChainComparator<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChainComparator<T> thenCompareBy<F extends Comparable<F>>(
|
ChainComparator<T> thenCompareBy<F extends Comparable<F>>(
|
||||||
F Function(T) extractor,
|
F Function(T) extractor, {
|
||||||
{bool reversed = false}) {
|
bool reversed = false,
|
||||||
|
}) {
|
||||||
return ChainComparator._create(
|
return ChainComparator._create(
|
||||||
this,
|
this,
|
||||||
reversed
|
reversed
|
||||||
@@ -36,18 +35,12 @@ class ChainComparator<T> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ChainComparator<T> thenWithComparator(Comparator<T> comparator,
|
ChainComparator<T> thenWithComparator(Comparator<T> comparator, {bool reversed = false}) {
|
||||||
{bool reversed = false}) {
|
return ChainComparator._create(this, !reversed ? comparator : (a, b) => comparator(b, a));
|
||||||
return ChainComparator._create(
|
|
||||||
this,
|
|
||||||
!reversed ? comparator : (a, b) => comparator(b, a),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(
|
ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(F Function(T) extractor) {
|
||||||
F Function(T) extractor) {
|
return ChainComparator._create(this, (a, b) => -extractor(a).compareTo(extractor(b)));
|
||||||
return ChainComparator._create(
|
|
||||||
this, (a, b) => -extractor(a).compareTo(extractor(b)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChainComparator<T> thenTrueFirst(bool Function(T) f) {
|
ChainComparator<T> thenTrueFirst(bool Function(T) f) {
|
||||||
@@ -63,8 +56,7 @@ class ChainComparator<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Comparators {
|
class Comparators {
|
||||||
static Comparator<String> compareStringCaseInsensitive(
|
static Comparator<String> compareStringCaseInsensitive({bool uppercaseFirst = false}) {
|
||||||
{bool uppercaseFirst = false}) {
|
|
||||||
return (String a, String b) {
|
return (String a, String b) {
|
||||||
final r = a.toLowerCase().compareTo(b.toLowerCase());
|
final r = a.toLowerCase().compareTo(b.toLowerCase());
|
||||||
if (r != 0) return r;
|
if (r != 0) return r;
|
||||||
|
|||||||
@@ -24,19 +24,12 @@ String decyptPem(List<String> args) {
|
|||||||
return sshKey.first.toPem();
|
return sshKey.first.toPem();
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GenSSHClientStatus {
|
enum GenSSHClientStatus { socket, key, pwd }
|
||||||
socket,
|
|
||||||
key,
|
|
||||||
pwd,
|
|
||||||
}
|
|
||||||
|
|
||||||
String getPrivateKey(String id) {
|
String getPrivateKey(String id) {
|
||||||
final pki = Stores.key.fetchOne(id);
|
final pki = Stores.key.fetchOne(id);
|
||||||
if (pki == null) {
|
if (pki == null) {
|
||||||
throw SSHErr(
|
throw SSHErr(type: SSHErrType.noPrivateKey, message: 'key [$id] not found');
|
||||||
type: SSHErrType.noPrivateKey,
|
|
||||||
message: 'key [$id] not found',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return pki.key;
|
return pki.key;
|
||||||
}
|
}
|
||||||
@@ -73,36 +66,21 @@ Future<SSHClient> genClient(
|
|||||||
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
||||||
}();
|
}();
|
||||||
if (jumpSpi_ != null) {
|
if (jumpSpi_ != null) {
|
||||||
final jumpClient = await genClient(
|
final jumpClient = await genClient(jumpSpi_, privateKey: jumpPrivateKey, timeout: timeout);
|
||||||
jumpSpi_,
|
|
||||||
privateKey: jumpPrivateKey,
|
|
||||||
timeout: timeout,
|
|
||||||
);
|
|
||||||
|
|
||||||
return await jumpClient.forwardLocal(
|
return await jumpClient.forwardLocal(spi.ip, spi.port);
|
||||||
spi.ip,
|
|
||||||
spi.port,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct
|
// Direct
|
||||||
try {
|
try {
|
||||||
return await SSHSocket.connect(
|
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
|
||||||
spi.ip,
|
|
||||||
spi.port,
|
|
||||||
timeout: timeout,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Loggers.app.warning('genClient', e);
|
Loggers.app.warning('genClient', e);
|
||||||
if (spi.alterUrl == null) rethrow;
|
if (spi.alterUrl == null) rethrow;
|
||||||
try {
|
try {
|
||||||
final res = spi.fromStringUrl();
|
final res = spi.fromStringUrl();
|
||||||
alterUser = res.$2;
|
alterUser = res.$2;
|
||||||
return await SSHSocket.connect(
|
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
||||||
res.$1,
|
|
||||||
res.$3,
|
|
||||||
timeout: timeout,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Loggers.app.warning('genClient alterUrl', e);
|
Loggers.app.warning('genClient alterUrl', e);
|
||||||
rethrow;
|
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');
|
_logger.info('Restore success');
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Backup.fromJsonString(String raw) =>
|
factory Backup.fromJsonString(String raw) => Backup.fromJson(json.decode(_diyDecrypt(raw)));
|
||||||
Backup.fromJson(json.decode(_diyDecrypt(raw)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _diyEncrypt(String raw) => json.encode(
|
String _diyEncrypt(String raw) => json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
|
||||||
raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false),
|
|
||||||
);
|
|
||||||
|
|
||||||
String _diyDecrypt(String raw) {
|
String _diyDecrypt(String raw) {
|
||||||
try {
|
try {
|
||||||
@@ -234,4 +231,3 @@ String _diyDecrypt(String raw) {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
|
||||||
enum SSHErrType {
|
enum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus }
|
||||||
unknown,
|
|
||||||
connect,
|
|
||||||
auth,
|
|
||||||
noPrivateKey,
|
|
||||||
chdir,
|
|
||||||
segements,
|
|
||||||
writeScript,
|
|
||||||
getStatus,
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SSHErr extends Err<SSHErrType> {
|
class SSHErr extends Err<SSHErrType> {
|
||||||
SSHErr({required super.type, super.message});
|
SSHErr({required super.type, super.message});
|
||||||
@@ -45,11 +35,7 @@ class ContainerErr extends Err<ContainerErrType> {
|
|||||||
String? get solution => null;
|
String? get solution => null;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ICloudErrType {
|
enum ICloudErrType { generic, notFound, multipleFiles }
|
||||||
generic,
|
|
||||||
notFound,
|
|
||||||
multipleFiles,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ICloudErr extends Err<ICloudErrType> {
|
class ICloudErr extends Err<ICloudErrType> {
|
||||||
ICloudErr({required super.type, super.message});
|
ICloudErr({required super.type, super.message});
|
||||||
@@ -58,11 +44,7 @@ class ICloudErr extends Err<ICloudErrType> {
|
|||||||
String? get solution => null;
|
String? get solution => null;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WebdavErrType {
|
enum WebdavErrType { generic, notFound }
|
||||||
generic,
|
|
||||||
notFound,
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebdavErr extends Err<WebdavErrType> {
|
class WebdavErr extends Err<WebdavErrType> {
|
||||||
WebdavErr({required super.type, super.message});
|
WebdavErr({required super.type, super.message});
|
||||||
@@ -71,12 +53,7 @@ class WebdavErr extends Err<WebdavErrType> {
|
|||||||
String? get solution => null;
|
String? get solution => null;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PveErrType {
|
enum PveErrType { unknown, net, loginFailed }
|
||||||
unknown,
|
|
||||||
net,
|
|
||||||
loginFailed,
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PveErr extends Err<PveErrType> {
|
class PveErr extends Err<PveErrType> {
|
||||||
PveErr({required super.type, super.message});
|
PveErr({required super.type, super.message});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ enum ContainerMenu {
|
|||||||
restart,
|
restart,
|
||||||
rm,
|
rm,
|
||||||
logs,
|
logs,
|
||||||
terminal,
|
terminal
|
||||||
//stats,
|
//stats,
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ enum ServerFuncBtn {
|
|||||||
snippet(),
|
snippet(),
|
||||||
iperf(),
|
iperf(),
|
||||||
// pve(),
|
// pve(),
|
||||||
systemd(1058),
|
systemd(1058);
|
||||||
;
|
|
||||||
|
|
||||||
final int? addedVersion;
|
final int? addedVersion;
|
||||||
|
|
||||||
|
|||||||
@@ -26,32 +26,17 @@ enum NetViewType {
|
|||||||
try {
|
try {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case NetViewType.conn:
|
case NetViewType.conn:
|
||||||
return (
|
return ('${l10n.conn}:\n${ss.tcp.maxConn}', '${libL10n.fail}:\n${ss.tcp.fail}');
|
||||||
'${l10n.conn}:\n${ss.tcp.maxConn}',
|
|
||||||
'${libL10n.fail}:\n${ss.tcp.fail}',
|
|
||||||
);
|
|
||||||
case NetViewType.speed:
|
case NetViewType.speed:
|
||||||
if (notSepcifyDev) {
|
if (notSepcifyDev) {
|
||||||
return (
|
return ('↓:\n${ss.netSpeed.cachedVals.speedIn}', '↑:\n${ss.netSpeed.cachedVals.speedOut}');
|
||||||
'↓:\n${ss.netSpeed.cachedVals.speedIn}',
|
|
||||||
'↑:\n${ss.netSpeed.cachedVals.speedOut}',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
return ('↓:\n${ss.netSpeed.speedIn(device: dev)}', '↑:\n${ss.netSpeed.speedOut(device: dev)}');
|
||||||
'↓:\n${ss.netSpeed.speedIn(device: dev)}',
|
|
||||||
'↑:\n${ss.netSpeed.speedOut(device: dev)}',
|
|
||||||
);
|
|
||||||
case NetViewType.traffic:
|
case NetViewType.traffic:
|
||||||
if (notSepcifyDev) {
|
if (notSepcifyDev) {
|
||||||
return (
|
return ('↓:\n${ss.netSpeed.cachedVals.sizeIn}', '↑:\n${ss.netSpeed.cachedVals.sizeOut}');
|
||||||
'↓:\n${ss.netSpeed.cachedVals.sizeIn}',
|
|
||||||
'↑:\n${ss.netSpeed.cachedVals.sizeOut}',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
return ('↓:\n${ss.netSpeed.sizeIn(device: dev)}', '↑:\n${ss.netSpeed.sizeOut(device: dev)}');
|
||||||
'↓:\n${ss.netSpeed.sizeIn(device: dev)}',
|
|
||||||
'↑:\n${ss.netSpeed.sizeOut(device: dev)}',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Loggers.app.warning('NetViewType.build', e, s);
|
Loggers.app.warning('NetViewType.build', e, s);
|
||||||
|
|||||||
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/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/model/server/system.dart';
|
||||||
import 'package:server_box/data/provider/server.dart';
|
import 'package:server_box/data/provider/server.dart';
|
||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
|
|
||||||
enum ShellFunc {
|
enum ShellFunc {
|
||||||
status,
|
status('SbStatus'),
|
||||||
//docker,
|
//docker,
|
||||||
process,
|
process('SbProcess'),
|
||||||
shutdown,
|
shutdown('SbShutdown'),
|
||||||
reboot,
|
reboot('SbReboot'),
|
||||||
suspend;
|
suspend('SbSuspend');
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const ShellFunc(this.name);
|
||||||
|
|
||||||
static const seperator = 'SrvBoxSep';
|
static const seperator = 'SrvBoxSep';
|
||||||
|
|
||||||
/// The suffix `\t` is for formatting
|
/// The suffix `\t` is for formatting
|
||||||
static const cmdDivider = '\necho $seperator\n\t';
|
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
|
/// srvboxm -> ServerBox Mobile
|
||||||
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
||||||
|
static const scriptFileWindows = 'srvboxm_v${BuildData.script}.ps1';
|
||||||
static const scriptDirHome = '~/.config/server_box';
|
static const scriptDirHome = '~/.config/server_box';
|
||||||
static const scriptDirTmp = '/tmp/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>{};
|
static final _scriptDirMap = <String, String>{};
|
||||||
|
|
||||||
@@ -33,31 +35,38 @@ enum ShellFunc {
|
|||||||
///
|
///
|
||||||
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
||||||
/// it will be changed to [scriptDirHome]/[scriptFile].
|
/// 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;
|
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||||
if (customScriptDir != null) return customScriptDir;
|
if (customScriptDir != null) return customScriptDir;
|
||||||
_scriptDirMap[id] ??= scriptDirTmp;
|
|
||||||
|
final defaultTmpDir = systemType == SystemType.windows ? scriptDirTmpWindows : scriptDirTmp;
|
||||||
|
_scriptDirMap[id] ??= defaultTmpDir;
|
||||||
return _scriptDirMap[id]!;
|
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,
|
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
|
||||||
|
scriptDirTmpWindows => _scriptDirMap[id] = scriptDirHomeWindows,
|
||||||
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
|
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
|
||||||
_ => _scriptDirMap[id] = scriptDirHome,
|
scriptDirHomeWindows => _scriptDirMap[id] = scriptDirTmpWindows,
|
||||||
|
_ => _scriptDirMap[id] = systemType == SystemType.windows ? scriptDirHomeWindows : scriptDirHome,
|
||||||
};
|
};
|
||||||
|
|
||||||
static String getScriptPath(String id) {
|
static String getScriptPath(String id, {SystemType? systemType}) {
|
||||||
return '${getScriptDir(id)}/$scriptFile';
|
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) {
|
static String getInstallShellCmd(String id, {SystemType? systemType}) {
|
||||||
final scriptDir = getScriptDir(id);
|
final scriptDir = getScriptDir(id, systemType: systemType);
|
||||||
final scriptPath = '$scriptDir/$scriptFile';
|
final isWindows = systemType == SystemType.windows;
|
||||||
return '''
|
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||||
mkdir -p $scriptDir
|
final separator = isWindows ? '\\' : '/';
|
||||||
cat > $scriptPath
|
final scriptPath = '$scriptDir$separator${builder.scriptFileName}';
|
||||||
chmod 755 $scriptPath
|
|
||||||
''';
|
return builder.getInstallCommand(scriptDir, scriptPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get flag => switch (this) {
|
String get flag => switch (this) {
|
||||||
@@ -69,120 +78,24 @@ chmod 755 $scriptPath
|
|||||||
// ShellFunc.docker=> 'd',
|
// ShellFunc.docker=> 'd',
|
||||||
};
|
};
|
||||||
|
|
||||||
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
|
String exec(String id, {SystemType? systemType}) {
|
||||||
|
final scriptPath = getScriptPath(id, systemType: systemType);
|
||||||
|
final isWindows = systemType == SystemType.windows;
|
||||||
|
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||||
|
|
||||||
String get name => switch (this) {
|
return builder.getExecCommand(scriptPath, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
''');
|
|
||||||
|
|
||||||
|
/// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension EnumX on Enum {
|
|
||||||
/// Find out the required segment from [segments]
|
|
||||||
String find(List<String> segments) {
|
|
||||||
return segments[index];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StatusCmdType {
|
enum StatusCmdType {
|
||||||
@@ -193,7 +106,10 @@ enum StatusCmdType {
|
|||||||
cpu._('cat /proc/stat | grep cpu'),
|
cpu._('cat /proc/stat | grep cpu'),
|
||||||
uptime._('uptime'),
|
uptime._('uptime'),
|
||||||
conn._('cat /proc/net/snmp'),
|
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'"),
|
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||||
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
||||||
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||||
@@ -201,7 +117,16 @@ enum StatusCmdType {
|
|||||||
diskio._('cat /proc/diskstats'),
|
diskio._('cat /proc/diskstats'),
|
||||||
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
|
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
|
||||||
nvidia._('nvidia-smi -q -x'),
|
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'),
|
sensors._('sensors'),
|
||||||
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
||||||
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
||||||
@@ -241,3 +166,77 @@ extension StatusCmdTypeX on StatusCmdType {
|
|||||||
final val => val.name,
|
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,
|
server,
|
||||||
ssh,
|
ssh,
|
||||||
file,
|
file,
|
||||||
snippet,
|
snippet
|
||||||
//settings,
|
//settings,
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,7 @@ final class PodmanImg implements ContainerImg {
|
|||||||
final int? size;
|
final int? size;
|
||||||
final int? containers;
|
final int? containers;
|
||||||
|
|
||||||
PodmanImg({
|
PodmanImg({this.repository, this.tag, this.id, this.created, this.size, this.containers});
|
||||||
this.repository,
|
|
||||||
this.tag,
|
|
||||||
this.id,
|
|
||||||
this.created,
|
|
||||||
this.size,
|
|
||||||
this.containers,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get sizeMB => size?.bytes2Str;
|
String? get sizeMB => size?.bytes2Str;
|
||||||
@@ -39,8 +32,7 @@ final class PodmanImg implements ContainerImg {
|
|||||||
@override
|
@override
|
||||||
int? get containersCount => containers;
|
int? get containersCount => containers;
|
||||||
|
|
||||||
factory PodmanImg.fromRawJson(String str) =>
|
factory PodmanImg.fromRawJson(String str) => PodmanImg.fromJson(json.decode(str));
|
||||||
PodmanImg.fromJson(json.decode(str));
|
|
||||||
|
|
||||||
String toRawJson() => json.encode(toJson());
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
@@ -87,11 +79,9 @@ final class DockerImg implements ContainerImg {
|
|||||||
String? get sizeMB => size;
|
String? get sizeMB => size;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int? get containersCount =>
|
int? get containersCount => containers == 'N/A' ? 0 : int.tryParse(containers);
|
||||||
containers == 'N/A' ? 0 : int.tryParse(containers);
|
|
||||||
|
|
||||||
factory DockerImg.fromRawJson(String str) =>
|
factory DockerImg.fromRawJson(String str) => DockerImg.fromJson(json.decode(str));
|
||||||
DockerImg.fromJson(json.decode(str));
|
|
||||||
|
|
||||||
String toRawJson() => json.encode(toJson());
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
|||||||
@@ -42,15 +42,7 @@ final class PodmanPs implements ContainerPs {
|
|||||||
@override
|
@override
|
||||||
String? disk;
|
String? disk;
|
||||||
|
|
||||||
PodmanPs({
|
PodmanPs({this.command, this.created, this.exited, this.id, this.image, this.names, this.startedAt});
|
||||||
this.command,
|
|
||||||
this.created,
|
|
||||||
this.exited,
|
|
||||||
this.id,
|
|
||||||
this.image,
|
|
||||||
this.names,
|
|
||||||
this.startedAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get name => names?.firstOrNull;
|
String? get name => names?.firstOrNull;
|
||||||
@@ -78,29 +70,22 @@ final class PodmanPs implements ContainerPs {
|
|||||||
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
|
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
|
||||||
}
|
}
|
||||||
|
|
||||||
factory PodmanPs.fromRawJson(String str) =>
|
factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));
|
||||||
PodmanPs.fromJson(json.decode(str));
|
|
||||||
|
|
||||||
String toRawJson() => json.encode(toJson());
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
|
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
|
||||||
command: json['Command'] == null
|
command: json['Command'] == null ? [] : List<String>.from(json['Command']!.map((x) => x)),
|
||||||
? []
|
created: json['Created'] == null ? null : DateTime.parse(json['Created']),
|
||||||
: List<String>.from(json['Command']!.map((x) => x)),
|
|
||||||
created:
|
|
||||||
json['Created'] == null ? null : DateTime.parse(json['Created']),
|
|
||||||
exited: json['Exited'],
|
exited: json['Exited'],
|
||||||
id: json['Id'],
|
id: json['Id'],
|
||||||
image: json['Image'],
|
image: json['Image'],
|
||||||
names: json['Names'] == null
|
names: json['Names'] == null ? [] : List<String>.from(json['Names']!.map((x) => x)),
|
||||||
? []
|
|
||||||
: List<String>.from(json['Names']!.map((x) => x)),
|
|
||||||
startedAt: json['StartedAt'],
|
startedAt: json['StartedAt'],
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'Command':
|
'Command': command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
||||||
command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
|
||||||
'Created': created?.toIso8601String(),
|
'Created': created?.toIso8601String(),
|
||||||
'Exited': exited,
|
'Exited': exited,
|
||||||
'Id': id,
|
'Id': id,
|
||||||
@@ -127,12 +112,7 @@ final class DockerPs implements ContainerPs {
|
|||||||
@override
|
@override
|
||||||
String? disk;
|
String? disk;
|
||||||
|
|
||||||
DockerPs({
|
DockerPs({this.id, this.image, this.names, this.state});
|
||||||
this.id,
|
|
||||||
this.image,
|
|
||||||
this.names,
|
|
||||||
this.state,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get name => names;
|
String? get name => names;
|
||||||
@@ -159,11 +139,6 @@ final class DockerPs implements ContainerPs {
|
|||||||
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
|
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
|
||||||
factory DockerPs.parse(String raw) {
|
factory DockerPs.parse(String raw) {
|
||||||
final parts = raw.split(Miscs.multiBlankreg);
|
final parts = raw.split(Miscs.multiBlankreg);
|
||||||
return DockerPs(
|
return DockerPs(id: parts[0], state: parts[1], names: parts[2], image: parts[3].trim());
|
||||||
id: parts[0],
|
|
||||||
state: parts[1],
|
|
||||||
names: parts[2],
|
|
||||||
image: parts[3].trim(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import 'package:server_box/data/model/container/ps.dart';
|
|||||||
|
|
||||||
enum ContainerType {
|
enum ContainerType {
|
||||||
docker,
|
docker,
|
||||||
podman,
|
podman;
|
||||||
;
|
|
||||||
|
|
||||||
ContainerPs Function(String str) get ps => switch (this) {
|
ContainerPs Function(String str) get ps => switch (this) {
|
||||||
ContainerType.docker => DockerPs.parse,
|
ContainerType.docker => DockerPs.parse,
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ enum PkgManager {
|
|||||||
case PkgManager.yum:
|
case PkgManager.yum:
|
||||||
list = list.sublist(2);
|
list = list.sublist(2);
|
||||||
list.removeWhere((element) => element.isEmpty);
|
list.removeWhere((element) => element.isEmpty);
|
||||||
final endLine = list.lastIndexWhere(
|
final endLine = list.lastIndexWhere((element) => element.contains('Obsoleting Packages'));
|
||||||
(element) => element.contains('Obsoleting Packages'));
|
|
||||||
if (endLine != -1 && list.isNotEmpty) {
|
if (endLine != -1 && list.isNotEmpty) {
|
||||||
list = list.sublist(0, endLine);
|
list = list.sublist(0, endLine);
|
||||||
}
|
}
|
||||||
@@ -71,8 +70,7 @@ enum PkgManager {
|
|||||||
case PkgManager.apt:
|
case PkgManager.apt:
|
||||||
// avoid other outputs
|
// 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...]
|
// 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 =
|
final idx = list.indexWhere((element) => element.contains('[upgradable from:'));
|
||||||
list.indexWhere((element) => element.contains('[upgradable from:'));
|
|
||||||
if (idx == -1) {
|
if (idx == -1) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,7 @@ class Battery {
|
|||||||
final int? cycle;
|
final int? cycle;
|
||||||
final String? tech;
|
final String? tech;
|
||||||
|
|
||||||
const Battery({
|
const Battery({required this.status, this.percent, this.name, this.cycle, this.tech});
|
||||||
required this.status,
|
|
||||||
this.percent,
|
|
||||||
this.name,
|
|
||||||
this.cycle,
|
|
||||||
this.tech,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory Battery.fromRaw(String raw) {
|
factory Battery.fromRaw(String raw) {
|
||||||
final lines = raw.split('\n');
|
final lines = raw.split('\n');
|
||||||
@@ -63,8 +57,7 @@ enum BatteryStatus {
|
|||||||
charging,
|
charging,
|
||||||
discharging,
|
discharging,
|
||||||
full,
|
full,
|
||||||
unknown,
|
unknown;
|
||||||
;
|
|
||||||
|
|
||||||
static BatteryStatus parse(String? status) {
|
static BatteryStatus parse(String? status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -6,17 +6,11 @@ class Conn {
|
|||||||
final int passive;
|
final int passive;
|
||||||
final int fail;
|
final int fail;
|
||||||
|
|
||||||
const Conn({
|
const Conn({required this.maxConn, required this.active, required this.passive, required this.fail});
|
||||||
required this.maxConn,
|
|
||||||
required this.active,
|
|
||||||
required this.passive,
|
|
||||||
required this.fail,
|
|
||||||
});
|
|
||||||
|
|
||||||
static Conn? parse(String raw) {
|
static Conn? parse(String raw) {
|
||||||
final lines = raw.split('\n');
|
final lines = raw.split('\n');
|
||||||
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
|
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'), orElse: () => '');
|
||||||
orElse: () => '');
|
|
||||||
if (idx != '') {
|
if (idx != '') {
|
||||||
final vals = idx.split(Miscs.blankReg);
|
final vals = idx.split(Miscs.blankReg);
|
||||||
return Conn(
|
return Conn(
|
||||||
|
|||||||
@@ -200,22 +200,98 @@ final class CpuBrand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final _bsdCpuPercentReg = RegExp(r'(\d+\.\d+)%');
|
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]:
|
/// Supports multiple formats:
|
||||||
/// CPU usage: 14.70% user, 12.76% sys, 72.52% idle
|
/// - 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) {
|
Cpus parseBsdCpu(String raw) {
|
||||||
final percents = _bsdCpuPercentReg
|
|
||||||
.allMatches(raw)
|
|
||||||
.map((e) => double.parse(e.group(1) ?? '0') * 100)
|
|
||||||
.toList();
|
|
||||||
if (percents.length != 3) return InitStatus.cpus;
|
|
||||||
|
|
||||||
final init = InitStatus.cpus;
|
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([
|
init.add([
|
||||||
SingleCpuCore('cpu', percents[0].toInt(), 0, 0,
|
SingleCpuCore(
|
||||||
percents[2].toInt() + percents[1].toInt(), 0, 0, 0),
|
'cpu0',
|
||||||
|
userPercent,
|
||||||
|
sysPercent,
|
||||||
|
0, // nice
|
||||||
|
idlePercent,
|
||||||
|
0, // iowait
|
||||||
|
0, // irq
|
||||||
|
0, // softirq
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
return init;
|
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'))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -154,8 +154,7 @@ class Disk with EquatableMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle common filesystem cases or parent devices with children
|
// Handle common filesystem cases or parent devices with children
|
||||||
if ((fstype != null && _shouldCalc(fstype, mount)) ||
|
if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
|
||||||
(childDisks.isNotEmpty && path.isNotEmpty)) {
|
|
||||||
final sizeStr = device['fssize']?.toString() ?? '0';
|
final sizeStr = device['fssize']?.toString() ?? '0';
|
||||||
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||||
|
|
||||||
@@ -221,14 +220,16 @@ class Disk with EquatableMixin {
|
|||||||
final fs = vals[0];
|
final fs = vals[0];
|
||||||
final mount = vals[5];
|
final mount = vals[5];
|
||||||
if (!_shouldCalc(fs, mount)) continue;
|
if (!_shouldCalc(fs, mount)) continue;
|
||||||
list.add(Disk(
|
list.add(
|
||||||
|
Disk(
|
||||||
path: fs,
|
path: fs,
|
||||||
mount: mount,
|
mount: mount,
|
||||||
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
|
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
|
||||||
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
|
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
|
||||||
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
|
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
|
||||||
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
|
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -237,8 +238,19 @@ class Disk with EquatableMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props =>
|
List<Object?> get props => [
|
||||||
[path, name, kname, fsTyp, mount, usedPercent, used, size, avail, uuid, children];
|
path,
|
||||||
|
name,
|
||||||
|
kname,
|
||||||
|
fsTyp,
|
||||||
|
mount,
|
||||||
|
usedPercent,
|
||||||
|
used,
|
||||||
|
size,
|
||||||
|
avail,
|
||||||
|
uuid,
|
||||||
|
children,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
||||||
@@ -314,12 +326,14 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
|||||||
try {
|
try {
|
||||||
final dev = vals[2];
|
final dev = vals[2];
|
||||||
if (dev.startsWith('loop')) continue;
|
if (dev.startsWith('loop')) continue;
|
||||||
items.add(DiskIOPiece(
|
items.add(
|
||||||
|
DiskIOPiece(
|
||||||
dev: dev,
|
dev: dev,
|
||||||
sectorsRead: int.parse(vals[5]),
|
sectorsRead: int.parse(vals[5]),
|
||||||
sectorsWrite: int.parse(vals[9]),
|
sectorsWrite: int.parse(vals[9]),
|
||||||
time: time,
|
time: time,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -334,12 +348,7 @@ class DiskIOPiece extends TimeSeqIface<DiskIOPiece> {
|
|||||||
final int sectorsWrite;
|
final int sectorsWrite;
|
||||||
final int time;
|
final int time;
|
||||||
|
|
||||||
DiskIOPiece({
|
DiskIOPiece({required this.dev, required this.sectorsRead, required this.sectorsWrite, required this.time});
|
||||||
required this.dev,
|
|
||||||
required this.sectorsRead,
|
|
||||||
required this.sectorsWrite,
|
|
||||||
required this.time,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool same(DiskIOPiece other) => dev == other.dev;
|
bool same(DiskIOPiece other) => dev == other.dev;
|
||||||
@@ -349,10 +358,7 @@ class DiskUsage {
|
|||||||
final BigInt used;
|
final BigInt used;
|
||||||
final BigInt size;
|
final BigInt size;
|
||||||
|
|
||||||
DiskUsage({
|
DiskUsage({required this.used, required this.size});
|
||||||
required this.used,
|
|
||||||
required this.size,
|
|
||||||
});
|
|
||||||
|
|
||||||
double get usedPercent {
|
double get usedPercent {
|
||||||
// Avoid division by zero
|
// Avoid division by zero
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ enum Dist {
|
|||||||
rocky,
|
rocky,
|
||||||
deepin,
|
deepin,
|
||||||
coreelec,
|
coreelec,
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StringX on String {
|
extension StringX on String {
|
||||||
@@ -34,6 +33,4 @@ extension StringX on String {
|
|||||||
|
|
||||||
// Special rules
|
// Special rules
|
||||||
|
|
||||||
const _wrts = [
|
const _wrts = ['istoreos'];
|
||||||
'istoreos',
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ class Memory {
|
|||||||
final int free;
|
final int free;
|
||||||
final int avail;
|
final int avail;
|
||||||
|
|
||||||
const Memory({
|
const Memory({required this.total, required this.free, required this.avail});
|
||||||
required this.total,
|
|
||||||
required this.free,
|
|
||||||
required this.avail,
|
|
||||||
});
|
|
||||||
|
|
||||||
double get availPercent {
|
double get availPercent {
|
||||||
if (avail == 0) {
|
if (avail == 0) {
|
||||||
@@ -23,46 +19,99 @@ class Memory {
|
|||||||
static Memory parse(String raw) {
|
static Memory parse(String raw) {
|
||||||
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
||||||
|
|
||||||
final total = int.tryParse(items
|
final total = int.tryParse(
|
||||||
.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
|
items.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
|
||||||
?.group(2) ??
|
?.group(2) ?? '1') ?? 1;
|
||||||
'1') ??
|
final free = int.tryParse(
|
||||||
1;
|
items.firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
|
||||||
final free = int.tryParse(items
|
?.group(2) ?? '0') ?? 0;
|
||||||
.firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
|
final available = int.tryParse(
|
||||||
?.group(2) ??
|
items.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
|
||||||
'0') ??
|
?.group(2) ?? '0') ?? 0;
|
||||||
0;
|
|
||||||
final available = int.tryParse(items
|
|
||||||
.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
|
|
||||||
?.group(2) ??
|
|
||||||
'0') ??
|
|
||||||
0;
|
|
||||||
|
|
||||||
return Memory(
|
return Memory(total: total, free: free, avail: available);
|
||||||
total: total,
|
|
||||||
free: free,
|
|
||||||
avail: available,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
|
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 {
|
class Swap {
|
||||||
final int total;
|
final int total;
|
||||||
final int free;
|
final int free;
|
||||||
final int cached;
|
final int cached;
|
||||||
|
|
||||||
const Swap({
|
const Swap({required this.total, required this.free, required this.cached});
|
||||||
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -72,26 +121,16 @@ class Swap {
|
|||||||
static Swap parse(String raw) {
|
static Swap parse(String raw) {
|
||||||
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
||||||
|
|
||||||
final total = int.tryParse(items
|
final total = int.tryParse(
|
||||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
|
items.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
|
||||||
?.group(2) ??
|
?.group(2) ?? '1') ?? 0;
|
||||||
'1') ??
|
final free = int.tryParse(
|
||||||
0;
|
items.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
|
||||||
final free = int.tryParse(items
|
?.group(2) ?? '1') ?? 0;
|
||||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
|
final cached = int.tryParse(
|
||||||
?.group(2) ??
|
items.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
|
||||||
'1') ??
|
?.group(2) ?? '0') ?? 0;
|
||||||
0;
|
|
||||||
final cached = int.tryParse(items
|
|
||||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
|
|
||||||
?.group(2) ??
|
|
||||||
'0') ??
|
|
||||||
0;
|
|
||||||
|
|
||||||
return Swap(
|
return Swap(total: total, free: free, cached: cached);
|
||||||
total: total,
|
|
||||||
free: free,
|
|
||||||
cached: cached,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,7 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
|
|||||||
bool same(NetSpeedPart other) => device == other.device;
|
bool same(NetSpeedPart other) => device == other.device;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef CachedNetVals = ({
|
typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});
|
||||||
String sizeIn,
|
|
||||||
String sizeOut,
|
|
||||||
String speedIn,
|
|
||||||
String speedOut,
|
|
||||||
});
|
|
||||||
|
|
||||||
class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
||||||
NetSpeed(super.init1, super.init2);
|
NetSpeed(super.init1, super.init2);
|
||||||
@@ -32,20 +27,14 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
|||||||
devices.addAll(now.map((e) => e.device).toList());
|
devices.addAll(now.map((e) => e.device).toList());
|
||||||
|
|
||||||
realIfaces.clear();
|
realIfaces.clear();
|
||||||
realIfaces.addAll(devices
|
realIfaces.addAll(devices.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
|
||||||
.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
|
|
||||||
|
|
||||||
final sizeIn = this.sizeIn();
|
final sizeIn = this.sizeIn();
|
||||||
final sizeOut = this.sizeOut();
|
final sizeOut = this.sizeOut();
|
||||||
final speedIn = this.speedIn();
|
final speedIn = this.speedIn();
|
||||||
final speedOut = this.speedOut();
|
final speedOut = this.speedOut();
|
||||||
|
|
||||||
cachedVals = (
|
cachedVals = (sizeIn: sizeIn, sizeOut: sizeOut, speedIn: speedIn, speedOut: speedOut);
|
||||||
sizeIn: sizeIn,
|
|
||||||
sizeOut: sizeOut,
|
|
||||||
speedIn: speedIn,
|
|
||||||
speedOut: speedOut,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cached network device list
|
/// Cached network device list
|
||||||
@@ -58,15 +47,13 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
|||||||
/// Cached non-virtual network device prefix
|
/// Cached non-virtual network device prefix
|
||||||
final realIfaces = <String>[];
|
final realIfaces = <String>[];
|
||||||
|
|
||||||
CachedNetVals cachedVals =
|
CachedNetVals cachedVals = (sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
|
||||||
(sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
|
|
||||||
|
|
||||||
/// Time diff between [pre] and [now]
|
/// Time diff between [pre] and [now]
|
||||||
BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);
|
BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);
|
||||||
|
|
||||||
double speedInBytes(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
|
double speedInBytes(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
|
||||||
double speedOutBytes(int i) =>
|
double speedOutBytes(int i) => (now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
|
||||||
(now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
|
|
||||||
BigInt sizeInBytes(int i) => now[i].bytesIn;
|
BigInt sizeInBytes(int i) => now[i].bytesIn;
|
||||||
BigInt sizeOutBytes(int i) => now[i].bytesOut;
|
BigInt sizeOutBytes(int i) => now[i].bytesOut;
|
||||||
|
|
||||||
|
|||||||
@@ -35,25 +35,17 @@ class NvidiaSmi {
|
|||||||
.firstOrNull
|
.firstOrNull
|
||||||
?.innerText;
|
?.innerText;
|
||||||
final power = gpu.findElements('gpu_power_readings').firstOrNull;
|
final power = gpu.findElements('gpu_power_readings').firstOrNull;
|
||||||
final powerDraw =
|
final powerDraw = power?.findElements('power_draw').firstOrNull?.innerText;
|
||||||
power?.findElements('power_draw').firstOrNull?.innerText;
|
final powerLimit = power?.findElements('current_power_limit').firstOrNull?.innerText;
|
||||||
final powerLimit =
|
|
||||||
power?.findElements('current_power_limit').firstOrNull?.innerText;
|
|
||||||
final memory = gpu.findElements('fb_memory_usage').firstOrNull;
|
final memory = gpu.findElements('fb_memory_usage').firstOrNull;
|
||||||
final memoryUsed = memory?.findElements('used').firstOrNull?.innerText;
|
final memoryUsed = memory?.findElements('used').firstOrNull?.innerText;
|
||||||
final memoryTotal = memory?.findElements('total').firstOrNull?.innerText;
|
final memoryTotal = memory?.findElements('total').firstOrNull?.innerText;
|
||||||
final processes = gpu
|
final processes = gpu.findElements('processes').firstOrNull?.findElements('process_info');
|
||||||
.findElements('processes')
|
final memoryProcesses = List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) {
|
||||||
.firstOrNull
|
|
||||||
?.findElements('process_info');
|
|
||||||
final memoryProcesses =
|
|
||||||
List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) {
|
|
||||||
final process = processes?.elementAt(index);
|
final process = processes?.elementAt(index);
|
||||||
final pid = process?.findElements('pid').firstOrNull?.innerText;
|
final pid = process?.findElements('pid').firstOrNull?.innerText;
|
||||||
final name =
|
final name = process?.findElements('process_name').firstOrNull?.innerText;
|
||||||
process?.findElements('process_name').firstOrNull?.innerText;
|
final memory = process?.findElements('used_memory').firstOrNull?.innerText;
|
||||||
final memory =
|
|
||||||
process?.findElements('used_memory').firstOrNull?.innerText;
|
|
||||||
if (pid != null && name != null && memory != null) {
|
if (pid != null && name != null && memory != null) {
|
||||||
return NvidiaSmiMemProcess(
|
return NvidiaSmiMemProcess(
|
||||||
int.tryParse(pid) ?? 0,
|
int.tryParse(pid) ?? 0,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
final parseFailed = Exception('Parse failed');
|
final parseFailed = Exception('Parse failed');
|
||||||
final seqReg = RegExp(r'seq=(.+) ttl=(.+) time=(.+) ms');
|
final seqReg = RegExp(r'seq=(.+) ttl=(.+) time=(.+) ms');
|
||||||
final packetReg =
|
final packetReg = RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
|
||||||
RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
|
|
||||||
final timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms');
|
final timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms');
|
||||||
final timeAlpineReg = RegExp(r'round-trip min/avg/max = (.+)/(.+)/(.+) ms');
|
final timeAlpineReg = RegExp(r'round-trip min/avg/max = (.+)/(.+)/(.+) ms');
|
||||||
final ipReg = RegExp(r' \((\S+)\)');
|
final ipReg = RegExp(r' \((\S+)\)');
|
||||||
@@ -15,17 +14,13 @@ class PingResult {
|
|||||||
PingResult.parse(this.serverName, String raw) {
|
PingResult.parse(this.serverName, String raw) {
|
||||||
final lines = raw.split('\n');
|
final lines = raw.split('\n');
|
||||||
lines.removeWhere((element) => element.isEmpty);
|
lines.removeWhere((element) => element.isEmpty);
|
||||||
final statisticIndex =
|
final statisticIndex = lines.indexWhere((element) => element.startsWith('---'));
|
||||||
lines.indexWhere((element) => element.startsWith('---'));
|
|
||||||
if (statisticIndex == -1) {
|
if (statisticIndex == -1) {
|
||||||
throw parseFailed;
|
throw parseFailed;
|
||||||
}
|
}
|
||||||
final statisticRaw = lines.sublist(statisticIndex + 1);
|
final statisticRaw = lines.sublist(statisticIndex + 1);
|
||||||
statistic = PingStatistics.parse(statisticRaw);
|
statistic = PingStatistics.parse(statisticRaw);
|
||||||
results = lines
|
results = lines.sublist(1, statisticIndex).map((e) => PingSeqResult.parse(e)).toList();
|
||||||
.sublist(1, statisticIndex)
|
|
||||||
.map((e) => PingSeqResult.parse(e))
|
|
||||||
.toList();
|
|
||||||
ip = ipReg.firstMatch(lines[0])?.group(1);
|
ip = ipReg.firstMatch(lines[0])?.group(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ class PrivateKeyInfo {
|
|||||||
@JsonKey(name: 'private_key')
|
@JsonKey(name: 'private_key')
|
||||||
final String key;
|
final String key;
|
||||||
|
|
||||||
const PrivateKeyInfo({
|
const PrivateKeyInfo({required this.id, required this.key});
|
||||||
required this.id,
|
|
||||||
required this.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory PrivateKeyInfo.fromJson(Map<String, dynamic> json) => _$PrivateKeyInfoFromJson(json);
|
factory PrivateKeyInfo.fromJson(Map<String, dynamic> json) => _$PrivateKeyInfoFromJson(json);
|
||||||
|
|
||||||
|
|||||||
@@ -107,10 +107,7 @@ class PsResult {
|
|||||||
final List<Proc> procs;
|
final List<Proc> procs;
|
||||||
final String? error;
|
final String? error;
|
||||||
|
|
||||||
const PsResult({
|
const PsResult({required this.procs, this.error});
|
||||||
required this.procs,
|
|
||||||
this.error,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
|
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
|
||||||
final lines = raw.split('\n').map((e) => e.trim()).toList();
|
final lines = raw.split('\n').map((e) => e.trim()).toList();
|
||||||
@@ -167,14 +164,7 @@ class PsResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProcSortMode {
|
enum ProcSortMode { cpu, mem, pid, user, name }
|
||||||
cpu,
|
|
||||||
mem,
|
|
||||||
pid,
|
|
||||||
user,
|
|
||||||
name,
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _StrIndex on List<String> {
|
extension _StrIndex on List<String> {
|
||||||
int? indexOfOrNull(String val) {
|
int? indexOfOrNull(String val) {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ enum PveResType {
|
|||||||
qemu,
|
qemu,
|
||||||
node,
|
node,
|
||||||
storage,
|
storage,
|
||||||
sdn,
|
sdn;
|
||||||
;
|
|
||||||
|
|
||||||
static PveResType? fromString(String type) => switch (type.toLowerCase()) {
|
static PveResType? fromString(String type) => switch (type.toLowerCase()) {
|
||||||
'lxc' => PveResType.lxc,
|
'lxc' => PveResType.lxc,
|
||||||
@@ -334,13 +333,7 @@ final class PveSdn extends PveResIface implements PveCtrlIface {
|
|||||||
@override
|
@override
|
||||||
final String status;
|
final String status;
|
||||||
|
|
||||||
PveSdn({
|
PveSdn({required this.id, required this.type, required this.sdn, required this.node, required this.status});
|
||||||
required this.id,
|
|
||||||
required this.type,
|
|
||||||
required this.sdn,
|
|
||||||
required this.node,
|
|
||||||
required this.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
static PveSdn fromJson(Map<String, dynamic> json) {
|
static PveSdn fromJson(Map<String, dynamic> json) {
|
||||||
return PveSdn(
|
return PveSdn(
|
||||||
@@ -379,8 +372,7 @@ final class PveRes {
|
|||||||
|
|
||||||
bool get onlyOneNode => nodes.length == 1;
|
bool get onlyOneNode => nodes.length == 1;
|
||||||
|
|
||||||
int get length =>
|
int get length => qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
|
||||||
qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
|
|
||||||
|
|
||||||
PveResIface operator [](int index) {
|
PveResIface operator [](int index) {
|
||||||
if (index < nodes.length) {
|
if (index < nodes.length) {
|
||||||
@@ -432,29 +424,13 @@ final class PveRes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (old != null) {
|
if (old != null) {
|
||||||
qemus.reorder(
|
qemus.reorder(order: old.qemus.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||||
order: old.qemus.map((e) => e.id).toList(),
|
lxcs.reorder(order: old.lxcs.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||||
finder: (e, s) => e.id == s);
|
nodes.reorder(order: old.nodes.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||||
lxcs.reorder(
|
storages.reorder(order: old.storages.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||||
order: old.lxcs.map((e) => e.id).toList(),
|
sdns.reorder(order: old.sdns.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||||
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(
|
return PveRes(qemus: qemus, lxcs: lxcs, nodes: nodes, storages: storages, sdns: sdns);
|
||||||
qemus: qemus,
|
|
||||||
lxcs: lxcs,
|
|
||||||
nodes: nodes,
|
|
||||||
storages: storages,
|
|
||||||
sdns: sdns,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,7 @@ final class SensorItem {
|
|||||||
final SensorAdaptor adapter;
|
final SensorAdaptor adapter;
|
||||||
final Map<String, String> details;
|
final Map<String, String> details;
|
||||||
|
|
||||||
const SensorItem({
|
const SensorItem({required this.device, required this.adapter, required this.details});
|
||||||
required this.device,
|
|
||||||
required this.adapter,
|
|
||||||
required this.details,
|
|
||||||
});
|
|
||||||
|
|
||||||
String get toMarkdown {
|
String get toMarkdown {
|
||||||
final sb = StringBuffer();
|
final sb = StringBuffer();
|
||||||
@@ -72,8 +68,7 @@ final class SensorItem {
|
|||||||
final len = sensorLines.length;
|
final len = sensorLines.length;
|
||||||
if (len < 3) continue;
|
if (len < 3) continue;
|
||||||
final device = sensorLines.first;
|
final device = sensorLines.first;
|
||||||
final adapter =
|
final adapter = SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
|
||||||
SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
|
|
||||||
|
|
||||||
final details = <String, String>{};
|
final details = <String, String>{};
|
||||||
for (var idx = 2; idx < len; idx++) {
|
for (var idx = 2; idx < len; idx++) {
|
||||||
@@ -84,11 +79,7 @@ final class SensorItem {
|
|||||||
final value = detailParts[1].trim();
|
final value = detailParts[1].trim();
|
||||||
details[key] = value;
|
details[key] = value;
|
||||||
}
|
}
|
||||||
sensors.add(SensorItem(
|
sensors.add(SensorItem(device: device, adapter: adapter, details: details));
|
||||||
device: device,
|
|
||||||
adapter: adapter,
|
|
||||||
details: details,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sensors;
|
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/app/error.dart';
|
||||||
import 'package:server_box/data/model/server/custom.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.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/server/wol_cfg.dart';
|
||||||
import 'package:server_box/data/provider/server.dart';
|
import 'package:server_box/data/provider/server.dart';
|
||||||
import 'package:server_box/data/store/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.
|
/// It only applies to SSH terminal.
|
||||||
Map<String, String>? envs,
|
Map<String, String>? envs,
|
||||||
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
|
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
|
||||||
|
|
||||||
|
/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||||
|
@JsonKey(includeIfNull: false) SystemType? customSystemType,
|
||||||
}) = _Spi;
|
}) = _Spi;
|
||||||
|
|
||||||
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||||
@@ -132,13 +136,12 @@ extension Spix on Spi {
|
|||||||
custom: ServerCustom(
|
custom: ServerCustom(
|
||||||
pveAddr: 'http://localhost:8006',
|
pveAddr: 'http://localhost:8006',
|
||||||
pveIgnoreCert: false,
|
pveIgnoreCert: false,
|
||||||
cmds: {
|
cmds: {'echo': 'echo hello'},
|
||||||
'echo': 'echo hello',
|
|
||||||
},
|
|
||||||
preferTempDev: 'nvme-pci-0400',
|
preferTempDev: 'nvme-pci-0400',
|
||||||
logoUrl: 'https://example.com/logo.png',
|
logoUrl: 'https://example.com/logo.png',
|
||||||
),
|
),
|
||||||
id: 'id');
|
id: 'id',
|
||||||
|
);
|
||||||
|
|
||||||
bool get isRoot => user == 'root';
|
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
|
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
|
@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.
|
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
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -32,12 +33,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@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;
|
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$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
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// 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(
|
return _then(_self.copyWith(
|
||||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
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
|
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 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 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 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)
|
@JsonSerializable(includeIfNull: false)
|
||||||
class _Spi extends Spi {
|
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);
|
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||||
|
|
||||||
@override final String name;
|
@override final String name;
|
||||||
@@ -129,6 +131,8 @@ class _Spi extends Spi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override@JsonKey(fromJson: Spi.parseId) final String id;
|
@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
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -143,12 +147,12 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@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;
|
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$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
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// 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(
|
return _then(_Spi(
|
||||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
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
|
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 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 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 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),
|
(k, e) => MapEntry(k, e as String),
|
||||||
),
|
),
|
||||||
id: json['id'] == null ? '' : Spi.parseId(json['id']),
|
id: json['id'] == null ? '' : Spi.parseId(json['id']),
|
||||||
|
customSystemType: $enumDecodeNullable(
|
||||||
|
_$SystemTypeEnumMap,
|
||||||
|
json['customSystemType'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
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.wolCfg case final value?) 'wolCfg': value,
|
||||||
if (instance.envs case final value?) 'envs': value,
|
if (instance.envs case final value?) 'envs': value,
|
||||||
'id': instance.id,
|
'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:fl_lib/fl_lib.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
import 'package:server_box/data/model/server/amd.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/sensors.dart';
|
||||||
import 'package:server_box/data/model/server/server.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/system.dart';
|
||||||
|
import 'package:server_box/data/model/server/temp.dart';
|
||||||
|
import 'package:server_box/data/model/server/windows_parser.dart';
|
||||||
|
|
||||||
class ServerStatusUpdateReq {
|
class ServerStatusUpdateReq {
|
||||||
final ServerStatus ss;
|
final ServerStatus ss;
|
||||||
@@ -31,6 +35,7 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
|
|||||||
return switch (req.system) {
|
return switch (req.system) {
|
||||||
SystemType.linux => _getLinuxStatus(req),
|
SystemType.linux => _getLinuxStatus(req),
|
||||||
SystemType.bsd => _getBsdStatus(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 {
|
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||||
final segments = req.segments;
|
final segments = req.segments;
|
||||||
|
|
||||||
final time =
|
final time = int.tryParse(StatusCmdType.time.find(segments)) ??
|
||||||
int.tryParse(StatusCmdType.time.find(segments)) ??
|
|
||||||
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -210,11 +214,11 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
|
|||||||
Loggers.app.warning(e, s);
|
Loggers.app.warning(e, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// try {
|
try {
|
||||||
// req.ss.mem = parseBsdMem(BSDStatusCmdType.mem.find(segments));
|
req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.find(segments));
|
||||||
// } catch (e, s) {
|
} catch (e, s) {
|
||||||
// Loggers.app.warning(e, s);
|
Loggers.app.warning(e, s);
|
||||||
// }
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final uptime = _parseUpTime(BSDStatusCmdType.uptime.find(segments));
|
final uptime = _parseUpTime(BSDStatusCmdType.uptime.find(segments));
|
||||||
@@ -235,14 +239,49 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
|
|||||||
|
|
||||||
// raw:
|
// raw:
|
||||||
// 19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00
|
// 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) {
|
String? _parseUpTime(String raw) {
|
||||||
final splitedUp = raw.split('up ');
|
final splitedUp = raw.split('up ');
|
||||||
if (splitedUp.length == 2) {
|
if (splitedUp.length == 2) {
|
||||||
final splitedComma = splitedUp[1].split(', ');
|
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) {
|
if (splitedComma.length >= 2) {
|
||||||
return splitedComma[0];
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,3 +298,406 @@ String? _parseHostName(String raw) {
|
|||||||
if (raw.contains(ShellFunc.scriptFile)) return null;
|
if (raw.contains(ShellFunc.scriptFile)) return null;
|
||||||
return raw;
|
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'\$\{[^{}]+\}');
|
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
||||||
|
|
||||||
String fmtWithSpi(Spi spi) {
|
String fmtWithSpi(Spi spi) {
|
||||||
return script.replaceAllMapped(
|
return script.replaceAllMapped(fmtFinder, (match) {
|
||||||
fmtFinder,
|
|
||||||
(match) {
|
|
||||||
final key = match.group(0);
|
final key = match.group(0);
|
||||||
final func = fmtArgs[key];
|
final func = fmtArgs[key];
|
||||||
if (func != null) return func(spi);
|
if (func != null) return func(spi);
|
||||||
// If not found, return the original content for further processing
|
// If not found, return the original content for further processing
|
||||||
return key ?? '';
|
return key ?? '';
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> runInTerm(
|
Future<void> runInTerm(Terminal terminal, Spi spi, {bool autoEnter = false}) async {
|
||||||
Terminal terminal,
|
|
||||||
Spi spi, {
|
|
||||||
bool autoEnter = false,
|
|
||||||
}) async {
|
|
||||||
final argsFmted = fmtWithSpi(spi);
|
final argsFmted = fmtWithSpi(spi);
|
||||||
final matches = fmtFinder.allMatches(argsFmted);
|
final matches = fmtFinder.allMatches(argsFmted);
|
||||||
|
|
||||||
@@ -119,11 +112,7 @@ extension SnippetX on Snippet {
|
|||||||
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _doTermKeys(
|
Future<void> _doTermKeys(Terminal terminal, MapEntry<String, TerminalKey> termKey, String key) async {
|
||||||
Terminal terminal,
|
|
||||||
MapEntry<String, TerminalKey> termKey,
|
|
||||||
String key,
|
|
||||||
) async {
|
|
||||||
// if (termKey.value == TerminalKey.enter) {
|
// if (termKey.value == TerminalKey.enter) {
|
||||||
// terminal.keyInput(TerminalKey.enter);
|
// terminal.keyInput(TerminalKey.enter);
|
||||||
// return;
|
// return;
|
||||||
@@ -140,11 +129,7 @@ extension SnippetX on Snippet {
|
|||||||
// `${ctrl+ad}` -> `ctrla + d`
|
// `${ctrl+ad}` -> `ctrla + d`
|
||||||
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
||||||
if (chars.isEmpty) return;
|
if (chars.isEmpty) return;
|
||||||
final ok = terminal.charInput(
|
final ok = terminal.charInput(chars.codeUnitAt(0), ctrl: ctrlAlt.ctrl, alt: ctrlAlt.alt);
|
||||||
chars.codeUnitAt(0),
|
|
||||||
ctrl: ctrlAlt.ctrl,
|
|
||||||
alt: ctrlAlt.alt,
|
|
||||||
);
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
Loggers.app.warning('Failed to input: $key');
|
Loggers.app.warning('Failed to input: $key');
|
||||||
}
|
}
|
||||||
@@ -166,10 +151,7 @@ extension SnippetX on Snippet {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
|
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
|
||||||
static final fmtTermKeys = {
|
static final fmtTermKeys = {r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt};
|
||||||
r'${ctrl': TerminalKey.control,
|
|
||||||
r'${alt': TerminalKey.alt,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SnippetResult {
|
class SnippetResult {
|
||||||
@@ -177,11 +159,7 @@ class SnippetResult {
|
|||||||
final String result;
|
final String result;
|
||||||
final Duration time;
|
final Duration time;
|
||||||
|
|
||||||
SnippetResult({
|
SnippetResult({required this.dest, required this.result, required this.time});
|
||||||
required this.dest,
|
|
||||||
required this.result,
|
|
||||||
required this.time,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef SnippetFuncCtx = ({Terminal term, String raw});
|
typedef SnippetFuncCtx = ({Terminal term, String raw});
|
||||||
@@ -193,10 +171,7 @@ abstract final class SnippetFuncs {
|
|||||||
r'${enter': SnippetFuncs.enter,
|
r'${enter': SnippetFuncs.enter,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const help = {
|
static const help = {'sleep': 'Sleep for a few seconds', 'enter': 'Enter a few times'};
|
||||||
'sleep': 'Sleep for a few seconds',
|
|
||||||
'enter': 'Enter a few times',
|
|
||||||
};
|
|
||||||
|
|
||||||
static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
|
static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
|
||||||
final seconds = int.tryParse(ctx.raw);
|
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';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
|
|
||||||
enum SystemType {
|
enum SystemType {
|
||||||
linux._(linuxSign),
|
linux(linuxSign),
|
||||||
bsd._(bsdSign),
|
bsd(bsdSign),
|
||||||
;
|
windows(windowsSign);
|
||||||
|
|
||||||
final String value;
|
final String? value;
|
||||||
|
|
||||||
const SystemType._(this.value);
|
const SystemType([this.value]);
|
||||||
|
|
||||||
static const linuxSign = '__linux';
|
static const linuxSign = '__linux';
|
||||||
static const bsdSign = '__bsd';
|
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) {
|
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)) {
|
if (value.contains(bsdSign)) {
|
||||||
|
Loggers.app.info('System detected as BSD from signature in: $truncatedValue');
|
||||||
return SystemType.bsd;
|
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;
|
return SystemType.linux;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +61,8 @@ enum SystemType {
|
|||||||
return StatusCmdType.values.length;
|
return StatusCmdType.values.length;
|
||||||
case SystemType.bsd:
|
case SystemType.bsd:
|
||||||
return BSDStatusCmdType.values.length;
|
return BSDStatusCmdType.values.length;
|
||||||
|
case SystemType.windows:
|
||||||
|
return WindowsStatusCmdType.values.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ enum SystemdUnitFunc {
|
|||||||
reload,
|
reload,
|
||||||
enable,
|
enable,
|
||||||
disable,
|
disable,
|
||||||
status,
|
status;
|
||||||
;
|
|
||||||
|
|
||||||
IconData get icon => switch (this) {
|
IconData get icon => switch (this) {
|
||||||
start => Icons.play_arrow,
|
start => Icons.play_arrow,
|
||||||
@@ -26,8 +25,7 @@ enum SystemdUnitType {
|
|||||||
service,
|
service,
|
||||||
socket,
|
socket,
|
||||||
mount,
|
mount,
|
||||||
timer,
|
timer;
|
||||||
;
|
|
||||||
|
|
||||||
static SystemdUnitType? fromString(String? value) {
|
static SystemdUnitType? fromString(String? value) {
|
||||||
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
|
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
|
||||||
@@ -36,8 +34,7 @@ enum SystemdUnitType {
|
|||||||
|
|
||||||
enum SystemdUnitScope {
|
enum SystemdUnitScope {
|
||||||
system,
|
system,
|
||||||
user,
|
user;
|
||||||
;
|
|
||||||
|
|
||||||
Color? get color => switch (this) {
|
Color? get color => switch (this) {
|
||||||
system => Colors.red,
|
system => Colors.red,
|
||||||
@@ -57,8 +54,7 @@ enum SystemdUnitState {
|
|||||||
inactive,
|
inactive,
|
||||||
failed,
|
failed,
|
||||||
activating,
|
activating,
|
||||||
deactivating,
|
deactivating;
|
||||||
;
|
|
||||||
|
|
||||||
static SystemdUnitState? fromString(String? value) {
|
static SystemdUnitState? fromString(String? value) {
|
||||||
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
|
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
|
||||||
@@ -85,10 +81,7 @@ final class SystemdUnit {
|
|||||||
required this.state,
|
required this.state,
|
||||||
});
|
});
|
||||||
|
|
||||||
String getCmd({
|
String getCmd({required SystemdUnitFunc func, required bool isRoot}) {
|
||||||
required SystemdUnitFunc func,
|
|
||||||
required bool isRoot,
|
|
||||||
}) {
|
|
||||||
final prefix = scope.getCmdPrefix(isRoot);
|
final prefix = scope.getCmdPrefix(isRoot);
|
||||||
return '$prefix ${func.name} $name';
|
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> {
|
abstract class TimeSeq<T extends List<TimeSeqIface>> extends Fifo<T> {
|
||||||
/// Due to the design, at least two elements are required, otherwise [pre] /
|
/// Due to the design, at least two elements are required, otherwise [pre] /
|
||||||
/// [now] will throw.
|
/// [now] will throw.
|
||||||
TimeSeq(
|
TimeSeq(T init1, T init2, {super.capacity}) : super(list: [init1, init2]);
|
||||||
T init1,
|
|
||||||
T init2, {
|
|
||||||
super.capacity,
|
|
||||||
}) : super(list: [init1, init2]);
|
|
||||||
|
|
||||||
T get pre {
|
T get pre {
|
||||||
return _list[length - 2];
|
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 ip;
|
||||||
final String? pwd;
|
final String? pwd;
|
||||||
|
|
||||||
const WakeOnLanCfg({
|
const WakeOnLanCfg({required this.mac, required this.ip, this.pwd});
|
||||||
required this.mac,
|
|
||||||
required this.ip,
|
|
||||||
this.pwd,
|
|
||||||
});
|
|
||||||
|
|
||||||
(Object?, bool) validate() {
|
(Object?, bool) validate() {
|
||||||
final macValidation = MACAddress.validate(mac);
|
final macValidation = MACAddress.validate(mac);
|
||||||
@@ -39,10 +35,7 @@ final class WakeOnLanCfg {
|
|||||||
final mac_ = MACAddress(mac);
|
final mac_ = MACAddress(mac);
|
||||||
final pwd_ = pwd != null ? SecureONPassword(pwd!) : null;
|
final pwd_ = pwd != null ? SecureONPassword(pwd!) : null;
|
||||||
final obj = WakeOnLAN(ip_, mac_, password: pwd_);
|
final obj = WakeOnLAN(ip_, mac_, password: pwd_);
|
||||||
return obj.wake(
|
return obj.wake(repeat: 3, repeatDelay: const Duration(milliseconds: 500));
|
||||||
repeat: 3,
|
|
||||||
repeatDelay: const Duration(milliseconds: 500),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) => _$WakeOnLanCfgFromJson(json);
|
factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) => _$WakeOnLanCfgFromJson(json);
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ class SftpReq {
|
|||||||
Spi? jumpSpi;
|
Spi? jumpSpi;
|
||||||
String? jumpPrivateKey;
|
String? jumpPrivateKey;
|
||||||
|
|
||||||
SftpReq(
|
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
||||||
this.spi,
|
|
||||||
this.remotePath,
|
|
||||||
this.localPath,
|
|
||||||
this.type,
|
|
||||||
) {
|
|
||||||
final keyId = spi.keyId;
|
final keyId = spi.keyId;
|
||||||
if (keyId != null) {
|
if (keyId != null) {
|
||||||
privateKey = getPrivateKey(keyId);
|
privateKey = getPrivateKey(keyId);
|
||||||
@@ -44,15 +39,9 @@ class SftpReqStatus {
|
|||||||
Exception? error;
|
Exception? error;
|
||||||
Duration? spentTime;
|
Duration? spentTime;
|
||||||
|
|
||||||
SftpReqStatus({
|
SftpReqStatus({required this.req, required this.notifyListeners, this.completer})
|
||||||
required this.req,
|
: id = DateTime.now().microsecondsSinceEpoch {
|
||||||
required this.notifyListeners,
|
worker = SftpWorker(onNotify: onNotify, req: req)..init();
|
||||||
this.completer,
|
|
||||||
}) : id = DateTime.now().microsecondsSinceEpoch {
|
|
||||||
worker = SftpWorker(
|
|
||||||
onNotify: onNotify,
|
|
||||||
req: req,
|
|
||||||
)..init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ class SftpWorker {
|
|||||||
|
|
||||||
final worker = Worker();
|
final worker = Worker();
|
||||||
|
|
||||||
SftpWorker({
|
SftpWorker({required this.onNotify, required this.req});
|
||||||
required this.onNotify,
|
|
||||||
required this.req,
|
|
||||||
});
|
|
||||||
|
|
||||||
void _dispose() {
|
void _dispose() {
|
||||||
worker.dispose();
|
worker.dispose();
|
||||||
@@ -31,11 +28,7 @@ class SftpWorker {
|
|||||||
/// the threads
|
/// the threads
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (worker.isInitialized) worker.dispose();
|
if (worker.isInitialized) worker.dispose();
|
||||||
await worker.init(
|
await worker.init(mainMessageHandler, isolateMessageHandler, errorHandler: print);
|
||||||
mainMessageHandler,
|
|
||||||
isolateMessageHandler,
|
|
||||||
errorHandler: print,
|
|
||||||
);
|
|
||||||
worker.sendMessage(req);
|
worker.sendMessage(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,11 +39,7 @@ class SftpWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle the messages coming from the main
|
/// Handle the messages coming from the main
|
||||||
Future<void> isolateMessageHandler(
|
Future<void> isolateMessageHandler(dynamic data, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||||
dynamic data,
|
|
||||||
SendPort mainSendPort,
|
|
||||||
SendErrorFunction sendError,
|
|
||||||
) async {
|
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case final SftpReq val:
|
case final SftpReq val:
|
||||||
switch (val.type) {
|
switch (val.type) {
|
||||||
@@ -67,11 +56,7 @@ Future<void> isolateMessageHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _download(
|
Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||||
SftpReq req,
|
|
||||||
SendPort mainSendPort,
|
|
||||||
SendErrorFunction sendError,
|
|
||||||
) async {
|
|
||||||
try {
|
try {
|
||||||
mainSendPort.send(SftpWorkerStatus.preparing);
|
mainSendPort.send(SftpWorkerStatus.preparing);
|
||||||
final watch = Stopwatch()..start();
|
final watch = Stopwatch()..start();
|
||||||
@@ -127,11 +112,7 @@ Future<void> _download(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _upload(
|
Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||||
SftpReq req,
|
|
||||||
SendPort mainSendPort,
|
|
||||||
SendErrorFunction sendError,
|
|
||||||
) async {
|
|
||||||
try {
|
try {
|
||||||
mainSendPort.send(SftpWorkerStatus.preparing);
|
mainSendPort.send(SftpWorkerStatus.preparing);
|
||||||
final watch = Stopwatch()..start();
|
final watch = Stopwatch()..start();
|
||||||
@@ -156,9 +137,7 @@ Future<void> _upload(
|
|||||||
// If remote exists, overwrite it
|
// If remote exists, overwrite it
|
||||||
final file = await sftp.open(
|
final file = await sftp.open(
|
||||||
req.remotePath,
|
req.remotePath,
|
||||||
mode: SftpFileOpenMode.truncate |
|
mode: SftpFileOpenMode.truncate | SftpFileOpenMode.create | SftpFileOpenMode.write,
|
||||||
SftpFileOpenMode.create |
|
|
||||||
SftpFileOpenMode.write,
|
|
||||||
);
|
);
|
||||||
final writer = file.write(
|
final writer = file.write(
|
||||||
localFile,
|
localFile,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ enum VirtKey {
|
|||||||
f9,
|
f9,
|
||||||
f10,
|
f10,
|
||||||
f11,
|
f11,
|
||||||
f12;
|
f12,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension VirtKeyX on VirtKey {
|
extension VirtKeyX on VirtKey {
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ part 'app.freezed.dart';
|
|||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class AppState with _$AppState {
|
abstract class AppState with _$AppState {
|
||||||
const factory AppState({
|
const factory AppState({@Default(false) bool desktopMode}) = _AppState;
|
||||||
@Default(false) bool desktopMode,
|
|
||||||
}) = _AppState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@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/model/container/type.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
final _dockerNotFound =
|
final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
||||||
RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
|
||||||
|
|
||||||
class ContainerProvider extends ChangeNotifier {
|
class ContainerProvider extends ChangeNotifier {
|
||||||
final SSHClient? client;
|
final SSHClient? client;
|
||||||
@@ -90,11 +89,7 @@ class ContainerProvider extends ChangeNotifier {
|
|||||||
final includeStats = Stores.setting.containerParseStat.fetch();
|
final includeStats = Stores.setting.containerParseStat.fetch();
|
||||||
|
|
||||||
var raw = '';
|
var raw = '';
|
||||||
final cmd = _wrap(ContainerCmdType.execAll(
|
final cmd = _wrap(ContainerCmdType.execAll(type, sudo: sudo, includeStats: includeStats));
|
||||||
type,
|
|
||||||
sudo: sudo,
|
|
||||||
includeStats: includeStats,
|
|
||||||
));
|
|
||||||
final code = await client?.execWithPwd(
|
final code = await client?.execWithPwd(
|
||||||
cmd,
|
cmd,
|
||||||
context: context,
|
context: context,
|
||||||
@@ -130,10 +125,7 @@ class ContainerProvider extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
version = json.decode(verRaw)['Client']['Version'];
|
version = json.decode(verRaw)['Client']['Version'];
|
||||||
} catch (e, trace) {
|
} catch (e, trace) {
|
||||||
error = ContainerErr(
|
error = ContainerErr(type: ContainerErrType.invalidVersion, message: '$e');
|
||||||
type: ContainerErrType.invalidVersion,
|
|
||||||
message: '$e',
|
|
||||||
);
|
|
||||||
Loggers.app.warning('Container version failed', e, trace);
|
Loggers.app.warning('Container version failed', e, trace);
|
||||||
} finally {
|
} finally {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -150,10 +142,7 @@ class ContainerProvider extends ChangeNotifier {
|
|||||||
lines.removeWhere((element) => element.isEmpty);
|
lines.removeWhere((element) => element.isEmpty);
|
||||||
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
|
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
|
||||||
} catch (e, trace) {
|
} catch (e, trace) {
|
||||||
error = ContainerErr(
|
error = ContainerErr(type: ContainerErrType.parsePs, message: '$e');
|
||||||
type: ContainerErrType.parsePs,
|
|
||||||
message: '$e',
|
|
||||||
);
|
|
||||||
Loggers.app.warning('Container ps failed', e, trace);
|
Loggers.app.warning('Container ps failed', e, trace);
|
||||||
} finally {
|
} finally {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -173,10 +162,7 @@ class ContainerProvider extends ChangeNotifier {
|
|||||||
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
|
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
|
||||||
}
|
}
|
||||||
} catch (e, trace) {
|
} catch (e, trace) {
|
||||||
error = ContainerErr(
|
error = ContainerErr(type: ContainerErrType.parseImages, message: '$e');
|
||||||
type: ContainerErrType.parseImages,
|
|
||||||
message: '$e',
|
|
||||||
);
|
|
||||||
Loggers.app.warning('Container images failed', e, trace);
|
Loggers.app.warning('Container images failed', e, trace);
|
||||||
} finally {
|
} finally {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -199,10 +185,7 @@ class ContainerProvider extends ChangeNotifier {
|
|||||||
item.parseStats(statsLine);
|
item.parseStats(statsLine);
|
||||||
}
|
}
|
||||||
} catch (e, trace) {
|
} catch (e, trace) {
|
||||||
error = ContainerErr(
|
error = ContainerErr(type: ContainerErrType.parseStats, message: '$e');
|
||||||
type: ContainerErrType.parseStats,
|
|
||||||
message: '$e',
|
|
||||||
);
|
|
||||||
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
|
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
|
||||||
} finally {
|
} finally {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -261,10 +244,7 @@ class ContainerProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
return ContainerErr(
|
return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim());
|
||||||
type: ContainerErrType.unknown,
|
|
||||||
message: errs.join('\n').trim(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (autoRefresh) await refresh();
|
if (autoRefresh) await refresh();
|
||||||
return null;
|
return null;
|
||||||
@@ -288,40 +268,32 @@ enum ContainerCmdType {
|
|||||||
version,
|
version,
|
||||||
ps,
|
ps,
|
||||||
stats,
|
stats,
|
||||||
images,
|
images
|
||||||
// No specific commands needed for prune actions as they are simple
|
// No specific commands needed for prune actions as they are simple
|
||||||
// and don't require splitting output with ShellFunc.seperator
|
// and don't require splitting output with ShellFunc.seperator
|
||||||
;
|
;
|
||||||
|
|
||||||
String exec(
|
String exec(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
||||||
ContainerType type, {
|
|
||||||
bool sudo = false,
|
|
||||||
bool includeStats = false,
|
|
||||||
}) {
|
|
||||||
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
|
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
||||||
ContainerCmdType.ps => switch (type) {
|
ContainerCmdType.ps => switch (type) {
|
||||||
/// TODO: Rollback to json format when permformance recovers.
|
/// TODO: Rollback to json format when permformance recovers.
|
||||||
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
||||||
ContainerType.docker => '$prefix ps -a --format "table {{printf \\"'
|
ContainerType.docker =>
|
||||||
|
'$prefix ps -a --format "table {{printf \\"'
|
||||||
'%-15.15s '
|
'%-15.15s '
|
||||||
'%-30.30s '
|
'%-30.30s '
|
||||||
'${"%-50.50s " * 2}\\"'
|
'${"%-50.50s " * 2}\\"'
|
||||||
' .ID .Status .Names .Image}}"',
|
' .ID .Status .Names .Image}}"',
|
||||||
ContainerType.podman => '$prefix ps -a $_jsonFmt',
|
ContainerType.podman => '$prefix ps -a $_jsonFmt',
|
||||||
},
|
},
|
||||||
ContainerCmdType.stats =>
|
ContainerCmdType.stats => includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
|
||||||
includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
|
|
||||||
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
|
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static String execAll(
|
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
||||||
ContainerType type, {
|
|
||||||
bool sudo = false,
|
|
||||||
bool includeStats = false,
|
|
||||||
}) {
|
|
||||||
return ContainerCmdType.values
|
return ContainerCmdType.values
|
||||||
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
|
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
|
||||||
.join('\necho ${ShellFunc.seperator}\n');
|
.join('\necho ${ShellFunc.seperator}\n');
|
||||||
|
|||||||
@@ -86,15 +86,18 @@ final class PveProvider extends ChangeNotifier {
|
|||||||
forward.stream.cast<List<int>>().pipe(socket);
|
forward.stream.cast<List<int>>().pipe(socket);
|
||||||
socket.cast<List<int>>().pipe(forward.sink);
|
socket.cast<List<int>>().pipe(forward.sink);
|
||||||
});
|
});
|
||||||
final newUrl = Uri.parse(addr)
|
final newUrl = Uri.parse(
|
||||||
.replace(host: 'localhost', port: _localPort)
|
addr,
|
||||||
.toString();
|
).replace(host: 'localhost', port: _localPort).toString();
|
||||||
debugPrint('Forwarding $newUrl to $addr');
|
debugPrint('Forwarding $newUrl to $addr');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ConnectionTask<Socket>> cf(
|
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 serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
|
||||||
final _localPort = serverSocket.port;
|
final _localPort = serverSocket.port;
|
||||||
serverSocket.listen((socket) async {
|
serverSocket.listen((socket) async {
|
||||||
@@ -105,8 +108,11 @@ final class PveProvider extends ChangeNotifier {
|
|||||||
});*/
|
});*/
|
||||||
|
|
||||||
if (url.isScheme('https')) {
|
if (url.isScheme('https')) {
|
||||||
return SecureSocket.startConnect('localhost', _localPort,
|
return SecureSocket.startConnect(
|
||||||
onBadCertificate: (_) => true);
|
'localhost',
|
||||||
|
_localPort,
|
||||||
|
onBadCertificate: (_) => true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return Socket.startConnect('localhost', _localPort);
|
return Socket.startConnect('localhost', _localPort);
|
||||||
}
|
}
|
||||||
@@ -119,7 +125,7 @@ final class PveProvider extends ChangeNotifier {
|
|||||||
'username': spi.user,
|
'username': spi.user,
|
||||||
'password': spi.pwd,
|
'password': spi.pwd,
|
||||||
'realm': 'pam',
|
'realm': 'pam',
|
||||||
'new-format': '1'
|
'new-format': '1',
|
||||||
},
|
},
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType},
|
headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType},
|
||||||
@@ -151,8 +157,10 @@ final class PveProvider extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final resp = await session.get('$addr/api2/json/cluster/resources');
|
final resp = await session.get('$addr/api2/json/cluster/resources');
|
||||||
final res = resp.data['data'] as List;
|
final res = resp.data['data'] as List;
|
||||||
final result =
|
final result = await Computer.shared.start(PveRes.parse, (
|
||||||
await Computer.shared.start(PveRes.parse, (res, data.value));
|
res,
|
||||||
|
data.value,
|
||||||
|
));
|
||||||
data.value = result;
|
data.value = result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Loggers.app.warning('PVE list failed', 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 {
|
Future<bool> reboot(String node, String id) async {
|
||||||
await connected.future;
|
await connected.future;
|
||||||
final resp =
|
final resp = await session.post(
|
||||||
await session.post('$addr/api2/json/nodes/$node/$id/status/reboot');
|
'$addr/api2/json/nodes/$node/$id/status/reboot',
|
||||||
|
);
|
||||||
return _isCtrlSuc(resp);
|
return _isCtrlSuc(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> start(String node, String id) async {
|
Future<bool> start(String node, String id) async {
|
||||||
await connected.future;
|
await connected.future;
|
||||||
final resp =
|
final resp = await session.post(
|
||||||
await session.post('$addr/api2/json/nodes/$node/$id/status/start');
|
'$addr/api2/json/nodes/$node/$id/status/start',
|
||||||
|
);
|
||||||
return _isCtrlSuc(resp);
|
return _isCtrlSuc(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> stop(String node, String id) async {
|
Future<bool> stop(String node, String id) async {
|
||||||
await connected.future;
|
await connected.future;
|
||||||
final resp =
|
final resp = await session.post(
|
||||||
await session.post('$addr/api2/json/nodes/$node/$id/status/stop');
|
'$addr/api2/json/nodes/$node/$id/status/stop',
|
||||||
|
);
|
||||||
return _isCtrlSuc(resp);
|
return _isCtrlSuc(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> shutdown(String node, String id) async {
|
Future<bool> shutdown(String node, String id) async {
|
||||||
await connected.future;
|
await connected.future;
|
||||||
final resp =
|
final resp = await session.post(
|
||||||
await session.post('$addr/api2/json/nodes/$node/$id/status/shutdown');
|
'$addr/api2/json/nodes/$node/$id/status/shutdown',
|
||||||
|
);
|
||||||
return _isCtrlSuc(resp);
|
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/sync.dart';
|
||||||
import 'package:server_box/core/utils/server.dart';
|
import 'package:server_box/core/utils/server.dart';
|
||||||
import 'package:server_box/core/utils/ssh_auth.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/error.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
import 'package:server_box/data/model/server/server.dart';
|
import 'package:server_box/data/model/server/server.dart';
|
||||||
@@ -32,6 +33,8 @@ class ServerProvider extends Provider {
|
|||||||
|
|
||||||
static final _manualDisconnectedIds = <String>{};
|
static final _manualDisconnectedIds = <String>{};
|
||||||
|
|
||||||
|
static final _serverIdsUpdating = <String, Future<void>?>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
super.load();
|
super.load();
|
||||||
@@ -124,11 +127,35 @@ class ServerProvider extends Provider {
|
|||||||
return;
|
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 {
|
static Future<void> startAutoRefresh() async {
|
||||||
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
||||||
stopAutoRefresh();
|
stopAutoRefresh();
|
||||||
@@ -305,13 +332,17 @@ class ServerProvider extends Provider {
|
|||||||
_setServerState(s, ServerConn.connected);
|
_setServerState(s, ServerConn.connected);
|
||||||
|
|
||||||
try {
|
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 (_, 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.add(scriptRaw);
|
||||||
session.stdin.close();
|
session.stdin.close();
|
||||||
}, entry: ShellFunc.getInstallShellCmd(spi.id));
|
}, entry: ShellFunc.getInstallShellCmd(spi.id, systemType: detectedSystemType));
|
||||||
if (writeScriptResult.isNotEmpty) {
|
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
|
||||||
ShellFunc.switchScriptDir(spi.id);
|
ShellFunc.switchScriptDir(spi.id, systemType: detectedSystemType);
|
||||||
throw writeScriptResult;
|
throw writeScriptResult;
|
||||||
}
|
}
|
||||||
} on SSHAuthAbortError catch (e) {
|
} on SSHAuthAbortError catch (e) {
|
||||||
@@ -351,7 +382,8 @@ class ServerProvider extends Provider {
|
|||||||
String? raw;
|
String? raw;
|
||||||
|
|
||||||
try {
|
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();
|
segments = raw?.split(ShellFunc.seperator).map((e) => e.trim()).toList();
|
||||||
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
|
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
|
||||||
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
||||||
|
|||||||
@@ -44,10 +44,8 @@ final class SystemdProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final parsedUserUnits =
|
final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);
|
||||||
await _parseUnitObj(userUnits, SystemdUnitScope.user);
|
final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);
|
||||||
final parsedSystemUnits =
|
|
||||||
await _parseUnitObj(systemUnits, SystemdUnitScope.system);
|
|
||||||
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
|
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Loggers.app.warning('Parse systemd', e, s);
|
Loggers.app.warning('Parse systemd', e, s);
|
||||||
@@ -56,14 +54,10 @@ final class SystemdProvider {
|
|||||||
isBusy.value = false;
|
isBusy.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SystemdUnit>> _parseUnitObj(
|
Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {
|
||||||
List<String> unitNames,
|
final unitNames_ = unitNames.map((e) => e.trim().split('/').last.split('.').first).toList();
|
||||||
SystemdUnitScope scope,
|
final script =
|
||||||
) async {
|
'''
|
||||||
final unitNames_ = unitNames
|
|
||||||
.map((e) => e.trim().split('/').last.split('.').first)
|
|
||||||
.toList();
|
|
||||||
final script = '''
|
|
||||||
for unit in ${unitNames_.join(' ')}; do
|
for unit in ${unitNames_.join(' ')}; do
|
||||||
state=\$(systemctl show --no-pager \$unit)
|
state=\$(systemctl show --no-pager \$unit)
|
||||||
echo -n "${ShellFunc.seperator}\n\$state"
|
echo -n "${ShellFunc.seperator}\n\$state"
|
||||||
@@ -108,13 +102,9 @@ done
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedUnits.add(SystemdUnit(
|
parsedUnits.add(
|
||||||
name: name,
|
SystemdUnit(name: name, type: unitType, scope: scope, state: unitState, description: description),
|
||||||
type: unitType,
|
);
|
||||||
scope: scope,
|
|
||||||
state: unitState,
|
|
||||||
description: description,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedUnits.sort((a, b) {
|
parsedUnits.sort((a, b) {
|
||||||
@@ -131,7 +121,8 @@ done
|
|||||||
return parsedUnits;
|
return parsedUnits;
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _getUnitsCmd = '''
|
late final _getUnitsCmd =
|
||||||
|
'''
|
||||||
get_files() {
|
get_files() {
|
||||||
unit_type=\$1
|
unit_type=\$1
|
||||||
base_dir=\$2
|
base_dir=\$2
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ class ContainerStore extends HiveStore {
|
|||||||
ContainerType getType([String id = '']) {
|
ContainerType getType([String id = '']) {
|
||||||
final cfg = box.get(_keyConfig + id);
|
final cfg = box.get(_keyConfig + id);
|
||||||
if (cfg != null) {
|
if (cfg != null) {
|
||||||
final type =
|
final type = ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);
|
||||||
ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);
|
|
||||||
if (type != null) return type;
|
if (type != null) return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ class _ListHistory {
|
|||||||
final String _name;
|
final String _name;
|
||||||
final Box _box;
|
final Box _box;
|
||||||
|
|
||||||
_ListHistory({
|
_ListHistory({required Box box, required String name})
|
||||||
required Box box,
|
: _box = box,
|
||||||
required String name,
|
|
||||||
}) : _box = box,
|
|
||||||
_name = name,
|
_name = name,
|
||||||
_history = box.get(name, defaultValue: [])!;
|
_history = box.get(name, defaultValue: [])!;
|
||||||
|
|
||||||
@@ -28,10 +26,8 @@ class _MapHistory {
|
|||||||
final String _name;
|
final String _name;
|
||||||
final Box _box;
|
final Box _box;
|
||||||
|
|
||||||
_MapHistory({
|
_MapHistory({required Box box, required String name})
|
||||||
required Box box,
|
: _box = box,
|
||||||
required String name,
|
|
||||||
}) : _box = box,
|
|
||||||
_name = name,
|
_name = name,
|
||||||
_history = box.get(name, defaultValue: <dynamic, dynamic>{})!;
|
_history = box.get(name, defaultValue: <dynamic, dynamic>{})!;
|
||||||
|
|
||||||
@@ -56,6 +52,5 @@ class HistoryStore extends HiveStore {
|
|||||||
late final sshCmds = _ListHistory(box: box, name: 'sshCmds');
|
late final sshCmds = _ListHistory(box: box, name: 'sshCmds');
|
||||||
|
|
||||||
/// Notify users that this app will write script to server to works properly
|
/// Notify users that this app will write script to server to works properly
|
||||||
late final writeScriptTipShown =
|
late final writeScriptTipShown = propertyDefault('writeScriptTipShown', false);
|
||||||
propertyDefault('writeScriptTipShown', false);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get acceptBeta => '接受测试版更新推送';
|
String get acceptBeta => '接受测试版更新推送';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get addSystemPrivateKeyTip => '当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?';
|
String get addSystemPrivateKeyTip => '检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get added2List => '已添加至任务列表';
|
String get added2List => '已添加至任务列表';
|
||||||
@@ -24,13 +24,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get addr => '地址';
|
String get addr => '地址';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get alreadyLastDir => '已经是最上层目录了';
|
String get alreadyLastDir => '已是顶级目录';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get authFailTip => '认证失败,请检查密码/密钥/主机/用户等是否错误';
|
String get authFailTip => '认证失败,请检查连接信息是否正确';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get autoBackupConflict => '只能同时开启一个自动备份';
|
String get autoBackupConflict => '仅可启用一个自动备份任务';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get autoConnect => '自动连接';
|
String get autoConnect => '自动连接';
|
||||||
@@ -42,10 +42,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get autoUpdateHomeWidget => '自动更新桌面小部件';
|
String get autoUpdateHomeWidget => '自动更新桌面小部件';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupTip => '导出的数据可以使用密码加密,请妥善保管。';
|
String get backupTip => '导出数据可通过密码加密,请妥善保管。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupVersionNotMatch => '备份版本不匹配,无法恢复';
|
String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupPassword => '备份密码';
|
String get backupPassword => '备份密码';
|
||||||
@@ -76,7 +76,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get bgRunTip =>
|
String get bgRunTip =>
|
||||||
'此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请修改省电策略为“无限制”。';
|
'此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请将省电策略改为“无限制”。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get closeAfterSave => '保存后关闭';
|
String get closeAfterSave => '保存后关闭';
|
||||||
@@ -132,10 +132,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get desktopTerminalTip => '启动 SSH 连接所用的终端模拟器命令';
|
String get desktopTerminalTip => '启动 SSH 连接所用的终端模拟器命令';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dirEmpty => '请确保文件夹为空';
|
String get dirEmpty => '请确保目录为空';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get disconnected => '连接断开';
|
String get disconnected => '已断开连接';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get disk => '磁盘';
|
String get disk => '磁盘';
|
||||||
@@ -160,11 +160,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String dockerImagesFmt(Object count) {
|
String dockerImagesFmt(Object count) {
|
||||||
return '共 $count 个镜像';
|
return '$count 个镜像';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dockerNotInstalled => 'Docker 未安装';
|
String get dockerNotInstalled => '未安装 Docker';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String dockerStatusRunningAndStoppedFmt(
|
String dockerStatusRunningAndStoppedFmt(
|
||||||
@@ -183,7 +183,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get doubleColumnMode => '双列模式';
|
String get doubleColumnMode => '双列模式';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doubleColumnTip => '此选项仅开启功能,实际是否能开启还取决于设备的宽度';
|
String get doubleColumnTip => '此选项仅用于启用该功能,是否生效取决于设备宽度';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get editVirtKeys => '编辑虚拟按键';
|
String get editVirtKeys => '编辑虚拟按键';
|
||||||
@@ -192,7 +192,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get editor => '编辑器';
|
String get editor => '编辑器';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get editorHighlightTip => '目前的代码高亮性能较为糟糕,可以选择关闭以改善。';
|
String get editorHighlightTip => '代码高亮功能可能影响性能,可选择关闭。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get emulator => '模拟器';
|
String get emulator => '模拟器';
|
||||||
@@ -246,7 +246,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get fullScreenJitter => '全屏模式抖动';
|
String get fullScreenJitter => '全屏模式抖动';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fullScreenJitterHelp => '防止烧屏';
|
String get fullScreenJitterHelp => '用于防止屏幕烧屏';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fullScreenTip => '当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。';
|
String get fullScreenTip => '当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。';
|
||||||
@@ -271,7 +271,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String httpFailedWithCode(Object code) {
|
String httpFailedWithCode(Object code) {
|
||||||
return '请求失败, 状态码: $code';
|
return '请求失败,状态码: $code';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -294,7 +294,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get installDockerWithUrl =>
|
String get installDockerWithUrl =>
|
||||||
'请先 https://docs.docker.com/engine/install docker';
|
'请先前往 https://docs.docker.com/engine/install 安装 Docker';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get invalid => '无效';
|
String get invalid => '无效';
|
||||||
@@ -303,7 +303,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get jumpServer => '跳板服务器';
|
String get jumpServer => '跳板服务器';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get keepForeground => '请保持应用处于前台!';
|
String get keepForeground => '请将应用保持在前台运行';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get keepStatusWhenErr => '保留上次的服务器状态';
|
String get keepStatusWhenErr => '保留上次的服务器状态';
|
||||||
@@ -344,7 +344,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get maxRetryCount => '服务器尝试重连次数';
|
String get maxRetryCount => '服务器尝试重连次数';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get maxRetryCountEqual0 => '会无限重试';
|
String get maxRetryCountEqual0 => '将无限次重试';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get min => '最小';
|
String get min => '最小';
|
||||||
@@ -409,7 +409,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get openLastPath => '打开上次的路径';
|
String get openLastPath => '打开上次的路径';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get openLastPathTip => '不同的服务器会有不同的记录,且记录的是退出时的路径';
|
String get openLastPathTip => '将为每台服务器记录其最后访问路径';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get parseContainerStatsTip => 'Docker 解析占用状态较为缓慢';
|
String get parseContainerStatsTip => 'Docker 解析占用状态较为缓慢';
|
||||||
@@ -462,7 +462,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get pveIgnoreCertTip => '不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项';
|
String get pveIgnoreCertTip => '不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pveLoginFailed => '登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。';
|
String get pveLoginFailed => '登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pveVersionLow => '当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用';
|
String get pveVersionLow => '当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用';
|
||||||
@@ -544,7 +544,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹';
|
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpSSHConnected => 'SFTP 已连接...';
|
String get sftpSSHConnected => 'SFTP 已连接';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpShowFoldersFirst => '文件夹显示在前';
|
String get sftpShowFoldersFirst => '文件夹显示在前';
|
||||||
@@ -575,7 +575,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String spentTime(Object time) {
|
String spentTime(Object time) {
|
||||||
return '耗时: $time';
|
return '耗时:$time';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -683,7 +683,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get update => '更新';
|
String get update => '更新';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get updateIntervalEqual0 => '你设置为 0,服务器状态不会自动刷新。\n且不能计算 CPU 使用情况。';
|
String get updateIntervalEqual0 => '设置为 0 将不自动刷新服务器状态。\n且无法计算 CPU 使用率。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get updateServerStatusInterval => '服务器状态刷新间隔';
|
String get updateServerStatusInterval => '服务器状态刷新间隔';
|
||||||
@@ -743,7 +743,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get whenOpenApp => '当打开 App 时';
|
String get whenOpenApp => '当打开 App 时';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wolTip => '在配置 WOL 后,每次连接服务器都会先发送一次 WOL 请求';
|
String get wolTip => '配置 WOL 后,每次连接服务器时将自动发送唤醒请求';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get write => '写';
|
String get write => '写';
|
||||||
@@ -767,7 +767,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get acceptBeta => '接受測試版更新推送';
|
String get acceptBeta => '接受測試版更新推送';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get addSystemPrivateKeyTip => '目前沒有任何私鑰,是否新增系統原有的 (~/.ssh/id_rsa)?';
|
String get addSystemPrivateKeyTip => '偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get added2List => '已新增至任務清單';
|
String get added2List => '已新增至任務清單';
|
||||||
@@ -776,13 +776,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get addr => '位址';
|
String get addr => '位址';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get alreadyLastDir => '已經是最上層目錄了';
|
String get alreadyLastDir => '已是頂層目錄';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get authFailTip => '認證失敗,請檢查密碼/金鑰/主機/使用者等是否錯誤。';
|
String get authFailTip => '認證失敗,請檢查連線資訊是否正確';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get autoBackupConflict => '只能同時開啓一個自動備份';
|
String get autoBackupConflict => '僅能啟用一項自動備份任務';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get autoConnect => '自動連線';
|
String get autoConnect => '自動連線';
|
||||||
@@ -794,10 +794,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get autoUpdateHomeWidget => '自動更新桌面小工具';
|
String get autoUpdateHomeWidget => '自動更新桌面小工具';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupTip => '匯出的資料可以使用密碼加密。 \n請妥善保管。';
|
String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupVersionNotMatch => '備份版本不相符,無法還原';
|
String get backupVersionNotMatch => '備份版本不相容,無法還原';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupPassword => '備份密碼';
|
String get backupPassword => '備份密碼';
|
||||||
@@ -828,7 +828,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get bgRunTip =>
|
String get bgRunTip =>
|
||||||
'此開關只代表程式會嘗試在背景執行,具體能否在後臺執行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池最佳化”,MIUI / HyperOS 請修改省電策略為“無限制”。';
|
'此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get closeAfterSave => '儲存後關閉';
|
String get closeAfterSave => '儲存後關閉';
|
||||||
@@ -884,10 +884,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get desktopTerminalTip => '啟動 SSH 連線時用於打開終端機模擬器的指令。';
|
String get desktopTerminalTip => '啟動 SSH 連線時用於打開終端機模擬器的指令。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dirEmpty => '請確保資料夾為空';
|
String get dirEmpty => '請確保目錄為空';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get disconnected => '連線中斷';
|
String get disconnected => '已中斷連線';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get disk => '磁碟';
|
String get disk => '磁碟';
|
||||||
@@ -912,11 +912,11 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String dockerImagesFmt(Object count) {
|
String dockerImagesFmt(Object count) {
|
||||||
return '共 $count 個映像檔';
|
return '$count 個映像檔';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dockerNotInstalled => 'Docker 未安裝';
|
String get dockerNotInstalled => '未安裝 Docker';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String dockerStatusRunningAndStoppedFmt(
|
String dockerStatusRunningAndStoppedFmt(
|
||||||
@@ -935,7 +935,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get doubleColumnMode => '雙列模式';
|
String get doubleColumnMode => '雙列模式';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doubleColumnTip => '此選項僅開啟功能,實際是否能開啟還取決於設備的頻寬';
|
String get doubleColumnTip => '此選項僅用於啟用此功能,是否生效取決於裝置寬度';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get editVirtKeys => '編輯虛擬按鍵';
|
String get editVirtKeys => '編輯虛擬按鍵';
|
||||||
@@ -944,7 +944,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get editor => '編輯器';
|
String get editor => '編輯器';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get editorHighlightTip => '目前的程式碼標記效能較為糟糕,可以選擇關閉以改善。';
|
String get editorHighlightTip => '程式碼高亮功能可能影響效能,可選擇性關閉。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get emulator => '模擬器';
|
String get emulator => '模擬器';
|
||||||
@@ -998,7 +998,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get fullScreenJitter => '全螢幕模式抖動';
|
String get fullScreenJitter => '全螢幕模式抖動';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fullScreenJitterHelp => '防止烙印';
|
String get fullScreenJitterHelp => '防止螢幕烙印';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fullScreenTip => '當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。';
|
String get fullScreenTip => '當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。';
|
||||||
@@ -1023,7 +1023,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String httpFailedWithCode(Object code) {
|
String httpFailedWithCode(Object code) {
|
||||||
return '請求失敗, 狀態碼: $code';
|
return '請求失敗,狀態碼:$code';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1046,7 +1046,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get installDockerWithUrl =>
|
String get installDockerWithUrl =>
|
||||||
'請先 https://docs.docker.com/engine/install docker';
|
'請先前往 https://docs.docker.com/engine/install 安裝 Docker';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get invalid => '無效';
|
String get invalid => '無效';
|
||||||
@@ -1055,7 +1055,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get jumpServer => '跳板伺服器';
|
String get jumpServer => '跳板伺服器';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get keepForeground => '請保持App處於前端!';
|
String get keepForeground => '請讓 App 保持在前景執行';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get keepStatusWhenErr => '保留上次的伺服器狀態';
|
String get keepStatusWhenErr => '保留上次的伺服器狀態';
|
||||||
@@ -1096,7 +1096,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get maxRetryCount => '伺服器嘗試重連次數';
|
String get maxRetryCount => '伺服器嘗試重連次數';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get maxRetryCountEqual0 => '會無限重試';
|
String get maxRetryCountEqual0 => '將無限次重試';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get min => '最小';
|
String get min => '最小';
|
||||||
@@ -1161,7 +1161,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get openLastPath => '打開上次的路徑';
|
String get openLastPath => '打開上次的路徑';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get openLastPathTip => '不同的伺服器會有不同的記錄,且記錄的是退出時的路徑';
|
String get openLastPathTip => '將為每台伺服器紀錄其最後存取路徑';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get parseContainerStatsTip => 'Docker 解析消耗狀態較為緩慢';
|
String get parseContainerStatsTip => 'Docker 解析消耗狀態較為緩慢';
|
||||||
@@ -1214,7 +1214,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get pveIgnoreCertTip => '不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。';
|
String get pveIgnoreCertTip => '不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pveLoginFailed => '登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。';
|
String get pveLoginFailed => '登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pveVersionLow => '此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。';
|
String get pveVersionLow => '此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。';
|
||||||
@@ -1296,7 +1296,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除檔案夾';
|
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除檔案夾';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpSSHConnected => 'SFTP 已連線...';
|
String get sftpSSHConnected => 'SFTP 已連線';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpShowFoldersFirst => '資料夾顯示在前';
|
String get sftpShowFoldersFirst => '資料夾顯示在前';
|
||||||
@@ -1327,7 +1327,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String spentTime(Object time) {
|
String spentTime(Object time) {
|
||||||
return '耗時: $time';
|
return '耗時:$time';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1435,7 +1435,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get update => '更新';
|
String get update => '更新';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get updateIntervalEqual0 => '你設定為 0,伺服器狀態不會自動更新。\n且不能計算CPU使用情況。';
|
String get updateIntervalEqual0 => '設定為 0 將不自動刷新伺服器狀態,\n也無法計算 CPU 使用率。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get updateServerStatusInterval => '伺服器狀態更新間隔';
|
String get updateServerStatusInterval => '伺服器狀態更新間隔';
|
||||||
@@ -1495,7 +1495,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get whenOpenApp => '當打開 App 時';
|
String get whenOpenApp => '當打開 App 時';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wolTip => '在配置 WOL(網絡喚醒)後,每次連線伺服器都會先發送一次 WOL 請求。';
|
String get wolTip => '設定 WOL 後,每次連線伺服器時將自動發送喚醒請求';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get write => '寫入';
|
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/private_key_info.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_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/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/server/wol_cfg.dart';
|
||||||
import 'package:server_box/data/model/ssh/virtual_key.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<ServerFuncBtn>(),
|
||||||
AdapterSpec<ServerCustom>(),
|
AdapterSpec<ServerCustom>(),
|
||||||
AdapterSpec<WakeOnLanCfg>(),
|
AdapterSpec<WakeOnLanCfg>(),
|
||||||
|
AdapterSpec<SystemType>(),
|
||||||
])
|
])
|
||||||
part 'hive_adapters.g.dart';
|
part 'hive_adapters.g.dart';
|
||||||
|
|||||||
@@ -111,13 +111,14 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
wolCfg: fields[11] as WakeOnLanCfg?,
|
wolCfg: fields[11] as WakeOnLanCfg?,
|
||||||
envs: (fields[12] as Map?)?.cast<String, String>(),
|
envs: (fields[12] as Map?)?.cast<String, String>(),
|
||||||
id: fields[13] == null ? '' : fields[13] as String,
|
id: fields[13] == null ? '' : fields[13] as String,
|
||||||
|
customSystemType: fields[14] as SystemType?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, Spi obj) {
|
void write(BinaryWriter writer, Spi obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(14)
|
..writeByte(15)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.name)
|
..write(obj.name)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -145,7 +146,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
..writeByte(12)
|
..writeByte(12)
|
||||||
..write(obj.envs)
|
..write(obj.envs)
|
||||||
..writeByte(13)
|
..writeByte(13)
|
||||||
..write(obj.id);
|
..write(obj.id)
|
||||||
|
..writeByte(14)
|
||||||
|
..write(obj.customSystemType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -557,3 +560,44 @@ class WakeOnLanCfgAdapter extends TypeAdapter<WakeOnLanCfg> {
|
|||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
typeId == other.typeId;
|
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
|
# Generated by Hive CE
|
||||||
# Manual modifications may be necessary for certain migrations
|
# Manual modifications may be necessary for certain migrations
|
||||||
# Check in to version control
|
# Check in to version control
|
||||||
nextTypeId: 9
|
nextTypeId: 10
|
||||||
types:
|
types:
|
||||||
PrivateKeyInfo:
|
PrivateKeyInfo:
|
||||||
typeId: 1
|
typeId: 1
|
||||||
@@ -27,7 +27,7 @@ types:
|
|||||||
index: 4
|
index: 4
|
||||||
Spi:
|
Spi:
|
||||||
typeId: 3
|
typeId: 3
|
||||||
nextIndex: 14
|
nextIndex: 15
|
||||||
fields:
|
fields:
|
||||||
name:
|
name:
|
||||||
index: 0
|
index: 0
|
||||||
@@ -57,6 +57,8 @@ types:
|
|||||||
index: 12
|
index: 12
|
||||||
id:
|
id:
|
||||||
index: 13
|
index: 13
|
||||||
|
customSystemType:
|
||||||
|
index: 14
|
||||||
VirtKey:
|
VirtKey:
|
||||||
typeId: 4
|
typeId: 4
|
||||||
nextIndex: 45
|
nextIndex: 45
|
||||||
@@ -207,3 +209,13 @@ types:
|
|||||||
index: 1
|
index: 1
|
||||||
pwd:
|
pwd:
|
||||||
index: 2
|
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(ServerFuncBtnAdapter());
|
||||||
registerAdapter(SnippetAdapter());
|
registerAdapter(SnippetAdapter());
|
||||||
registerAdapter(SpiAdapter());
|
registerAdapter(SpiAdapter());
|
||||||
|
registerAdapter(SystemTypeAdapter());
|
||||||
registerAdapter(VirtKeyAdapter());
|
registerAdapter(VirtKeyAdapter());
|
||||||
registerAdapter(WakeOnLanCfgAdapter());
|
registerAdapter(WakeOnLanCfgAdapter());
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
|||||||
registerAdapter(ServerFuncBtnAdapter());
|
registerAdapter(ServerFuncBtnAdapter());
|
||||||
registerAdapter(SnippetAdapter());
|
registerAdapter(SnippetAdapter());
|
||||||
registerAdapter(SpiAdapter());
|
registerAdapter(SpiAdapter());
|
||||||
|
registerAdapter(SystemTypeAdapter());
|
||||||
registerAdapter(VirtKeyAdapter());
|
registerAdapter(VirtKeyAdapter());
|
||||||
registerAdapter(WakeOnLanCfgAdapter());
|
registerAdapter(WakeOnLanCfgAdapter());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ final class _IntroPage extends StatelessWidget {
|
|||||||
|
|
||||||
const _IntroPage(this.pages);
|
const _IntroPage(this.pages);
|
||||||
|
|
||||||
static const _builders = {
|
static const _builders = {1: _buildAppSettings};
|
||||||
1: _buildAppSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -20,9 +18,7 @@ final class _IntroPage extends StatelessWidget {
|
|||||||
pages: pages_,
|
pages: pages_,
|
||||||
onDone: (ctx) {
|
onDone: (ctx) {
|
||||||
Stores.setting.introVer.put(BuildData.build);
|
Stores.setting.introVer.put(BuildData.build);
|
||||||
Navigator.of(ctx).pushReplacement(
|
Navigator.of(ctx).pushReplacement(MaterialPageRoute(builder: (_) => const HomePage()));
|
||||||
MaterialPageRoute(builder: (_) => const HomePage()),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -52,17 +48,12 @@ final class _IntroPage extends StatelessWidget {
|
|||||||
RNodes.app.notify();
|
RNodes.app.notify();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailing: Text(
|
trailing: Text(ctx.localeNativeName, style: const TextStyle(fontSize: 15, color: Colors.grey)),
|
||||||
ctx.localeNativeName,
|
|
||||||
style: const TextStyle(fontSize: 15, color: Colors.grey),
|
|
||||||
),
|
|
||||||
).cardx,
|
).cardx,
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.update),
|
leading: const Icon(Icons.update),
|
||||||
title: Text(libL10n.checkUpdate),
|
title: Text(libL10n.checkUpdate),
|
||||||
subtitle: isAndroid
|
subtitle: isAndroid ? Text(l10n.fdroidReleaseTip, style: UIs.textGrey) : null,
|
||||||
? Text(l10n.fdroidReleaseTip, style: UIs.textGrey)
|
|
||||||
: null,
|
|
||||||
trailing: StoreSwitch(prop: _setting.autoCheckAppUpdate),
|
trailing: StoreSwitch(prop: _setting.autoCheckAppUpdate),
|
||||||
).cardx,
|
).cardx,
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -87,10 +78,7 @@ final class _IntroPage extends StatelessWidget {
|
|||||||
|
|
||||||
static List<IntroPageBuilder> get builders {
|
static List<IntroPageBuilder> get builders {
|
||||||
final storedVer = _setting.introVer.fetch();
|
final storedVer = _setting.introVer.fetch();
|
||||||
return _builders.entries
|
return _builders.entries.where((e) => e.key > storedVer).map((e) => e.value).toList();
|
||||||
.where((e) => e.key > storedVer)
|
|
||||||
.map((e) => e.value)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static final _setting = Stores.setting;
|
static final _setting = Stores.setting;
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
"@@locale": "zh",
|
"@@locale": "zh",
|
||||||
"aboutThanks": "感谢以下参与的各位。",
|
"aboutThanks": "感谢以下参与的各位。",
|
||||||
"acceptBeta": "接受测试版更新推送",
|
"acceptBeta": "接受测试版更新推送",
|
||||||
"addSystemPrivateKeyTip": "当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?",
|
||||||
"added2List": "已添加至任务列表",
|
"added2List": "已添加至任务列表",
|
||||||
"addr": "地址",
|
"addr": "地址",
|
||||||
"alreadyLastDir": "已经是最上层目录了",
|
"alreadyLastDir": "已是顶级目录",
|
||||||
"authFailTip": "认证失败,请检查密码/密钥/主机/用户等是否错误",
|
"authFailTip": "认证失败,请检查连接信息是否正确",
|
||||||
"autoBackupConflict": "只能同时开启一个自动备份",
|
"autoBackupConflict": "仅可启用一个自动备份任务",
|
||||||
"autoConnect": "自动连接",
|
"autoConnect": "自动连接",
|
||||||
"autoRun": "自动运行",
|
"autoRun": "自动运行",
|
||||||
"autoUpdateHomeWidget": "自动更新桌面小部件",
|
"autoUpdateHomeWidget": "自动更新桌面小部件",
|
||||||
"backupTip": "导出的数据可以使用密码加密,请妥善保管。",
|
"backupTip": "导出数据可通过密码加密,请妥善保管。",
|
||||||
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
|
"backupVersionNotMatch": "备份版本不兼容,无法恢复",
|
||||||
"backupPassword": "备份密码",
|
"backupPassword": "备份密码",
|
||||||
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
|
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
|
||||||
"backupPasswordWrong": "备份密码错误",
|
"backupPasswordWrong": "备份密码错误",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"backupPasswordRemoved": "备份密码已移除",
|
"backupPasswordRemoved": "备份密码已移除",
|
||||||
"battery": "电池",
|
"battery": "电池",
|
||||||
"bgRun": "后台运行",
|
"bgRun": "后台运行",
|
||||||
"bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请修改省电策略为“无限制”。",
|
"bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请将省电策略改为“无限制”。",
|
||||||
"closeAfterSave": "保存后关闭",
|
"closeAfterSave": "保存后关闭",
|
||||||
"cmd": "命令",
|
"cmd": "命令",
|
||||||
"collapseUITip": "是否默认折叠 UI 中的长列表",
|
"collapseUITip": "是否默认折叠 UI 中的长列表",
|
||||||
@@ -40,23 +40,23 @@
|
|||||||
"decompress": "解压缩",
|
"decompress": "解压缩",
|
||||||
"deleteServers": "批量删除服务器",
|
"deleteServers": "批量删除服务器",
|
||||||
"desktopTerminalTip": "启动 SSH 连接所用的终端模拟器命令",
|
"desktopTerminalTip": "启动 SSH 连接所用的终端模拟器命令",
|
||||||
"dirEmpty": "请确保文件夹为空",
|
"dirEmpty": "请确保目录为空",
|
||||||
"disconnected": "连接断开",
|
"disconnected": "已断开连接",
|
||||||
"disk": "磁盘",
|
"disk": "磁盘",
|
||||||
"diskHealth": "磁盘健康",
|
"diskHealth": "磁盘健康",
|
||||||
"diskIgnorePath": "忽略的磁盘路径",
|
"diskIgnorePath": "忽略的磁盘路径",
|
||||||
"displayCpuIndex": "显示 CPU 索引",
|
"displayCpuIndex": "显示 CPU 索引",
|
||||||
"dl2Local": "下载 {fileName} 到本地?",
|
"dl2Local": "下载 {fileName} 到本地?",
|
||||||
"dockerEmptyRunningItems": "没有正在运行的容器。\n这可能是因为:\n- Docker 安装用户与 App 内配置的用户名不同\n- 环境变量 DOCKER_HOST 没有被正确读取。可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。",
|
"dockerEmptyRunningItems": "没有正在运行的容器。\n这可能是因为:\n- Docker 安装用户与 App 内配置的用户名不同\n- 环境变量 DOCKER_HOST 没有被正确读取。可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。",
|
||||||
"dockerImagesFmt": "共 {count} 个镜像",
|
"dockerImagesFmt": "{count} 个镜像",
|
||||||
"dockerNotInstalled": "Docker 未安装",
|
"dockerNotInstalled": "未安装 Docker",
|
||||||
"dockerStatusRunningAndStoppedFmt": "{runningCount} 个正在运行, {stoppedCount} 个已停止",
|
"dockerStatusRunningAndStoppedFmt": "{runningCount} 个正在运行, {stoppedCount} 个已停止",
|
||||||
"dockerStatusRunningFmt": "{count} 个容器正在运行",
|
"dockerStatusRunningFmt": "{count} 个容器正在运行",
|
||||||
"doubleColumnMode": "双列模式",
|
"doubleColumnMode": "双列模式",
|
||||||
"doubleColumnTip": "此选项仅开启功能,实际是否能开启还取决于设备的宽度",
|
"doubleColumnTip": "此选项仅用于启用该功能,是否生效取决于设备宽度",
|
||||||
"editVirtKeys": "编辑虚拟按键",
|
"editVirtKeys": "编辑虚拟按键",
|
||||||
"editor": "编辑器",
|
"editor": "编辑器",
|
||||||
"editorHighlightTip": "目前的代码高亮性能较为糟糕,可以选择关闭以改善。",
|
"editorHighlightTip": "代码高亮功能可能影响性能,可选择关闭。",
|
||||||
"emulator": "模拟器",
|
"emulator": "模拟器",
|
||||||
"encode": "编码",
|
"encode": "编码",
|
||||||
"envVars": "环境变量",
|
"envVars": "环境变量",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
"force": "强制",
|
"force": "强制",
|
||||||
"fullScreen": "全屏模式",
|
"fullScreen": "全屏模式",
|
||||||
"fullScreenJitter": "全屏模式抖动",
|
"fullScreenJitter": "全屏模式抖动",
|
||||||
"fullScreenJitterHelp": "防止烧屏",
|
"fullScreenJitterHelp": "用于防止屏幕烧屏",
|
||||||
"fullScreenTip": "当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。",
|
"fullScreenTip": "当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。",
|
||||||
"goBackQ": "返回?",
|
"goBackQ": "返回?",
|
||||||
"goto": "前往",
|
"goto": "前往",
|
||||||
@@ -81,17 +81,17 @@
|
|||||||
"highlight": "代码高亮",
|
"highlight": "代码高亮",
|
||||||
"homeWidgetUrlConfig": "桌面部件链接配置",
|
"homeWidgetUrlConfig": "桌面部件链接配置",
|
||||||
"host": "主机",
|
"host": "主机",
|
||||||
"httpFailedWithCode": "请求失败, 状态码: {code}",
|
"httpFailedWithCode": "请求失败,状态码: {code}",
|
||||||
"ignoreCert": "忽略证书",
|
"ignoreCert": "忽略证书",
|
||||||
"image": "镜像",
|
"image": "镜像",
|
||||||
"imagesList": "镜像列表",
|
"imagesList": "镜像列表",
|
||||||
"init": "初始化",
|
"init": "初始化",
|
||||||
"inner": "内置",
|
"inner": "内置",
|
||||||
"install": "安装",
|
"install": "安装",
|
||||||
"installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
|
"installDockerWithUrl": "请先前往 https://docs.docker.com/engine/install 安装 Docker",
|
||||||
"invalid": "无效",
|
"invalid": "无效",
|
||||||
"jumpServer": "跳板服务器",
|
"jumpServer": "跳板服务器",
|
||||||
"keepForeground": "请保持应用处于前台!",
|
"keepForeground": "请将应用保持在前台运行",
|
||||||
"keepStatusWhenErr": "保留上次的服务器状态",
|
"keepStatusWhenErr": "保留上次的服务器状态",
|
||||||
"keepStatusWhenErrTip": "仅限于执行脚本出错",
|
"keepStatusWhenErrTip": "仅限于执行脚本出错",
|
||||||
"keyAuth": "密钥认证",
|
"keyAuth": "密钥认证",
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"manual": "手动",
|
"manual": "手动",
|
||||||
"max": "最大",
|
"max": "最大",
|
||||||
"maxRetryCount": "服务器尝试重连次数",
|
"maxRetryCount": "服务器尝试重连次数",
|
||||||
"maxRetryCountEqual0": "会无限重试",
|
"maxRetryCountEqual0": "将无限次重试",
|
||||||
"min": "最小",
|
"min": "最小",
|
||||||
"mission": "任务",
|
"mission": "任务",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
"onlyOneLine": "仅显示为一行(可滚动)",
|
"onlyOneLine": "仅显示为一行(可滚动)",
|
||||||
"onlyWhenCoreBiggerThan8": "仅当核心数大于 8 时生效",
|
"onlyWhenCoreBiggerThan8": "仅当核心数大于 8 时生效",
|
||||||
"openLastPath": "打开上次的路径",
|
"openLastPath": "打开上次的路径",
|
||||||
"openLastPathTip": "不同的服务器会有不同的记录,且记录的是退出时的路径",
|
"openLastPathTip": "将为每台服务器记录其最后访问路径",
|
||||||
"parseContainerStatsTip": "Docker 解析占用状态较为缓慢",
|
"parseContainerStatsTip": "Docker 解析占用状态较为缓慢",
|
||||||
"percentOfSize": "{size} 的 {percent}%",
|
"percentOfSize": "{size} 的 {percent}%",
|
||||||
"permission": "权限",
|
"permission": "权限",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"prune": "修剪",
|
"prune": "修剪",
|
||||||
"pushToken": "消息推送 Token",
|
"pushToken": "消息推送 Token",
|
||||||
"pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项",
|
"pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项",
|
||||||
"pveLoginFailed": "登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。",
|
"pveLoginFailed": "登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。",
|
||||||
"pveVersionLow": "当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用",
|
"pveVersionLow": "当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用",
|
||||||
"read": "读",
|
"read": "读",
|
||||||
"reboot": "重启",
|
"reboot": "重启",
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
"sftpDlPrepare": "准备连接至服务器...",
|
"sftpDlPrepare": "准备连接至服务器...",
|
||||||
"sftpEditorTip": "如果为空, 使用App内置的文件编辑器. 如果有值, 这是用远程服务器的编辑器, 例如 `vim` (建议根据 `EDITOR` 自动获取).",
|
"sftpEditorTip": "如果为空, 使用App内置的文件编辑器. 如果有值, 这是用远程服务器的编辑器, 例如 `vim` (建议根据 `EDITOR` 自动获取).",
|
||||||
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 来删除文件夹",
|
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 来删除文件夹",
|
||||||
"sftpSSHConnected": "SFTP 已连接...",
|
"sftpSSHConnected": "SFTP 已连接",
|
||||||
"sftpShowFoldersFirst": "文件夹显示在前",
|
"sftpShowFoldersFirst": "文件夹显示在前",
|
||||||
"showDistLogo": "显示发行版 Logo",
|
"showDistLogo": "显示发行版 Logo",
|
||||||
"shutdown": "关机",
|
"shutdown": "关机",
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
"specifyDev": "指定设备",
|
"specifyDev": "指定设备",
|
||||||
"specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备",
|
"specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备",
|
||||||
"speed": "速度",
|
"speed": "速度",
|
||||||
"spentTime": "耗时: {time}",
|
"spentTime": "耗时:{time}",
|
||||||
"sshTermHelp": "在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容,在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。",
|
"sshTermHelp": "在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容,在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。",
|
||||||
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
|
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
|
||||||
"sshVirtualKeyAutoOff": "虚拟按键自动切换",
|
"sshVirtualKeyAutoOff": "虚拟按键自动切换",
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"unkownConvertMode": "未知转换模式",
|
"unkownConvertMode": "未知转换模式",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updateIntervalEqual0": "你设置为 0,服务器状态不会自动刷新。\n且不能计算 CPU 使用情况。",
|
"updateIntervalEqual0": "设置为 0 将不自动刷新服务器状态。\n且无法计算 CPU 使用率。",
|
||||||
"updateServerStatusInterval": "服务器状态刷新间隔",
|
"updateServerStatusInterval": "服务器状态刷新间隔",
|
||||||
"upload": "上传",
|
"upload": "上传",
|
||||||
"upsideDown": "上下交换",
|
"upsideDown": "上下交换",
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
"watchNotPaired": "没有已配对的 Apple Watch",
|
"watchNotPaired": "没有已配对的 Apple Watch",
|
||||||
"webdavSettingEmpty": "WebDav 设置项为空",
|
"webdavSettingEmpty": "WebDav 设置项为空",
|
||||||
"whenOpenApp": "当打开 App 时",
|
"whenOpenApp": "当打开 App 时",
|
||||||
"wolTip": "在配置 WOL 后,每次连接服务器都会先发送一次 WOL 请求",
|
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
||||||
"write": "写",
|
"write": "写",
|
||||||
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
||||||
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
|
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
"@@locale": "zh_TW",
|
"@@locale": "zh_TW",
|
||||||
"aboutThanks": "感謝以下參與的各位。",
|
"aboutThanks": "感謝以下參與的各位。",
|
||||||
"acceptBeta": "接受測試版更新推送",
|
"acceptBeta": "接受測試版更新推送",
|
||||||
"addSystemPrivateKeyTip": "目前沒有任何私鑰,是否新增系統原有的 (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?",
|
||||||
"added2List": "已新增至任務清單",
|
"added2List": "已新增至任務清單",
|
||||||
"addr": "位址",
|
"addr": "位址",
|
||||||
"alreadyLastDir": "已經是最上層目錄了",
|
"alreadyLastDir": "已是頂層目錄",
|
||||||
"authFailTip": "認證失敗,請檢查密碼/金鑰/主機/使用者等是否錯誤。",
|
"authFailTip": "認證失敗,請檢查連線資訊是否正確",
|
||||||
"autoBackupConflict": "只能同時開啓一個自動備份",
|
"autoBackupConflict": "僅能啟用一項自動備份任務",
|
||||||
"autoConnect": "自動連線",
|
"autoConnect": "自動連線",
|
||||||
"autoRun": "自動執行",
|
"autoRun": "自動執行",
|
||||||
"autoUpdateHomeWidget": "自動更新桌面小工具",
|
"autoUpdateHomeWidget": "自動更新桌面小工具",
|
||||||
"backupTip": "匯出的資料可以使用密碼加密。 \n請妥善保管。",
|
"backupTip": "匯出的資料可透過密碼加密,請妥善保管。",
|
||||||
"backupVersionNotMatch": "備份版本不相符,無法還原",
|
"backupVersionNotMatch": "備份版本不相容,無法還原",
|
||||||
"backupPassword": "備份密碼",
|
"backupPassword": "備份密碼",
|
||||||
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
|
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
|
||||||
"backupPasswordWrong": "備份密碼錯誤",
|
"backupPasswordWrong": "備份密碼錯誤",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"backupPasswordRemoved": "備份密碼已移除",
|
"backupPasswordRemoved": "備份密碼已移除",
|
||||||
"battery": "電池",
|
"battery": "電池",
|
||||||
"bgRun": "背景執行",
|
"bgRun": "背景執行",
|
||||||
"bgRunTip": "此開關只代表程式會嘗試在背景執行,具體能否在後臺執行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池最佳化”,MIUI / HyperOS 請修改省電策略為“無限制”。",
|
"bgRunTip": "此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。",
|
||||||
"closeAfterSave": "儲存後關閉",
|
"closeAfterSave": "儲存後關閉",
|
||||||
"cmd": "指令",
|
"cmd": "指令",
|
||||||
"collapseUITip": "是否預設折疊 UI 中存在的長列表",
|
"collapseUITip": "是否預設折疊 UI 中存在的長列表",
|
||||||
@@ -40,23 +40,23 @@
|
|||||||
"decompress": "解壓縮",
|
"decompress": "解壓縮",
|
||||||
"deleteServers": "大量刪除伺服器",
|
"deleteServers": "大量刪除伺服器",
|
||||||
"desktopTerminalTip": "啟動 SSH 連線時用於打開終端機模擬器的指令。",
|
"desktopTerminalTip": "啟動 SSH 連線時用於打開終端機模擬器的指令。",
|
||||||
"dirEmpty": "請確保資料夾為空",
|
"dirEmpty": "請確保目錄為空",
|
||||||
"disconnected": "連線中斷",
|
"disconnected": "已中斷連線",
|
||||||
"disk": "磁碟",
|
"disk": "磁碟",
|
||||||
"diskHealth": "磁碟健康",
|
"diskHealth": "磁碟健康",
|
||||||
"diskIgnorePath": "忽略的磁碟路徑",
|
"diskIgnorePath": "忽略的磁碟路徑",
|
||||||
"displayCpuIndex": "顯示 CPU 索引",
|
"displayCpuIndex": "顯示 CPU 索引",
|
||||||
"dl2Local": "下載 {fileName} 到本地?",
|
"dl2Local": "下載 {fileName} 到本地?",
|
||||||
"dockerEmptyRunningItems": "沒有正在執行的容器。\n這可能是因為:\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變數 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo $DOCKER_HOST` 來獲取。",
|
"dockerEmptyRunningItems": "沒有正在執行的容器。\n這可能是因為:\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變數 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo $DOCKER_HOST` 來獲取。",
|
||||||
"dockerImagesFmt": "共 {count} 個映像檔",
|
"dockerImagesFmt": "{count} 個映像檔",
|
||||||
"dockerNotInstalled": "Docker 未安裝",
|
"dockerNotInstalled": "未安裝 Docker",
|
||||||
"dockerStatusRunningAndStoppedFmt": "{runningCount} 個正在執行, {stoppedCount} 個已停止",
|
"dockerStatusRunningAndStoppedFmt": "{runningCount} 個正在執行, {stoppedCount} 個已停止",
|
||||||
"dockerStatusRunningFmt": "{count} 個容器正在執行",
|
"dockerStatusRunningFmt": "{count} 個容器正在執行",
|
||||||
"doubleColumnMode": "雙列模式",
|
"doubleColumnMode": "雙列模式",
|
||||||
"doubleColumnTip": "此選項僅開啟功能,實際是否能開啟還取決於設備的頻寬",
|
"doubleColumnTip": "此選項僅用於啟用此功能,是否生效取決於裝置寬度",
|
||||||
"editVirtKeys": "編輯虛擬按鍵",
|
"editVirtKeys": "編輯虛擬按鍵",
|
||||||
"editor": "編輯器",
|
"editor": "編輯器",
|
||||||
"editorHighlightTip": "目前的程式碼標記效能較為糟糕,可以選擇關閉以改善。",
|
"editorHighlightTip": "程式碼高亮功能可能影響效能,可選擇性關閉。",
|
||||||
"emulator": "模擬器",
|
"emulator": "模擬器",
|
||||||
"encode": "編碼",
|
"encode": "編碼",
|
||||||
"envVars": "環境變數",
|
"envVars": "環境變數",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
"force": "強制",
|
"force": "強制",
|
||||||
"fullScreen": "全螢幕模式",
|
"fullScreen": "全螢幕模式",
|
||||||
"fullScreenJitter": "全螢幕模式抖動",
|
"fullScreenJitter": "全螢幕模式抖動",
|
||||||
"fullScreenJitterHelp": "防止烙印",
|
"fullScreenJitterHelp": "防止螢幕烙印",
|
||||||
"fullScreenTip": "當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。",
|
"fullScreenTip": "當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。",
|
||||||
"goBackQ": "返回?",
|
"goBackQ": "返回?",
|
||||||
"goto": "前往",
|
"goto": "前往",
|
||||||
@@ -81,17 +81,17 @@
|
|||||||
"highlight": "程式碼標記",
|
"highlight": "程式碼標記",
|
||||||
"homeWidgetUrlConfig": "桌面小工具連結配置",
|
"homeWidgetUrlConfig": "桌面小工具連結配置",
|
||||||
"host": "主機",
|
"host": "主機",
|
||||||
"httpFailedWithCode": "請求失敗, 狀態碼: {code}",
|
"httpFailedWithCode": "請求失敗,狀態碼:{code}",
|
||||||
"ignoreCert": "忽略憑證",
|
"ignoreCert": "忽略憑證",
|
||||||
"image": "映像檔",
|
"image": "映像檔",
|
||||||
"imagesList": "映像檔列表",
|
"imagesList": "映像檔列表",
|
||||||
"init": "初始化",
|
"init": "初始化",
|
||||||
"inner": "內建",
|
"inner": "內建",
|
||||||
"install": "安裝",
|
"install": "安裝",
|
||||||
"installDockerWithUrl": "請先 https://docs.docker.com/engine/install docker",
|
"installDockerWithUrl": "請先前往 https://docs.docker.com/engine/install 安裝 Docker",
|
||||||
"invalid": "無效",
|
"invalid": "無效",
|
||||||
"jumpServer": "跳板伺服器",
|
"jumpServer": "跳板伺服器",
|
||||||
"keepForeground": "請保持App處於前端!",
|
"keepForeground": "請讓 App 保持在前景執行",
|
||||||
"keepStatusWhenErr": "保留上次的伺服器狀態",
|
"keepStatusWhenErr": "保留上次的伺服器狀態",
|
||||||
"keepStatusWhenErrTip": "僅在執行腳本出錯時",
|
"keepStatusWhenErrTip": "僅在執行腳本出錯時",
|
||||||
"keyAuth": "金鑰認證",
|
"keyAuth": "金鑰認證",
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"manual": "手動",
|
"manual": "手動",
|
||||||
"max": "最大",
|
"max": "最大",
|
||||||
"maxRetryCount": "伺服器嘗試重連次數",
|
"maxRetryCount": "伺服器嘗試重連次數",
|
||||||
"maxRetryCountEqual0": "會無限重試",
|
"maxRetryCountEqual0": "將無限次重試",
|
||||||
"min": "最小",
|
"min": "最小",
|
||||||
"mission": "任務",
|
"mission": "任務",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
"onlyOneLine": "僅顯示為一行(可捲動)",
|
"onlyOneLine": "僅顯示為一行(可捲動)",
|
||||||
"onlyWhenCoreBiggerThan8": "僅當核心數大於 8 時生效",
|
"onlyWhenCoreBiggerThan8": "僅當核心數大於 8 時生效",
|
||||||
"openLastPath": "打開上次的路徑",
|
"openLastPath": "打開上次的路徑",
|
||||||
"openLastPathTip": "不同的伺服器會有不同的記錄,且記錄的是退出時的路徑",
|
"openLastPathTip": "將為每台伺服器紀錄其最後存取路徑",
|
||||||
"parseContainerStatsTip": "Docker 解析消耗狀態較為緩慢",
|
"parseContainerStatsTip": "Docker 解析消耗狀態較為緩慢",
|
||||||
"percentOfSize": "{size} 的 {percent}%",
|
"percentOfSize": "{size} 的 {percent}%",
|
||||||
"permission": "權限",
|
"permission": "權限",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"prune": "修剪",
|
"prune": "修剪",
|
||||||
"pushToken": "消息推送 Token",
|
"pushToken": "消息推送 Token",
|
||||||
"pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。",
|
"pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。",
|
||||||
"pveLoginFailed": "登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。",
|
"pveLoginFailed": "登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。",
|
||||||
"pveVersionLow": "此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。",
|
"pveVersionLow": "此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。",
|
||||||
"read": "讀取",
|
"read": "讀取",
|
||||||
"reboot": "重開",
|
"reboot": "重開",
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
"sftpDlPrepare": "準備連線至伺服器...",
|
"sftpDlPrepare": "準備連線至伺服器...",
|
||||||
"sftpEditorTip": "如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠端伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。",
|
"sftpEditorTip": "如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠端伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。",
|
||||||
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除檔案夾",
|
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除檔案夾",
|
||||||
"sftpSSHConnected": "SFTP 已連線...",
|
"sftpSSHConnected": "SFTP 已連線",
|
||||||
"sftpShowFoldersFirst": "資料夾顯示在前",
|
"sftpShowFoldersFirst": "資料夾顯示在前",
|
||||||
"showDistLogo": "顯示發行版 Logo",
|
"showDistLogo": "顯示發行版 Logo",
|
||||||
"shutdown": "關機",
|
"shutdown": "關機",
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
"specifyDev": "指定裝置",
|
"specifyDev": "指定裝置",
|
||||||
"specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。",
|
"specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。",
|
||||||
"speed": "速度",
|
"speed": "速度",
|
||||||
"spentTime": "耗時: {time}",
|
"spentTime": "耗時:{time}",
|
||||||
"sshTermHelp": "在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容,在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。",
|
"sshTermHelp": "在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容,在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。",
|
||||||
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 回饋問題,或者加入我們開發。",
|
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 回饋問題,或者加入我們開發。",
|
||||||
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換",
|
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換",
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"unkownConvertMode": "未知轉換模式",
|
"unkownConvertMode": "未知轉換模式",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updateIntervalEqual0": "你設定為 0,伺服器狀態不會自動更新。\n且不能計算CPU使用情況。",
|
"updateIntervalEqual0": "設定為 0 將不自動刷新伺服器狀態,\n也無法計算 CPU 使用率。",
|
||||||
"updateServerStatusInterval": "伺服器狀態更新間隔",
|
"updateServerStatusInterval": "伺服器狀態更新間隔",
|
||||||
"upload": "上傳",
|
"upload": "上傳",
|
||||||
"upsideDown": "上下交換",
|
"upsideDown": "上下交換",
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
"watchNotPaired": "沒有已配對的 Apple Watch",
|
"watchNotPaired": "沒有已配對的 Apple Watch",
|
||||||
"webdavSettingEmpty": "WebDav 設定項爲空",
|
"webdavSettingEmpty": "WebDav 設定項爲空",
|
||||||
"whenOpenApp": "當打開 App 時",
|
"whenOpenApp": "當打開 App 時",
|
||||||
"wolTip": "在配置 WOL(網絡喚醒)後,每次連線伺服器都會先發送一次 WOL 請求。",
|
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
||||||
"write": "寫入",
|
"write": "寫入",
|
||||||
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
||||||
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
|
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(libL10n.backup),
|
title: Text(libL10n.backup),
|
||||||
trailing: const Icon(Icons.save),
|
trailing: const Icon(Icons.save),
|
||||||
onTap: () => BackupService.backup(context, FileBackupSource())
|
onTap: () => BackupService.backup(context, FileBackupSource()),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
trailing: const Icon(Icons.restore),
|
trailing: const Icon(Icons.restore),
|
||||||
@@ -264,7 +264,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
|||||||
).cardx;
|
).cardx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _onTapWebdavDl(BuildContext context) async {
|
Future<void> _onTapWebdavDl(BuildContext context) async {
|
||||||
webdavLoading.value = true;
|
webdavLoading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -357,7 +356,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _onBulkImportServers(BuildContext context) async {
|
void _onBulkImportServers(BuildContext context) async {
|
||||||
final data = await context.showImportDialog(title: l10n.server, modelDef: Spix.example.toJson());
|
final data = await context.showImportDialog(title: l10n.server, modelDef: Spix.example.toJson());
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
@@ -394,11 +392,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ class HomePage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<HomePage> createState() => _HomePageState();
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
|
||||||
static const route = AppRouteNoArg(
|
static const route = AppRouteNoArg(page: HomePage.new, path: '/');
|
||||||
page: HomePage.new,
|
|
||||||
path: '/',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage>
|
class _HomePageState extends State<HomePage>
|
||||||
@@ -181,11 +178,7 @@ class _HomePageState extends State<HomePage>
|
|||||||
//_reqNotiPerm();
|
//_reqNotiPerm();
|
||||||
|
|
||||||
if (Stores.setting.autoCheckAppUpdate.fetch()) {
|
if (Stores.setting.autoCheckAppUpdate.fetch()) {
|
||||||
AppUpdateIface.doUpdate(
|
AppUpdateIface.doUpdate(build: BuildData.build, url: Urls.updateCfg, context: context);
|
||||||
build: BuildData.build,
|
|
||||||
url: Urls.updateCfg,
|
|
||||||
context: context,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
MethodChans.updateHomeWidget();
|
MethodChans.updateHomeWidget();
|
||||||
await ServerProvider.refresh();
|
await ServerProvider.refresh();
|
||||||
@@ -216,10 +209,7 @@ class _HomePageState extends State<HomePage>
|
|||||||
void _goAuth() {
|
void _goAuth() {
|
||||||
if (Stores.setting.useBioAuth.fetch()) {
|
if (Stores.setting.useBioAuth.fetch()) {
|
||||||
if (LocalAuthPage.route.alreadyIn) return;
|
if (LocalAuthPage.route.alreadyIn) return;
|
||||||
LocalAuthPage.route.go(
|
LocalAuthPage.route.go(context, args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false));
|
||||||
context,
|
|
||||||
args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,9 +235,7 @@ final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(height: preferredSize.height);
|
||||||
height: preferredSize.height,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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/core/route.dart';
|
||||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||||
|
|
||||||
|
|
||||||
class IPerfPage extends StatefulWidget {
|
class IPerfPage extends StatefulWidget {
|
||||||
final SpiRequiredArgs args;
|
final SpiRequiredArgs args;
|
||||||
|
|
||||||
@@ -13,10 +12,7 @@ class IPerfPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<IPerfPage> createState() => _IPerfPageState();
|
State<IPerfPage> createState() => _IPerfPageState();
|
||||||
|
|
||||||
static const route = AppRouteArg<void, SpiRequiredArgs>(
|
static const route = AppRouteArg<void, SpiRequiredArgs>(page: IPerfPage.new, path: '/iperf');
|
||||||
page: IPerfPage.new,
|
|
||||||
path: '/iperf',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _IPerfPageState extends State<IPerfPage> {
|
class _IPerfPageState extends State<IPerfPage> {
|
||||||
@@ -33,9 +29,7 @@ class _IPerfPageState extends State<IPerfPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(title: const Text('iperf')),
|
||||||
title: const Text('iperf'),
|
|
||||||
),
|
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
floatingActionButton: _buildFAB(),
|
floatingActionButton: _buildFAB(),
|
||||||
);
|
);
|
||||||
@@ -63,12 +57,7 @@ class _IPerfPageState extends State<IPerfPage> {
|
|||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||||
children: [
|
children: [
|
||||||
Input(
|
Input(controller: _hostCtrl, label: l10n.host, icon: Icons.computer, suggestion: false),
|
||||||
controller: _hostCtrl,
|
|
||||||
label: l10n.host,
|
|
||||||
icon: Icons.computer,
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
Input(
|
||||||
controller: _portCtrl,
|
controller: _portCtrl,
|
||||||
label: l10n.port,
|
label: l10n.port,
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ class PrivateKeyEditPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
|
State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
|
||||||
|
|
||||||
static const route = AppRoute(
|
static const route = AppRoute(page: PrivateKeyEditPage.new, path: '/private_key/edit');
|
||||||
page: PrivateKeyEditPage.new,
|
|
||||||
path: '/private_key/edit',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||||
@@ -82,11 +79,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(appBar: _buildAppBar(), body: _buildBody(), floatingActionButton: _buildFAB());
|
||||||
appBar: _buildAppBar(),
|
|
||||||
body: _buildBody(),
|
|
||||||
floatingActionButton: _buildFAB(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomAppBar _buildAppBar() {
|
CustomAppBar _buildAppBar() {
|
||||||
@@ -98,9 +91,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
child: Text(libL10n.askContinue(
|
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.privateKey}(${pki.id})')),
|
||||||
'${libL10n.delete} ${l10n.privateKey}(${pki.id})',
|
|
||||||
)),
|
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
PrivateKeyProvider.delete(pki);
|
PrivateKeyProvider.delete(pki);
|
||||||
@@ -112,13 +103,10 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
: null;
|
: null;
|
||||||
return CustomAppBar(
|
return CustomAppBar(title: Text(libL10n.edit), actions: actions);
|
||||||
title: Text(libL10n.edit),
|
|
||||||
actions: actions,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _standardizeLineSeparators(String value) {
|
String _standardizeLineSeparators(String value) {
|
||||||
@@ -126,11 +114,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFAB() {
|
Widget _buildFAB() {
|
||||||
return FloatingActionButton(
|
return FloatingActionButton(tooltip: l10n.save, onPressed: _onTapSave, child: const Icon(Icons.save));
|
||||||
tooltip: l10n.save,
|
|
||||||
onPressed: _onTapSave,
|
|
||||||
child: const Icon(Icons.save),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
@@ -170,11 +154,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
final size = (await file.stat()).size;
|
final size = (await file.stat()).size;
|
||||||
if (size > Miscs.privateKeyMaxSize) {
|
if (size > Miscs.privateKeyMaxSize) {
|
||||||
context.showSnackBar(
|
context.showSnackBar(
|
||||||
l10n.fileTooLarge(
|
l10n.fileTooLarge(path, size.bytes2Str, Miscs.privateKeyMaxSize.bytes2Str),
|
||||||
path,
|
|
||||||
size.bytes2Str,
|
|
||||||
Miscs.privateKeyMaxSize.bytes2Str,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -196,10 +176,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
onSubmitted: (_) => _onTapSave(),
|
onSubmitted: (_) => _onTapSave(),
|
||||||
),
|
),
|
||||||
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
|
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
|
||||||
ValBuilder(
|
ValBuilder(listenable: _loading, builder: (val) => val ?? UIs.placeholder),
|
||||||
listenable: _loading,
|
|
||||||
builder: (val) => val ?? UIs.placeholder,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ class PrivateKeysListPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<PrivateKeysListPage> createState() => _PrivateKeyListState();
|
State<PrivateKeysListPage> createState() => _PrivateKeyListState();
|
||||||
|
|
||||||
static const route = AppRouteNoArg(
|
static const route = AppRouteNoArg(page: PrivateKeysListPage.new, path: '/private_key');
|
||||||
page: PrivateKeysListPage.new,
|
|
||||||
path: '/private_key',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMixin {
|
class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMixin {
|
||||||
@@ -34,26 +31,21 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMi
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
return PrivateKeyProvider.pkis.listenVal(
|
return PrivateKeyProvider.pkis.listenVal((pkis) {
|
||||||
(pkis) {
|
|
||||||
if (pkis.isEmpty) {
|
if (pkis.isEmpty) {
|
||||||
return Center(child: Text(libL10n.empty));
|
return Center(child: Text(libL10n.empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
final children = pkis.map(_buildKeyItem).toList();
|
final children = pkis.map(_buildKeyItem).toList();
|
||||||
return AutoMultiList(children: children);
|
return AutoMultiList(children: children);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildKeyItem(PrivateKeyInfo item) {
|
Widget _buildKeyItem(PrivateKeyInfo item) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(item.id),
|
title: Text(item.id),
|
||||||
subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),
|
subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),
|
||||||
onTap: () => PrivateKeyEditPage.route.go(
|
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: item)),
|
||||||
context,
|
|
||||||
args: PrivateKeyEditPageArgs(pki: item),
|
|
||||||
),
|
|
||||||
trailing: const Icon(Icons.edit),
|
trailing: const Icon(Icons.edit),
|
||||||
).cardx;
|
).cardx;
|
||||||
}
|
}
|
||||||
@@ -72,20 +64,16 @@ extension on _PrivateKeyListState {
|
|||||||
if (home == null) return;
|
if (home == null) return;
|
||||||
final idRsaFile = File(home.joinPath('.ssh/id_rsa'));
|
final idRsaFile = File(home.joinPath('.ssh/id_rsa'));
|
||||||
if (!idRsaFile.existsSync()) return;
|
if (!idRsaFile.existsSync()) return;
|
||||||
final sysPk = PrivateKeyInfo(
|
final sysPk = PrivateKeyInfo(id: 'system', key: await idRsaFile.readAsString());
|
||||||
id: 'system',
|
|
||||||
key: await idRsaFile.readAsString(),
|
|
||||||
);
|
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
child: Text(l10n.addSystemPrivateKeyTip),
|
child: Text(l10n.addSystemPrivateKeyTip),
|
||||||
actions: Btn.ok(onTap: () {
|
actions: Btn.ok(
|
||||||
|
onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
PrivateKeyEditPage.route.go(
|
PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: sysPk));
|
||||||
context,
|
},
|
||||||
args: PrivateKeyEditPageArgs(pki: sysPk),
|
).toList,
|
||||||
);
|
|
||||||
}).toList,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ class ProcessPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<ProcessPage> createState() => _ProcessPageState();
|
State<ProcessPage> createState() => _ProcessPageState();
|
||||||
|
|
||||||
static const route = AppRouteArg(
|
static const route = AppRouteArg(page: ProcessPage.new, path: '/process');
|
||||||
page: ProcessPage.new,
|
|
||||||
path: '/process',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProcessPageState extends State<ProcessPage> {
|
class _ProcessPageState extends State<ProcessPage> {
|
||||||
@@ -49,8 +46,7 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_client = widget.args.spi.server?.value.client;
|
_client = widget.args.spi.server?.value.client;
|
||||||
final duration =
|
final duration = Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
|
||||||
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
|
|
||||||
_timer = Timer.periodic(duration, (_) => _refresh());
|
_timer = Timer.periodic(duration, (_) => _refresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +58,10 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
|
|
||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final result =
|
final systemType = widget.args.spi.server?.value.status.system;
|
||||||
await _client?.run(ShellFunc.process.exec(widget.args.spi.id)).string;
|
final result = await _client
|
||||||
|
?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType))
|
||||||
|
.string;
|
||||||
if (result == null || result.isEmpty) {
|
if (result == null || result.isEmpty) {
|
||||||
context.showSnackBar(libL10n.empty);
|
context.showSnackBar(libL10n.empty);
|
||||||
return;
|
return;
|
||||||
@@ -72,8 +70,7 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
|
|
||||||
// If there are any [Proc]'s data is not complete,
|
// If there are any [Proc]'s data is not complete,
|
||||||
// the option to sort by cpu/mem will not be available.
|
// the option to sort by cpu/mem will not be available.
|
||||||
final isAnyProcDataNotComplete =
|
final isAnyProcDataNotComplete = _result.procs.any((e) => e.cpu == null || e.mem == null);
|
||||||
_result.procs.any((e) => e.cpu == null || e.mem == null);
|
|
||||||
if (isAnyProcDataNotComplete) {
|
if (isAnyProcDataNotComplete) {
|
||||||
_sortModes.removeWhere((e) => e == ProcSortMode.cpu);
|
_sortModes.removeWhere((e) => e == ProcSortMode.cpu);
|
||||||
_sortModes.removeWhere((e) => e == ProcSortMode.mem);
|
_sortModes.removeWhere((e) => e == ProcSortMode.mem);
|
||||||
@@ -97,25 +94,20 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
},
|
},
|
||||||
icon: const Icon(Icons.sort),
|
icon: const Icon(Icons.sort),
|
||||||
initialValue: _procSortMode,
|
initialValue: _procSortMode,
|
||||||
itemBuilder: (_) => _sortModes
|
itemBuilder: (_) => _sortModes.map((e) => PopupMenuItem(value: e, child: Text(e.name))).toList(),
|
||||||
.map((e) => PopupMenuItem(value: e, child: Text(e.name)))
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (_result.error != null) {
|
if (_result.error != null) {
|
||||||
actions.add(IconButton(
|
actions.add(
|
||||||
|
IconButton(
|
||||||
icon: const Icon(Icons.error),
|
icon: const Icon(Icons.error),
|
||||||
onPressed: () => context.showRoundDialog(
|
onPressed: () => context.showRoundDialog(
|
||||||
title: libL10n.error,
|
title: libL10n.error,
|
||||||
child: SingleChildScrollView(child: Text(_result.error!)),
|
child: SingleChildScrollView(child: Text(_result.error!)),
|
||||||
actions: [
|
actions: [TextButton(onPressed: () => Pfs.copy(_result.error!), child: Text(libL10n.copy))],
|
||||||
TextButton(
|
|
||||||
onPressed: () => Pfs.copy(_result.error!),
|
|
||||||
child: Text(libL10n.copy),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
Widget child;
|
Widget child;
|
||||||
if (_result.procs.isEmpty) {
|
if (_result.procs.isEmpty) {
|
||||||
@@ -144,32 +136,26 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
return CardX(
|
return CardX(
|
||||||
key: ValueKey(proc.pid),
|
key: ValueKey(proc.pid),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: SizedBox(
|
leading: SizedBox(width: _media.size.width / 6, child: leading),
|
||||||
width: _media.size.width / 6,
|
|
||||||
child: leading,
|
|
||||||
),
|
|
||||||
title: Text(proc.binary),
|
title: Text(proc.binary),
|
||||||
subtitle: Text(
|
subtitle: Text(proc.command, style: UIs.textGrey, maxLines: 3, overflow: TextOverflow.fade),
|
||||||
proc.command,
|
|
||||||
style: UIs.textGrey,
|
|
||||||
maxLines: 3,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
trailing: _buildItemTrail(proc),
|
trailing: _buildItemTrail(proc),
|
||||||
onTap: () => _lastFocusId = proc.pid,
|
onTap: () => _lastFocusId = proc.pid,
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
child: Text(libL10n.askContinue(
|
child: Text(libL10n.askContinue('${l10n.stop} ${l10n.process}(${proc.pid})')),
|
||||||
'${l10n.stop} ${l10n.process}(${proc.pid})',
|
actions: Btn.ok(
|
||||||
)),
|
onTap: () async {
|
||||||
actions: Btn.ok(onTap: () async {
|
|
||||||
context.pop();
|
context.pop();
|
||||||
await context.showLoadingDialog(fn: () async {
|
await context.showLoadingDialog(
|
||||||
|
fn: () async {
|
||||||
await _client?.run('kill ${proc.pid}');
|
await _client?.run('kill ${proc.pid}');
|
||||||
await _refresh();
|
await _refresh();
|
||||||
});
|
},
|
||||||
}).toList,
|
);
|
||||||
|
},
|
||||||
|
).toList,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
selected: _lastFocusId == proc.pid,
|
selected: _lastFocusId == proc.pid,
|
||||||
@@ -185,17 +171,9 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (proc.cpu != null)
|
if (proc.cpu != null) TwoLineText(up: proc.cpu!.toStringAsFixed(1), down: 'cpu'),
|
||||||
TwoLineText(
|
|
||||||
up: proc.cpu!.toStringAsFixed(1),
|
|
||||||
down: 'cpu',
|
|
||||||
),
|
|
||||||
UIs.width13,
|
UIs.width13,
|
||||||
if (proc.mem != null)
|
if (proc.mem != null) TwoLineText(up: proc.mem!.toStringAsFixed(1), down: 'mem'),
|
||||||
TwoLineText(
|
|
||||||
up: proc.mem!.toStringAsFixed(1),
|
|
||||||
down: 'mem',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,18 +18,12 @@ final class PvePageArgs {
|
|||||||
final class PvePage extends StatefulWidget {
|
final class PvePage extends StatefulWidget {
|
||||||
final PvePageArgs args;
|
final PvePageArgs args;
|
||||||
|
|
||||||
const PvePage({
|
const PvePage({super.key, required this.args});
|
||||||
super.key,
|
|
||||||
required this.args,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PvePage> createState() => _PvePageState();
|
State<PvePage> createState() => _PvePageState();
|
||||||
|
|
||||||
static const route = AppRouteArg<void, PvePageArgs>(
|
static const route = AppRouteArg<void, PvePageArgs>(page: PvePage.new, path: '/pve');
|
||||||
page: PvePage.new,
|
|
||||||
path: '/pve',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const _kHorziPadding = 11.0;
|
const _kHorziPadding = 11.0;
|
||||||
@@ -87,9 +81,7 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(13),
|
padding: const EdgeInsets.all(13),
|
||||||
child: Center(
|
child: Center(child: Text(val)),
|
||||||
child: Text(val),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ValBuilder(
|
return ValBuilder(
|
||||||
@@ -110,10 +102,7 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
|
|
||||||
PveResType? lastType;
|
PveResType? lastType;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: _kHorziPadding, vertical: 7),
|
||||||
horizontal: _kHorziPadding,
|
|
||||||
vertical: 7,
|
|
||||||
),
|
|
||||||
itemCount: data.length * 2,
|
itemCount: data.length * 2,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = data[index ~/ 2];
|
final item = data[index ~/ 2];
|
||||||
@@ -135,10 +124,7 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
type.toStr,
|
type.toStr,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -183,18 +169,11 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
UIs.width7,
|
UIs.width7,
|
||||||
const Text('CPU', style: UIs.text12Grey),
|
const Text('CPU', style: UIs.text12Grey),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text('${(item.cpu * 100).toStringAsFixed(1)} %', style: UIs.text12Grey),
|
||||||
'${(item.cpu * 100).toStringAsFixed(1)} %',
|
|
||||||
style: UIs.text12Grey,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(value: item.cpu / item.maxcpu, minHeight: 7, valueColor: valueAnim),
|
||||||
value: item.cpu / item.maxcpu,
|
|
||||||
minHeight: 7,
|
|
||||||
valueColor: valueAnim,
|
|
||||||
),
|
|
||||||
UIs.height7,
|
UIs.height7,
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -202,18 +181,11 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
UIs.width7,
|
UIs.width7,
|
||||||
const Text('RAM', style: UIs.text12Grey),
|
const Text('RAM', style: UIs.text12Grey),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text('${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}', style: UIs.text12Grey),
|
||||||
'${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}',
|
|
||||||
style: UIs.text12Grey,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(value: item.mem / item.maxmem, minHeight: 7, valueColor: valueAnim),
|
||||||
value: item.mem / item.maxmem,
|
|
||||||
minHeight: 7,
|
|
||||||
valueColor: valueAnim,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).cardx;
|
).cardx;
|
||||||
@@ -232,14 +204,8 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 15),
|
const SizedBox(width: 15),
|
||||||
Text(
|
Text(_wrapNodeName(item), style: UIs.text13Bold),
|
||||||
_wrapNodeName(item),
|
Text(' / ${item.summary}', style: UIs.text12Grey),
|
||||||
style: UIs.text13Bold,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
' / ${item.summary}',
|
|
||||||
style: UIs.text12Grey,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
_buildCtrlBtns(item),
|
_buildCtrlBtns(item),
|
||||||
UIs.width13,
|
UIs.width13,
|
||||||
@@ -266,34 +232,23 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
|
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
|
||||||
style: UIs.text11Grey,
|
style: UIs.text11Grey,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('↓:\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
|
||||||
'↓:\n${item.netin.bytes2Str}',
|
|
||||||
style: UIs.text11Grey,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(
|
Text('↑:\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
|
||||||
'↑:\n${item.netout.bytes2Str}',
|
|
||||||
style: UIs.text11Grey,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 21)
|
const SizedBox(height: 21),
|
||||||
];
|
];
|
||||||
return Column(
|
return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: children,
|
|
||||||
).cardx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLxc(PveLxc item) {
|
Widget _buildLxc(PveLxc item) {
|
||||||
@@ -309,14 +264,8 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 15),
|
const SizedBox(width: 15),
|
||||||
Text(
|
Text(_wrapNodeName(item), style: UIs.text13Bold),
|
||||||
_wrapNodeName(item),
|
Text(' / ${item.summary}', style: UIs.text12Grey),
|
||||||
style: UIs.text13Bold,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
' / ${item.summary}',
|
|
||||||
style: UIs.text12Grey,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
_buildCtrlBtns(item),
|
_buildCtrlBtns(item),
|
||||||
UIs.width13,
|
UIs.width13,
|
||||||
@@ -343,34 +292,23 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
|
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
|
||||||
style: UIs.text11Grey,
|
style: UIs.text11Grey,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('↓:\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
|
||||||
'↓:\n${item.netin.bytes2Str}',
|
|
||||||
style: UIs.text11Grey,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(
|
Text('↑:\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
|
||||||
'↑:\n${item.netout.bytes2Str}',
|
|
||||||
style: UIs.text11Grey,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 21)
|
const SizedBox(height: 21),
|
||||||
];
|
];
|
||||||
return Column(
|
return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: children,
|
|
||||||
).cardx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStorage(PveStorage item) {
|
Widget _buildStorage(PveStorage item) {
|
||||||
@@ -396,10 +334,7 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSdn(PveSdn item) {
|
Widget _buildSdn(PveSdn item) {
|
||||||
return ListTile(
|
return ListTile(title: Text(_wrapNodeName(item)), trailing: Text(item.summary)).cardx;
|
||||||
title: Text(_wrapNodeName(item)),
|
|
||||||
trailing: Text(item.summary),
|
|
||||||
).cardx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCtrlBtns(PveCtrlIface item) {
|
Widget _buildCtrlBtns(PveCtrlIface item) {
|
||||||
@@ -407,22 +342,26 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
if (!item.available) {
|
if (!item.available) {
|
||||||
return Btn.icon(
|
return Btn.icon(
|
||||||
icon: const Icon(Icons.play_arrow, color: Colors.grey),
|
icon: const Icon(Icons.play_arrow, color: Colors.grey),
|
||||||
onTap: () => _onCtrl(_pve.start, l10n.start, item));
|
onTap: () => _onCtrl(_pve.start, l10n.start, item),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Btn.icon(
|
Btn.icon(
|
||||||
icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
|
icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
|
||||||
padding: pad,
|
padding: pad,
|
||||||
onTap: () => _onCtrl(_pve.stop, l10n.stop, item)),
|
onTap: () => _onCtrl(_pve.stop, l10n.stop, item),
|
||||||
|
),
|
||||||
Btn.icon(
|
Btn.icon(
|
||||||
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
|
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
|
||||||
padding: pad,
|
padding: pad,
|
||||||
onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item)),
|
onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item),
|
||||||
|
),
|
||||||
Btn.icon(
|
Btn.icon(
|
||||||
icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
|
icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
|
||||||
padding: pad,
|
padding: pad,
|
||||||
onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item)),
|
onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -437,9 +376,7 @@ extension on _PvePageState {
|
|||||||
);
|
);
|
||||||
if (sure != true) return;
|
if (sure != true) return;
|
||||||
|
|
||||||
final (suc, err) = await context.showLoadingDialog(
|
final (suc, err) = await context.showLoadingDialog(fn: () => func(item.node, item.id));
|
||||||
fn: () => func(item.node, item.id),
|
|
||||||
);
|
|
||||||
if (suc == true) {
|
if (suc == true) {
|
||||||
context.showSnackBar(libL10n.success);
|
context.showSnackBar(libL10n.success);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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/custom.dart';
|
||||||
import 'package:server_box/data/model/server/server.dart';
|
import 'package:server_box/data/model/server/server.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_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/model/server/wol_cfg.dart';
|
||||||
import 'package:server_box/data/provider/private_key.dart';
|
import 'package:server_box/data/provider/private_key.dart';
|
||||||
import 'package:server_box/data/provider/server.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 _env = <String, String>{}.vn;
|
||||||
final _customCmds = <String, String>{}.vn;
|
final _customCmds = <String, String>{}.vn;
|
||||||
final _tags = <String>{}.vn;
|
final _tags = <String>{}.vn;
|
||||||
|
final _systemType = ValueNotifier<SystemType?>(null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -91,6 +93,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
_env.dispose();
|
_env.dispose();
|
||||||
_customCmds.dispose();
|
_customCmds.dispose();
|
||||||
_tags.dispose();
|
_tags.dispose();
|
||||||
|
_systemType.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -174,6 +177,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildAuth(),
|
_buildAuth(),
|
||||||
|
_buildSystemType(),
|
||||||
_buildJumpServer(),
|
_buildJumpServer(),
|
||||||
_buildMore(),
|
_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() {
|
Widget _buildAltUrl() {
|
||||||
return Input(
|
return Input(
|
||||||
controller: _altUrlController,
|
controller: _altUrlController,
|
||||||
@@ -614,6 +638,7 @@ extension on _ServerEditPageState {
|
|||||||
wolCfg: wol,
|
wolCfg: wol,
|
||||||
envs: _env.value.isEmpty ? null : _env.value,
|
envs: _env.value.isEmpty ? null : _env.value,
|
||||||
id: widget.args?.spi.id ?? ShortId.generate(),
|
id: widget.args?.spi.id ?? ShortId.generate(),
|
||||||
|
customSystemType: _systemType.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.spi == null) {
|
if (this.spi == null) {
|
||||||
@@ -668,5 +693,7 @@ extension on _ServerEditPageState {
|
|||||||
|
|
||||||
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
||||||
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
||||||
|
|
||||||
|
_systemType.value = spi.customSystemType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,9 @@ class _CardStatus {
|
|||||||
final bool? diskIO;
|
final bool? diskIO;
|
||||||
final NetViewType? net;
|
final NetViewType? net;
|
||||||
|
|
||||||
const _CardStatus({
|
const _CardStatus({this.flip = false, this.diskIO, this.net});
|
||||||
this.flip = false,
|
|
||||||
this.diskIO,
|
|
||||||
this.net,
|
|
||||||
});
|
|
||||||
|
|
||||||
_CardStatus copyWith({
|
_CardStatus copyWith({bool? flip, bool? diskIO, NetViewType? net}) {
|
||||||
bool? flip,
|
return _CardStatus(flip: flip ?? this.flip, diskIO: diskIO ?? this.diskIO, net: net ?? this.net);
|
||||||
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,
|
UIs.height13,
|
||||||
if (Stores.setting.moveServerFuncs.fetch())
|
if (Stores.setting.moveServerFuncs.fetch()) SizedBox(height: 27, child: ServerFuncBtns(spi: spi)),
|
||||||
SizedBox(height: 27, child: ServerFuncBtns(spi: spi)),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final void Function(String) onTagChanged;
|
final void Function(String) onTagChanged;
|
||||||
final String initTag;
|
final String initTag;
|
||||||
|
|
||||||
const _TopBar({
|
const _TopBar({required this.initTag, required this.onTagChanged, required this.tags});
|
||||||
required this.initTag,
|
|
||||||
required this.onTagChanged,
|
|
||||||
required this.tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -31,15 +27,9 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(BuildData.name, style: TextStyle(fontSize: 19)),
|
||||||
BuildData.name,
|
|
||||||
style: TextStyle(fontSize: 19),
|
|
||||||
),
|
|
||||||
SizedBox(width: 5),
|
SizedBox(width: 5),
|
||||||
Icon(
|
Icon(Icons.settings, size: 17),
|
||||||
Icons.settings,
|
|
||||||
size: 17,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ extension _Operation on _ServerPageState {
|
|||||||
await context.showRoundDialog(title: libL10n.attention, child: Text(l10n.suspendTip));
|
await context.showRoundDialog(title: libL10n.attention, child: Text(l10n.suspendTip));
|
||||||
Stores.setting.showSuspendTip.put(false);
|
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,
|
typ: l10n.suspend,
|
||||||
name: srv.spi.name,
|
name: srv.spi.name,
|
||||||
@@ -58,7 +62,11 @@ extension _Operation on _ServerPageState {
|
|||||||
|
|
||||||
void _onTapShutdown(Server srv) {
|
void _onTapShutdown(Server srv) {
|
||||||
_askFor(
|
_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,
|
typ: l10n.shutdown,
|
||||||
name: srv.spi.name,
|
name: srv.spi.name,
|
||||||
);
|
);
|
||||||
@@ -66,7 +74,11 @@ extension _Operation on _ServerPageState {
|
|||||||
|
|
||||||
void _onTapReboot(Server srv) {
|
void _onTapReboot(Server srv) {
|
||||||
_askFor(
|
_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,
|
typ: l10n.reboot,
|
||||||
name: srv.spi.name,
|
name: srv.spi.name,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ final class _AppAboutPage extends StatefulWidget {
|
|||||||
State<_AppAboutPage> createState() => _AppAboutPageState();
|
State<_AppAboutPage> createState() => _AppAboutPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _AppAboutPageState extends State<_AppAboutPage>
|
final class _AppAboutPageState extends State<_AppAboutPage> with AutomaticKeepAliveClientMixin {
|
||||||
with AutomaticKeepAliveClientMixin {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
@@ -16,15 +15,8 @@ final class _AppAboutPageState extends State<_AppAboutPage>
|
|||||||
padding: const EdgeInsets.all(13),
|
padding: const EdgeInsets.all(13),
|
||||||
children: [
|
children: [
|
||||||
UIs.height13,
|
UIs.height13,
|
||||||
ConstrainedBox(
|
ConstrainedBox(constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47), child: UIs.appIcon),
|
||||||
constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47),
|
const Text('${BuildData.name}\nv${BuildData.build}', textAlign: TextAlign.center, style: UIs.text15),
|
||||||
child: UIs.appIcon,
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
'${BuildData.name}\nv${BuildData.build}',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: UIs.text15,
|
|
||||||
),
|
|
||||||
UIs.height13,
|
UIs.height13,
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 77,
|
height: 77,
|
||||||
@@ -52,7 +44,8 @@ final class _AppAboutPageState extends State<_AppAboutPage>
|
|||||||
),
|
),
|
||||||
UIs.height13,
|
UIs.height13,
|
||||||
SimpleMarkdown(
|
SimpleMarkdown(
|
||||||
data: '''
|
data:
|
||||||
|
'''
|
||||||
#### Contributors
|
#### Contributors
|
||||||
${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')}
|
${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ class ServerDetailOrderPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();
|
State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();
|
||||||
|
|
||||||
static const route = AppRouteNoArg(
|
static const route = AppRouteNoArg(page: ServerDetailOrderPage.new, path: '/settings/order/server_detail');
|
||||||
page: ServerDetailOrderPage.new,
|
|
||||||
path: '/settings/order/server_detail',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
|
class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
|
||||||
@@ -31,8 +28,7 @@ class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
|
|||||||
return ValBuilder(
|
return ValBuilder(
|
||||||
listenable: prop.listenable(),
|
listenable: prop.listenable(),
|
||||||
builder: (keys) {
|
builder: (keys) {
|
||||||
final disabled =
|
final disabled = ServerDetailCards.names.where((e) => !keys.contains(e)).toList();
|
||||||
ServerDetailCards.names.where((e) => !keys.contains(e)).toList();
|
|
||||||
final allKeys = [...keys, ...disabled];
|
final allKeys = [...keys, ...disabled];
|
||||||
return ReorderableListView.builder(
|
return ReorderableListView.builder(
|
||||||
padding: const EdgeInsets.all(7),
|
padding: const EdgeInsets.all(7),
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ class ServerFuncBtnsOrderPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<ServerFuncBtnsOrderPage> createState() => _ServerDetailOrderPageState();
|
State<ServerFuncBtnsOrderPage> createState() => _ServerDetailOrderPageState();
|
||||||
|
|
||||||
static const route = AppRouteNoArg(
|
static const route = AppRouteNoArg(page: ServerFuncBtnsOrderPage.new, path: '/setting/seq/srv_func');
|
||||||
page: ServerFuncBtnsOrderPage.new,
|
|
||||||
path: '/setting/seq/srv_func',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
||||||
@@ -67,12 +64,7 @@ class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCheckBox(
|
Widget _buildCheckBox(List<int> keys, int key, int idx, bool value) {
|
||||||
List<int> keys,
|
|
||||||
int key,
|
|
||||||
int idx,
|
|
||||||
bool value,
|
|
||||||
) {
|
|
||||||
return Checkbox(
|
return Checkbox(
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ class ServerOrderPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<ServerOrderPage> createState() => _ServerOrderPageState();
|
State<ServerOrderPage> createState() => _ServerOrderPageState();
|
||||||
|
|
||||||
static const route = AppRouteNoArg(
|
static const route = AppRouteNoArg(page: ServerOrderPage.new, path: '/settings/order/server');
|
||||||
page: ServerOrderPage.new,
|
|
||||||
path: '/settings/order/server',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerOrderPageState extends State<ServerOrderPage> {
|
class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||||
@@ -36,10 +33,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
|||||||
final double scale = lerpDouble(1, 1.02, animValue)!;
|
final double scale = lerpDouble(1, 1.02, animValue)!;
|
||||||
return Transform.scale(
|
return Transform.scale(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
child: Card(
|
child: Card(elevation: elevation, child: child),
|
||||||
elevation: elevation,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: _buildCardTile(index),
|
child: _buildCardTile(index),
|
||||||
@@ -56,11 +50,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
|||||||
footer: const SizedBox(height: 77),
|
footer: const SizedBox(height: 77),
|
||||||
onReorder: (oldIndex, newIndex) {
|
onReorder: (oldIndex, newIndex) {
|
||||||
setState(() {
|
setState(() {
|
||||||
orders.value.move(
|
orders.value.move(oldIndex, newIndex, property: Stores.setting.serverOrder);
|
||||||
oldIndex,
|
|
||||||
newIndex,
|
|
||||||
property: Stores.setting.serverOrder,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
@@ -78,9 +68,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
|||||||
index: index,
|
index: index,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: CardX(
|
child: CardX(child: _buildCardTile(index)),
|
||||||
child: _buildCardTile(index),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -93,20 +81,14 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(spi.name, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
spi.name,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
subtitle: Text(spi.oldId, style: UIs.textGrey),
|
subtitle: Text(spi.oldId, style: UIs.textGrey),
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
child: Text(spi.name[0]),
|
child: Text(spi.name[0]),
|
||||||
),
|
),
|
||||||
trailing: ReorderableDragStartListener(
|
trailing: ReorderableDragStartListener(index: index, child: const Icon(Icons.drag_handle)),
|
||||||
index: index,
|
|
||||||
child: const Icon(Icons.drag_handle),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ class SnippetEditPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<SnippetEditPage> createState() => _SnippetEditPageState();
|
State<SnippetEditPage> createState() => _SnippetEditPageState();
|
||||||
|
|
||||||
static const route = AppRoute(
|
static const route = AppRoute(page: SnippetEditPage.new, path: '/snippets/edit');
|
||||||
page: SnippetEditPage.new,
|
|
||||||
path: '/snippets/edit',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin {
|
class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin {
|
||||||
@@ -47,10 +44,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(title: Text(libL10n.edit), actions: _buildAppBarActions()),
|
||||||
title: Text(libL10n.edit),
|
|
||||||
actions: _buildAppBarActions(),
|
|
||||||
),
|
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
floatingActionButton: _buildFAB(),
|
floatingActionButton: _buildFAB(),
|
||||||
);
|
);
|
||||||
@@ -64,9 +58,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
child: Text(libL10n.askContinue(
|
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.snippet}(${snippet.name})')),
|
||||||
'${libL10n.delete} ${l10n.snippet}(${snippet.name})',
|
|
||||||
)),
|
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SnippetProvider.del(snippet);
|
SnippetProvider.del(snippet);
|
||||||
@@ -79,7 +71,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
|||||||
},
|
},
|
||||||
tooltip: libL10n.delete,
|
tooltip: libL10n.delete,
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,12 +160,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
|||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
subtitle: subtitle == null
|
subtitle: subtitle == null
|
||||||
? null
|
? null
|
||||||
: Text(
|
: Text(subtitle, maxLines: 1, style: UIs.textGrey, overflow: TextOverflow.ellipsis),
|
||||||
subtitle,
|
|
||||||
maxLines: 1,
|
|
||||||
style: UIs.textGrey,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e));
|
vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e));
|
||||||
final serverIds = await context.showPickDialog(
|
final serverIds = await context.showPickDialog(
|
||||||
@@ -198,7 +185,8 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(13),
|
padding: const EdgeInsets.all(13),
|
||||||
child: SimpleMarkdown(
|
child: SimpleMarkdown(
|
||||||
data: '''
|
data:
|
||||||
|
'''
|
||||||
📌 ${l10n.supportFmtArgs}\n
|
📌 ${l10n.supportFmtArgs}\n
|
||||||
${SnippetX.fmtArgs.keys.map((e) => '`$e`').join(', ')}\n
|
${SnippetX.fmtArgs.keys.map((e) => '`$e`').join(', ')}\n
|
||||||
|
|
||||||
@@ -207,11 +195,7 @@ ${libL10n.example}:
|
|||||||
- `\${ctrl+c}` (Control + C)
|
- `\${ctrl+c}` (Control + C)
|
||||||
- `\${ctrl+b}d` (Tmux Detach)
|
- `\${ctrl+b}d` (Tmux Detach)
|
||||||
''',
|
''',
|
||||||
styleSheet: MarkdownStyleSheet(
|
styleSheet: MarkdownStyleSheet(codeblockDecoration: const BoxDecoration(color: Colors.transparent)),
|
||||||
codeblockDecoration: const BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ class SnippetListPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<SnippetListPage> createState() => _SnippetListPageState();
|
State<SnippetListPage> createState() => _SnippetListPageState();
|
||||||
|
|
||||||
static const route = AppRouteNoArg(
|
static const route = AppRouteNoArg(page: SnippetListPage.new, path: '/snippets');
|
||||||
page: SnippetListPage.new,
|
|
||||||
path: '/snippets',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAliveClientMixin {
|
class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAliveClientMixin {
|
||||||
@@ -38,8 +35,7 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
|||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||||
return SnippetProvider.snippets.listenVal(
|
return SnippetProvider.snippets.listenVal((snippets) {
|
||||||
(snippets) {
|
|
||||||
return _tag.listenVal((tag) {
|
return _tag.listenVal((tag) {
|
||||||
final child = _buildScaffold(snippets, tag);
|
final child = _buildScaffold(snippets, tag);
|
||||||
// if (isMobile) {
|
// if (isMobile) {
|
||||||
@@ -54,8 +50,7 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
|||||||
// leftBuilder: (_, __) => child,
|
// leftBuilder: (_, __) => child,
|
||||||
// );
|
// );
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScaffold(List<Snippet> snippets, String tag) {
|
Widget _buildScaffold(List<Snippet> snippets, String tag) {
|
||||||
@@ -104,11 +99,7 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
|||||||
Widget _buildSnippetItem(Snippet snippet) {
|
Widget _buildSnippetItem(Snippet snippet) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.only(left: 23, right: 17),
|
contentPadding: const EdgeInsets.only(left: 23, right: 17),
|
||||||
title: Text(
|
title: Text(snippet.name, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||||
snippet.name,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
snippet.note ?? snippet.script,
|
snippet.note ?? snippet.script,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -119,10 +110,7 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||||
// if (isMobile) {
|
// if (isMobile) {
|
||||||
SnippetEditPage.route.go(
|
SnippetEditPage.route.go(context, args: SnippetEditPageArgs(snippet: snippet));
|
||||||
context,
|
|
||||||
args: SnippetEditPageArgs(snippet: snippet),
|
|
||||||
);
|
|
||||||
// } else {
|
// } else {
|
||||||
// _splitViewCtrl.replace(SnippetEditPage(
|
// _splitViewCtrl.replace(SnippetEditPage(
|
||||||
// args: SnippetEditPageArgs(snippet: snippet),
|
// args: SnippetEditPageArgs(snippet: snippet),
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ class SnippetResultPage extends StatelessWidget {
|
|||||||
|
|
||||||
const SnippetResultPage({super.key, required this.args});
|
const SnippetResultPage({super.key, required this.args});
|
||||||
|
|
||||||
static const route = AppRouteArg(
|
static const route = AppRouteArg(page: SnippetResultPage.new, path: '/snippets/result');
|
||||||
page: SnippetResultPage.new,
|
|
||||||
path: '/snippets/result',
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -37,10 +34,7 @@ class SnippetResultPage extends StatelessWidget {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Text(
|
child: Text(item.result, textAlign: TextAlign.start),
|
||||||
item.result,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -88,10 +88,7 @@ extension _VirtKey on SSHPageState {
|
|||||||
while (initPath == null) {
|
while (initPath == null) {
|
||||||
// Check if we've exceeded timeout
|
// Check if we've exceeded timeout
|
||||||
if (DateTime.now().difference(startTime) > timeout) {
|
if (DateTime.now().difference(startTime) > timeout) {
|
||||||
contextSafe?.showRoundDialog(
|
contextSafe?.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty));
|
||||||
title: libL10n.error,
|
|
||||||
child: Text(libL10n.empty),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,10 +116,7 @@ extension _VirtKey on SSHPageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!initPath.startsWith('/')) {
|
if (!initPath.startsWith('/')) {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(title: libL10n.error, child: Text('${l10n.remotePath}: $initPath'));
|
||||||
title: libL10n.error,
|
|
||||||
child: Text('${l10n.remotePath}: $initPath'),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +132,7 @@ extension _VirtKey on SSHPageState {
|
|||||||
if (text != null) {
|
if (text != null) {
|
||||||
_terminal.textInput(text);
|
_terminal.textInput(text);
|
||||||
} else {
|
} else {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty));
|
||||||
title: libL10n.error,
|
|
||||||
child: Text(libL10n.empty),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,19 +16,14 @@ class SSHTabPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<SSHTabPage> createState() => _SSHTabPageState();
|
State<SSHTabPage> createState() => _SSHTabPageState();
|
||||||
|
|
||||||
static const route = AppRouteNoArg(
|
static const route = AppRouteNoArg(page: SSHTabPage.new, path: '/ssh');
|
||||||
page: SSHTabPage.new,
|
|
||||||
path: '/ssh',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
|
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
|
||||||
|
|
||||||
class _SSHTabPageState extends State<SSHTabPage>
|
class _SSHTabPageState extends State<SSHTabPage>
|
||||||
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||||
late final _TabMap _tabMap = {
|
late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null)};
|
||||||
libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null),
|
|
||||||
};
|
|
||||||
final _pageCtrl = PageController();
|
final _pageCtrl = PageController();
|
||||||
final _fabVN = 0.vn;
|
final _fabVN = 0.vn;
|
||||||
final _tabRN = RNode();
|
final _tabRN = RNode();
|
||||||
@@ -48,12 +43,7 @@ class _SSHTabPageState extends State<SSHTabPage>
|
|||||||
appBar: PreferredSizeListenBuilder(
|
appBar: PreferredSizeListenBuilder(
|
||||||
listenable: _tabRN,
|
listenable: _tabRN,
|
||||||
builder: () {
|
builder: () {
|
||||||
return _TabBar(
|
return _TabBar(idxVN: _fabVN, map: _tabMap, onTap: _onTapTab, onClose: _onTapClose);
|
||||||
idxVN: _fabVN,
|
|
||||||
map: _tabMap,
|
|
||||||
onTap: _onTapTab,
|
|
||||||
onClose: _onTapClose,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
@@ -159,12 +149,7 @@ extension on _SSHTabPageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
|
final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
const _TabBar({
|
const _TabBar({required this.idxVN, required this.map, required this.onTap, required this.onClose});
|
||||||
required this.idxVN,
|
|
||||||
required this.map,
|
|
||||||
required this.onTap,
|
|
||||||
required this.onClose,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ValueListenable<int> idxVN;
|
final ValueListenable<int> idxVN;
|
||||||
final _TabMap map;
|
final _TabMap map;
|
||||||
@@ -188,10 +173,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
itemBuilder: (_, idx) => _buildItem(idx),
|
itemBuilder: (_, idx) => _buildItem(idx),
|
||||||
separatorBuilder: (_, _) => Padding(
|
separatorBuilder: (_, _) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 17),
|
padding: const EdgeInsets.symmetric(vertical: 17),
|
||||||
child: Container(
|
child: Container(color: const Color.fromARGB(61, 158, 158, 158), width: 3),
|
||||||
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,
|
width: selected ? kWideWidth : kNarrowWidth,
|
||||||
duration: Durations.medium3,
|
duration: Durations.medium3,
|
||||||
curve: Curves.fastEaseInToSlowEaseOut,
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
child: OverflowBox(
|
child: OverflowBox(maxWidth: selected ? kWideWidth : null, child: btn),
|
||||||
maxWidth: selected ? kWideWidth : null,
|
|
||||||
child: btn,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,9 +259,7 @@ class _AddPage extends StatelessWidget {
|
|||||||
|
|
||||||
return ServerProvider.serverOrder.listenVal((order) {
|
return ServerProvider.serverOrder.listenVal((order) {
|
||||||
if (order.isEmpty) {
|
if (order.isEmpty) {
|
||||||
return Center(
|
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
|
||||||
child: Text(libL10n.empty, textAlign: TextAlign.center),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom grid
|
// Custom grid
|
||||||
@@ -316,7 +293,7 @@ class _AddPage extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
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 class LocalFilePageArgs {
|
||||||
final bool? isPickFile;
|
final bool? isPickFile;
|
||||||
final String? initDir;
|
final String? initDir;
|
||||||
const LocalFilePageArgs({
|
const LocalFilePageArgs({this.isPickFile, this.initDir});
|
||||||
this.isPickFile,
|
|
||||||
this.initDir,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalFilePage extends StatefulWidget {
|
class LocalFilePage extends StatefulWidget {
|
||||||
@@ -27,10 +24,7 @@ class LocalFilePage extends StatefulWidget {
|
|||||||
|
|
||||||
const LocalFilePage({super.key, this.args});
|
const LocalFilePage({super.key, this.args});
|
||||||
|
|
||||||
static const route = AppRoute<String, LocalFilePageArgs>(
|
static const route = AppRoute<String, LocalFilePageArgs>(page: LocalFilePage.new, path: '/files/local');
|
||||||
page: LocalFilePage.new,
|
|
||||||
path: '/files/local',
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LocalFilePage> createState() => _LocalFilePageState();
|
State<LocalFilePage> createState() => _LocalFilePageState();
|
||||||
@@ -98,9 +92,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
|
|||||||
Future<List<(FileSystemEntity, FileStat)>> getEntities() async {
|
Future<List<(FileSystemEntity, FileStat)>> getEntities() async {
|
||||||
final files = await Directory(_path.path).list().toList();
|
final files = await Directory(_path.path).list().toList();
|
||||||
final sorted = _sortType.value.sort(files);
|
final sorted = _sortType.value.sort(files);
|
||||||
final stats = await Future.wait(
|
final stats = await Future.wait(sorted.map((e) async => (e, await e.stat())));
|
||||||
sorted.map((e) async => (e, await e.stat())),
|
|
||||||
);
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +125,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
|
|||||||
final stat = item.$2;
|
final stat = item.$2;
|
||||||
final isDir = stat.type == FileSystemEntityType.directory;
|
final isDir = stat.type == FileSystemEntityType.directory;
|
||||||
|
|
||||||
return _buildItem(
|
return _buildItem(file: file, fileName: fileName, stat: stat, isDir: isDir);
|
||||||
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),
|
leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file),
|
||||||
title: Text(fileName),
|
title: Text(fileName),
|
||||||
subtitle: isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
|
subtitle: isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
|
||||||
trailing: Text(
|
trailing: Text(stat.modified.ymdhms(), style: UIs.textGrey),
|
||||||
stat.modified.ymdhms(),
|
|
||||||
style: UIs.textGrey,
|
|
||||||
),
|
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (isDir) {
|
if (isDir) {
|
||||||
_showDirActionDialog(file);
|
_showDirActionDialog(file);
|
||||||
@@ -187,8 +171,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSortBtn() {
|
Widget _buildSortBtn() {
|
||||||
return _sortType.listenVal(
|
return _sortType.listenVal((value) {
|
||||||
(value) {
|
|
||||||
return PopupMenuButton<_SortType>(
|
return PopupMenuButton<_SortType>(
|
||||||
icon: const Icon(Icons.sort),
|
icon: const Icon(Icons.sort),
|
||||||
itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(),
|
itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(),
|
||||||
@@ -196,8 +179,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
|
|||||||
_sortType.value = value;
|
_sortType.value = value;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -238,10 +220,12 @@ extension _Actions on _LocalFilePageState {
|
|||||||
title: libL10n.file,
|
title: libL10n.file,
|
||||||
child: Text(fileName),
|
child: Text(fileName),
|
||||||
actions: [
|
actions: [
|
||||||
Btn.ok(onTap: () {
|
Btn.ok(
|
||||||
|
onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
context.pop(file.path);
|
context.pop(file.path);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -382,21 +366,13 @@ extension _OnTapFile on _LocalFilePageState {
|
|||||||
);
|
);
|
||||||
if (spi == null) return;
|
if (spi == null) return;
|
||||||
|
|
||||||
final args = SftpPageArgs(
|
final args = SftpPageArgs(spi: spi, isSelect: true);
|
||||||
spi: spi,
|
|
||||||
isSelect: true,
|
|
||||||
);
|
|
||||||
final remotePath = await SftpPage.route.go(context, args);
|
final remotePath = await SftpPage.route.go(context, args);
|
||||||
if (remotePath == null) {
|
if (remotePath == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SftpProvider.add(SftpReq(
|
SftpProvider.add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload));
|
||||||
spi,
|
|
||||||
'$remotePath/$fileName',
|
|
||||||
file.absolute.path,
|
|
||||||
SftpReqType.upload,
|
|
||||||
));
|
|
||||||
context.showSnackBar(l10n.added2List);
|
context.showSnackBar(l10n.added2List);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,8 +380,7 @@ extension _OnTapFile on _LocalFilePageState {
|
|||||||
enum _SortType {
|
enum _SortType {
|
||||||
name,
|
name,
|
||||||
size,
|
size,
|
||||||
time,
|
time;
|
||||||
;
|
|
||||||
|
|
||||||
List<FileSystemEntity> sort(List<FileSystemEntity> files) {
|
List<FileSystemEntity> sort(List<FileSystemEntity> files) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@@ -437,13 +412,7 @@ enum _SortType {
|
|||||||
PopupMenuItem<_SortType> get menuItem {
|
PopupMenuItem<_SortType> get menuItem {
|
||||||
return PopupMenuItem(
|
return PopupMenuItem(
|
||||||
value: this,
|
value: this,
|
||||||
child: Row(
|
child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [Icon(icon), Text(i18n)]),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
Icon(icon),
|
|
||||||
Text(i18n),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,14 @@ class SftpMissionPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<SftpMissionPage> createState() => _SftpMissionPageState();
|
State<SftpMissionPage> createState() => _SftpMissionPageState();
|
||||||
|
|
||||||
static const route = AppRouteNoArg(
|
static const route = AppRouteNoArg(page: SftpMissionPage.new, path: '/sftp/mission');
|
||||||
page: SftpMissionPage.new,
|
|
||||||
path: '/sftp/mission',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SftpMissionPageState extends State<SftpMissionPage> {
|
class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(title: Text(l10n.mission, style: UIs.text18)),
|
||||||
title: Text(l10n.mission, style: UIs.text18),
|
|
||||||
),
|
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,10 +45,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
|||||||
status: status,
|
status: status,
|
||||||
subtitle: libL10n.error,
|
subtitle: libL10n.error,
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
onPressed: () => context.showRoundDialog(
|
onPressed: () => context.showRoundDialog(title: libL10n.error, child: Text(err.toString())),
|
||||||
title: libL10n.error,
|
|
||||||
child: Text(err.toString()),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.error),
|
icon: const Icon(Icons.error),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -109,9 +101,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
|||||||
|
|
||||||
Widget _buildFinished(SftpReqStatus status) {
|
Widget _buildFinished(SftpReqStatus status) {
|
||||||
final time = status.spentTime.toString();
|
final time = status.spentTime.toString();
|
||||||
final str = l10n.spentTime(
|
final str = l10n.spentTime(time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7)));
|
||||||
time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7)),
|
|
||||||
);
|
|
||||||
|
|
||||||
final btns = Row(
|
final btns = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -120,41 +110,26 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
final idx = status.req.localPath.lastIndexOf(Pfs.seperator);
|
final idx = status.req.localPath.lastIndexOf(Pfs.seperator);
|
||||||
final dir = status.req.localPath.substring(0, idx);
|
final dir = status.req.localPath.substring(0, idx);
|
||||||
LocalFilePage.route.go(
|
LocalFilePage.route.go(context, args: LocalFilePageArgs(initDir: dir));
|
||||||
context,
|
|
||||||
args: LocalFilePageArgs(initDir: dir),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.file_open),
|
icon: const Icon(Icons.file_open),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Pfs.sharePaths(paths: [status.req.localPath]),
|
onPressed: () => Pfs.sharePaths(paths: [status.req.localPath]),
|
||||||
icon: const Icon(Icons.open_in_new),
|
icon: const Icon(Icons.open_in_new),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return _wrapInCard(
|
return _wrapInCard(status: status, subtitle: str, trailing: btns);
|
||||||
status: status,
|
|
||||||
subtitle: str,
|
|
||||||
trailing: btns,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _wrapInCard({
|
Widget _wrapInCard({required SftpReqStatus status, String? subtitle, Widget? trailing}) {
|
||||||
required SftpReqStatus status,
|
|
||||||
String? subtitle,
|
|
||||||
Widget? trailing,
|
|
||||||
}) {
|
|
||||||
final time = DateTime.fromMicrosecondsSinceEpoch(status.id);
|
final time = DateTime.fromMicrosecondsSinceEpoch(status.id);
|
||||||
return CardX(
|
return CardX(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(time.hourMinute),
|
leading: Text(time.hourMinute),
|
||||||
title: Text(
|
title: Text(status.fileName, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||||
status.fileName,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
subtitle: subtitle == null ? null : Text(subtitle, style: UIs.textGrey),
|
subtitle: subtitle == null ? null : Text(subtitle, style: UIs.textGrey),
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
),
|
),
|
||||||
@@ -165,9 +140,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () => context.showRoundDialog(
|
onPressed: () => context.showRoundDialog(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
child: Text(libL10n.askContinue(
|
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.mission}($name)')),
|
||||||
'${libL10n.delete} ${l10n.mission}($name)',
|
|
||||||
)),
|
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SftpProvider.cancel(id);
|
SftpProvider.cancel(id);
|
||||||
|
|||||||
@@ -9,15 +9,9 @@ import 'package:server_box/view/page/ssh/page/page.dart';
|
|||||||
final class SystemdPage extends StatefulWidget {
|
final class SystemdPage extends StatefulWidget {
|
||||||
final SpiRequiredArgs args;
|
final SpiRequiredArgs args;
|
||||||
|
|
||||||
const SystemdPage({
|
const SystemdPage({super.key, required this.args});
|
||||||
super.key,
|
|
||||||
required this.args,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const route = AppRouteArg<void, SpiRequiredArgs>(
|
static const route = AppRouteArg<void, SpiRequiredArgs>(page: SystemdPage.new, path: '/systemd');
|
||||||
page: SystemdPage.new,
|
|
||||||
path: '/systemd',
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SystemdPage> createState() => _SystemdPageState();
|
State<SystemdPage> createState() => _SystemdPageState();
|
||||||
@@ -37,9 +31,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: const Text('Systemd'),
|
title: const Text('Systemd'),
|
||||||
actions: isDesktop
|
actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)] : null,
|
||||||
? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)]
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(onRefresh: _pro.getUnits, child: _buildBody()),
|
body: RefreshIndicator(onRefresh: _pro.getUnits, child: _buildBody()),
|
||||||
);
|
);
|
||||||
@@ -54,9 +46,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
|||||||
duration: Durations.medium1,
|
duration: Durations.medium1,
|
||||||
curve: Curves.fastEaseInToSlowEaseOut,
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
height: isBusy ? SizedLoading.medium.size : 0,
|
height: isBusy ? SizedLoading.medium.size : 0,
|
||||||
child: isBusy
|
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
|
||||||
? SizedLoading.medium
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -66,35 +56,24 @@ final class _SystemdPageState extends State<SystemdPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUnitList(VNode<List<SystemdUnit>> units) {
|
Widget _buildUnitList(VNode<List<SystemdUnit>> units) {
|
||||||
return units.listenVal(
|
return units.listenVal((units) {
|
||||||
(units) {
|
|
||||||
if (units.isEmpty) {
|
if (units.isEmpty) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
|
||||||
child:
|
|
||||||
CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return SliverList(
|
return SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
(context, index) {
|
|
||||||
final unit = units[index];
|
final unit = units[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: _buildScopeTag(unit.scope),
|
leading: _buildScopeTag(unit.scope),
|
||||||
title: unit.description != null
|
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
|
||||||
? TipText(unit.name, unit.description!)
|
subtitle: Wrap(
|
||||||
: Text(unit.name),
|
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
|
||||||
subtitle: Wrap(children: [
|
).paddingOnly(top: 7),
|
||||||
_buildStateTag(unit.state),
|
|
||||||
_buildTypeTag(unit.type),
|
|
||||||
]).paddingOnly(top: 7),
|
|
||||||
trailing: _buildUnitFuncs(unit),
|
trailing: _buildUnitFuncs(unit),
|
||||||
).cardx.paddingSymmetric(horizontal: 13);
|
).cardx.paddingSymmetric(horizontal: 13);
|
||||||
},
|
}, childCount: units.length),
|
||||||
childCount: units.length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUnitFuncs(SystemdUnit unit) {
|
Widget _buildUnitFuncs(SystemdUnit unit) {
|
||||||
@@ -128,11 +107,7 @@ final class _SystemdPageState extends State<SystemdPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [Icon(func.icon, size: 19), const SizedBox(width: 10), Text(func.name.capitalize)],
|
||||||
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,
|
color: color?.withValues(alpha: 0.7) ?? UIs.halfAlpha,
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
child: Text(tag, style: UIs.text11)
|
child: Text(tag, style: UIs.text11).paddingSymmetric(horizontal: 5, vertical: 1),
|
||||||
.paddingSymmetric(horizontal: 5, vertical: 1),
|
|
||||||
).paddingOnly(right: noPad ? 0 : 5);
|
).paddingOnly(right: noPad ? 0 : 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,12 @@ class OmitStartText extends StatelessWidget {
|
|||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
final TextOverflow? overflow;
|
final TextOverflow? overflow;
|
||||||
|
|
||||||
const OmitStartText(
|
const OmitStartText(this.text, {super.key, this.maxLines, this.style, this.overflow});
|
||||||
this.text, {
|
|
||||||
super.key,
|
|
||||||
this.maxLines,
|
|
||||||
this.style,
|
|
||||||
this.overflow,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(builder: (context, size) {
|
return LayoutBuilder(
|
||||||
|
builder: (context, size) {
|
||||||
bool exceeded = false;
|
bool exceeded = false;
|
||||||
int len = 0;
|
int len = 0;
|
||||||
for (; !exceeded && len < text.length; len++) {
|
for (; !exceeded && len < text.length; len++) {
|
||||||
@@ -27,11 +22,7 @@ class OmitStartText extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Use a textpainter to determine if it will exceed max lines
|
// Use a textpainter to determine if it will exceed max lines
|
||||||
final tp = TextPainter(
|
final tp = TextPainter(maxLines: maxLines ?? 1, textDirection: TextDirection.ltr, text: span);
|
||||||
maxLines: maxLines ?? 1,
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
text: span,
|
|
||||||
);
|
|
||||||
|
|
||||||
// trigger it to layout
|
// trigger it to layout
|
||||||
tp.layout(maxWidth: size.maxWidth);
|
tp.layout(maxWidth: size.maxWidth);
|
||||||
@@ -47,6 +38,7 @@ class OmitStartText extends StatelessWidget {
|
|||||||
maxLines: maxLines ?? 1,
|
maxLines: maxLines ?? 1,
|
||||||
style: style,
|
style: style,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,13 @@ import 'package:flutter/material.dart';
|
|||||||
final class PercentCircle extends StatelessWidget {
|
final class PercentCircle extends StatelessWidget {
|
||||||
final double percent;
|
final double percent;
|
||||||
|
|
||||||
const PercentCircle({
|
const PercentCircle({super.key, required this.percent});
|
||||||
super.key,
|
|
||||||
required this.percent,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final percent = switch (this.percent) {
|
final percent = switch (this.percent) {
|
||||||
0 => 0.01,
|
<= 0.01 => 0.01,
|
||||||
100 => 99.9,
|
>= 99.9 => 99.9,
|
||||||
// NaN
|
|
||||||
final val when val.isNaN => 0.01,
|
|
||||||
_ => this.percent,
|
_ => this.percent,
|
||||||
};
|
};
|
||||||
return Stack(
|
return Stack(
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ final class UnixPermOp {
|
|||||||
final bool w;
|
final bool w;
|
||||||
final bool x;
|
final bool x;
|
||||||
|
|
||||||
const UnixPermOp({
|
const UnixPermOp({required this.r, required this.w, required this.x});
|
||||||
required this.r,
|
|
||||||
required this.w,
|
|
||||||
required this.x,
|
|
||||||
});
|
|
||||||
|
|
||||||
UnixPermOp copyWith({bool? r, bool? w, bool? x}) {
|
UnixPermOp copyWith({bool? r, bool? w, bool? x}) {
|
||||||
return UnixPermOp(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x);
|
return UnixPermOp(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x);
|
||||||
@@ -24,8 +20,7 @@ final class UnixPermOp {
|
|||||||
enum UnixPermScope {
|
enum UnixPermScope {
|
||||||
user,
|
user,
|
||||||
group,
|
group,
|
||||||
other,
|
other;
|
||||||
;
|
|
||||||
|
|
||||||
String get title {
|
String get title {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
@@ -150,9 +145,6 @@ final class _UnixPermEditorState extends State<UnixPermEditor> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSwitch(bool value, void Function(bool) onChanged) {
|
Widget _buildSwitch(bool value, void Function(bool) onChanged) {
|
||||||
return Switch(
|
return Switch(value: value, onChanged: onChanged);
|
||||||
value: value,
|
|
||||||
onChanged: onChanged,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,9 +96,12 @@ include(flutter/generated_plugins.cmake)
|
|||||||
# By default, "installing" just makes a relocatable bundle in the build
|
# By default, "installing" just makes a relocatable bundle in the build
|
||||||
# directory.
|
# directory.
|
||||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
# 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)
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
endif()
|
|
||||||
|
|
||||||
# Start with a clean build bundle directory every time.
|
# Start with a clean build bundle directory every time.
|
||||||
install(CODE "
|
install(CODE "
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ fa1215b4be74 Up 12 hours firefly
|
|||||||
const images = [
|
const images = [
|
||||||
'rustdesk/rustdesk-server:latest',
|
'rustdesk/rustdesk-server:latest',
|
||||||
'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'];
|
const states = ['Up 2 hours', 'Up 41 minutes', 'Up 12 hours'];
|
||||||
for (var idx = 1; idx < lines.length; idx++) {
|
for (var idx = 1; idx < lines.length; idx++) {
|
||||||
|
|||||||
@@ -113,15 +113,12 @@ void main() {
|
|||||||
SensorAdaptor.virtual,
|
SensorAdaptor.virtual,
|
||||||
SensorAdaptor.pci,
|
SensorAdaptor.pci,
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(sensors.map((e) => e.summary), [
|
||||||
sensors.map((e) => e.summary),
|
|
||||||
[
|
|
||||||
'+56.0°C (high = +105.0°C, crit = +105.0°C)',
|
'+56.0°C (high = +105.0°C, crit = +105.0°C)',
|
||||||
'+27.8°C (crit = +119.0°C)',
|
'+27.8°C (crit = +119.0°C)',
|
||||||
'+56.0°C',
|
'+56.0°C',
|
||||||
'+45.9°C (low = -273.1°C, high = +83.8°C)',
|
'+45.9°C (low = -273.1°C, high = +83.8°C)',
|
||||||
],
|
]);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parse sensors2', () {
|
test('parse sensors2', () {
|
||||||
@@ -138,14 +135,11 @@ void main() {
|
|||||||
SensorAdaptor.pci,
|
SensorAdaptor.pci,
|
||||||
SensorAdaptor.pci,
|
SensorAdaptor.pci,
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(sensors.map((e) => e.summary), [
|
||||||
sensors.map((e) => e.summary),
|
|
||||||
[
|
|
||||||
'1.26 V',
|
'1.26 V',
|
||||||
'1.19 V (min = +0.00 V, max = +1.74 V)',
|
'1.19 V (min = +0.00 V, max = +1.74 V)',
|
||||||
'+45.9°C (low = -273.1°C, high = +69.8°C)',
|
'+45.9°C (low = -273.1°C, high = +69.8°C)',
|
||||||
'+44.9°C',
|
'+44.9°C',
|
||||||
],
|
]);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
87
test/uptime_test.dart
Normal file
87
test/uptime_test.dart
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Linux uptime parsing tests', () {
|
||||||
|
test('should parse uptime with days and hours:minutes', () {
|
||||||
|
const raw = '19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00';
|
||||||
|
final result = _testParseUpTime(raw);
|
||||||
|
expect(result, '61 days, 18:16');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse uptime with single day and hours:minutes', () {
|
||||||
|
const raw = '19:39:15 up 1 day, 2:34, 1 user, load average: 0.00, 0.00, 0.00';
|
||||||
|
final result = _testParseUpTime(raw);
|
||||||
|
expect(result, '1 day, 2:34');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse uptime with only hours:minutes', () {
|
||||||
|
const raw = '19:39:15 up 2:34, 1 user, load average: 0.00, 0.00, 0.00';
|
||||||
|
final result = _testParseUpTime(raw);
|
||||||
|
expect(result, '2:34');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse uptime with only minutes', () {
|
||||||
|
const raw = '19:39:15 up 34 min, 1 user, load average: 0.00, 0.00, 0.00';
|
||||||
|
final result = _testParseUpTime(raw);
|
||||||
|
expect(result, '34 min');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse uptime with days only (no time part)', () {
|
||||||
|
const raw = '19:39:15 up 5 days, 1 user, load average: 0.00, 0.00, 0.00';
|
||||||
|
final result = _testParseUpTime(raw);
|
||||||
|
expect(result, '5 days');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for invalid format', () {
|
||||||
|
const raw = 'invalid uptime format';
|
||||||
|
final result = _testParseUpTime(raw);
|
||||||
|
expect(result, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle edge case with empty string', () {
|
||||||
|
const raw = '';
|
||||||
|
final result = _testParseUpTime(raw);
|
||||||
|
expect(result, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to test the private _parseUpTime function
|
||||||
|
String? _testParseUpTime(String raw) {
|
||||||
|
final splitedUp = raw.split('up ');
|
||||||
|
if (splitedUp.length == 2) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
473
test/windows_test.dart
Normal file
473
test/windows_test.dart
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
|
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
||||||
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
|
import 'package:server_box/data/res/status.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Windows System Tests', () {
|
||||||
|
test('should verify Windows segments length matches command types', () {
|
||||||
|
final systemType = SystemType.windows;
|
||||||
|
final expectedLength = WindowsStatusCmdType.values.length;
|
||||||
|
expect(systemType.segmentsLen, equals(expectedLength));
|
||||||
|
expect(systemType.isSegmentsLenMatch(expectedLength), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate Windows PowerShell script correctly', () {
|
||||||
|
final script = ShellFunc.allScript({'custom_cmd': 'echo "test"'}, systemType: SystemType.windows);
|
||||||
|
|
||||||
|
expect(script, contains('PowerShell script for ServerBox'));
|
||||||
|
expect(script, contains('function SbStatus'));
|
||||||
|
expect(script, contains('function SbProcess'));
|
||||||
|
expect(script, contains('function SbShutdown'));
|
||||||
|
expect(script, contains('function SbReboot'));
|
||||||
|
expect(script, contains('function SbSuspend'));
|
||||||
|
expect(script, contains('switch (\$args[0])'));
|
||||||
|
expect(script, contains('"-s" { SbStatus }'));
|
||||||
|
expect(script, contains('echo "test"'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Windows system parsing with real data', () async {
|
||||||
|
final segments = _windowsStatusSegments;
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
|
||||||
|
// Verify system information was parsed
|
||||||
|
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
|
||||||
|
expect(result.more[StatusCmdType.host], equals('LKH6'));
|
||||||
|
|
||||||
|
// Verify CPU information
|
||||||
|
expect(result.cpu.now, isNotEmpty);
|
||||||
|
expect(result.cpu.brand.keys.first, contains('12th Gen Intel(R) Core(TM) i5-12490F'));
|
||||||
|
|
||||||
|
// Verify memory information
|
||||||
|
expect(result.mem, isNotNull);
|
||||||
|
expect(result.mem.total, equals(66943944));
|
||||||
|
expect(result.mem.free, equals(58912812));
|
||||||
|
|
||||||
|
// Verify disk information
|
||||||
|
expect(result.disk, isNotEmpty);
|
||||||
|
final cDrive = result.disk.firstWhere((disk) => disk.path == 'C:');
|
||||||
|
expect(cDrive.fsTyp, equals('NTFS'));
|
||||||
|
expect(cDrive.size, equals(BigInt.parse('999271952384') ~/ BigInt.from(1024)));
|
||||||
|
expect(cDrive.avail, equals(BigInt.parse('386084032512') ~/ BigInt.from(1024)));
|
||||||
|
|
||||||
|
// Verify TCP connections
|
||||||
|
expect(result.tcp, isNotNull);
|
||||||
|
expect(result.tcp.active, equals(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse Windows CPU data correctly', () async {
|
||||||
|
const cpuJson = '''
|
||||||
|
{
|
||||||
|
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
|
||||||
|
"LoadPercentage": 42
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
final segments = ['__windows', '1754151483', '', '', cpuJson];
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
|
||||||
|
expect(result.cpu.now, hasLength(1));
|
||||||
|
expect(result.cpu.now.first.user, equals(42));
|
||||||
|
expect(result.cpu.now.first.idle, equals(58));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse Windows memory data correctly', () async {
|
||||||
|
const memoryJson = '''
|
||||||
|
{
|
||||||
|
"TotalVisibleMemorySize": 66943944,
|
||||||
|
"FreePhysicalMemory": 58912812
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
final segments = ['__windows', '1754151483', '', '', '', '', '', '', memoryJson];
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
|
||||||
|
expect(result.mem, isNotNull);
|
||||||
|
expect(result.mem.total, equals(66943944));
|
||||||
|
expect(result.mem.free, equals(58912812));
|
||||||
|
expect(result.mem.avail, equals(58912812));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse Windows disk data correctly', () async {
|
||||||
|
const diskJson = '''
|
||||||
|
{
|
||||||
|
"DeviceID": "C:",
|
||||||
|
"Size": 999271952384,
|
||||||
|
"FreeSpace": 386084032512,
|
||||||
|
"FileSystem": "NTFS"
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
final segments = ['__windows', '1754151483', '', '', '', '', '', diskJson];
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
|
||||||
|
expect(result.disk, hasLength(1));
|
||||||
|
final disk = result.disk.first;
|
||||||
|
expect(disk.path, equals('C:'));
|
||||||
|
expect(disk.mount, equals('C:'));
|
||||||
|
expect(disk.fsTyp, equals('NTFS'));
|
||||||
|
expect(disk.size, equals(BigInt.parse('999271952384') ~/ BigInt.from(1024)));
|
||||||
|
expect(disk.avail, equals(BigInt.parse('386084032512') ~/ BigInt.from(1024)));
|
||||||
|
expect(disk.usedPercent, equals(61));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse Windows battery data correctly', () async {
|
||||||
|
const batteryJson = '''
|
||||||
|
{
|
||||||
|
"EstimatedChargeRemaining": 85,
|
||||||
|
"BatteryStatus": 6
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
// Create segments with enough elements to reach battery position
|
||||||
|
final segments = List.filled(WindowsStatusCmdType.values.length, '');
|
||||||
|
segments[0] = '__windows';
|
||||||
|
segments[WindowsStatusCmdType.battery.index] = batteryJson;
|
||||||
|
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
|
||||||
|
expect(result.batteries, hasLength(1));
|
||||||
|
final battery = result.batteries.first;
|
||||||
|
expect(battery.name, equals('Battery'));
|
||||||
|
expect(battery.percent, equals(85));
|
||||||
|
expect(battery.status.name, equals('charging'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Windows uptime parsing correctly', () async {
|
||||||
|
// Test new format with date line + uptime days
|
||||||
|
const uptimeNewFormat = 'Friday, July 25, 2025 2:26:42 PM\n2';
|
||||||
|
|
||||||
|
final segments = List.filled(WindowsStatusCmdType.values.length, '');
|
||||||
|
segments[0] = '__windows';
|
||||||
|
segments[WindowsStatusCmdType.uptime.index] = uptimeNewFormat;
|
||||||
|
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
|
||||||
|
expect(result.more[StatusCmdType.uptime], isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Windows uptime parsing with old format', () async {
|
||||||
|
const uptimeDateTime = 'Friday, July 25, 2025 2:26:42 PM';
|
||||||
|
|
||||||
|
final segments = List.filled(WindowsStatusCmdType.values.length, '');
|
||||||
|
segments[0] = '__windows';
|
||||||
|
segments[WindowsStatusCmdType.uptime.index] = uptimeDateTime;
|
||||||
|
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
|
||||||
|
expect(result.more[StatusCmdType.uptime], isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Windows script path generation', () {
|
||||||
|
const serverId = 'test-server';
|
||||||
|
|
||||||
|
final scriptPath = ShellFunc.getScriptPath(serverId, systemType: SystemType.windows);
|
||||||
|
expect(scriptPath, contains('.ps1'));
|
||||||
|
expect(scriptPath, contains('\\'));
|
||||||
|
|
||||||
|
final installCmd = ShellFunc.getInstallShellCmd(serverId, systemType: SystemType.windows);
|
||||||
|
expect(installCmd, contains('New-Item'));
|
||||||
|
expect(installCmd, contains('Set-Content'));
|
||||||
|
// No longer contains 'powershell' prefix as commands now run in PowerShell session
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should execute Windows commands correctly', () {
|
||||||
|
const serverId = 'test-server';
|
||||||
|
|
||||||
|
final statusCmd = ShellFunc.status.exec(serverId, systemType: SystemType.windows);
|
||||||
|
expect(statusCmd, contains('powershell'));
|
||||||
|
expect(statusCmd, contains('-ExecutionPolicy Bypass'));
|
||||||
|
expect(statusCmd, contains('-s'));
|
||||||
|
|
||||||
|
final processCmd = ShellFunc.process.exec(serverId, systemType: SystemType.windows);
|
||||||
|
expect(processCmd, contains('powershell'));
|
||||||
|
expect(processCmd, contains('-p'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle GPU detection on Windows', () async {
|
||||||
|
const nvidiaNotFound = 'NVIDIA driver not found';
|
||||||
|
const amdNotFound = 'AMD driver not found';
|
||||||
|
|
||||||
|
final segments = List.filled(WindowsStatusCmdType.values.length, '');
|
||||||
|
segments[0] = '__windows';
|
||||||
|
segments[WindowsStatusCmdType.nvidia.index] = nvidiaNotFound;
|
||||||
|
segments[WindowsStatusCmdType.amd.index] = amdNotFound;
|
||||||
|
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
|
||||||
|
// Should not throw errors even when GPU drivers are not found
|
||||||
|
expect(result.nvidia, anyOf(isNull, isEmpty));
|
||||||
|
expect(result.amd, anyOf(isNull, isEmpty));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Windows error conditions gracefully', () async {
|
||||||
|
// Test with malformed JSON and error messages
|
||||||
|
final segments = [
|
||||||
|
'__windows',
|
||||||
|
'1754151483',
|
||||||
|
'Network adapter error',
|
||||||
|
'Microsoft Windows 11 Pro for Workstations',
|
||||||
|
'invalid json {',
|
||||||
|
'uptime error',
|
||||||
|
'connection error',
|
||||||
|
'disk error',
|
||||||
|
'memory error',
|
||||||
|
'temp error',
|
||||||
|
'LKH6',
|
||||||
|
'diskio error',
|
||||||
|
'battery error',
|
||||||
|
'NVIDIA driver not found',
|
||||||
|
'AMD driver not found',
|
||||||
|
'sensor error',
|
||||||
|
'smart error',
|
||||||
|
'12th Gen Intel(R) Core(TM) i5-12490F',
|
||||||
|
];
|
||||||
|
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not throw exceptions
|
||||||
|
expect(() async => await getStatus(req), returnsNormally);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
|
||||||
|
expect(result.more[StatusCmdType.host], equals('LKH6'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Windows temperature error output gracefully', () async {
|
||||||
|
// Test with actual error output from win_raw.txt
|
||||||
|
final segments = [
|
||||||
|
'__windows',
|
||||||
|
'1754151483',
|
||||||
|
'', // network
|
||||||
|
'Microsoft Windows 11 Pro for Workstations', // system
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
|
||||||
|
"LoadPercentage": 42
|
||||||
|
}
|
||||||
|
''', // cpu
|
||||||
|
'Friday, July 25, 2025 2:26:42 PM', // uptime
|
||||||
|
'2', // connections
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"DeviceID": "C:",
|
||||||
|
"Size": 999271952384,
|
||||||
|
"FreeSpace": 386084032512,
|
||||||
|
"FileSystem": "NTFS"
|
||||||
|
}
|
||||||
|
''', // disk
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"TotalVisibleMemorySize": 66943944,
|
||||||
|
"FreePhysicalMemory": 58912812
|
||||||
|
}
|
||||||
|
''', // memory
|
||||||
|
'''
|
||||||
|
The string is missing the terminator: ".
|
||||||
|
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
|
||||||
|
+ FullyQualifiedErrorId : TerminatorExpectedAtEndOfString
|
||||||
|
''', // temp (error output)
|
||||||
|
'LKH6', // host
|
||||||
|
'', // diskio
|
||||||
|
'', // battery
|
||||||
|
'NVIDIA driver not found', // nvidia
|
||||||
|
'AMD driver not found', // amd
|
||||||
|
'', // sensors
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"DeviceId": "0",
|
||||||
|
"Temperature": 41,
|
||||||
|
"TemperatureMax": 70,
|
||||||
|
"Wear": 0,
|
||||||
|
"PowerOnHours": null
|
||||||
|
}
|
||||||
|
''', // smart
|
||||||
|
'12th Gen Intel(R) Core(TM) i5-12490F', // cpu brand
|
||||||
|
];
|
||||||
|
|
||||||
|
final serverStatus = InitStatus.status;
|
||||||
|
|
||||||
|
final req = ServerStatusUpdateReq(
|
||||||
|
system: SystemType.windows,
|
||||||
|
ss: serverStatus,
|
||||||
|
segments: segments,
|
||||||
|
customCmds: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not throw exceptions even with error output in temperature values
|
||||||
|
expect(() async => await getStatus(req), returnsNormally);
|
||||||
|
|
||||||
|
final result = await getStatus(req);
|
||||||
|
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
|
||||||
|
expect(result.more[StatusCmdType.host], equals('LKH6'));
|
||||||
|
// Temperature should be empty since we got error output
|
||||||
|
expect(result.temps.isEmpty, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample Windows status segments based on real PowerShell output
|
||||||
|
final _windowsStatusSegments = [
|
||||||
|
'__windows', // System type marker
|
||||||
|
'1754151483', // Unix timestamp
|
||||||
|
'', // Network data (empty for now)
|
||||||
|
'Microsoft Windows 11 Pro for Workstations', // System name
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
|
||||||
|
"LoadPercentage": 42
|
||||||
|
}
|
||||||
|
''', // CPU data
|
||||||
|
'Friday, July 25, 2025 2:26:42 PM', // Uptime (boot time)
|
||||||
|
'2', // Connection count
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"DeviceID": "C:",
|
||||||
|
"Size": 999271952384,
|
||||||
|
"FreeSpace": 386084032512,
|
||||||
|
"FileSystem": "NTFS"
|
||||||
|
}
|
||||||
|
''', // Disk data
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"TotalVisibleMemorySize": 66943944,
|
||||||
|
"FreePhysicalMemory": 58912812
|
||||||
|
}
|
||||||
|
''', // Memory data
|
||||||
|
'', // Temperature (combined command - empty due to OpenHardwareMonitor error)
|
||||||
|
'LKH6', // Hostname
|
||||||
|
'', // Disk I/O (empty for now)
|
||||||
|
'', // Battery data (empty)
|
||||||
|
'NVIDIA driver not found', // NVIDIA GPU
|
||||||
|
'AMD driver not found', // AMD GPU
|
||||||
|
'', // Sensors (empty due to OpenHardwareMonitor error)
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"CimClass": {
|
||||||
|
"CimSuperClassName": "MSFT_StorageObject",
|
||||||
|
"CimSuperClass": {
|
||||||
|
"CimSuperClassName": null,
|
||||||
|
"CimSuperClass": null,
|
||||||
|
"CimClassProperties": "ObjectId PassThroughClass PassThroughIds PassThroughNamespace PassThroughServer UniqueId",
|
||||||
|
"CimClassQualifiers": "Abstract = True locale = 1033",
|
||||||
|
"CimClassMethods": "",
|
||||||
|
"CimSystemProperties": "Microsoft.Management.Infrastructure.CimSystemProperties"
|
||||||
|
},
|
||||||
|
"CimClassProperties": [
|
||||||
|
"ObjectId",
|
||||||
|
"PassThroughClass",
|
||||||
|
"PassThroughIds",
|
||||||
|
"PassThroughNamespace",
|
||||||
|
"PassThroughServer",
|
||||||
|
"UniqueId",
|
||||||
|
"DeviceId",
|
||||||
|
"FlushLatencyMax",
|
||||||
|
"LoadUnloadCycleCount",
|
||||||
|
"LoadUnloadCycleCountMax",
|
||||||
|
"ManufactureDate",
|
||||||
|
"PowerOnHours",
|
||||||
|
"ReadErrorsCorrected",
|
||||||
|
"ReadErrorsTotal",
|
||||||
|
"ReadErrorsUncorrected",
|
||||||
|
"ReadLatencyMax",
|
||||||
|
"StartStopCycleCount",
|
||||||
|
"StartStopCycleCountMax",
|
||||||
|
"Temperature",
|
||||||
|
"TemperatureMax",
|
||||||
|
"Wear",
|
||||||
|
"WriteErrorsCorrected",
|
||||||
|
"WriteErrorsTotal",
|
||||||
|
"WriteErrorsUncorrected",
|
||||||
|
"WriteLatencyMax"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Temperature": 46,
|
||||||
|
"TemperatureMax": 70,
|
||||||
|
"Wear": 0,
|
||||||
|
"ReadLatencyMax": 1930,
|
||||||
|
"WriteLatencyMax": 1903,
|
||||||
|
"FlushLatencyMax": 262
|
||||||
|
}
|
||||||
|
''', // Disk SMART data
|
||||||
|
'12th Gen Intel(R) Core(TM) i5-12490F', // CPU brand
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user