mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-15 20:55:25 +01:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
584af5423a | ||
|
|
95f8e571c1 | ||
|
|
9c9648656d | ||
|
|
6880bcc192 | ||
|
|
3a615449e3 | ||
|
|
46a12bc844 | ||
|
|
8d597294a4 | ||
|
|
682a6e4f2d | ||
|
|
8c3302cf0d | ||
|
|
ec4bf3df24 | ||
|
|
263d4eabb4 | ||
|
|
c6439673b8 | ||
|
|
a35d21981b | ||
|
|
dbc873c0c0 | ||
|
|
e69808a2f6 | ||
|
|
55b3ba63ec | ||
|
|
006e66d825 | ||
|
|
c556c0f1b5 | ||
|
|
c42c701ffc | ||
|
|
e6db2db320 | ||
|
|
66ecb02d9e | ||
|
|
8e7de604ee |
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.32.1"
|
||||
flutter-version: "3.32.2"
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
@@ -66,8 +66,7 @@ jobs:
|
||||
sudo chmod +x /bin/appimagetool
|
||||
- name: Build
|
||||
run: |
|
||||
dart run fl_build
|
||||
dart run flutter_distributor:main package --platform=linux --target=appimage
|
||||
dart run fl_build -p linux
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
appimage_name=$(ls dist/*/*.AppImage)
|
||||
|
||||
17
README.md
17
README.md
@@ -10,7 +10,7 @@ English | [简体中文](README_zh.md)
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
A Flutter project which provide charts to display <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> server status and tools to manage server.
|
||||
A Flutter project which provides charts to display Linux, Unix and Windows server status and tools to manage servers.
|
||||
<br>
|
||||
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
|
||||
</p>
|
||||
@@ -26,25 +26,26 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 📥 Install
|
||||
## 📥 Installation
|
||||
|
||||
| Platform | From |
|
||||
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|Platform| From|
|
||||
|--|--|
|
||||
| iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) |
|
||||
| Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) |
|
||||
| Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) |
|
||||
|
||||
Please only download pkgs from the source that **you trust**!
|
||||
|
||||
## 🔖 Feature
|
||||
## 🔖 Features
|
||||
|
||||
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
|
||||
- `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`...
|
||||
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)
|
||||
|
||||
## 🆘 Help
|
||||
|
||||
<div align="center">
|
||||
<a href="https://qm.qq.com/q/daCGa7eShG"><img alt="qq" src="https://img.shields.io/badge/QQ-Group-pink"></a>
|
||||
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
|
||||
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
|
||||
</div>
|
||||
@@ -60,10 +61,12 @@ Before you open an issue, please read the following:
|
||||
|
||||
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
|
||||
|
||||
## 🧱 Contribution
|
||||
## 🧱 Contributions
|
||||
|
||||
Any positive contribution is welcome.
|
||||
|
||||
If I forgot to add your name to the contributors list, please add a comment in the issue or PR you opened to let me know, I will add it as soon as possible.
|
||||
|
||||
### Development
|
||||
|
||||
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
|
||||
|
||||
25
README_zh.md
25
README_zh.md
@@ -10,13 +10,13 @@
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
使用 Flutter 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
|
||||
使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱,提供服务器状态图表和管理工具。
|
||||
<br>
|
||||
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>。
|
||||
</p>
|
||||
|
||||
|
||||
## 🏙️ 截屏
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
|
||||
@@ -26,20 +26,19 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## 📥 安装
|
||||
|
||||
平台 | 下载
|
||||
----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
平台|下载
|
||||
--|--
|
||||
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
|
||||
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
|
||||
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)
|
||||
|
||||
请从 **信任** 的来源下载!
|
||||
|
||||
|
||||
## 🔖 特点
|
||||
- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
|
||||
|
||||
- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理,`S.M.A.R.T`...
|
||||
- 特殊支持:`生物认证`、`推送`、`桌面小部件`、`watchOS App`、`跟随系统颜色`...
|
||||
- 本地化
|
||||
- English, 简体中文
|
||||
@@ -47,10 +46,10 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
||||
- Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix);
|
||||
- 感谢贡献者们!
|
||||
|
||||
|
||||
## 🆘 帮助
|
||||
|
||||
<div align="center">
|
||||
<a href="https://qm.qq.com/q/daCGa7eShG"><img alt="qq" src="https://img.shields.io/badge/QQ-群-pink"></a>
|
||||
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
|
||||
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
|
||||
</div>
|
||||
@@ -59,26 +58,32 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
||||
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
|
||||
|
||||
反馈前须知:
|
||||
|
||||
1. 反馈问题请附带 log(点击首页右上角),并以 bug 模版提交。
|
||||
2. 反馈问题前请检查是否是 serverbox 的问题。
|
||||
3. 欢迎所有有效、正面的反馈,主观(比如你觉得其他UI更好看)的反馈不一定会接受
|
||||
|
||||
|
||||
## 🧱 贡献
|
||||
|
||||
任何正面的贡献都欢迎。
|
||||
|
||||
如果我忘记在贡献者列表中添加你的名字,请在你打开的 issue 或 PR 中添加评论让我知道,我会尽快添加。
|
||||
|
||||
### 开发
|
||||
|
||||
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
|
||||
2. 克隆这个仓库, 运行 `flutter run` 启动应用
|
||||
3. 运行 `dart run fl_build -p PLATFORM` 构建应用
|
||||
|
||||
### 翻译
|
||||
|
||||
[指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。
|
||||
|
||||
## 💡 我的其它 Apps
|
||||
|
||||
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。
|
||||
- [更多](https://github.com/lollipopkit) - 工具 & etc.
|
||||
|
||||
|
||||
## 📝 协议
|
||||
|
||||
`GPL v3 lollipopkit`
|
||||
|
||||
@@ -92,6 +92,13 @@ android {
|
||||
// No applicationIdSuffix or resValue here
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs.
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles.
|
||||
includeInBundle = false
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:hasFragileUserData="true"
|
||||
android:restoreAnyVersion="true"
|
||||
tools:targetApi="q">
|
||||
|
||||
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
|
||||
</full-backup-content>
|
||||
6505
coverage/lcov.info
Normal file
6505
coverage/lcov.info
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- icloud_storage (0.0.1):
|
||||
- Flutter
|
||||
- local_auth_darwin (0.0.1):
|
||||
@@ -38,6 +40,7 @@ DEPENDENCIES:
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- icloud_storage (from `.symlinks/plugins/icloud_storage/ios`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
@@ -60,6 +63,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
icloud_storage:
|
||||
:path: ".symlinks/plugins/icloud_storage/ios"
|
||||
local_auth_darwin:
|
||||
@@ -87,6 +92,7 @@ SPEC CHECKSUMS:
|
||||
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
|
||||
@@ -672,7 +672,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -682,7 +682,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -808,7 +808,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -818,7 +818,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -836,7 +836,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -846,7 +846,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -867,7 +867,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -880,7 +880,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
@@ -906,7 +906,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -919,7 +919,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -942,7 +942,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -955,7 +955,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -978,7 +978,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -990,7 +990,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
@@ -1019,7 +1019,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1031,7 +1031,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
PRODUCT_NAME = ServerBox;
|
||||
@@ -1057,7 +1057,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1184;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1069,7 +1069,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1184;
|
||||
MARKETING_VERSION = 1.0.1206;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
PRODUCT_NAME = ServerBox;
|
||||
|
||||
@@ -14,13 +14,20 @@ class PhoneConnMgr: NSObject, WCSessionDelegate, ObservableObject {
|
||||
set {
|
||||
Store.setCtx(newValue)
|
||||
updateUrls(newValue)
|
||||
|
||||
// Notify the view to update, but the [urls] are already published
|
||||
// so the view will automatically update when [urls] changes.
|
||||
// DispatchQueue.main.async {
|
||||
// self.objectWillChange.send()
|
||||
// }
|
||||
}
|
||||
get {
|
||||
return _ctx
|
||||
}
|
||||
}
|
||||
var userInfo: [String: Any] = [:]
|
||||
@Published var urls: [String] = []
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
if !WCSession.isSupported() {
|
||||
@@ -29,24 +36,85 @@ class PhoneConnMgr: NSObject, WCSessionDelegate, ObservableObject {
|
||||
session = WCSession.default
|
||||
session?.delegate = self
|
||||
session?.activate()
|
||||
|
||||
ctx = Store.getCtx()
|
||||
|
||||
_ctx = Store.getCtx()
|
||||
updateUrls(_ctx)
|
||||
}
|
||||
|
||||
|
||||
func updateUrls(_ val: [String: Any]) {
|
||||
if let urls = val["urls"] as? [String] {
|
||||
self.urls = urls.filter { !$0.isEmpty }
|
||||
DispatchQueue.main.async {
|
||||
self.urls = urls.filter { !$0.isEmpty }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||
|
||||
|
||||
func session(
|
||||
_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState,
|
||||
error: Error?
|
||||
) {
|
||||
// Request latest data when the session is activated
|
||||
if activationState == .activated {
|
||||
requestLatestData()
|
||||
}
|
||||
}
|
||||
|
||||
// implement session:didReceiveApplicationContext:
|
||||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
|
||||
ctx = applicationContext
|
||||
// Receive realtime msgs
|
||||
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
|
||||
DispatchQueue.main.async {
|
||||
self.ctx = message
|
||||
}
|
||||
}
|
||||
|
||||
// Receive UserInfo
|
||||
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
||||
DispatchQueue.main.async {
|
||||
self.ctx = userInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Receive Application Context
|
||||
func session(
|
||||
_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]
|
||||
) {
|
||||
DispatchQueue.main.async {
|
||||
self.ctx = applicationContext
|
||||
}
|
||||
}
|
||||
|
||||
private func requestLatestData(timeout: TimeInterval = 5.0, maxRetries: Int = 1) {
|
||||
guard let session = session, session.isReachable else { return }
|
||||
|
||||
var didReceiveResponse = false
|
||||
var retries = 0
|
||||
|
||||
func sendRequest() {
|
||||
session.sendMessage(["action": "requestData"]) { response in
|
||||
didReceiveResponse = true
|
||||
DispatchQueue.main.async {
|
||||
self.ctx = response
|
||||
}
|
||||
} errorHandler: { error in
|
||||
print("Request data failed: \(error)")
|
||||
// Optionally, handle error UI here
|
||||
}
|
||||
|
||||
// Timeout handling
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if !didReceiveResponse {
|
||||
if retries < maxRetries {
|
||||
retries += 1
|
||||
print("No response, retrying requestLatestData (\(retries))...")
|
||||
sendRequest()
|
||||
} else {
|
||||
print("Request data timed out after \(retries + 1) attempts.")
|
||||
// Optionally, update UI to indicate timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendRequest()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
41
lib/app.dart
41
lib/app.dart
@@ -40,17 +40,13 @@ class MyApp extends StatelessWidget {
|
||||
light: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: UIs.colorSeed,
|
||||
appBarTheme: AppBarTheme(
|
||||
scrolledUnderElevation: 0.0,
|
||||
),
|
||||
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||
),
|
||||
dark: ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorSchemeSeed: UIs.colorSeed,
|
||||
appBarTheme: AppBarTheme(
|
||||
scrolledUnderElevation: 0.0,
|
||||
),
|
||||
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -58,15 +54,8 @@ class MyApp extends StatelessWidget {
|
||||
Widget _buildDynamicColor(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (light, dark) {
|
||||
final lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: light,
|
||||
);
|
||||
final darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: dark,
|
||||
);
|
||||
final lightTheme = ThemeData(useMaterial3: true, colorScheme: light);
|
||||
final darkTheme = ThemeData(useMaterial3: true, brightness: Brightness.dark, colorScheme: dark);
|
||||
if (context.isDark && dark != null) {
|
||||
UIs.primaryColor = dark.primary;
|
||||
} else if (!context.isDark && light != null) {
|
||||
@@ -78,11 +67,7 @@ class MyApp extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildApp(
|
||||
BuildContext ctx, {
|
||||
required ThemeData light,
|
||||
required ThemeData dark,
|
||||
}) {
|
||||
Widget _buildApp(BuildContext ctx, {required ThemeData light, required ThemeData dark}) {
|
||||
final tMode = Stores.setting.themeMode.fetch();
|
||||
// Issue #57
|
||||
final themeMode = switch (tMode) {
|
||||
@@ -97,16 +82,13 @@ class MyApp extends StatelessWidget {
|
||||
builder: (context, child) => ResponsiveBreakpoints.builder(
|
||||
child: child ?? UIs.placeholder,
|
||||
breakpoints: const [
|
||||
Breakpoint(start: 0, end: 450, name: MOBILE),
|
||||
Breakpoint(start: 451, end: 800, name: TABLET),
|
||||
Breakpoint(start: 801, end: 1920, name: DESKTOP),
|
||||
Breakpoint(start: 0, end: 600, name: MOBILE),
|
||||
Breakpoint(start: 600, end: 1199, name: TABLET),
|
||||
Breakpoint(start: 1199, end: 3840, name: DESKTOP),
|
||||
],
|
||||
),
|
||||
locale: locale,
|
||||
localizationsDelegates: const [
|
||||
LibLocalizations.delegate,
|
||||
...AppLocalizations.localizationsDelegates,
|
||||
],
|
||||
localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeListResolutionCallback: LocaleUtil.resolve,
|
||||
navigatorObservers: [AppRouteObserver.instance],
|
||||
@@ -128,10 +110,7 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
child = const HomePage();
|
||||
|
||||
return VirtualWindowFrame(
|
||||
title: BuildData.name,
|
||||
child: child,
|
||||
);
|
||||
return VirtualWindowFrame(title: BuildData.name, child: child);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -12,21 +12,9 @@ extension SftpFileX on SftpFileMode {
|
||||
|
||||
UnixPerm toUnixPerm() {
|
||||
return UnixPerm(
|
||||
user: UnixPermOp(
|
||||
r: userRead,
|
||||
w: userWrite,
|
||||
x: userExecute,
|
||||
),
|
||||
group: UnixPermOp(
|
||||
r: groupRead,
|
||||
w: groupWrite,
|
||||
x: groupExecute,
|
||||
),
|
||||
other: UnixPermOp(
|
||||
r: otherRead,
|
||||
w: otherWrite,
|
||||
x: otherExecute,
|
||||
),
|
||||
user: UnixPermOp(r: userRead, w: userWrite, x: userExecute),
|
||||
group: UnixPermOp(r: groupRead, w: groupWrite, x: groupExecute),
|
||||
other: UnixPermOp(r: otherRead, w: otherWrite, x: otherExecute),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
|
||||
@@ -13,6 +14,52 @@ typedef OnStdin = void Function(SSHSession session);
|
||||
typedef PwdRequestFunc = Future<String?> Function(String? user);
|
||||
|
||||
extension SSHClientX on SSHClient {
|
||||
/// Create a persistent PowerShell session for Windows commands
|
||||
Future<(SSHSession, String)> execPowerShell(
|
||||
OnStdin onStdin, {
|
||||
SSHPtyConfig? pty,
|
||||
OnStdout? onStdout,
|
||||
OnStdout? onStderr,
|
||||
bool stdout = true,
|
||||
bool stderr = true,
|
||||
Map<String, String>? env,
|
||||
}) async {
|
||||
final session = await execute(
|
||||
'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
|
||||
pty: pty,
|
||||
environment: env,
|
||||
);
|
||||
|
||||
final result = BytesBuilder(copy: false);
|
||||
final stdoutDone = Completer<void>();
|
||||
final stderrDone = Completer<void>();
|
||||
|
||||
session.stdout.listen(
|
||||
(e) {
|
||||
onStdout?.call(e.string, session);
|
||||
if (stdout) result.add(e);
|
||||
},
|
||||
onDone: stdoutDone.complete,
|
||||
onError: stderrDone.completeError,
|
||||
);
|
||||
|
||||
session.stderr.listen(
|
||||
(e) {
|
||||
onStderr?.call(e.string, session);
|
||||
if (stderr) result.add(e);
|
||||
},
|
||||
onDone: stderrDone.complete,
|
||||
onError: stderrDone.completeError,
|
||||
);
|
||||
|
||||
onStdin(session);
|
||||
|
||||
await stdoutDone.future;
|
||||
await stderrDone.future;
|
||||
|
||||
return (session, result.takeBytes().string);
|
||||
}
|
||||
|
||||
Future<(SSHSession, String)> exec(
|
||||
OnStdin onStdin, {
|
||||
String? entry,
|
||||
@@ -22,9 +69,14 @@ extension SSHClientX on SSHClient {
|
||||
bool stdout = true,
|
||||
bool stderr = true,
|
||||
Map<String, String>? env,
|
||||
SystemType? systemType,
|
||||
}) async {
|
||||
final session = await execute(
|
||||
entry ?? 'cat | sh',
|
||||
entry ??
|
||||
switch (systemType) {
|
||||
SystemType.windows => 'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
|
||||
_ => 'cat | sh',
|
||||
},
|
||||
pty: pty,
|
||||
environment: env,
|
||||
);
|
||||
@@ -81,9 +133,7 @@ extension SSHClientX on SSHClient {
|
||||
isRequestingPwd = true;
|
||||
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
||||
if (context == null) return;
|
||||
final pwd = context.mounted
|
||||
? await context.showPwdDialog(title: user, id: id)
|
||||
: null;
|
||||
final pwd = context.mounted ? await context.showPwdDialog(title: user, id: id) : null;
|
||||
if (pwd == null || pwd.isEmpty) {
|
||||
session.stdin.close();
|
||||
} else {
|
||||
|
||||
@@ -6,10 +6,8 @@ class ChainComparator<T> {
|
||||
ChainComparator.empty() : this._create(null, (a, b) => 0);
|
||||
ChainComparator.create() : this._create(null, (a, b) => 0);
|
||||
|
||||
static ChainComparator<T> comparing<T, F extends Comparable<F>>(
|
||||
F Function(T) extractor) {
|
||||
return ChainComparator._create(
|
||||
null, (a, b) => extractor(a).compareTo(extractor(b)));
|
||||
static ChainComparator<T> comparing<T, F extends Comparable<F>>(F Function(T) extractor) {
|
||||
return ChainComparator._create(null, (a, b) => extractor(a).compareTo(extractor(b)));
|
||||
}
|
||||
|
||||
int compare(T a, T b) {
|
||||
@@ -26,8 +24,9 @@ class ChainComparator<T> {
|
||||
}
|
||||
|
||||
ChainComparator<T> thenCompareBy<F extends Comparable<F>>(
|
||||
F Function(T) extractor,
|
||||
{bool reversed = false}) {
|
||||
F Function(T) extractor, {
|
||||
bool reversed = false,
|
||||
}) {
|
||||
return ChainComparator._create(
|
||||
this,
|
||||
reversed
|
||||
@@ -36,18 +35,12 @@ class ChainComparator<T> {
|
||||
);
|
||||
}
|
||||
|
||||
ChainComparator<T> thenWithComparator(Comparator<T> comparator,
|
||||
{bool reversed = false}) {
|
||||
return ChainComparator._create(
|
||||
this,
|
||||
!reversed ? comparator : (a, b) => comparator(b, a),
|
||||
);
|
||||
ChainComparator<T> thenWithComparator(Comparator<T> comparator, {bool reversed = false}) {
|
||||
return ChainComparator._create(this, !reversed ? comparator : (a, b) => comparator(b, a));
|
||||
}
|
||||
|
||||
ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(
|
||||
F Function(T) extractor) {
|
||||
return ChainComparator._create(
|
||||
this, (a, b) => -extractor(a).compareTo(extractor(b)));
|
||||
ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(F Function(T) extractor) {
|
||||
return ChainComparator._create(this, (a, b) => -extractor(a).compareTo(extractor(b)));
|
||||
}
|
||||
|
||||
ChainComparator<T> thenTrueFirst(bool Function(T) f) {
|
||||
@@ -63,8 +56,7 @@ class ChainComparator<T> {
|
||||
}
|
||||
|
||||
class Comparators {
|
||||
static Comparator<String> compareStringCaseInsensitive(
|
||||
{bool uppercaseFirst = false}) {
|
||||
static Comparator<String> compareStringCaseInsensitive({bool uppercaseFirst = false}) {
|
||||
return (String a, String b) {
|
||||
final r = a.toLowerCase().compareTo(b.toLowerCase());
|
||||
if (r != 0) return r;
|
||||
|
||||
@@ -24,19 +24,12 @@ String decyptPem(List<String> args) {
|
||||
return sshKey.first.toPem();
|
||||
}
|
||||
|
||||
enum GenSSHClientStatus {
|
||||
socket,
|
||||
key,
|
||||
pwd,
|
||||
}
|
||||
enum GenSSHClientStatus { socket, key, pwd }
|
||||
|
||||
String getPrivateKey(String id) {
|
||||
final pki = Stores.key.fetchOne(id);
|
||||
if (pki == null) {
|
||||
throw SSHErr(
|
||||
type: SSHErrType.noPrivateKey,
|
||||
message: 'key [$id] not found',
|
||||
);
|
||||
throw SSHErr(type: SSHErrType.noPrivateKey, message: 'key [$id] not found');
|
||||
}
|
||||
return pki.key;
|
||||
}
|
||||
@@ -58,7 +51,7 @@ Future<SSHClient> genClient(
|
||||
Spi? jumpSpi,
|
||||
|
||||
/// Handle keyboard-interactive authentication
|
||||
FutureOr<List<String>?> Function(SSHUserInfoRequest)? onKeyboardInteractive,
|
||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||
}) async {
|
||||
onStatus?.call(GenSSHClientStatus.socket);
|
||||
|
||||
@@ -73,36 +66,21 @@ Future<SSHClient> genClient(
|
||||
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
||||
}();
|
||||
if (jumpSpi_ != null) {
|
||||
final jumpClient = await genClient(
|
||||
jumpSpi_,
|
||||
privateKey: jumpPrivateKey,
|
||||
timeout: timeout,
|
||||
);
|
||||
final jumpClient = await genClient(jumpSpi_, privateKey: jumpPrivateKey, timeout: timeout);
|
||||
|
||||
return await jumpClient.forwardLocal(
|
||||
spi.ip,
|
||||
spi.port,
|
||||
);
|
||||
return await jumpClient.forwardLocal(spi.ip, spi.port);
|
||||
}
|
||||
|
||||
// Direct
|
||||
try {
|
||||
return await SSHSocket.connect(
|
||||
spi.ip,
|
||||
spi.port,
|
||||
timeout: timeout,
|
||||
);
|
||||
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
|
||||
} catch (e) {
|
||||
Loggers.app.warning('genClient', e);
|
||||
if (spi.alterUrl == null) rethrow;
|
||||
try {
|
||||
final res = spi.fromStringUrl();
|
||||
alterUser = res.$2;
|
||||
return await SSHSocket.connect(
|
||||
res.$1,
|
||||
res.$3,
|
||||
timeout: timeout,
|
||||
);
|
||||
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
||||
} catch (e) {
|
||||
Loggers.app.warning('genClient alterUrl', e);
|
||||
rethrow;
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/app.dart';
|
||||
|
||||
@@ -13,7 +12,7 @@ abstract final class KeybordInteractive {
|
||||
}) async {
|
||||
try {
|
||||
final res = await (ctx ?? AppProvider.ctx)?.showPwdDialog(
|
||||
title: l10n.pwd,
|
||||
title: libL10n.pwd,
|
||||
id: spi.id,
|
||||
label: spi.id,
|
||||
);
|
||||
|
||||
57
lib/data/helper/system_detector.dart
Normal file
57
lib/data/helper/system_detector.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
|
||||
/// Helper class for detecting remote system types
|
||||
class SystemDetector {
|
||||
/// Detects the system type of a remote server
|
||||
///
|
||||
/// First checks if a custom system type is configured in [spi].
|
||||
/// If not, attempts to detect the system by running commands:
|
||||
/// 1. 'ver' command to detect Windows
|
||||
/// 2. 'uname -a' command to detect Linux/BSD/Darwin
|
||||
///
|
||||
/// Returns [SystemType.linux] as default if detection fails.
|
||||
static Future<SystemType> detect(
|
||||
SSHClient client,
|
||||
Spi spi,
|
||||
) async {
|
||||
// First, check if custom system type is defined
|
||||
SystemType? detectedSystemType = spi.customSystemType;
|
||||
if (detectedSystemType != null) {
|
||||
dprint('Using custom system type ${detectedSystemType.name} for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to detect Windows systems first (more reliable detection)
|
||||
final powershellResult = await client.run('ver 2>nul').string;
|
||||
if (powershellResult.isNotEmpty &&
|
||||
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
|
||||
detectedSystemType = SystemType.windows;
|
||||
dprint('Detected Windows system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
|
||||
// Try to detect Unix/Linux/BSD systems
|
||||
final unixResult = await client.run('uname -a').string;
|
||||
if (unixResult.contains('Linux')) {
|
||||
detectedSystemType = SystemType.linux;
|
||||
dprint('Detected Linux system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
} else if (unixResult.contains('Darwin') || unixResult.contains('BSD')) {
|
||||
detectedSystemType = SystemType.bsd;
|
||||
dprint('Detected BSD system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
} catch (e) {
|
||||
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
detectedSystemType = SystemType.linux;
|
||||
dprint('Defaulting to Linux system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
}
|
||||
@@ -213,13 +213,10 @@ class Backup implements Mergeable {
|
||||
_logger.info('Restore success');
|
||||
}
|
||||
|
||||
factory Backup.fromJsonString(String raw) =>
|
||||
Backup.fromJson(json.decode(_diyDecrypt(raw)));
|
||||
factory Backup.fromJsonString(String raw) => Backup.fromJson(json.decode(_diyDecrypt(raw)));
|
||||
}
|
||||
|
||||
String _diyEncrypt(String raw) => json.encode(
|
||||
raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false),
|
||||
);
|
||||
String _diyEncrypt(String raw) => json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
|
||||
|
||||
String _diyDecrypt(String raw) {
|
||||
try {
|
||||
@@ -234,4 +231,3 @@ String _diyDecrypt(String raw) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,15 +74,27 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<String> backup([String? name]) async {
|
||||
static Future<String> backup([String? name, String? password]) async {
|
||||
final bak = await BackupV2.loadFromStore();
|
||||
final result = json.encode(bak.toJson());
|
||||
var result = json.encode(bak.toJson());
|
||||
|
||||
if (password != null && password.isNotEmpty) {
|
||||
result = Cryptor.encrypt(result, password);
|
||||
}
|
||||
|
||||
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
|
||||
await File(path).writeAsString(result);
|
||||
return path;
|
||||
}
|
||||
|
||||
factory BackupV2.fromJsonString(String jsonString) {
|
||||
factory BackupV2.fromJsonString(String jsonString, [String? password]) {
|
||||
if (Cryptor.isEncrypted(jsonString)) {
|
||||
if (password == null || password.isEmpty) {
|
||||
throw Exception('Backup is encrypted but no password provided');
|
||||
}
|
||||
jsonString = Cryptor.decrypt(jsonString, password);
|
||||
}
|
||||
|
||||
final map = json.decode(jsonString) as Map<String, dynamic>;
|
||||
return BackupV2.fromJson(map);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
@@ -9,364 +10,196 @@ part of 'backup2.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
BackupV2 _$BackupV2FromJson(Map<String, dynamic> json) {
|
||||
return _BackupV2.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$BackupV2 {
|
||||
int get version => throw _privateConstructorUsedError;
|
||||
int get date => throw _privateConstructorUsedError;
|
||||
Map<String, Object?> get spis => throw _privateConstructorUsedError;
|
||||
Map<String, Object?> get snippets => throw _privateConstructorUsedError;
|
||||
Map<String, Object?> get keys => throw _privateConstructorUsedError;
|
||||
Map<String, Object?> get container => throw _privateConstructorUsedError;
|
||||
Map<String, Object?> get history => throw _privateConstructorUsedError;
|
||||
Map<String, Object?> get settings => throw _privateConstructorUsedError;
|
||||
|
||||
int get version; int get date; Map<String, Object?> get spis; Map<String, Object?> get snippets; Map<String, Object?> get keys; Map<String, Object?> get container; Map<String, Object?> get history; Map<String, Object?> get settings;
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$BackupV2CopyWith<BackupV2> get copyWith => _$BackupV2CopyWithImpl<BackupV2>(this as BackupV2, _$identity);
|
||||
|
||||
/// Serializes this BackupV2 to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other.spis, spis)&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.keys, keys)&&const DeepCollectionEquality().equals(other.container, container)&&const DeepCollectionEquality().equals(other.history, history)&&const DeepCollectionEquality().equals(other.settings, settings));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(spis),const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(keys),const DeepCollectionEquality().hash(container),const DeepCollectionEquality().hash(history),const DeepCollectionEquality().hash(settings));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)';
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$BackupV2CopyWith<BackupV2> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $BackupV2CopyWith<$Res> {
|
||||
factory $BackupV2CopyWith(BackupV2 value, $Res Function(BackupV2) then) =
|
||||
_$BackupV2CopyWithImpl<$Res, BackupV2>;
|
||||
@useResult
|
||||
$Res call({
|
||||
int version,
|
||||
int date,
|
||||
Map<String, Object?> spis,
|
||||
Map<String, Object?> snippets,
|
||||
Map<String, Object?> keys,
|
||||
Map<String, Object?> container,
|
||||
Map<String, Object?> history,
|
||||
Map<String, Object?> settings,
|
||||
});
|
||||
}
|
||||
abstract mixin class $BackupV2CopyWith<$Res> {
|
||||
factory $BackupV2CopyWith(BackupV2 value, $Res Function(BackupV2) _then) = _$BackupV2CopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int version, int date, Map<String, Object?> spis, Map<String, Object?> snippets, Map<String, Object?> keys, Map<String, Object?> container, Map<String, Object?> history, Map<String, Object?> settings
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$BackupV2CopyWithImpl<$Res, $Val extends BackupV2>
|
||||
class _$BackupV2CopyWithImpl<$Res>
|
||||
implements $BackupV2CopyWith<$Res> {
|
||||
_$BackupV2CopyWithImpl(this._value, this._then);
|
||||
_$BackupV2CopyWithImpl(this._self, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
final BackupV2 _self;
|
||||
final $Res Function(BackupV2) _then;
|
||||
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? version = null,
|
||||
Object? date = null,
|
||||
Object? spis = null,
|
||||
Object? snippets = null,
|
||||
Object? keys = null,
|
||||
Object? container = null,
|
||||
Object? history = null,
|
||||
Object? settings = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
version: null == version
|
||||
? _value.version
|
||||
: version // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
date: null == date
|
||||
? _value.date
|
||||
: date // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
spis: null == spis
|
||||
? _value.spis
|
||||
: spis // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
snippets: null == snippets
|
||||
? _value.snippets
|
||||
: snippets // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
keys: null == keys
|
||||
? _value.keys
|
||||
: keys // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
container: null == container
|
||||
? _value.container
|
||||
: container // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
history: null == history
|
||||
? _value.history
|
||||
: history // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
settings: null == settings
|
||||
? _value.settings
|
||||
: settings // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
|
||||
as int,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||
as int,spis: null == spis ? _self.spis : spis // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,snippets: null == snippets ? _self.snippets : snippets // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,keys: null == keys ? _self.keys : keys // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,container: null == container ? _self.container : container // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,history: null == history ? _self.history : history // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,settings: null == settings ? _self.settings : settings // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
));
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$BackupV2ImplCopyWith<$Res>
|
||||
implements $BackupV2CopyWith<$Res> {
|
||||
factory _$$BackupV2ImplCopyWith(
|
||||
_$BackupV2Impl value,
|
||||
$Res Function(_$BackupV2Impl) then,
|
||||
) = __$$BackupV2ImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
int version,
|
||||
int date,
|
||||
Map<String, Object?> spis,
|
||||
Map<String, Object?> snippets,
|
||||
Map<String, Object?> keys,
|
||||
Map<String, Object?> container,
|
||||
Map<String, Object?> history,
|
||||
Map<String, Object?> settings,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$BackupV2ImplCopyWithImpl<$Res>
|
||||
extends _$BackupV2CopyWithImpl<$Res, _$BackupV2Impl>
|
||||
implements _$$BackupV2ImplCopyWith<$Res> {
|
||||
__$$BackupV2ImplCopyWithImpl(
|
||||
_$BackupV2Impl _value,
|
||||
$Res Function(_$BackupV2Impl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? version = null,
|
||||
Object? date = null,
|
||||
Object? spis = null,
|
||||
Object? snippets = null,
|
||||
Object? keys = null,
|
||||
Object? container = null,
|
||||
Object? history = null,
|
||||
Object? settings = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$BackupV2Impl(
|
||||
version: null == version
|
||||
? _value.version
|
||||
: version // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
date: null == date
|
||||
? _value.date
|
||||
: date // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
spis: null == spis
|
||||
? _value._spis
|
||||
: spis // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
snippets: null == snippets
|
||||
? _value._snippets
|
||||
: snippets // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
keys: null == keys
|
||||
? _value._keys
|
||||
: keys // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
container: null == container
|
||||
? _value._container
|
||||
: container // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
history: null == history
|
||||
? _value._history
|
||||
: history // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
settings: null == settings
|
||||
? _value._settings
|
||||
: settings // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$BackupV2Impl extends _BackupV2 {
|
||||
const _$BackupV2Impl({
|
||||
required this.version,
|
||||
required this.date,
|
||||
required final Map<String, Object?> spis,
|
||||
required final Map<String, Object?> snippets,
|
||||
required final Map<String, Object?> keys,
|
||||
required final Map<String, Object?> container,
|
||||
required final Map<String, Object?> history,
|
||||
required final Map<String, Object?> settings,
|
||||
}) : _spis = spis,
|
||||
_snippets = snippets,
|
||||
_keys = keys,
|
||||
_container = container,
|
||||
_history = history,
|
||||
_settings = settings,
|
||||
super._();
|
||||
|
||||
factory _$BackupV2Impl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$BackupV2ImplFromJson(json);
|
||||
class _BackupV2 extends BackupV2 {
|
||||
const _BackupV2({required this.version, required this.date, required final Map<String, Object?> spis, required final Map<String, Object?> snippets, required final Map<String, Object?> keys, required final Map<String, Object?> container, required final Map<String, Object?> history, required final Map<String, Object?> settings}): _spis = spis,_snippets = snippets,_keys = keys,_container = container,_history = history,_settings = settings,super._();
|
||||
factory _BackupV2.fromJson(Map<String, dynamic> json) => _$BackupV2FromJson(json);
|
||||
|
||||
@override
|
||||
final int version;
|
||||
@override
|
||||
final int date;
|
||||
final Map<String, Object?> _spis;
|
||||
@override
|
||||
Map<String, Object?> get spis {
|
||||
if (_spis is EqualUnmodifiableMapView) return _spis;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_spis);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _snippets;
|
||||
@override
|
||||
Map<String, Object?> get snippets {
|
||||
if (_snippets is EqualUnmodifiableMapView) return _snippets;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_snippets);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _keys;
|
||||
@override
|
||||
Map<String, Object?> get keys {
|
||||
if (_keys is EqualUnmodifiableMapView) return _keys;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_keys);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _container;
|
||||
@override
|
||||
Map<String, Object?> get container {
|
||||
if (_container is EqualUnmodifiableMapView) return _container;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_container);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _history;
|
||||
@override
|
||||
Map<String, Object?> get history {
|
||||
if (_history is EqualUnmodifiableMapView) return _history;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_history);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _settings;
|
||||
@override
|
||||
Map<String, Object?> get settings {
|
||||
if (_settings is EqualUnmodifiableMapView) return _settings;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_settings);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$BackupV2Impl &&
|
||||
(identical(other.version, version) || other.version == version) &&
|
||||
(identical(other.date, date) || other.date == date) &&
|
||||
const DeepCollectionEquality().equals(other._spis, _spis) &&
|
||||
const DeepCollectionEquality().equals(other._snippets, _snippets) &&
|
||||
const DeepCollectionEquality().equals(other._keys, _keys) &&
|
||||
const DeepCollectionEquality().equals(
|
||||
other._container,
|
||||
_container,
|
||||
) &&
|
||||
const DeepCollectionEquality().equals(other._history, _history) &&
|
||||
const DeepCollectionEquality().equals(other._settings, _settings));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
version,
|
||||
date,
|
||||
const DeepCollectionEquality().hash(_spis),
|
||||
const DeepCollectionEquality().hash(_snippets),
|
||||
const DeepCollectionEquality().hash(_keys),
|
||||
const DeepCollectionEquality().hash(_container),
|
||||
const DeepCollectionEquality().hash(_history),
|
||||
const DeepCollectionEquality().hash(_settings),
|
||||
);
|
||||
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$BackupV2ImplCopyWith<_$BackupV2Impl> get copyWith =>
|
||||
__$$BackupV2ImplCopyWithImpl<_$BackupV2Impl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$BackupV2ImplToJson(this);
|
||||
}
|
||||
@override final int version;
|
||||
@override final int date;
|
||||
final Map<String, Object?> _spis;
|
||||
@override Map<String, Object?> get spis {
|
||||
if (_spis is EqualUnmodifiableMapView) return _spis;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_spis);
|
||||
}
|
||||
|
||||
abstract class _BackupV2 extends BackupV2 {
|
||||
const factory _BackupV2({
|
||||
required final int version,
|
||||
required final int date,
|
||||
required final Map<String, Object?> spis,
|
||||
required final Map<String, Object?> snippets,
|
||||
required final Map<String, Object?> keys,
|
||||
required final Map<String, Object?> container,
|
||||
required final Map<String, Object?> history,
|
||||
required final Map<String, Object?> settings,
|
||||
}) = _$BackupV2Impl;
|
||||
const _BackupV2._() : super._();
|
||||
|
||||
factory _BackupV2.fromJson(Map<String, dynamic> json) =
|
||||
_$BackupV2Impl.fromJson;
|
||||
|
||||
@override
|
||||
int get version;
|
||||
@override
|
||||
int get date;
|
||||
@override
|
||||
Map<String, Object?> get spis;
|
||||
@override
|
||||
Map<String, Object?> get snippets;
|
||||
@override
|
||||
Map<String, Object?> get keys;
|
||||
@override
|
||||
Map<String, Object?> get container;
|
||||
@override
|
||||
Map<String, Object?> get history;
|
||||
@override
|
||||
Map<String, Object?> get settings;
|
||||
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$BackupV2ImplCopyWith<_$BackupV2Impl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
final Map<String, Object?> _snippets;
|
||||
@override Map<String, Object?> get snippets {
|
||||
if (_snippets is EqualUnmodifiableMapView) return _snippets;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_snippets);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _keys;
|
||||
@override Map<String, Object?> get keys {
|
||||
if (_keys is EqualUnmodifiableMapView) return _keys;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_keys);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _container;
|
||||
@override Map<String, Object?> get container {
|
||||
if (_container is EqualUnmodifiableMapView) return _container;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_container);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _history;
|
||||
@override Map<String, Object?> get history {
|
||||
if (_history is EqualUnmodifiableMapView) return _history;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_history);
|
||||
}
|
||||
|
||||
final Map<String, Object?> _settings;
|
||||
@override Map<String, Object?> get settings {
|
||||
if (_settings is EqualUnmodifiableMapView) return _settings;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_settings);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$BackupV2CopyWith<_BackupV2> get copyWith => __$BackupV2CopyWithImpl<_BackupV2>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$BackupV2ToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other._spis, _spis)&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._keys, _keys)&&const DeepCollectionEquality().equals(other._container, _container)&&const DeepCollectionEquality().equals(other._history, _history)&&const DeepCollectionEquality().equals(other._settings, _settings));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(_spis),const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_keys),const DeepCollectionEquality().hash(_container),const DeepCollectionEquality().hash(_history),const DeepCollectionEquality().hash(_settings));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$BackupV2CopyWith<$Res> implements $BackupV2CopyWith<$Res> {
|
||||
factory _$BackupV2CopyWith(_BackupV2 value, $Res Function(_BackupV2) _then) = __$BackupV2CopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int version, int date, Map<String, Object?> spis, Map<String, Object?> snippets, Map<String, Object?> keys, Map<String, Object?> container, Map<String, Object?> history, Map<String, Object?> settings
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$BackupV2CopyWithImpl<$Res>
|
||||
implements _$BackupV2CopyWith<$Res> {
|
||||
__$BackupV2CopyWithImpl(this._self, this._then);
|
||||
|
||||
final _BackupV2 _self;
|
||||
final $Res Function(_BackupV2) _then;
|
||||
|
||||
/// Create a copy of BackupV2
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,}) {
|
||||
return _then(_BackupV2(
|
||||
version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
|
||||
as int,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||
as int,spis: null == spis ? _self._spis : spis // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,snippets: null == snippets ? _self._snippets : snippets // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,keys: null == keys ? _self._keys : keys // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,container: null == container ? _self._container : container // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,history: null == history ? _self._history : history // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,settings: null == settings ? _self._settings : settings // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Object?>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -6,26 +6,24 @@ part of 'backup2.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$BackupV2Impl _$$BackupV2ImplFromJson(Map<String, dynamic> json) =>
|
||||
_$BackupV2Impl(
|
||||
version: (json['version'] as num).toInt(),
|
||||
date: (json['date'] as num).toInt(),
|
||||
spis: json['spis'] as Map<String, dynamic>,
|
||||
snippets: json['snippets'] as Map<String, dynamic>,
|
||||
keys: json['keys'] as Map<String, dynamic>,
|
||||
container: json['container'] as Map<String, dynamic>,
|
||||
history: json['history'] as Map<String, dynamic>,
|
||||
settings: json['settings'] as Map<String, dynamic>,
|
||||
);
|
||||
_BackupV2 _$BackupV2FromJson(Map<String, dynamic> json) => _BackupV2(
|
||||
version: (json['version'] as num).toInt(),
|
||||
date: (json['date'] as num).toInt(),
|
||||
spis: json['spis'] as Map<String, dynamic>,
|
||||
snippets: json['snippets'] as Map<String, dynamic>,
|
||||
keys: json['keys'] as Map<String, dynamic>,
|
||||
container: json['container'] as Map<String, dynamic>,
|
||||
history: json['history'] as Map<String, dynamic>,
|
||||
settings: json['settings'] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$BackupV2ImplToJson(_$BackupV2Impl instance) =>
|
||||
<String, dynamic>{
|
||||
'version': instance.version,
|
||||
'date': instance.date,
|
||||
'spis': instance.spis,
|
||||
'snippets': instance.snippets,
|
||||
'keys': instance.keys,
|
||||
'container': instance.container,
|
||||
'history': instance.history,
|
||||
'settings': instance.settings,
|
||||
};
|
||||
Map<String, dynamic> _$BackupV2ToJson(_BackupV2 instance) => <String, dynamic>{
|
||||
'version': instance.version,
|
||||
'date': instance.date,
|
||||
'spis': instance.spis,
|
||||
'snippets': instance.snippets,
|
||||
'keys': instance.keys,
|
||||
'container': instance.container,
|
||||
'history': instance.history,
|
||||
'settings': instance.settings,
|
||||
};
|
||||
|
||||
198
lib/data/model/app/bak/backup_service.dart
Normal file
198
lib/data/model/app/bak/backup_service.dart
Normal file
@@ -0,0 +1,198 @@
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/app/bak/backup2.dart';
|
||||
import 'package:server_box/data/model/app/bak/backup_source.dart';
|
||||
import 'package:server_box/data/model/app/bak/utils.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
/// Service class for handling backup operations
|
||||
class BackupService {
|
||||
/// Perform backup operation with the given source
|
||||
static Future<void> backup(BuildContext context, BackupSource source) async {
|
||||
final password = await _getBackupPassword(context);
|
||||
if (password == null) return;
|
||||
|
||||
try {
|
||||
final path = await BackupV2.backup(null, password.isEmpty ? null : password);
|
||||
await source.saveContent(path);
|
||||
|
||||
// Show success message for clipboard source
|
||||
if (source is ClipboardBackupSource) {
|
||||
context.showSnackBar(libL10n.success);
|
||||
}
|
||||
} catch (e, s) {
|
||||
context.showErrDialog(e, s, libL10n.backup);
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform restore operation with the given source
|
||||
static Future<void> restore(BuildContext context, BackupSource source) async {
|
||||
final text = await source.getContent();
|
||||
if (text == null) {
|
||||
// Show empty message for clipboard source
|
||||
if (source is ClipboardBackupSource) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await restoreFromText(context, text);
|
||||
}
|
||||
|
||||
/// Handle password dialog for backup operations
|
||||
static Future<String?> _getBackupPassword(BuildContext context) async {
|
||||
final savedPassword = await Stores.setting.backupasswd.read();
|
||||
String? password;
|
||||
|
||||
if (savedPassword != null && savedPassword.isNotEmpty) {
|
||||
// Use saved password or ask for custom password
|
||||
final useCustom = await context.showRoundDialog<bool>(
|
||||
title: l10n.backupPassword,
|
||||
child: Text(l10n.backupPasswordTip),
|
||||
actions: [
|
||||
Btn.cancel(),
|
||||
TextButton(onPressed: () => context.pop(false), child: Text(l10n.backupPasswordSet)),
|
||||
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.custom)),
|
||||
],
|
||||
);
|
||||
|
||||
if (useCustom == null) return null;
|
||||
|
||||
if (useCustom) {
|
||||
password = await _showPasswordDialog(context, initial: savedPassword);
|
||||
} else {
|
||||
password = savedPassword;
|
||||
}
|
||||
} else {
|
||||
// No saved password, ask if user wants to set one
|
||||
password = await _showPasswordDialog(context);
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
/// Handle restore from text with decryption support
|
||||
static Future<void> restoreFromText(BuildContext context, String text) async {
|
||||
// Check if backup is encrypted
|
||||
final isEncrypted = Cryptor.isEncrypted(text);
|
||||
String? password;
|
||||
|
||||
if (!isEncrypted) {
|
||||
try {
|
||||
final (backup, err) = await context.showLoadingDialog(
|
||||
fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text),
|
||||
);
|
||||
if (err != null || backup == null) return;
|
||||
|
||||
await _confirmAndRestore(context, backup);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Import backup failed', e, s);
|
||||
context.showErrDialog(e, s, libL10n.restore);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Try with saved password first
|
||||
final savedPassword = await Stores.setting.backupasswd.read();
|
||||
if (savedPassword != null && savedPassword.isNotEmpty) {
|
||||
try {
|
||||
final (backup, err) = await context.showLoadingDialog(
|
||||
fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), (
|
||||
text,
|
||||
savedPassword,
|
||||
)),
|
||||
);
|
||||
if (err == null && backup != null) {
|
||||
await _confirmAndRestore(context, backup);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Saved password failed, will prompt for manual input
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for password with retry logic
|
||||
while (true) {
|
||||
password = await _showPasswordDialog(context, title: libL10n.pwd, hint: l10n.backupEncrypted);
|
||||
if (password == null) return; // User cancelled
|
||||
|
||||
try {
|
||||
final (backup, err) = await context.showLoadingDialog(
|
||||
fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), (
|
||||
text,
|
||||
password,
|
||||
)),
|
||||
);
|
||||
if (err != null || backup == null) continue;
|
||||
|
||||
await _confirmAndRestore(context, backup);
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e.toString().contains('incorrect password') || e.toString().contains('Failed to decrypt')) {
|
||||
final retry = await context.showRoundDialog<bool>(
|
||||
title: l10n.backupPasswordWrong,
|
||||
child: Text(l10n.backupPasswordWrong),
|
||||
actions: [
|
||||
TextButton(onPressed: () => context.pop(false), child: Text(libL10n.cancel)),
|
||||
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.retry)),
|
||||
],
|
||||
);
|
||||
if (retry != true) return;
|
||||
continue; // Try again
|
||||
} else {
|
||||
// Other error, show and exit
|
||||
context.showErrDialog(e, null, libL10n.restore);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirm and execute restore operation
|
||||
static Future<void> _confirmAndRestore(BuildContext context, (dynamic, String) backup) async {
|
||||
await context.showRoundDialog(
|
||||
title: libL10n.restore,
|
||||
child: Text(libL10n.askContinue('${libL10n.restore} ${libL10n.backup}(${backup.$2})')),
|
||||
actions: Btn.ok(
|
||||
onTap: () async {
|
||||
await backup.$1.merge(force: true);
|
||||
context.pop();
|
||||
},
|
||||
).toList,
|
||||
);
|
||||
}
|
||||
|
||||
/// Show password input dialog
|
||||
static Future<String?> _showPasswordDialog(
|
||||
BuildContext context, {
|
||||
String? initial,
|
||||
String? title,
|
||||
String? hint,
|
||||
}) async {
|
||||
final controller = TextEditingController(text: initial ?? '');
|
||||
final result = await context.showRoundDialog<String>(
|
||||
title: title ?? libL10n.pwd,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(hint ?? l10n.backupPasswordTip, style: UIs.textGrey),
|
||||
UIs.height13,
|
||||
Input(
|
||||
label: l10n.backupPassword,
|
||||
controller: controller,
|
||||
obscureText: true,
|
||||
onSubmitted: (_) => context.pop(controller.text),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Btn.cancel(),
|
||||
TextButton(onPressed: () => context.pop(controller.text), child: Text(libL10n.ok)),
|
||||
],
|
||||
);
|
||||
controller.dispose();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
62
lib/data/model/app/bak/backup_source.dart
Normal file
62
lib/data/model/app/bak/backup_source.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Abstract interface for backup content sources
|
||||
abstract class BackupSource {
|
||||
/// Get content from this source for restore
|
||||
Future<String?> getContent();
|
||||
|
||||
/// Save content to this source for backup
|
||||
Future<void> saveContent(String filePath);
|
||||
|
||||
/// Display name for this source
|
||||
String get displayName;
|
||||
|
||||
/// Icon for this source
|
||||
IconData get icon;
|
||||
}
|
||||
|
||||
/// File-based backup source
|
||||
class FileBackupSource implements BackupSource {
|
||||
@override
|
||||
Future<String?> getContent() async {
|
||||
return await Pfs.pickFileString();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveContent(String filePath) async {
|
||||
await Pfs.sharePaths(paths: [filePath]);
|
||||
}
|
||||
|
||||
@override
|
||||
String get displayName => libL10n.file;
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.file_open;
|
||||
}
|
||||
|
||||
/// Clipboard-based backup source
|
||||
class ClipboardBackupSource implements BackupSource {
|
||||
@override
|
||||
Future<String?> getContent() async {
|
||||
final text = await Pfs.paste();
|
||||
if (text == null || text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveContent(String filePath) async {
|
||||
final content = await File(filePath).readAsString();
|
||||
Pfs.copy(content);
|
||||
}
|
||||
|
||||
@override
|
||||
String get displayName => libL10n.clipboard;
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.content_paste;
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import 'package:server_box/data/model/app/bak/backup.dart';
|
||||
import 'package:server_box/data/model/app/bak/backup2.dart';
|
||||
|
||||
abstract final class MergeableUtils {
|
||||
static (Mergeable, String) fromJsonString(String json) {
|
||||
static (Mergeable, String) fromJsonString(String json, [String? password]) {
|
||||
try {
|
||||
final bak = BackupV2.fromJsonString(json);
|
||||
final bak = BackupV2.fromJsonString(json, password);
|
||||
return (bak, DateTime.fromMillisecondsSinceEpoch(bak.date).hms());
|
||||
} catch (e) {
|
||||
final bak = Backup.fromJsonString(json);
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
|
||||
enum SSHErrType {
|
||||
unknown,
|
||||
connect,
|
||||
auth,
|
||||
noPrivateKey,
|
||||
chdir,
|
||||
segements,
|
||||
writeScript,
|
||||
getStatus,
|
||||
;
|
||||
}
|
||||
enum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus }
|
||||
|
||||
class SSHErr extends Err<SSHErrType> {
|
||||
SSHErr({required super.type, super.message});
|
||||
|
||||
@override
|
||||
String? get solution => switch (type) {
|
||||
SSHErrType.chdir => l10n.needHomeDir,
|
||||
SSHErrType.auth => l10n.authFailTip,
|
||||
SSHErrType.writeScript => l10n.writeScriptFailTip,
|
||||
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
|
||||
_ => null,
|
||||
};
|
||||
SSHErrType.chdir => l10n.needHomeDir,
|
||||
SSHErrType.auth => l10n.authFailTip,
|
||||
SSHErrType.writeScript => l10n.writeScriptFailTip,
|
||||
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
enum ContainerErrType {
|
||||
@@ -45,11 +35,7 @@ class ContainerErr extends Err<ContainerErrType> {
|
||||
String? get solution => null;
|
||||
}
|
||||
|
||||
enum ICloudErrType {
|
||||
generic,
|
||||
notFound,
|
||||
multipleFiles,
|
||||
}
|
||||
enum ICloudErrType { generic, notFound, multipleFiles }
|
||||
|
||||
class ICloudErr extends Err<ICloudErrType> {
|
||||
ICloudErr({required super.type, super.message});
|
||||
@@ -58,11 +44,7 @@ class ICloudErr extends Err<ICloudErrType> {
|
||||
String? get solution => null;
|
||||
}
|
||||
|
||||
enum WebdavErrType {
|
||||
generic,
|
||||
notFound,
|
||||
;
|
||||
}
|
||||
enum WebdavErrType { generic, notFound }
|
||||
|
||||
class WebdavErr extends Err<WebdavErrType> {
|
||||
WebdavErr({required super.type, super.message});
|
||||
@@ -71,12 +53,7 @@ class WebdavErr extends Err<WebdavErrType> {
|
||||
String? get solution => null;
|
||||
}
|
||||
|
||||
enum PveErrType {
|
||||
unknown,
|
||||
net,
|
||||
loginFailed,
|
||||
;
|
||||
}
|
||||
enum PveErrType { unknown, net, loginFailed }
|
||||
|
||||
class PveErr extends Err<PveErrType> {
|
||||
PveErr({required super.type, super.message});
|
||||
|
||||
@@ -8,7 +8,7 @@ enum ContainerMenu {
|
||||
restart,
|
||||
rm,
|
||||
logs,
|
||||
terminal,
|
||||
terminal
|
||||
//stats,
|
||||
;
|
||||
|
||||
@@ -27,22 +27,22 @@ enum ContainerMenu {
|
||||
}
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
ContainerMenu.start => Icons.play_arrow,
|
||||
ContainerMenu.stop => Icons.stop,
|
||||
ContainerMenu.restart => Icons.restart_alt,
|
||||
ContainerMenu.rm => Icons.delete,
|
||||
ContainerMenu.logs => Icons.logo_dev,
|
||||
ContainerMenu.terminal => Icons.terminal,
|
||||
// DockerMenuType.stats => Icons.bar_chart,
|
||||
};
|
||||
ContainerMenu.start => Icons.play_arrow,
|
||||
ContainerMenu.stop => Icons.stop,
|
||||
ContainerMenu.restart => Icons.restart_alt,
|
||||
ContainerMenu.rm => Icons.delete,
|
||||
ContainerMenu.logs => Icons.logo_dev,
|
||||
ContainerMenu.terminal => Icons.terminal,
|
||||
// DockerMenuType.stats => Icons.bar_chart,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
ContainerMenu.start => l10n.start,
|
||||
ContainerMenu.stop => l10n.stop,
|
||||
ContainerMenu.restart => l10n.restart,
|
||||
ContainerMenu.rm => libL10n.delete,
|
||||
ContainerMenu.logs => libL10n.log,
|
||||
ContainerMenu.terminal => l10n.terminal,
|
||||
// DockerMenuType.stats => s.stats,
|
||||
};
|
||||
ContainerMenu.start => l10n.start,
|
||||
ContainerMenu.stop => l10n.stop,
|
||||
ContainerMenu.restart => l10n.restart,
|
||||
ContainerMenu.rm => libL10n.delete,
|
||||
ContainerMenu.logs => libL10n.log,
|
||||
ContainerMenu.terminal => l10n.terminal,
|
||||
// DockerMenuType.stats => s.stats,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ enum ServerFuncBtn {
|
||||
snippet(),
|
||||
iperf(),
|
||||
// pve(),
|
||||
systemd(1058),
|
||||
;
|
||||
systemd(1058);
|
||||
|
||||
final int? addedVersion;
|
||||
|
||||
@@ -41,24 +40,24 @@ enum ServerFuncBtn {
|
||||
].map((e) => e.index).toList();
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
sftp => Icons.insert_drive_file,
|
||||
snippet => Icons.code,
|
||||
//pkg => Icons.system_security_update,
|
||||
container => FontAwesome.docker_brand,
|
||||
process => Icons.list_alt_outlined,
|
||||
terminal => Icons.terminal,
|
||||
iperf => Icons.speed,
|
||||
systemd => MingCute.plugin_2_fill,
|
||||
};
|
||||
sftp => Icons.insert_drive_file,
|
||||
snippet => Icons.code,
|
||||
//pkg => Icons.system_security_update,
|
||||
container => FontAwesome.docker_brand,
|
||||
process => Icons.list_alt_outlined,
|
||||
terminal => Icons.terminal,
|
||||
iperf => Icons.speed,
|
||||
systemd => MingCute.plugin_2_fill,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
sftp => 'SFTP',
|
||||
snippet => l10n.snippet,
|
||||
//pkg => l10n.pkg,
|
||||
container => l10n.container,
|
||||
process => l10n.process,
|
||||
terminal => l10n.terminal,
|
||||
iperf => 'iperf',
|
||||
systemd => 'Systemd',
|
||||
};
|
||||
sftp => 'SFTP',
|
||||
snippet => l10n.snippet,
|
||||
//pkg => l10n.pkg,
|
||||
container => l10n.container,
|
||||
process => l10n.process,
|
||||
terminal => l10n.terminal,
|
||||
iperf => 'iperf',
|
||||
systemd => 'Systemd',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@ enum NetViewType {
|
||||
traffic;
|
||||
|
||||
NetViewType get next => switch (this) {
|
||||
conn => speed,
|
||||
speed => traffic,
|
||||
traffic => conn,
|
||||
};
|
||||
conn => speed,
|
||||
speed => traffic,
|
||||
traffic => conn,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
NetViewType.conn => l10n.conn,
|
||||
NetViewType.traffic => l10n.traffic,
|
||||
NetViewType.speed => l10n.speed,
|
||||
};
|
||||
NetViewType.conn => l10n.conn,
|
||||
NetViewType.traffic => l10n.traffic,
|
||||
NetViewType.speed => l10n.speed,
|
||||
};
|
||||
|
||||
/// If no device is specified, return the cached value (only real devices,
|
||||
/// such as ethX, wlanX...).
|
||||
@@ -26,32 +26,17 @@ enum NetViewType {
|
||||
try {
|
||||
switch (this) {
|
||||
case NetViewType.conn:
|
||||
return (
|
||||
'${l10n.conn}:\n${ss.tcp.maxConn}',
|
||||
'${libL10n.fail}:\n${ss.tcp.fail}',
|
||||
);
|
||||
return ('${l10n.conn}:\n${ss.tcp.maxConn}', '${libL10n.fail}:\n${ss.tcp.fail}');
|
||||
case NetViewType.speed:
|
||||
if (notSepcifyDev) {
|
||||
return (
|
||||
'↓:\n${ss.netSpeed.cachedVals.speedIn}',
|
||||
'↑:\n${ss.netSpeed.cachedVals.speedOut}',
|
||||
);
|
||||
return ('↓:\n${ss.netSpeed.cachedVals.speedIn}', '↑:\n${ss.netSpeed.cachedVals.speedOut}');
|
||||
}
|
||||
return (
|
||||
'↓:\n${ss.netSpeed.speedIn(device: dev)}',
|
||||
'↑:\n${ss.netSpeed.speedOut(device: dev)}',
|
||||
);
|
||||
return ('↓:\n${ss.netSpeed.speedIn(device: dev)}', '↑:\n${ss.netSpeed.speedOut(device: dev)}');
|
||||
case NetViewType.traffic:
|
||||
if (notSepcifyDev) {
|
||||
return (
|
||||
'↓:\n${ss.netSpeed.cachedVals.sizeIn}',
|
||||
'↑:\n${ss.netSpeed.cachedVals.sizeOut}',
|
||||
);
|
||||
return ('↓:\n${ss.netSpeed.cachedVals.sizeIn}', '↑:\n${ss.netSpeed.cachedVals.sizeOut}');
|
||||
}
|
||||
return (
|
||||
'↓:\n${ss.netSpeed.sizeIn(device: dev)}',
|
||||
'↑:\n${ss.netSpeed.sizeOut(device: dev)}',
|
||||
);
|
||||
return ('↓:\n${ss.netSpeed.sizeIn(device: dev)}', '↑:\n${ss.netSpeed.sizeOut(device: dev)}');
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('NetViewType.build', e, s);
|
||||
@@ -60,14 +45,14 @@ enum NetViewType {
|
||||
}
|
||||
|
||||
int toJson() => switch (this) {
|
||||
NetViewType.conn => 0,
|
||||
NetViewType.speed => 1,
|
||||
NetViewType.traffic => 2,
|
||||
};
|
||||
NetViewType.conn => 0,
|
||||
NetViewType.speed => 1,
|
||||
NetViewType.traffic => 2,
|
||||
};
|
||||
|
||||
static NetViewType fromJson(int json) => switch (json) {
|
||||
0 => NetViewType.conn,
|
||||
1 => NetViewType.speed,
|
||||
_ => NetViewType.traffic,
|
||||
};
|
||||
0 => NetViewType.conn,
|
||||
1 => NetViewType.speed,
|
||||
_ => NetViewType.traffic,
|
||||
};
|
||||
}
|
||||
|
||||
274
lib/data/model/app/scripts/cmd_types.dart
Normal file
274
lib/data/model/app/scripts/cmd_types.dart
Normal file
@@ -0,0 +1,274 @@
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
|
||||
/// Base class for all command type enums
|
||||
abstract class CommandType implements Enum {
|
||||
String get cmd;
|
||||
|
||||
/// Get command-specific separator
|
||||
String get separator;
|
||||
|
||||
/// Get command-specific divider (separator with echo and formatting)
|
||||
String get divider;
|
||||
}
|
||||
|
||||
/// Linux/Unix status commands
|
||||
enum StatusCmdType implements CommandType {
|
||||
echo('echo ${SystemType.linuxSign}'),
|
||||
time('date +%s'),
|
||||
net('cat /proc/net/dev'),
|
||||
sys('cat /etc/*-release | grep ^PRETTY_NAME'),
|
||||
cpu('cat /proc/stat | grep cpu'),
|
||||
uptime('uptime'),
|
||||
conn('cat /proc/net/snmp'),
|
||||
disk(
|
||||
'lsblk --bytes --json --output '
|
||||
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
||||
),
|
||||
mem("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||
tempType('cat /sys/class/thermal/thermal_zone*/type'),
|
||||
tempVal('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||
host('cat /etc/hostname'),
|
||||
diskio('cat /proc/diskstats'),
|
||||
|
||||
/// Get battery information from Linux power supply subsystem
|
||||
///
|
||||
/// Reads battery data from sysfs power supply interface:
|
||||
/// - Iterates through all power supply devices in /sys/class/power_supply/
|
||||
/// - Each device has a uevent file with key-value pairs of power supply properties
|
||||
/// - Includes battery level, status, technology type, and other attributes
|
||||
/// - Works with laptops, UPS devices, and other power supplies
|
||||
/// - Adds echo after each file to separate multiple power supplies
|
||||
/// - Returns empty if no power supplies are detected (e.g., desktop systems)
|
||||
battery('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
|
||||
|
||||
/// Get NVIDIA GPU information using nvidia-smi in XML format
|
||||
/// Requires NVIDIA drivers and nvidia-smi utility to be installed
|
||||
nvidia('nvidia-smi -q -x'),
|
||||
|
||||
/// Get AMD GPU information using multiple fallback methods
|
||||
///
|
||||
/// This command tries three different AMD monitoring tools in order of preference:
|
||||
/// 1. amd-smi: Modern AMD System Management Interface (ROCm 5.0+)
|
||||
/// - Uses 'amd-smi list --json' to get GPU list
|
||||
/// - Uses 'amd-smi metric --json' to get performance metrics
|
||||
/// 2. rocm-smi: ROCm System Management Interface (older versions)
|
||||
/// - First tries '--json' output format if supported
|
||||
/// - Falls back to human-readable format with comprehensive metrics
|
||||
/// 3. radeontop: Real-time GPU usage monitor for older AMD cards
|
||||
/// - Uses 2-second timeout to avoid hanging
|
||||
/// - Skips header line with 'tail -n +2'
|
||||
/// - Outputs single line of usage data
|
||||
///
|
||||
/// If none of these tools are available, outputs error message
|
||||
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'),
|
||||
|
||||
/// Get SMART disk health information for all storage devices
|
||||
///
|
||||
/// Uses a combination of lsblk and smartctl to collect disk health data:
|
||||
/// - lsblk -dn -o KNAME lists all block devices (kernel names only, no dependencies)
|
||||
/// - For each device, runs smartctl with -a (all info) and -j (JSON output)
|
||||
/// - Targets raw device nodes in /dev/ (e.g., /dev/sda, /dev/nvme0n1)
|
||||
/// - Adds echo after each device to separate output blocks
|
||||
/// - May require elevated privileges for some drives
|
||||
/// - smartctl must be installed (part of smartmontools package)
|
||||
diskSmart('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
||||
cpuBrand('cat /proc/cpuinfo | grep "model name"');
|
||||
|
||||
@override
|
||||
final String cmd;
|
||||
|
||||
const StatusCmdType(this.cmd);
|
||||
|
||||
@override
|
||||
String get separator => ScriptConstants.getCmdSeparator(name);
|
||||
|
||||
@override
|
||||
String get divider => ScriptConstants.getCmdDivider(name);
|
||||
}
|
||||
|
||||
/// BSD/macOS status commands
|
||||
enum BSDStatusCmdType implements CommandType {
|
||||
echo('echo ${SystemType.bsdSign}'),
|
||||
time('date +%s'),
|
||||
net('netstat -ibn'),
|
||||
sys('uname -or'),
|
||||
cpu('top -l 1 | grep "CPU usage"'),
|
||||
uptime('uptime'),
|
||||
disk('df -k'), // Keep df -k for BSD systems as lsblk is not available on macOS/BSD
|
||||
mem('top -l 1 | grep PhysMem'),
|
||||
host('hostname'),
|
||||
cpuBrand('sysctl -n machdep.cpu.brand_string');
|
||||
|
||||
@override
|
||||
final String cmd;
|
||||
|
||||
const BSDStatusCmdType(this.cmd);
|
||||
|
||||
@override
|
||||
String get separator => ScriptConstants.getCmdSeparator(name);
|
||||
|
||||
@override
|
||||
String get divider => ScriptConstants.getCmdDivider(name);
|
||||
}
|
||||
|
||||
/// Windows PowerShell status commands
|
||||
enum WindowsStatusCmdType implements CommandType {
|
||||
echo('echo ${SystemType.windowsSign}'),
|
||||
time('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
|
||||
|
||||
/// Get network interface statistics using Windows Performance Counters
|
||||
///
|
||||
/// Uses Get-Counter to collect network I/O metrics from all network interfaces:
|
||||
/// - Collects bytes received and sent per second for all network interfaces
|
||||
/// - Takes 2 samples with 1 second interval to calculate rates
|
||||
/// - Outputs results in JSON format for easy parsing
|
||||
/// - Counter paths use double backslashes to escape PowerShell string literals
|
||||
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',
|
||||
),
|
||||
|
||||
/// Get system temperature using Windows Management Instrumentation (WMI)
|
||||
///
|
||||
/// Queries the MSAcpi_ThermalZoneTemperature class from the WMI root/wmi namespace:
|
||||
/// - Uses Get-CimInstance to access ACPI thermal zone data
|
||||
/// - ErrorAction SilentlyContinue prevents errors on systems without thermal sensors
|
||||
/// - Converts temperature from 10ths of Kelvin to Celsius: (temp - 2732) / 10
|
||||
/// - Uses calculated property to perform the temperature conversion
|
||||
/// - Returns JSON with InstanceName and converted Temperature values
|
||||
/// - May return empty result on systems without ACPI thermal sensor support
|
||||
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'),
|
||||
|
||||
/// Get disk I/O statistics using Windows Performance Counters
|
||||
///
|
||||
/// Uses Get-Counter to collect disk I/O metrics from all physical disks:
|
||||
/// - Monitors read and write bytes per second for all physical disks
|
||||
/// - Takes 2 samples with 1 second interval to calculate I/O rates
|
||||
/// - Physical disk counters provide hardware-level I/O statistics
|
||||
/// - Outputs results in JSON format for parsing
|
||||
/// - Counter names use wildcard (*) to capture all disk instances
|
||||
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',
|
||||
),
|
||||
|
||||
/// Get NVIDIA GPU information on Windows
|
||||
///
|
||||
/// Checks if nvidia-smi is available before attempting to use it:
|
||||
/// - Uses Get-Command to test if nvidia-smi.exe exists in PATH
|
||||
/// - ErrorAction SilentlyContinue prevents PowerShell errors if not found
|
||||
/// - If available, runs nvidia-smi with -q (query) and -x (XML output) flags
|
||||
/// - If not available, outputs standard error message for consistent handling
|
||||
nvidia(
|
||||
'if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { '
|
||||
'nvidia-smi -q -x } else { echo "NVIDIA driver not found" }',
|
||||
),
|
||||
|
||||
/// Get AMD GPU information on Windows
|
||||
///
|
||||
/// Checks for AMD monitoring tools using similar pattern to Linux version:
|
||||
/// - Uses Get-Command to test if amd-smi.exe exists in PATH
|
||||
/// - ErrorAction SilentlyContinue prevents PowerShell errors if not found
|
||||
/// - If available, runs amd-smi list command with JSON output
|
||||
/// - If not available, outputs standard error message for consistent handling
|
||||
/// - Windows version is simpler than Linux due to fewer AMD tool variations
|
||||
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',
|
||||
),
|
||||
|
||||
/// Get SMART disk health information on Windows using Storage cmdlets
|
||||
///
|
||||
/// Uses Windows PowerShell storage management cmdlets:
|
||||
/// - Get-PhysicalDisk retrieves all physical storage devices
|
||||
/// - Get-StorageReliabilityCounter gets SMART health data via pipeline
|
||||
/// - Selects key health metrics: DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours
|
||||
/// - Outputs results in JSON format for consistent parsing
|
||||
/// - Works with NVMe, SATA, and other storage interfaces supported by Windows
|
||||
/// - May require elevated privileges on some systems
|
||||
diskSmart(
|
||||
'Get-PhysicalDisk | Get-StorageReliabilityCounter | '
|
||||
'Select-Object DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours | '
|
||||
'ConvertTo-Json',
|
||||
),
|
||||
cpuBrand('(Get-WmiObject -Class Win32_Processor).Name');
|
||||
|
||||
@override
|
||||
final String cmd;
|
||||
|
||||
const WindowsStatusCmdType(this.cmd);
|
||||
|
||||
@override
|
||||
String get separator => ScriptConstants.getCmdSeparator(name);
|
||||
|
||||
@override
|
||||
String get divider => ScriptConstants.getCmdDivider(name);
|
||||
}
|
||||
|
||||
/// Extensions for StatusCmdType
|
||||
extension StatusCmdTypeX on StatusCmdType {
|
||||
String get i18n => switch (this) {
|
||||
StatusCmdType.sys => l10n.system,
|
||||
StatusCmdType.host => l10n.host,
|
||||
StatusCmdType.uptime => l10n.uptime,
|
||||
StatusCmdType.battery => l10n.battery,
|
||||
StatusCmdType.sensors => l10n.sensors,
|
||||
StatusCmdType.disk => l10n.disk,
|
||||
final val => val.name,
|
||||
};
|
||||
}
|
||||
|
||||
/// Extension for CommandType to find content in parsed map
|
||||
extension CommandTypeX on CommandType {
|
||||
/// Find the command output from the parsed script output map
|
||||
String findInMap(Map<String, String> parsedOutput) {
|
||||
return parsedOutput[name] ?? '';
|
||||
}
|
||||
}
|
||||
271
lib/data/model/app/scripts/script_builders.dart
Normal file
271
lib/data/model/app/scripts/script_builders.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||
|
||||
/// Abstract base class for platform-specific script builders
|
||||
sealed class ScriptBuilder {
|
||||
const ScriptBuilder();
|
||||
|
||||
/// Generate a complete script for all shell functions
|
||||
String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]);
|
||||
|
||||
/// 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);
|
||||
|
||||
/// Get the script header for this platform
|
||||
String get scriptHeader;
|
||||
}
|
||||
|
||||
/// Windows PowerShell script builder
|
||||
class WindowsScriptBuilder extends ScriptBuilder {
|
||||
const WindowsScriptBuilder();
|
||||
|
||||
@override
|
||||
String get scriptFileName => ScriptConstants.scriptFileWindows;
|
||||
|
||||
@override
|
||||
String get scriptHeader => ScriptConstants.windowsScriptHeader;
|
||||
|
||||
@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) {
|
||||
final sb = StringBuffer();
|
||||
for (final e in customCmds.entries) {
|
||||
final cmdDivider = ScriptConstants.getCustomCmdSeparator(e.key);
|
||||
sb.writeln(' Write-Host "$cmdDivider"');
|
||||
sb.writeln(' ${e.value}');
|
||||
}
|
||||
return '\n$sb';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {
|
||||
final sb = StringBuffer();
|
||||
sb.write(scriptHeader);
|
||||
|
||||
// Write each function
|
||||
for (final func in ShellFunc.values) {
|
||||
final customCmdsStr = getCustomCmdsString(func, customCmds);
|
||||
|
||||
sb.write('''
|
||||
function ${func.name} {
|
||||
${_getWindowsCommand(func, disabledCmdTypes).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();
|
||||
}
|
||||
|
||||
/// Get Windows-specific command for a shell function
|
||||
String _getWindowsCommand(ShellFunc func, [List<String>? disabledCmdTypes]) => switch (func) {
|
||||
ShellFunc.status => _getWindowsStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),
|
||||
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)',
|
||||
};
|
||||
|
||||
/// Get Windows status command with command-specific separators
|
||||
String _getWindowsStatusCommand({required List<String> disabledCmdTypes}) {
|
||||
final cmdTypes = WindowsStatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
|
||||
return cmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight(); // Remove trailing divider
|
||||
}
|
||||
}
|
||||
|
||||
/// Unix shell script builder
|
||||
class UnixScriptBuilder extends ScriptBuilder {
|
||||
const UnixScriptBuilder();
|
||||
|
||||
@override
|
||||
String get scriptFileName => ScriptConstants.scriptFile;
|
||||
|
||||
@override
|
||||
String get scriptHeader => ScriptConstants.unixScriptHeader;
|
||||
|
||||
@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) {
|
||||
final sb = StringBuffer();
|
||||
for (final e in customCmds.entries) {
|
||||
final cmdDivider = ScriptConstants.getCustomCmdSeparator(e.key);
|
||||
sb.writeln('echo "$cmdDivider"');
|
||||
sb.writeln(e.value);
|
||||
}
|
||||
return '\n$sb';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {
|
||||
final sb = StringBuffer();
|
||||
sb.write(scriptHeader);
|
||||
// Write each function
|
||||
for (final func in ShellFunc.values) {
|
||||
final customCmdsStr = getCustomCmdsString(func, customCmds);
|
||||
sb.write('''
|
||||
${func.name}() {
|
||||
${_getUnixCommand(func, disabledCmdTypes).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();
|
||||
}
|
||||
|
||||
/// Get Unix-specific command for a shell function
|
||||
String _getUnixCommand(ShellFunc func, [List<String>? disabledCmdTypes]) {
|
||||
return switch (func) {
|
||||
ShellFunc.status => _getUnixStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),
|
||||
ShellFunc.process => _getUnixProcessCommand(),
|
||||
ShellFunc.shutdown => _getUnixShutdownCommand(),
|
||||
ShellFunc.reboot => _getUnixRebootCommand(),
|
||||
ShellFunc.suspend => _getUnixSuspendCommand(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Get Unix status command with OS detection
|
||||
String _getUnixStatusCommand({required List<String> disabledCmdTypes}) {
|
||||
// Generate command lists with command-specific separators, filtering disabled commands
|
||||
final filteredLinuxCmdTypes = StatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
|
||||
final linuxCommands = filteredLinuxCmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight();
|
||||
|
||||
final filteredBsdCmdTypes = BSDStatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
|
||||
final bsdCommands = filteredBsdCmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight();
|
||||
|
||||
return '''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\t$linuxCommands
|
||||
else
|
||||
\t$bsdCommands
|
||||
fi''';
|
||||
}
|
||||
|
||||
/// Get Unix process command with busybox detection
|
||||
String _getUnixProcessCommand() {
|
||||
return '''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\tif [ "\$isBusybox" != "" ]; then
|
||||
\t\tps w
|
||||
\telse
|
||||
\t\tps -aux
|
||||
\tfi
|
||||
else
|
||||
\tps -ax
|
||||
fi''';
|
||||
}
|
||||
|
||||
/// Get Unix shutdown command with privilege detection
|
||||
String _getUnixShutdownCommand() {
|
||||
return '''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\tshutdown -h now
|
||||
else
|
||||
\tsudo -S shutdown -h now
|
||||
fi''';
|
||||
}
|
||||
|
||||
/// Get Unix reboot command with privilege detection
|
||||
String _getUnixRebootCommand() {
|
||||
return '''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\treboot
|
||||
else
|
||||
\tsudo -S reboot
|
||||
fi''';
|
||||
}
|
||||
|
||||
/// Get Unix suspend command with privilege detection
|
||||
String _getUnixSuspendCommand() {
|
||||
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._();
|
||||
|
||||
/// Get the appropriate script builder based on platform
|
||||
static ScriptBuilder getBuilder(bool isWindows) {
|
||||
return isWindows ? const WindowsScriptBuilder() : const UnixScriptBuilder();
|
||||
}
|
||||
|
||||
/// Get all available builders (useful for testing)
|
||||
static List<ScriptBuilder> getAllBuilders() {
|
||||
return const [WindowsScriptBuilder(), UnixScriptBuilder()];
|
||||
}
|
||||
}
|
||||
150
lib/data/model/app/scripts/script_consts.dart
Normal file
150
lib/data/model/app/scripts/script_consts.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
|
||||
/// Constants used throughout the script system
|
||||
class ScriptConstants {
|
||||
const ScriptConstants._();
|
||||
|
||||
// Script file names
|
||||
static const String scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
||||
static const String scriptFileWindows = 'srvboxm_v${BuildData.script}.ps1';
|
||||
|
||||
// Script directories
|
||||
static const String scriptDirHome = '~/.config/server_box';
|
||||
static const String scriptDirTmp = '/tmp/server_box';
|
||||
static const String scriptDirHomeWindows = '%USERPROFILE%/.config/server_box';
|
||||
static const String scriptDirTmpWindows = '%TEMP%/server_box';
|
||||
|
||||
// Command separators and dividers
|
||||
static const String separator = 'SrvBoxSep';
|
||||
|
||||
/// Custom command separator
|
||||
static const String customCmdSep = 'SrvBoxCusCmdSep';
|
||||
|
||||
/// Generate command-specific separator
|
||||
static String getCmdSeparator(String cmdName) => '$separator.$cmdName';
|
||||
|
||||
/// Generate command-specific divider for custom commands
|
||||
static String getCustomCmdSeparator(String cmdName) => '$customCmdSep.$cmdName';
|
||||
|
||||
/// Generate command-specific divider
|
||||
static String getCmdDivider(String cmdName) => '\necho ${getCmdSeparator(cmdName)}\n\t';
|
||||
|
||||
/// Parse script output into command-specific map
|
||||
static Map<String, String> parseScriptOutput(String raw) {
|
||||
final result = <String, String>{};
|
||||
|
||||
if (raw.isEmpty) return result;
|
||||
|
||||
// Parse line by line to properly handle command-specific separators
|
||||
final lines = raw.split('\n');
|
||||
String? currentCmd;
|
||||
final buffer = StringBuffer();
|
||||
|
||||
for (final line in lines) {
|
||||
if (line.startsWith('$separator.')) {
|
||||
// Save previous command content
|
||||
if (currentCmd != null) {
|
||||
result[currentCmd] = buffer.toString().trim();
|
||||
buffer.clear();
|
||||
}
|
||||
// Start new command
|
||||
currentCmd = line.substring('$separator.'.length);
|
||||
} else if (line.startsWith('$customCmdSep.')) {
|
||||
// Save previous command content
|
||||
if (currentCmd != null) {
|
||||
result[currentCmd] = buffer.toString().trim();
|
||||
buffer.clear();
|
||||
}
|
||||
// Start new custom command
|
||||
currentCmd = line.substring('$customCmdSep.'.length);
|
||||
} else if (currentCmd != null) {
|
||||
buffer.writeln(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last command
|
||||
if (currentCmd != null) {
|
||||
result[currentCmd] = buffer.toString().trim();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Path separators
|
||||
static const String unixPathSeparator = '/';
|
||||
static const String windowsPathSeparator = '\\';
|
||||
|
||||
// Script headers
|
||||
static const String unixScriptHeader =
|
||||
'''
|
||||
#!/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
|
||||
|
||||
''';
|
||||
|
||||
static const String windowsScriptHeader =
|
||||
'''
|
||||
# PowerShell script for ServerBox app v1.0.${BuildData.build}
|
||||
# DO NOT delete this file while app is running
|
||||
|
||||
\$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
''';
|
||||
}
|
||||
|
||||
/// Script path configuration and management
|
||||
class ScriptPaths {
|
||||
ScriptPaths._();
|
||||
|
||||
static final Map<String, String> _scriptDirMap = <String, String>{};
|
||||
|
||||
/// Get the script directory for the given [id].
|
||||
///
|
||||
/// Default is [ScriptConstants.scriptDirTmp]/[ScriptConstants.scriptFile],
|
||||
/// if this path is not accessible, it will be changed to
|
||||
/// [ScriptConstants.scriptDirHome]/[ScriptConstants.scriptFile].
|
||||
static String getScriptDir(String id, {bool isWindows = false}) {
|
||||
final defaultTmpDir = isWindows ? ScriptConstants.scriptDirTmpWindows : ScriptConstants.scriptDirTmp;
|
||||
_scriptDirMap[id] ??= defaultTmpDir;
|
||||
return _scriptDirMap[id]!;
|
||||
}
|
||||
|
||||
/// Switch between tmp and home directories for script storage
|
||||
static String switchScriptDir(String id, {bool isWindows = false}) {
|
||||
return switch (_scriptDirMap[id]) {
|
||||
ScriptConstants.scriptDirTmp => _scriptDirMap[id] = ScriptConstants.scriptDirHome,
|
||||
ScriptConstants.scriptDirTmpWindows => _scriptDirMap[id] = ScriptConstants.scriptDirHomeWindows,
|
||||
ScriptConstants.scriptDirHome => _scriptDirMap[id] = ScriptConstants.scriptDirTmp,
|
||||
ScriptConstants.scriptDirHomeWindows => _scriptDirMap[id] = ScriptConstants.scriptDirTmpWindows,
|
||||
_ =>
|
||||
_scriptDirMap[id] = isWindows ? ScriptConstants.scriptDirHomeWindows : ScriptConstants.scriptDirHome,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the full script path for the given [id]
|
||||
static String getScriptPath(String id, {bool isWindows = false}) {
|
||||
final dir = getScriptDir(id, isWindows: isWindows);
|
||||
final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;
|
||||
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||
return '$dir$separator$fileName';
|
||||
}
|
||||
|
||||
/// Clear cached script directories (useful for testing)
|
||||
static void clearCache() {
|
||||
_scriptDirMap.clear();
|
||||
}
|
||||
}
|
||||
102
lib/data/model/app/scripts/shell_func.dart
Normal file
102
lib/data/model/app/scripts/shell_func.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'package:server_box/data/model/app/scripts/script_builders.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
|
||||
/// Shell functions available in the ServerBox application
|
||||
enum ShellFunc {
|
||||
status('SbStatus'),
|
||||
process('SbProcess'),
|
||||
shutdown('SbShutdown'),
|
||||
reboot('SbReboot'),
|
||||
suspend('SbSuspend');
|
||||
|
||||
/// The function name used in scripts
|
||||
final String name;
|
||||
|
||||
const ShellFunc(this.name);
|
||||
|
||||
/// Get the command line flag for this function
|
||||
String get flag => switch (this) {
|
||||
ShellFunc.process => 'p',
|
||||
ShellFunc.shutdown => 'sd',
|
||||
ShellFunc.reboot => 'r',
|
||||
ShellFunc.suspend => 'sp',
|
||||
ShellFunc.status => 's',
|
||||
};
|
||||
|
||||
/// Execute this shell function on the specified server
|
||||
String exec(String id, {SystemType? systemType}) {
|
||||
final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType);
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||
|
||||
return builder.getExecCommand(scriptPath, this);
|
||||
}
|
||||
}
|
||||
|
||||
/// Manager class for shell function operations
|
||||
class ShellFuncManager {
|
||||
const ShellFuncManager._();
|
||||
|
||||
/// Normalize a directory path to ensure it doesn't end with trailing separators
|
||||
static String _normalizeDir(String dir, bool isWindows) {
|
||||
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||
|
||||
// Remove all trailing separators
|
||||
final pattern = RegExp('${RegExp.escape(separator)}+\$');
|
||||
return dir.replaceAll(pattern, '');
|
||||
}
|
||||
|
||||
/// Get the script directory for the given [id].
|
||||
///
|
||||
/// Checks for custom script directory first, then falls back to default.
|
||||
static String getScriptDir(String id, {SystemType? systemType}) {
|
||||
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
|
||||
if (customScriptDir != null) return _normalizeDir(customScriptDir, isWindows);
|
||||
return ScriptPaths.getScriptDir(id, isWindows: isWindows);
|
||||
}
|
||||
|
||||
/// Switch between tmp and home directories for script storage
|
||||
static void switchScriptDir(String id, {SystemType? systemType}) {
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
ScriptPaths.switchScriptDir(id, isWindows: isWindows);
|
||||
}
|
||||
|
||||
/// Get the full script path for the given [id]
|
||||
static String getScriptPath(String id, {SystemType? systemType}) {
|
||||
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||
if (customScriptDir != null) {
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final normalizedDir = _normalizeDir(customScriptDir, isWindows);
|
||||
final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;
|
||||
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||
return '$normalizedDir$separator$fileName';
|
||||
}
|
||||
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
return ScriptPaths.getScriptPath(id, isWindows: isWindows);
|
||||
}
|
||||
|
||||
/// Get the installation shell command for the script
|
||||
static String getInstallShellCmd(String id, {SystemType? systemType}) {
|
||||
final scriptDir = getScriptDir(id, systemType: systemType);
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final normalizedDir = _normalizeDir(scriptDir, isWindows);
|
||||
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||
final scriptPath = '$normalizedDir$separator${builder.scriptFileName}';
|
||||
|
||||
return builder.getInstallCommand(normalizedDir, scriptPath);
|
||||
}
|
||||
|
||||
/// Generate complete script based on system type
|
||||
static String allScript(Map<String, String>? customCmds, {SystemType? systemType, List<String>? disabledCmdTypes}) {
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||
|
||||
return builder.buildScript(customCmds, disabledCmdTypes);
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
|
||||
enum ShellFunc {
|
||||
status,
|
||||
//docker,
|
||||
process,
|
||||
shutdown,
|
||||
reboot,
|
||||
suspend;
|
||||
|
||||
static const seperator = 'SrvBoxSep';
|
||||
|
||||
/// The suffix `\t` is for formatting
|
||||
static const cmdDivider = '\necho $seperator\n\t';
|
||||
|
||||
/// srvboxm -> ServerBox Mobile
|
||||
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
||||
static const scriptDirHome = '~/.config/server_box';
|
||||
static const scriptDirTmp = '/tmp/server_box';
|
||||
|
||||
static final _scriptDirMap = <String, String>{};
|
||||
|
||||
/// Get the script directory for the given [id].
|
||||
///
|
||||
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
||||
/// it will be changed to [scriptDirHome]/[scriptFile].
|
||||
static String getScriptDir(String id) {
|
||||
final customScriptDir = ServerProvider.pick(
|
||||
id: id,
|
||||
)?.value.spi.custom?.scriptDir;
|
||||
if (customScriptDir != null) return customScriptDir;
|
||||
return _scriptDirMap.putIfAbsent(id, () {
|
||||
return scriptDirTmp;
|
||||
});
|
||||
}
|
||||
|
||||
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
|
||||
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
|
||||
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
|
||||
_ => _scriptDirMap[id] = scriptDirHome,
|
||||
};
|
||||
|
||||
static String getScriptPath(String id) {
|
||||
return '${getScriptDir(id)}/$scriptFile';
|
||||
}
|
||||
|
||||
static String getInstallShellCmd(String id) {
|
||||
final scriptDir = getScriptDir(id);
|
||||
final scriptPath = '$scriptDir/$scriptFile';
|
||||
return '''
|
||||
mkdir -p $scriptDir
|
||||
cat > $scriptPath
|
||||
chmod 755 $scriptPath
|
||||
''';
|
||||
}
|
||||
|
||||
String get flag => switch (this) {
|
||||
ShellFunc.process => 'p',
|
||||
ShellFunc.shutdown => 'sd',
|
||||
ShellFunc.reboot => 'r',
|
||||
ShellFunc.suspend => 'sp',
|
||||
ShellFunc.status => 's',
|
||||
// ShellFunc.docker=> 'd',
|
||||
};
|
||||
|
||||
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
|
||||
|
||||
String get name {
|
||||
switch (this) {
|
||||
case ShellFunc.status:
|
||||
return 'status';
|
||||
// case ShellFunc.docker:
|
||||
// // `dockeR` -> avoid conflict with `docker` command
|
||||
// return 'dockeR';
|
||||
case ShellFunc.process:
|
||||
return 'process';
|
||||
case ShellFunc.shutdown:
|
||||
return 'ShutDown';
|
||||
case ShellFunc.reboot:
|
||||
return 'Reboot';
|
||||
case ShellFunc.suspend:
|
||||
return 'Suspend';
|
||||
}
|
||||
}
|
||||
|
||||
String get _cmd {
|
||||
switch (this) {
|
||||
case ShellFunc.status:
|
||||
return '''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\t${StatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
||||
else
|
||||
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
||||
fi''';
|
||||
// case ShellFunc.docker:
|
||||
// return '''
|
||||
// result=\$(docker version 2>&1 | grep "permission denied")
|
||||
// if [ "\$result" != "" ]; then
|
||||
// \t${_dockerCmds.join(_cmdDivider)}
|
||||
// else
|
||||
// \t${_dockerCmds.map((e) => "sudo -S $e").join(_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''';
|
||||
}
|
||||
}
|
||||
|
||||
static String allScript(Map<String, String>? customCmds) {
|
||||
final sb = StringBuffer();
|
||||
sb.write('''
|
||||
#!/bin/sh
|
||||
# Script for ServerBox app v1.0.${BuildData.build}
|
||||
# DO NOT delete this file while app is running
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
|
||||
# If macSign & bsdSign are both empty, then it's linux
|
||||
macSign=\$(uname -a 2>&1 | grep "Darwin")
|
||||
bsdSign=\$(uname -a 2>&1 | grep "BSD")
|
||||
|
||||
# Link /bin/sh to busybox?
|
||||
isBusybox=\$(ls -l /bin/sh | grep "busybox")
|
||||
|
||||
userId=\$(id -u)
|
||||
|
||||
exec 2>/dev/null
|
||||
|
||||
''');
|
||||
// Write each func
|
||||
for (final func in values) {
|
||||
final customCmdsStr = () {
|
||||
if (func == ShellFunc.status &&
|
||||
customCmds != null &&
|
||||
customCmds.isNotEmpty) {
|
||||
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
|
||||
}
|
||||
return '';
|
||||
}();
|
||||
sb.write('''
|
||||
${func.name}() {
|
||||
${func._cmd.split('\n').map((e) => '\t$e').join('\n')}
|
||||
$customCmdsStr
|
||||
}
|
||||
|
||||
''');
|
||||
}
|
||||
|
||||
// Write switch case
|
||||
sb.write('case \$1 in\n');
|
||||
for (final func in values) {
|
||||
sb.write('''
|
||||
'-${func.flag}')
|
||||
${func.name}
|
||||
;;
|
||||
''');
|
||||
}
|
||||
sb.write('''
|
||||
*)
|
||||
echo "Invalid argument \$1"
|
||||
;;
|
||||
esac''');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
extension EnumX on Enum {
|
||||
/// Find out the required segment from [segments]
|
||||
String find(List<String> segments) {
|
||||
return segments[index];
|
||||
}
|
||||
}
|
||||
|
||||
enum StatusCmdType {
|
||||
echo._('echo ${SystemType.linuxSign}'),
|
||||
time._('date +%s'),
|
||||
net._('cat /proc/net/dev'),
|
||||
sys._('cat /etc/*-release | grep ^PRETTY_NAME'),
|
||||
cpu._('cat /proc/stat | grep cpu'),
|
||||
uptime._('uptime'),
|
||||
conn._('cat /proc/net/snmp'),
|
||||
disk._(
|
||||
'lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
||||
),
|
||||
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
||||
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||
host._('cat /etc/hostname'),
|
||||
diskio._('cat /proc/diskstats'),
|
||||
battery._(
|
||||
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done',
|
||||
),
|
||||
nvidia._('nvidia-smi -q -x'),
|
||||
sensors._('sensors'),
|
||||
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -j /dev/\$d; echo; done'),
|
||||
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
||||
|
||||
final String cmd;
|
||||
|
||||
const StatusCmdType._(this.cmd);
|
||||
}
|
||||
|
||||
enum BSDStatusCmdType {
|
||||
echo._('echo ${SystemType.bsdSign}'),
|
||||
time._('date +%s'),
|
||||
net._('netstat -ibn'),
|
||||
sys._('uname -or'),
|
||||
cpu._('top -l 1 | grep "CPU usage"'),
|
||||
uptime._('uptime'),
|
||||
// Keep df -k for BSD systems as lsblk is not available on macOS/BSD
|
||||
disk._('df -k'),
|
||||
mem._('top -l 1 | grep PhysMem'),
|
||||
//temp,
|
||||
host._('hostname'),
|
||||
cpuBrand._('sysctl -n machdep.cpu.brand_string');
|
||||
|
||||
final String cmd;
|
||||
|
||||
const BSDStatusCmdType._(this.cmd);
|
||||
}
|
||||
|
||||
extension StatusCmdTypeX on StatusCmdType {
|
||||
String get i18n => switch (this) {
|
||||
StatusCmdType.sys => l10n.system,
|
||||
StatusCmdType.host => l10n.host,
|
||||
StatusCmdType.uptime => l10n.uptime,
|
||||
StatusCmdType.battery => l10n.battery,
|
||||
final val => val.name,
|
||||
};
|
||||
}
|
||||
@@ -12,7 +12,7 @@ enum AppTab {
|
||||
server,
|
||||
ssh,
|
||||
file,
|
||||
snippet,
|
||||
snippet
|
||||
//settings,
|
||||
;
|
||||
|
||||
@@ -29,60 +29,60 @@ enum AppTab {
|
||||
NavigationDestination get navDestination {
|
||||
return switch (this) {
|
||||
server => NavigationDestination(
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: l10n.server,
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: l10n.server,
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
// settings => NavigationDestination(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// label: libL10n.setting,
|
||||
// selectedIcon: const Icon(Icons.settings),
|
||||
// ),
|
||||
ssh => const NavigationDestination(
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: 'SSH',
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: 'SSH',
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
snippet => NavigationDestination(
|
||||
icon: const Icon(Icons.code),
|
||||
label: l10n.snippet,
|
||||
selectedIcon: const Icon(Icons.code),
|
||||
),
|
||||
icon: const Icon(Icons.code),
|
||||
label: l10n.snippet,
|
||||
selectedIcon: const Icon(Icons.code),
|
||||
),
|
||||
file => NavigationDestination(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: libL10n.file,
|
||||
selectedIcon: const Icon(Icons.folder),
|
||||
),
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: libL10n.file,
|
||||
selectedIcon: const Icon(Icons.folder),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
NavigationRailDestination get navRailDestination {
|
||||
return switch (this) {
|
||||
server => NavigationRailDestination(
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: Text(l10n.server),
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: Text(l10n.server),
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
// settings => NavigationRailDestination(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// label: libL10n.setting,
|
||||
// selectedIcon: const Icon(Icons.settings),
|
||||
// ),
|
||||
ssh => const NavigationRailDestination(
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: Text('SSH'),
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: Text('SSH'),
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
snippet => NavigationRailDestination(
|
||||
icon: const Icon(Icons.code),
|
||||
label: Text(l10n.snippet),
|
||||
selectedIcon: const Icon(Icons.code),
|
||||
),
|
||||
icon: const Icon(Icons.code),
|
||||
label: Text(l10n.snippet),
|
||||
selectedIcon: const Icon(Icons.code),
|
||||
),
|
||||
file => NavigationRailDestination(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: Text(libL10n.file),
|
||||
selectedIcon: const Icon(Icons.folder),
|
||||
),
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: Text(libL10n.file),
|
||||
selectedIcon: const Icon(Icons.folder),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,14 +24,7 @@ final class PodmanImg implements ContainerImg {
|
||||
final int? size;
|
||||
final int? containers;
|
||||
|
||||
PodmanImg({
|
||||
this.repository,
|
||||
this.tag,
|
||||
this.id,
|
||||
this.created,
|
||||
this.size,
|
||||
this.containers,
|
||||
});
|
||||
PodmanImg({this.repository, this.tag, this.id, this.created, this.size, this.containers});
|
||||
|
||||
@override
|
||||
String? get sizeMB => size?.bytes2Str;
|
||||
@@ -39,28 +32,27 @@ final class PodmanImg implements ContainerImg {
|
||||
@override
|
||||
int? get containersCount => containers;
|
||||
|
||||
factory PodmanImg.fromRawJson(String str) =>
|
||||
PodmanImg.fromJson(json.decode(str));
|
||||
factory PodmanImg.fromRawJson(String str) => PodmanImg.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
|
||||
repository: json['repository'],
|
||||
tag: json['tag'],
|
||||
id: json['Id'],
|
||||
created: json['Created'],
|
||||
size: json['Size'],
|
||||
containers: json['Containers'],
|
||||
);
|
||||
repository: json['repository'],
|
||||
tag: json['tag'],
|
||||
id: json['Id'],
|
||||
created: json['Created'],
|
||||
size: json['Size'],
|
||||
containers: json['Containers'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'repository': repository,
|
||||
'tag': tag,
|
||||
'Id': id,
|
||||
'Created': created,
|
||||
'Size': size,
|
||||
'Containers': containers,
|
||||
};
|
||||
'repository': repository,
|
||||
'tag': tag,
|
||||
'Id': id,
|
||||
'Created': created,
|
||||
'Size': size,
|
||||
'Containers': containers,
|
||||
};
|
||||
}
|
||||
|
||||
final class DockerImg implements ContainerImg {
|
||||
@@ -87,11 +79,9 @@ final class DockerImg implements ContainerImg {
|
||||
String? get sizeMB => size;
|
||||
|
||||
@override
|
||||
int? get containersCount =>
|
||||
containers == 'N/A' ? 0 : int.tryParse(containers);
|
||||
int? get containersCount => containers == 'N/A' ? 0 : int.tryParse(containers);
|
||||
|
||||
factory DockerImg.fromRawJson(String str) =>
|
||||
DockerImg.fromJson(json.decode(str));
|
||||
factory DockerImg.fromRawJson(String str) => DockerImg.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
@@ -121,11 +111,11 @@ final class DockerImg implements ContainerImg {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'Containers': containers,
|
||||
'CreatedAt': createdAt,
|
||||
'ID': id,
|
||||
'Repository': repository,
|
||||
'Size': size,
|
||||
'Tag': tag,
|
||||
};
|
||||
'Containers': containers,
|
||||
'CreatedAt': createdAt,
|
||||
'ID': id,
|
||||
'Repository': repository,
|
||||
'Size': size,
|
||||
'Tag': tag,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,15 +42,7 @@ final class PodmanPs implements ContainerPs {
|
||||
@override
|
||||
String? disk;
|
||||
|
||||
PodmanPs({
|
||||
this.command,
|
||||
this.created,
|
||||
this.exited,
|
||||
this.id,
|
||||
this.image,
|
||||
this.names,
|
||||
this.startedAt,
|
||||
});
|
||||
PodmanPs({this.command, this.created, this.exited, this.id, this.image, this.names, this.startedAt});
|
||||
|
||||
@override
|
||||
String? get name => names?.firstOrNull;
|
||||
@@ -78,36 +70,29 @@ final class PodmanPs implements ContainerPs {
|
||||
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
|
||||
}
|
||||
|
||||
factory PodmanPs.fromRawJson(String str) =>
|
||||
PodmanPs.fromJson(json.decode(str));
|
||||
factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
|
||||
command: json['Command'] == null
|
||||
? []
|
||||
: List<String>.from(json['Command']!.map((x) => x)),
|
||||
created:
|
||||
json['Created'] == null ? null : DateTime.parse(json['Created']),
|
||||
exited: json['Exited'],
|
||||
id: json['Id'],
|
||||
image: json['Image'],
|
||||
names: json['Names'] == null
|
||||
? []
|
||||
: List<String>.from(json['Names']!.map((x) => x)),
|
||||
startedAt: json['StartedAt'],
|
||||
);
|
||||
command: json['Command'] == null ? [] : List<String>.from(json['Command']!.map((x) => x)),
|
||||
created: json['Created'] == null ? null : DateTime.parse(json['Created']),
|
||||
exited: json['Exited'],
|
||||
id: json['Id'],
|
||||
image: json['Image'],
|
||||
names: json['Names'] == null ? [] : List<String>.from(json['Names']!.map((x) => x)),
|
||||
startedAt: json['StartedAt'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'Command':
|
||||
command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
||||
'Created': created?.toIso8601String(),
|
||||
'Exited': exited,
|
||||
'Id': id,
|
||||
'Image': image,
|
||||
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
|
||||
'StartedAt': startedAt,
|
||||
};
|
||||
'Command': command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
||||
'Created': created?.toIso8601String(),
|
||||
'Exited': exited,
|
||||
'Id': id,
|
||||
'Image': image,
|
||||
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
|
||||
'StartedAt': startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
final class DockerPs implements ContainerPs {
|
||||
@@ -127,12 +112,7 @@ final class DockerPs implements ContainerPs {
|
||||
@override
|
||||
String? disk;
|
||||
|
||||
DockerPs({
|
||||
this.id,
|
||||
this.image,
|
||||
this.names,
|
||||
this.state,
|
||||
});
|
||||
DockerPs({this.id, this.image, this.names, this.state});
|
||||
|
||||
@override
|
||||
String? get name => names;
|
||||
@@ -159,11 +139,6 @@ final class DockerPs implements ContainerPs {
|
||||
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
|
||||
factory DockerPs.parse(String raw) {
|
||||
final parts = raw.split(Miscs.multiBlankreg);
|
||||
return DockerPs(
|
||||
id: parts[0],
|
||||
state: parts[1],
|
||||
names: parts[2],
|
||||
image: parts[3].trim(),
|
||||
);
|
||||
return DockerPs(id: parts[0], state: parts[1], names: parts[2], image: parts[3].trim());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,15 @@ import 'package:server_box/data/model/container/ps.dart';
|
||||
|
||||
enum ContainerType {
|
||||
docker,
|
||||
podman,
|
||||
;
|
||||
podman;
|
||||
|
||||
ContainerPs Function(String str) get ps => switch (this) {
|
||||
ContainerType.docker => DockerPs.parse,
|
||||
ContainerType.podman => PodmanPs.fromRawJson,
|
||||
};
|
||||
ContainerType.docker => DockerPs.parse,
|
||||
ContainerType.podman => PodmanPs.fromRawJson,
|
||||
};
|
||||
|
||||
ContainerImg Function(String str) get img => switch (this) {
|
||||
ContainerType.docker => DockerImg.fromRawJson,
|
||||
ContainerType.podman => PodmanImg.fromRawJson,
|
||||
};
|
||||
ContainerType.docker => DockerImg.fromRawJson,
|
||||
ContainerType.podman => PodmanImg.fromRawJson,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,8 +62,7 @@ enum PkgManager {
|
||||
case PkgManager.yum:
|
||||
list = list.sublist(2);
|
||||
list.removeWhere((element) => element.isEmpty);
|
||||
final endLine = list.lastIndexWhere(
|
||||
(element) => element.contains('Obsoleting Packages'));
|
||||
final endLine = list.lastIndexWhere((element) => element.contains('Obsoleting Packages'));
|
||||
if (endLine != -1 && list.isNotEmpty) {
|
||||
list = list.sublist(0, endLine);
|
||||
}
|
||||
@@ -71,8 +70,7 @@ enum PkgManager {
|
||||
case PkgManager.apt:
|
||||
// avoid other outputs
|
||||
// such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...]
|
||||
final idx =
|
||||
list.indexWhere((element) => element.contains('[upgradable from:'));
|
||||
final idx = list.indexWhere((element) => element.contains('[upgradable from:'));
|
||||
if (idx == -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
188
lib/data/model/server/amd.dart
Normal file
188
lib/data/model/server/amd.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// AMD GPU monitoring data structures
|
||||
/// Supports both amd-smi and rocm-smi tools
|
||||
/// Example JSON output:
|
||||
/// [
|
||||
/// {
|
||||
/// "name": "AMD Radeon RX 7900 XTX",
|
||||
/// "device_id": "0",
|
||||
/// "temp": 45,
|
||||
/// "power": "120W / 355W",
|
||||
/// "memory": {
|
||||
/// "total": 24576,
|
||||
/// "used": 1024,
|
||||
/// "unit": "MB",
|
||||
/// "processes": [
|
||||
/// {
|
||||
/// "pid": 2456,
|
||||
/// "name": "firefox",
|
||||
/// "memory": 512
|
||||
/// }
|
||||
/// ]
|
||||
/// },
|
||||
/// "utilization": 75,
|
||||
/// "fan_speed": 1200,
|
||||
/// "clock_speed": 2400
|
||||
/// }
|
||||
/// ]
|
||||
|
||||
class AmdSmi {
|
||||
static List<AmdSmiItem> fromJson(String raw) {
|
||||
try {
|
||||
final jsonData = json.decode(raw);
|
||||
if (jsonData is! List) return [];
|
||||
|
||||
return jsonData
|
||||
.map((gpu) => _parseGpuItem(gpu))
|
||||
.where((item) => item != null)
|
||||
.cast<AmdSmiItem>()
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static AmdSmiItem? _parseGpuItem(Map<String, dynamic> gpu) {
|
||||
try {
|
||||
final name = gpu['name'] ?? gpu['card_model'] ?? gpu['device_name'] ?? 'Unknown AMD GPU';
|
||||
final deviceId = gpu['device_id']?.toString() ?? gpu['gpu_id']?.toString() ?? '0';
|
||||
|
||||
// Temperature parsing
|
||||
final tempRaw = gpu['temperature'] ?? gpu['temp'] ?? gpu['gpu_temp'];
|
||||
final temp = _parseIntValue(tempRaw);
|
||||
|
||||
// Power parsing
|
||||
final powerDraw = gpu['power_draw'] ?? gpu['current_power'];
|
||||
final powerCap = gpu['power_cap'] ?? gpu['power_limit'] ?? gpu['max_power'];
|
||||
final power = _formatPower(powerDraw, powerCap);
|
||||
|
||||
// Memory parsing
|
||||
final memory = _parseMemory(gpu['memory'] ?? gpu['vram'] ?? {});
|
||||
|
||||
// Utilization parsing
|
||||
final utilization = _parseIntValue(gpu['utilization'] ?? gpu['gpu_util'] ?? gpu['activity']);
|
||||
|
||||
// Fan speed parsing
|
||||
final fanSpeed = _parseIntValue(gpu['fan_speed'] ?? gpu['fan_rpm']);
|
||||
|
||||
// Clock speed parsing
|
||||
final clockSpeed = _parseIntValue(gpu['clock_speed'] ?? gpu['gpu_clock'] ?? gpu['sclk']);
|
||||
|
||||
return AmdSmiItem(
|
||||
deviceId: deviceId,
|
||||
name: name,
|
||||
temp: temp,
|
||||
power: power,
|
||||
memory: memory,
|
||||
utilization: utilization,
|
||||
fanSpeed: fanSpeed,
|
||||
clockSpeed: clockSpeed,
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static int _parseIntValue(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is String) {
|
||||
// Remove units and parse (e.g., "45°C" -> 45, "1200 RPM" -> 1200)
|
||||
final cleanValue = value.replaceAll(RegExp(r'[^\d]'), '');
|
||||
return int.tryParse(cleanValue) ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static String _formatPower(dynamic draw, dynamic cap) {
|
||||
final drawValue = _parseIntValue(draw);
|
||||
final capValue = _parseIntValue(cap);
|
||||
|
||||
if (drawValue == 0 && capValue == 0) return 'N/A';
|
||||
if (capValue == 0) return '${drawValue}W';
|
||||
return '${drawValue}W / ${capValue}W';
|
||||
}
|
||||
|
||||
static AmdSmiMem _parseMemory(Map<String, dynamic> memData) {
|
||||
final total = _parseIntValue(memData['total'] ?? memData['total_memory']);
|
||||
final used = _parseIntValue(memData['used'] ?? memData['used_memory']);
|
||||
final unit = memData['unit']?.toString() ?? 'MB';
|
||||
|
||||
final processes = <AmdSmiMemProcess>[];
|
||||
final processesData = memData['processes'];
|
||||
if (processesData is List) {
|
||||
for (final proc in processesData) {
|
||||
if (proc is Map<String, dynamic>) {
|
||||
final process = _parseProcess(proc);
|
||||
if (process != null) processes.add(process);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AmdSmiMem(total, used, unit, processes);
|
||||
}
|
||||
|
||||
static AmdSmiMemProcess? _parseProcess(Map<String, dynamic> procData) {
|
||||
final pid = _parseIntValue(procData['pid']);
|
||||
final name = procData['name']?.toString() ?? procData['process_name']?.toString() ?? 'Unknown';
|
||||
final memory = _parseIntValue(procData['memory'] ?? procData['used_memory']);
|
||||
|
||||
if (pid == 0) return null;
|
||||
return AmdSmiMemProcess(pid, name, memory);
|
||||
}
|
||||
}
|
||||
|
||||
class AmdSmiItem {
|
||||
final String deviceId;
|
||||
final String name;
|
||||
final int temp;
|
||||
final String power;
|
||||
final AmdSmiMem memory;
|
||||
final int utilization;
|
||||
final int fanSpeed;
|
||||
final int clockSpeed;
|
||||
|
||||
const AmdSmiItem({
|
||||
required this.deviceId,
|
||||
required this.name,
|
||||
required this.temp,
|
||||
required this.power,
|
||||
required this.memory,
|
||||
required this.utilization,
|
||||
required this.fanSpeed,
|
||||
required this.clockSpeed,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AmdSmiItem{name: $name, temp: $temp, power: $power, utilization: $utilization%, memory: $memory}';
|
||||
}
|
||||
}
|
||||
|
||||
class AmdSmiMem {
|
||||
final int total;
|
||||
final int used;
|
||||
final String unit;
|
||||
final List<AmdSmiMemProcess> processes;
|
||||
|
||||
const AmdSmiMem(this.total, this.used, this.unit, this.processes);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AmdSmiMem{total: $total, used: $used, unit: $unit, processes: ${processes.length}}';
|
||||
}
|
||||
}
|
||||
|
||||
class AmdSmiMemProcess {
|
||||
final int pid;
|
||||
final String name;
|
||||
final int memory;
|
||||
|
||||
const AmdSmiMemProcess(this.pid, this.name, this.memory);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AmdSmiMemProcess{pid: $pid, name: $name, memory: $memory}';
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,7 @@ class Battery {
|
||||
final int? cycle;
|
||||
final String? tech;
|
||||
|
||||
const Battery({
|
||||
required this.status,
|
||||
this.percent,
|
||||
this.name,
|
||||
this.cycle,
|
||||
this.tech,
|
||||
});
|
||||
const Battery({required this.status, this.percent, this.name, this.cycle, this.tech});
|
||||
|
||||
factory Battery.fromRaw(String raw) {
|
||||
final lines = raw.split('\n');
|
||||
@@ -63,8 +57,7 @@ enum BatteryStatus {
|
||||
charging,
|
||||
discharging,
|
||||
full,
|
||||
unknown,
|
||||
;
|
||||
unknown;
|
||||
|
||||
static BatteryStatus parse(String? status) {
|
||||
switch (status) {
|
||||
|
||||
@@ -6,17 +6,11 @@ class Conn {
|
||||
final int passive;
|
||||
final int fail;
|
||||
|
||||
const Conn({
|
||||
required this.maxConn,
|
||||
required this.active,
|
||||
required this.passive,
|
||||
required this.fail,
|
||||
});
|
||||
const Conn({required this.maxConn, required this.active, required this.passive, required this.fail});
|
||||
|
||||
static Conn? parse(String raw) {
|
||||
final lines = raw.split('\n');
|
||||
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
|
||||
orElse: () => '');
|
||||
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'), orElse: () => '');
|
||||
if (idx != '') {
|
||||
final vals = idx.split(Miscs.blankReg);
|
||||
return Conn(
|
||||
|
||||
@@ -200,22 +200,98 @@ final class CpuBrand {
|
||||
}
|
||||
|
||||
final _bsdCpuPercentReg = RegExp(r'(\d+\.\d+)%');
|
||||
final _macCpuPercentReg = RegExp(
|
||||
r'CPU usage: ([\d.]+)% user, ([\d.]+)% sys, ([\d.]+)% idle');
|
||||
final _freebsdCpuPercentReg = RegExp(
|
||||
r'CPU: ([\d.]+)% user, ([\d.]+)% nice, ([\d.]+)% system, '
|
||||
r'([\d.]+)% interrupt, ([\d.]+)% idle');
|
||||
|
||||
/// TODO: Change this implementation to parse cpu status on BSD system
|
||||
/// Parse CPU status on BSD system with support for different BSD variants
|
||||
///
|
||||
/// [raw]:
|
||||
/// CPU usage: 14.70% user, 12.76% sys, 72.52% idle
|
||||
/// Supports multiple formats:
|
||||
/// - macOS: "CPU usage: 14.70% user, 12.76% sys, 72.52% idle"
|
||||
/// - FreeBSD: "CPU: 5.2% user, 0.0% nice, 3.1% system, 0.1% interrupt, 91.6% idle"
|
||||
/// - Generic BSD: fallback to percentage extraction
|
||||
Cpus parseBsdCpu(String raw) {
|
||||
final init = InitStatus.cpus;
|
||||
|
||||
// Try macOS format first
|
||||
final macMatch = _macCpuPercentReg.firstMatch(raw);
|
||||
if (macMatch != null) {
|
||||
final userPercent = double.parse(macMatch.group(1)!).toInt();
|
||||
final sysPercent = double.parse(macMatch.group(2)!).toInt();
|
||||
final idlePercent = double.parse(macMatch.group(3)!).toInt();
|
||||
|
||||
init.add([
|
||||
SingleCpuCore(
|
||||
'cpu0',
|
||||
userPercent,
|
||||
sysPercent,
|
||||
0, // nice
|
||||
idlePercent,
|
||||
0, // iowait
|
||||
0, // irq
|
||||
0, // softirq
|
||||
),
|
||||
]);
|
||||
return init;
|
||||
}
|
||||
|
||||
// Try FreeBSD format
|
||||
final freebsdMatch = _freebsdCpuPercentReg.firstMatch(raw);
|
||||
if (freebsdMatch != null) {
|
||||
final userPercent = double.parse(freebsdMatch.group(1)!).toInt();
|
||||
final nicePercent = double.parse(freebsdMatch.group(2)!).toInt();
|
||||
final sysPercent = double.parse(freebsdMatch.group(3)!).toInt();
|
||||
final irqPercent = double.parse(freebsdMatch.group(4)!).toInt();
|
||||
final idlePercent = double.parse(freebsdMatch.group(5)!).toInt();
|
||||
|
||||
init.add([
|
||||
SingleCpuCore(
|
||||
'cpu0',
|
||||
userPercent,
|
||||
sysPercent,
|
||||
nicePercent,
|
||||
idlePercent,
|
||||
0, // iowait
|
||||
irqPercent,
|
||||
0, // softirq
|
||||
),
|
||||
]);
|
||||
return init;
|
||||
}
|
||||
|
||||
// Fallback to generic percentage extraction
|
||||
final percents = _bsdCpuPercentReg
|
||||
.allMatches(raw)
|
||||
.map((e) => double.parse(e.group(1) ?? '0') * 100)
|
||||
.map((e) => double.parse(e.group(1) ?? '0'))
|
||||
.toList();
|
||||
if (percents.length != 3) return InitStatus.cpus;
|
||||
|
||||
final init = InitStatus.cpus;
|
||||
init.add([
|
||||
SingleCpuCore('cpu', percents[0].toInt(), 0, 0,
|
||||
percents[2].toInt() + percents[1].toInt(), 0, 0, 0),
|
||||
]);
|
||||
|
||||
if (percents.length >= 3) {
|
||||
// Validate that percentages are reasonable (0-100 range)
|
||||
final validPercents = percents.where((p) => p >= 0 && p <= 100).toList();
|
||||
if (validPercents.length != percents.length) {
|
||||
Loggers.app.warning('BSD CPU fallback parsing found invalid percentages in: $raw');
|
||||
}
|
||||
|
||||
init.add([
|
||||
SingleCpuCore(
|
||||
'cpu0',
|
||||
percents[0].toInt(), // user
|
||||
percents.length > 1 ? percents[1].toInt() : 0, // sys
|
||||
0, // nice
|
||||
percents.length > 2 ? percents[2].toInt() : 0, // idle
|
||||
0, // iowait
|
||||
0, // irq
|
||||
0, // softirq
|
||||
),
|
||||
]);
|
||||
return init;
|
||||
} else if (percents.isNotEmpty) {
|
||||
Loggers.app.warning('BSD CPU fallback parsing found ${percents.length} percentages (expected at least 3) in: $raw');
|
||||
} else {
|
||||
Loggers.app.warning('BSD CPU fallback parsing found no percentages in: $raw');
|
||||
}
|
||||
|
||||
return init;
|
||||
}
|
||||
|
||||
@@ -70,14 +70,14 @@ class Disk with EquatableMixin {
|
||||
if (disk != null) {
|
||||
list.add(disk);
|
||||
}
|
||||
|
||||
|
||||
// For devices with children (like physical disks with partitions),
|
||||
// also process each child individually to ensure BTRFS RAID disks are properly handled
|
||||
final List<dynamic> childDevices = device['children'] ?? [];
|
||||
for (final childDevice in childDevices) {
|
||||
final String childPath = childDevice['path']?.toString() ?? '';
|
||||
final String childFsType = childDevice['fstype']?.toString() ?? '';
|
||||
|
||||
|
||||
// If this is a BTRFS partition, add it directly to ensure it's properly represented
|
||||
if (childFsType == 'btrfs' && childPath.isNotEmpty) {
|
||||
final childDisk = _processSingleDevice(childDevice);
|
||||
@@ -93,11 +93,11 @@ class Disk with EquatableMixin {
|
||||
final fstype = device['fstype']?.toString();
|
||||
final String mountpoint = device['mountpoint']?.toString() ?? '';
|
||||
final String path = device['path']?.toString() ?? '';
|
||||
|
||||
|
||||
if (path.isEmpty || (fstype == null && mountpoint.isEmpty)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (!_shouldCalc(fstype ?? '', mountpoint)) {
|
||||
return null;
|
||||
}
|
||||
@@ -154,8 +154,7 @@ class Disk with EquatableMixin {
|
||||
}
|
||||
|
||||
// Handle common filesystem cases or parent devices with children
|
||||
if ((fstype != null && _shouldCalc(fstype, mount)) ||
|
||||
(childDisks.isNotEmpty && path.isNotEmpty)) {
|
||||
if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
|
||||
final sizeStr = device['fssize']?.toString() ?? '0';
|
||||
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
@@ -221,14 +220,16 @@ class Disk with EquatableMixin {
|
||||
final fs = vals[0];
|
||||
final mount = vals[5];
|
||||
if (!_shouldCalc(fs, mount)) continue;
|
||||
list.add(Disk(
|
||||
path: fs,
|
||||
mount: mount,
|
||||
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
|
||||
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
|
||||
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
|
||||
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
|
||||
));
|
||||
list.add(
|
||||
Disk(
|
||||
path: fs,
|
||||
mount: mount,
|
||||
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
|
||||
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
|
||||
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
|
||||
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
@@ -237,8 +238,19 @@ class Disk with EquatableMixin {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[path, name, kname, fsTyp, mount, usedPercent, used, size, avail, uuid, children];
|
||||
List<Object?> get props => [
|
||||
path,
|
||||
name,
|
||||
kname,
|
||||
fsTyp,
|
||||
mount,
|
||||
usedPercent,
|
||||
used,
|
||||
size,
|
||||
avail,
|
||||
uuid,
|
||||
children,
|
||||
];
|
||||
}
|
||||
|
||||
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
||||
@@ -314,12 +326,14 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
||||
try {
|
||||
final dev = vals[2];
|
||||
if (dev.startsWith('loop')) continue;
|
||||
items.add(DiskIOPiece(
|
||||
dev: dev,
|
||||
sectorsRead: int.parse(vals[5]),
|
||||
sectorsWrite: int.parse(vals[9]),
|
||||
time: time,
|
||||
));
|
||||
items.add(
|
||||
DiskIOPiece(
|
||||
dev: dev,
|
||||
sectorsRead: int.parse(vals[5]),
|
||||
sectorsWrite: int.parse(vals[9]),
|
||||
time: time,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
@@ -334,12 +348,7 @@ class DiskIOPiece extends TimeSeqIface<DiskIOPiece> {
|
||||
final int sectorsWrite;
|
||||
final int time;
|
||||
|
||||
DiskIOPiece({
|
||||
required this.dev,
|
||||
required this.sectorsRead,
|
||||
required this.sectorsWrite,
|
||||
required this.time,
|
||||
});
|
||||
DiskIOPiece({required this.dev, required this.sectorsRead, required this.sectorsWrite, required this.time});
|
||||
|
||||
@override
|
||||
bool same(DiskIOPiece other) => dev == other.dev;
|
||||
@@ -349,10 +358,7 @@ class DiskUsage {
|
||||
final BigInt used;
|
||||
final BigInt size;
|
||||
|
||||
DiskUsage({
|
||||
required this.used,
|
||||
required this.size,
|
||||
});
|
||||
DiskUsage({required this.used, required this.size});
|
||||
|
||||
double get usedPercent {
|
||||
// Avoid division by zero
|
||||
|
||||
@@ -7,7 +7,7 @@ part 'disk_smart.freezed.dart';
|
||||
part 'disk_smart.g.dart';
|
||||
|
||||
@freezed
|
||||
class DiskSmart with _$DiskSmart {
|
||||
abstract class DiskSmart with _$DiskSmart {
|
||||
const DiskSmart._();
|
||||
|
||||
const factory DiskSmart({
|
||||
@@ -35,7 +35,10 @@ class DiskSmart with _$DiskSmart {
|
||||
|
||||
// Basic
|
||||
final device = data['device']?['name']?.toString() ?? '';
|
||||
final healthy = data['smart_status']?['passed'] as bool?;
|
||||
|
||||
if (!_isPhysicalDisk(device)) continue;
|
||||
|
||||
final healthy = _parseHealthStatus(data);
|
||||
|
||||
// Model and Serial
|
||||
final model =
|
||||
@@ -72,6 +75,92 @@ class DiskSmart with _$DiskSmart {
|
||||
return results;
|
||||
}
|
||||
|
||||
static bool _isPhysicalDisk(String device) {
|
||||
if (device.isEmpty) return false;
|
||||
|
||||
// Common patterns for physical disks
|
||||
final patterns = [
|
||||
RegExp(r'^/dev/sd[a-z]$'), // SATA/SCSI: /dev/sda, /dev/sdb
|
||||
RegExp(r'^/dev/hd[a-z]$'), // IDE: /dev/hda, /dev/hdb
|
||||
RegExp(r'^/dev/nvme\d+n\d+$'), // NVMe: /dev/nvme0n1, /dev/nvme1n1
|
||||
RegExp(r'^/dev/mmcblk\d+$'), // MMC: /dev/mmcblk0
|
||||
RegExp(r'^/dev/vd[a-z]$'), // VirtIO: /dev/vda, /dev/vdb
|
||||
RegExp(r'^/dev/xvd[a-z]$'), // Xen: /dev/xvda, /dev/xvdb
|
||||
];
|
||||
|
||||
return patterns.any((pattern) => pattern.hasMatch(device));
|
||||
}
|
||||
|
||||
static bool? _parseHealthStatus(Map<String, dynamic> data) {
|
||||
// smart_status.passed
|
||||
final smartStatus = data['smart_status'];
|
||||
if (smartStatus is Map<String, dynamic>) {
|
||||
final passed = smartStatus['passed'];
|
||||
if (passed is bool) return passed;
|
||||
}
|
||||
|
||||
// smart_status.status
|
||||
if (smartStatus is Map<String, dynamic>) {
|
||||
final status = smartStatus['status']?.toString().toLowerCase();
|
||||
if (status != null) {
|
||||
if (status.contains('pass') || status.contains('ok')) return true;
|
||||
if (status.contains('fail')) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// smart_status
|
||||
final rootSmartStatus = data['smart_status']?.toString().toLowerCase();
|
||||
if (rootSmartStatus != null) {
|
||||
if (rootSmartStatus.contains('pass') || rootSmartStatus.contains('ok')) return true;
|
||||
if (rootSmartStatus.contains('fail')) return false;
|
||||
}
|
||||
|
||||
// health attrs
|
||||
final attrTable = data['ata_smart_attributes']?['table'] as List?;
|
||||
if (attrTable != null) {
|
||||
var hasFailingAttributes = false;
|
||||
|
||||
for (final attr in attrTable) {
|
||||
if (attr is Map<String, dynamic>) {
|
||||
final whenFailed = attr['when_failed']?.toString();
|
||||
if (whenFailed != null && whenFailed.isNotEmpty && whenFailed != 'never') {
|
||||
hasFailingAttributes = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Whether the attribute is critical
|
||||
final name = attr['name']?.toString();
|
||||
final value = attr['value'] as int?;
|
||||
final thresh = attr['thresh'] as int?;
|
||||
|
||||
if (name != null && value != null && thresh != null && thresh > 0) {
|
||||
const criticalAttrs = [
|
||||
'Reallocated_Sector_Ct',
|
||||
'Reallocated_Event_Count',
|
||||
'Current_Pending_Sector',
|
||||
'Offline_Uncorrectable',
|
||||
'UDMA_CRC_Error_Count',
|
||||
];
|
||||
|
||||
if (criticalAttrs.contains(name) && value < thresh) {
|
||||
hasFailingAttributes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFailingAttributes) return false;
|
||||
}
|
||||
|
||||
if (attrTable != null && attrTable.isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Uncertain status, assume healthy
|
||||
return true;
|
||||
}
|
||||
|
||||
static Map<String, SmartAttribute> _parseSmartAttributes(Map<String, dynamic> data) {
|
||||
final attributes = <String, SmartAttribute>{};
|
||||
|
||||
@@ -144,7 +233,7 @@ class DiskSmart with _$DiskSmart {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SmartAttribute with _$SmartAttribute {
|
||||
abstract class SmartAttribute with _$SmartAttribute {
|
||||
const SmartAttribute._();
|
||||
|
||||
const factory SmartAttribute({
|
||||
@@ -168,7 +257,7 @@ class SmartAttribute with _$SmartAttribute {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SmartAttributeFlags with _$SmartAttributeFlags {
|
||||
abstract class SmartAttributeFlags with _$SmartAttributeFlags {
|
||||
const SmartAttributeFlags._();
|
||||
|
||||
const factory SmartAttributeFlags({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,23 +6,21 @@ part of 'disk_smart.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$DiskSmartImpl _$$DiskSmartImplFromJson(Map<String, dynamic> json) =>
|
||||
_$DiskSmartImpl(
|
||||
device: json['device'] as String,
|
||||
healthy: json['healthy'] as bool?,
|
||||
temperature: (json['temperature'] as num?)?.toDouble(),
|
||||
model: json['model'] as String?,
|
||||
serial: json['serial'] as String?,
|
||||
powerOnHours: (json['powerOnHours'] as num?)?.toInt(),
|
||||
powerCycleCount: (json['powerCycleCount'] as num?)?.toInt(),
|
||||
rawData: json['rawData'] as Map<String, dynamic>,
|
||||
smartAttributes: (json['smartAttributes'] as Map<String, dynamic>).map(
|
||||
(k, e) =>
|
||||
MapEntry(k, SmartAttribute.fromJson(e as Map<String, dynamic>)),
|
||||
),
|
||||
);
|
||||
_DiskSmart _$DiskSmartFromJson(Map<String, dynamic> json) => _DiskSmart(
|
||||
device: json['device'] as String,
|
||||
healthy: json['healthy'] as bool?,
|
||||
temperature: (json['temperature'] as num?)?.toDouble(),
|
||||
model: json['model'] as String?,
|
||||
serial: json['serial'] as String?,
|
||||
powerOnHours: (json['powerOnHours'] as num?)?.toInt(),
|
||||
powerCycleCount: (json['powerCycleCount'] as num?)?.toInt(),
|
||||
rawData: json['rawData'] as Map<String, dynamic>,
|
||||
smartAttributes: (json['smartAttributes'] as Map<String, dynamic>).map(
|
||||
(k, e) => MapEntry(k, SmartAttribute.fromJson(e as Map<String, dynamic>)),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$DiskSmartImplToJson(_$DiskSmartImpl instance) =>
|
||||
Map<String, dynamic> _$DiskSmartToJson(_DiskSmart instance) =>
|
||||
<String, dynamic>{
|
||||
'device': instance.device,
|
||||
'healthy': instance.healthy,
|
||||
@@ -35,8 +33,8 @@ Map<String, dynamic> _$$DiskSmartImplToJson(_$DiskSmartImpl instance) =>
|
||||
'smartAttributes': instance.smartAttributes,
|
||||
};
|
||||
|
||||
_$SmartAttributeImpl _$$SmartAttributeImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SmartAttributeImpl(
|
||||
_SmartAttribute _$SmartAttributeFromJson(Map<String, dynamic> json) =>
|
||||
_SmartAttribute(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
name: json['name'] as String,
|
||||
value: (json['value'] as num?)?.toInt(),
|
||||
@@ -50,35 +48,33 @@ _$SmartAttributeImpl _$$SmartAttributeImplFromJson(Map<String, dynamic> json) =>
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SmartAttributeImplToJson(
|
||||
_$SmartAttributeImpl instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'value': instance.value,
|
||||
'worst': instance.worst,
|
||||
'thresh': instance.thresh,
|
||||
'whenFailed': instance.whenFailed,
|
||||
'rawValue': instance.rawValue,
|
||||
'rawString': instance.rawString,
|
||||
'flags': instance.flags,
|
||||
};
|
||||
Map<String, dynamic> _$SmartAttributeToJson(_SmartAttribute instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'value': instance.value,
|
||||
'worst': instance.worst,
|
||||
'thresh': instance.thresh,
|
||||
'whenFailed': instance.whenFailed,
|
||||
'rawValue': instance.rawValue,
|
||||
'rawString': instance.rawString,
|
||||
'flags': instance.flags,
|
||||
};
|
||||
|
||||
_$SmartAttributeFlagsImpl _$$SmartAttributeFlagsImplFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _$SmartAttributeFlagsImpl(
|
||||
value: (json['value'] as num?)?.toInt(),
|
||||
string: json['string'] as String?,
|
||||
prefailure: json['prefailure'] as bool? ?? false,
|
||||
updatedOnline: json['updatedOnline'] as bool? ?? false,
|
||||
performance: json['performance'] as bool? ?? false,
|
||||
errorRate: json['errorRate'] as bool? ?? false,
|
||||
eventCount: json['eventCount'] as bool? ?? false,
|
||||
autoKeep: json['autoKeep'] as bool? ?? false,
|
||||
);
|
||||
_SmartAttributeFlags _$SmartAttributeFlagsFromJson(Map<String, dynamic> json) =>
|
||||
_SmartAttributeFlags(
|
||||
value: (json['value'] as num?)?.toInt(),
|
||||
string: json['string'] as String?,
|
||||
prefailure: json['prefailure'] as bool? ?? false,
|
||||
updatedOnline: json['updatedOnline'] as bool? ?? false,
|
||||
performance: json['performance'] as bool? ?? false,
|
||||
errorRate: json['errorRate'] as bool? ?? false,
|
||||
eventCount: json['eventCount'] as bool? ?? false,
|
||||
autoKeep: json['autoKeep'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SmartAttributeFlagsImplToJson(
|
||||
_$SmartAttributeFlagsImpl instance,
|
||||
Map<String, dynamic> _$SmartAttributeFlagsToJson(
|
||||
_SmartAttributeFlags instance,
|
||||
) => <String, dynamic>{
|
||||
'value': instance.value,
|
||||
'string': instance.string,
|
||||
|
||||
@@ -12,7 +12,6 @@ enum Dist {
|
||||
rocky,
|
||||
deepin,
|
||||
coreelec,
|
||||
;
|
||||
}
|
||||
|
||||
extension StringX on String {
|
||||
@@ -34,6 +33,4 @@ extension StringX on String {
|
||||
|
||||
// Special rules
|
||||
|
||||
const _wrts = [
|
||||
'istoreos',
|
||||
];
|
||||
const _wrts = ['istoreos'];
|
||||
|
||||
@@ -5,11 +5,7 @@ class Memory {
|
||||
final int free;
|
||||
final int avail;
|
||||
|
||||
const Memory({
|
||||
required this.total,
|
||||
required this.free,
|
||||
required this.avail,
|
||||
});
|
||||
const Memory({required this.total, required this.free, required this.avail});
|
||||
|
||||
double get availPercent {
|
||||
if (avail == 0) {
|
||||
@@ -23,46 +19,99 @@ class Memory {
|
||||
static Memory parse(String raw) {
|
||||
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
||||
|
||||
final total = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
|
||||
?.group(2) ??
|
||||
'1') ??
|
||||
1;
|
||||
final free = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
|
||||
?.group(2) ??
|
||||
'0') ??
|
||||
0;
|
||||
final available = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
|
||||
?.group(2) ??
|
||||
'0') ??
|
||||
0;
|
||||
final total = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
|
||||
?.group(2) ?? '1') ?? 1;
|
||||
final free = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
|
||||
?.group(2) ?? '0') ?? 0;
|
||||
final available = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
|
||||
?.group(2) ?? '0') ?? 0;
|
||||
|
||||
return Memory(
|
||||
total: total,
|
||||
free: free,
|
||||
avail: available,
|
||||
);
|
||||
return Memory(total: total, free: free, avail: available);
|
||||
}
|
||||
}
|
||||
|
||||
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
|
||||
|
||||
/// Parse BSD/macOS memory from top output
|
||||
///
|
||||
/// Supports formats like:
|
||||
/// - macOS: "PhysMem: 32G used (1536M wired), 64G unused."
|
||||
/// - FreeBSD: "Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free"
|
||||
Memory parseBsdMemory(String raw) {
|
||||
// Try macOS format first: "PhysMem: 32G used (1536M wired), 64G unused."
|
||||
final macMemReg = RegExp(
|
||||
r'PhysMem:\s*([\d.]+)([KMGT])\s*used.*?,\s*([\d.]+)([KMGT])\s*unused');
|
||||
final macMatch = macMemReg.firstMatch(raw);
|
||||
|
||||
if (macMatch != null) {
|
||||
final usedAmount = double.parse(macMatch.group(1)!);
|
||||
final usedUnit = macMatch.group(2)!;
|
||||
final freeAmount = double.parse(macMatch.group(3)!);
|
||||
final freeUnit = macMatch.group(4)!;
|
||||
|
||||
final usedKB = _convertToKB(usedAmount, usedUnit);
|
||||
final freeKB = _convertToKB(freeAmount, freeUnit);
|
||||
return Memory(total: usedKB + freeKB, free: freeKB, avail: freeKB);
|
||||
}
|
||||
|
||||
// Try FreeBSD format: "Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free"
|
||||
final freeBsdReg = RegExp(
|
||||
r'(\d+)([KMGT])\s+(Active|Inact|Wired|Cache|Buf|Free)', caseSensitive: false);
|
||||
final matches = freeBsdReg.allMatches(raw);
|
||||
|
||||
if (matches.isNotEmpty) {
|
||||
double usedKB = 0;
|
||||
double freeKB = 0;
|
||||
for (final match in matches) {
|
||||
final amount = double.parse(match.group(1)!);
|
||||
final unit = match.group(2)!;
|
||||
final keyword = match.group(3)!.toLowerCase();
|
||||
final kb = _convertToKB(amount, unit);
|
||||
|
||||
// Only sum known keywords
|
||||
if (keyword == 'active' || keyword == 'inact' || keyword == 'wired' || keyword == 'cache' || keyword == 'buf') {
|
||||
usedKB += kb;
|
||||
} else if (keyword == 'free') {
|
||||
freeKB += kb;
|
||||
}
|
||||
}
|
||||
return Memory(total: (usedKB + freeKB).round(), free: freeKB.round(), avail: freeKB.round());
|
||||
}
|
||||
|
||||
// If neither format matches, throw an error to avoid misinterpretation
|
||||
throw FormatException('Unrecognized BSD/macOS memory format: $raw');
|
||||
}
|
||||
|
||||
/// Convert memory size to KB based on unit
|
||||
int _convertToKB(double amount, String unit) {
|
||||
switch (unit.toUpperCase()) {
|
||||
case 'T':
|
||||
return (amount * 1024 * 1024 * 1024).round();
|
||||
case 'G':
|
||||
return (amount * 1024 * 1024).round();
|
||||
case 'M':
|
||||
return (amount * 1024).round();
|
||||
case 'K':
|
||||
case '':
|
||||
return amount.round();
|
||||
default:
|
||||
return amount.round();
|
||||
}
|
||||
}
|
||||
|
||||
class Swap {
|
||||
final int total;
|
||||
final int free;
|
||||
final int cached;
|
||||
|
||||
const Swap({
|
||||
required this.total,
|
||||
required this.free,
|
||||
required this.cached,
|
||||
});
|
||||
const Swap({required this.total, required this.free, required this.cached});
|
||||
|
||||
double get usedPercent => 1 - free / total;
|
||||
double get usedPercent => total == 0 ? 0.0 : 1 - free / total;
|
||||
|
||||
double get freePercent => free / total;
|
||||
double get freePercent => total == 0 ? 0.0 : free / total;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -72,26 +121,16 @@ class Swap {
|
||||
static Swap parse(String raw) {
|
||||
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
||||
|
||||
final total = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
|
||||
?.group(2) ??
|
||||
'1') ??
|
||||
0;
|
||||
final free = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
|
||||
?.group(2) ??
|
||||
'1') ??
|
||||
0;
|
||||
final cached = int.tryParse(items
|
||||
.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
|
||||
?.group(2) ??
|
||||
'0') ??
|
||||
0;
|
||||
final total = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
|
||||
?.group(2) ?? '1') ?? 0;
|
||||
final free = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
|
||||
?.group(2) ?? '1') ?? 0;
|
||||
final cached = int.tryParse(
|
||||
items.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
|
||||
?.group(2) ?? '0') ?? 0;
|
||||
|
||||
return Swap(
|
||||
total: total,
|
||||
free: free,
|
||||
cached: cached,
|
||||
);
|
||||
return Swap(total: total, free: free, cached: cached);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,7 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
|
||||
bool same(NetSpeedPart other) => device == other.device;
|
||||
}
|
||||
|
||||
typedef CachedNetVals = ({
|
||||
String sizeIn,
|
||||
String sizeOut,
|
||||
String speedIn,
|
||||
String speedOut,
|
||||
});
|
||||
typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});
|
||||
|
||||
class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
||||
NetSpeed(super.init1, super.init2);
|
||||
@@ -32,20 +27,14 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
||||
devices.addAll(now.map((e) => e.device).toList());
|
||||
|
||||
realIfaces.clear();
|
||||
realIfaces.addAll(devices
|
||||
.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
|
||||
realIfaces.addAll(devices.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
|
||||
|
||||
final sizeIn = this.sizeIn();
|
||||
final sizeOut = this.sizeOut();
|
||||
final speedIn = this.speedIn();
|
||||
final speedOut = this.speedOut();
|
||||
|
||||
cachedVals = (
|
||||
sizeIn: sizeIn,
|
||||
sizeOut: sizeOut,
|
||||
speedIn: speedIn,
|
||||
speedOut: speedOut,
|
||||
);
|
||||
cachedVals = (sizeIn: sizeIn, sizeOut: sizeOut, speedIn: speedIn, speedOut: speedOut);
|
||||
}
|
||||
|
||||
/// Cached network device list
|
||||
@@ -58,15 +47,13 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
||||
/// Cached non-virtual network device prefix
|
||||
final realIfaces = <String>[];
|
||||
|
||||
CachedNetVals cachedVals =
|
||||
(sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
|
||||
CachedNetVals cachedVals = (sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
|
||||
|
||||
/// Time diff between [pre] and [now]
|
||||
BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);
|
||||
|
||||
double speedInBytes(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
|
||||
double speedOutBytes(int i) =>
|
||||
(now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
|
||||
double speedOutBytes(int i) => (now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
|
||||
BigInt sizeInBytes(int i) => now[i].bytesIn;
|
||||
BigInt sizeOutBytes(int i) => now[i].bytesOut;
|
||||
|
||||
|
||||
@@ -35,25 +35,17 @@ class NvidiaSmi {
|
||||
.firstOrNull
|
||||
?.innerText;
|
||||
final power = gpu.findElements('gpu_power_readings').firstOrNull;
|
||||
final powerDraw =
|
||||
power?.findElements('power_draw').firstOrNull?.innerText;
|
||||
final powerLimit =
|
||||
power?.findElements('current_power_limit').firstOrNull?.innerText;
|
||||
final powerDraw = power?.findElements('power_draw').firstOrNull?.innerText;
|
||||
final powerLimit = power?.findElements('current_power_limit').firstOrNull?.innerText;
|
||||
final memory = gpu.findElements('fb_memory_usage').firstOrNull;
|
||||
final memoryUsed = memory?.findElements('used').firstOrNull?.innerText;
|
||||
final memoryTotal = memory?.findElements('total').firstOrNull?.innerText;
|
||||
final processes = gpu
|
||||
.findElements('processes')
|
||||
.firstOrNull
|
||||
?.findElements('process_info');
|
||||
final memoryProcesses =
|
||||
List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) {
|
||||
final processes = gpu.findElements('processes').firstOrNull?.findElements('process_info');
|
||||
final memoryProcesses = List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) {
|
||||
final process = processes?.elementAt(index);
|
||||
final pid = process?.findElements('pid').firstOrNull?.innerText;
|
||||
final name =
|
||||
process?.findElements('process_name').firstOrNull?.innerText;
|
||||
final memory =
|
||||
process?.findElements('used_memory').firstOrNull?.innerText;
|
||||
final name = process?.findElements('process_name').firstOrNull?.innerText;
|
||||
final memory = process?.findElements('used_memory').firstOrNull?.innerText;
|
||||
if (pid != null && name != null && memory != null) {
|
||||
return NvidiaSmiMemProcess(
|
||||
int.tryParse(pid) ?? 0,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
final parseFailed = Exception('Parse failed');
|
||||
final seqReg = RegExp(r'seq=(.+) ttl=(.+) time=(.+) ms');
|
||||
final packetReg =
|
||||
RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
|
||||
final packetReg = RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
|
||||
final timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms');
|
||||
final timeAlpineReg = RegExp(r'round-trip min/avg/max = (.+)/(.+)/(.+) ms');
|
||||
final ipReg = RegExp(r' \((\S+)\)');
|
||||
@@ -15,17 +14,13 @@ class PingResult {
|
||||
PingResult.parse(this.serverName, String raw) {
|
||||
final lines = raw.split('\n');
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
final statisticIndex =
|
||||
lines.indexWhere((element) => element.startsWith('---'));
|
||||
final statisticIndex = lines.indexWhere((element) => element.startsWith('---'));
|
||||
if (statisticIndex == -1) {
|
||||
throw parseFailed;
|
||||
}
|
||||
final statisticRaw = lines.sublist(statisticIndex + 1);
|
||||
statistic = PingStatistics.parse(statisticRaw);
|
||||
results = lines
|
||||
.sublist(1, statisticIndex)
|
||||
.map((e) => PingSeqResult.parse(e))
|
||||
.toList();
|
||||
results = lines.sublist(1, statisticIndex).map((e) => PingSeqResult.parse(e)).toList();
|
||||
ip = ipReg.firstMatch(lines[0])?.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ class PrivateKeyInfo {
|
||||
@JsonKey(name: 'private_key')
|
||||
final String key;
|
||||
|
||||
const PrivateKeyInfo({
|
||||
required this.id,
|
||||
required this.key,
|
||||
});
|
||||
const PrivateKeyInfo({required this.id, required this.key});
|
||||
|
||||
factory PrivateKeyInfo.fromJson(Map<String, dynamic> json) => _$PrivateKeyInfoFromJson(json);
|
||||
|
||||
|
||||
@@ -107,10 +107,7 @@ class PsResult {
|
||||
final List<Proc> procs;
|
||||
final String? error;
|
||||
|
||||
const PsResult({
|
||||
required this.procs,
|
||||
this.error,
|
||||
});
|
||||
const PsResult({required this.procs, this.error});
|
||||
|
||||
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
|
||||
final lines = raw.split('\n').map((e) => e.trim()).toList();
|
||||
@@ -167,14 +164,7 @@ class PsResult {
|
||||
}
|
||||
}
|
||||
|
||||
enum ProcSortMode {
|
||||
cpu,
|
||||
mem,
|
||||
pid,
|
||||
user,
|
||||
name,
|
||||
;
|
||||
}
|
||||
enum ProcSortMode { cpu, mem, pid, user, name }
|
||||
|
||||
extension _StrIndex on List<String> {
|
||||
int? indexOfOrNull(String val) {
|
||||
|
||||
@@ -6,25 +6,24 @@ enum PveResType {
|
||||
qemu,
|
||||
node,
|
||||
storage,
|
||||
sdn,
|
||||
;
|
||||
sdn;
|
||||
|
||||
static PveResType? fromString(String type) => switch (type.toLowerCase()) {
|
||||
'lxc' => PveResType.lxc,
|
||||
'qemu' => PveResType.qemu,
|
||||
'node' => PveResType.node,
|
||||
'storage' => PveResType.storage,
|
||||
'sdn' => PveResType.sdn,
|
||||
_ => null,
|
||||
};
|
||||
'lxc' => PveResType.lxc,
|
||||
'qemu' => PveResType.qemu,
|
||||
'node' => PveResType.node,
|
||||
'storage' => PveResType.storage,
|
||||
'sdn' => PveResType.sdn,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
PveResType.node => l10n.node,
|
||||
PveResType.qemu => 'QEMU',
|
||||
PveResType.lxc => 'LXC',
|
||||
PveResType.storage => l10n.storage,
|
||||
PveResType.sdn => 'SDN',
|
||||
};
|
||||
PveResType.node => l10n.node,
|
||||
PveResType.qemu => 'QEMU',
|
||||
PveResType.lxc => 'LXC',
|
||||
PveResType.storage => l10n.storage,
|
||||
PveResType.sdn => 'SDN',
|
||||
};
|
||||
}
|
||||
|
||||
sealed class PveResIface {
|
||||
@@ -334,13 +333,7 @@ final class PveSdn extends PveResIface implements PveCtrlIface {
|
||||
@override
|
||||
final String status;
|
||||
|
||||
PveSdn({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.sdn,
|
||||
required this.node,
|
||||
required this.status,
|
||||
});
|
||||
PveSdn({required this.id, required this.type, required this.sdn, required this.node, required this.status});
|
||||
|
||||
static PveSdn fromJson(Map<String, dynamic> json) {
|
||||
return PveSdn(
|
||||
@@ -379,8 +372,7 @@ final class PveRes {
|
||||
|
||||
bool get onlyOneNode => nodes.length == 1;
|
||||
|
||||
int get length =>
|
||||
qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
|
||||
int get length => qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
|
||||
|
||||
PveResIface operator [](int index) {
|
||||
if (index < nodes.length) {
|
||||
@@ -432,29 +424,13 @@ final class PveRes {
|
||||
}
|
||||
|
||||
if (old != null) {
|
||||
qemus.reorder(
|
||||
order: old.qemus.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
lxcs.reorder(
|
||||
order: old.lxcs.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
nodes.reorder(
|
||||
order: old.nodes.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
storages.reorder(
|
||||
order: old.storages.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
sdns.reorder(
|
||||
order: old.sdns.map((e) => e.id).toList(),
|
||||
finder: (e, s) => e.id == s);
|
||||
qemus.reorder(order: old.qemus.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
lxcs.reorder(order: old.lxcs.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
nodes.reorder(order: old.nodes.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
storages.reorder(order: old.storages.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
sdns.reorder(order: old.sdns.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
|
||||
}
|
||||
|
||||
return PveRes(
|
||||
qemus: qemus,
|
||||
lxcs: lxcs,
|
||||
nodes: nodes,
|
||||
storages: storages,
|
||||
sdns: sdns,
|
||||
);
|
||||
return PveRes(qemus: qemus, lxcs: lxcs, nodes: nodes, storages: storages, sdns: sdns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ final class SensorAdaptor {
|
||||
static const isa = SensorAdaptor(isaRaw);
|
||||
|
||||
static SensorAdaptor parse(String raw) => switch (raw) {
|
||||
acpiRaw => acpi,
|
||||
pciRaw => pci,
|
||||
virtualRaw => virtual,
|
||||
isaRaw => isa,
|
||||
_ => SensorAdaptor(raw),
|
||||
};
|
||||
acpiRaw => acpi,
|
||||
pciRaw => pci,
|
||||
virtualRaw => virtual,
|
||||
isaRaw => isa,
|
||||
_ => SensorAdaptor(raw),
|
||||
};
|
||||
}
|
||||
|
||||
final class SensorItem {
|
||||
@@ -28,11 +28,7 @@ final class SensorItem {
|
||||
final SensorAdaptor adapter;
|
||||
final Map<String, String> details;
|
||||
|
||||
const SensorItem({
|
||||
required this.device,
|
||||
required this.adapter,
|
||||
required this.details,
|
||||
});
|
||||
const SensorItem({required this.device, required this.adapter, required this.details});
|
||||
|
||||
String get toMarkdown {
|
||||
final sb = StringBuffer();
|
||||
@@ -72,8 +68,7 @@ final class SensorItem {
|
||||
final len = sensorLines.length;
|
||||
if (len < 3) continue;
|
||||
final device = sensorLines.first;
|
||||
final adapter =
|
||||
SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
|
||||
final adapter = SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
|
||||
|
||||
final details = <String, String>{};
|
||||
for (var idx = 2; idx < len; idx++) {
|
||||
@@ -84,11 +79,7 @@ final class SensorItem {
|
||||
final value = detailParts[1].trim();
|
||||
details[key] = value;
|
||||
}
|
||||
sensors.add(SensorItem(
|
||||
device: device,
|
||||
adapter: adapter,
|
||||
details: details,
|
||||
));
|
||||
sensors.add(SensorItem(device: device, adapter: adapter, details: details));
|
||||
}
|
||||
|
||||
return sensors;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dartssh2/dartssh2.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/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/server/amd.dart';
|
||||
import 'package:server_box/data/model/server/battery.dart';
|
||||
import 'package:server_box/data/model/server/conn.dart';
|
||||
import 'package:server_box/data/model/server/cpu.dart';
|
||||
@@ -42,6 +43,7 @@ class ServerStatus {
|
||||
DiskIO diskIO;
|
||||
List<DiskSmart> diskSmart;
|
||||
List<NvidiaSmiItem>? nvidia;
|
||||
List<AmdSmiItem>? amd;
|
||||
final List<Battery> batteries = [];
|
||||
final Map<StatusCmdType, String> more = {};
|
||||
final List<SensorItem> sensors = [];
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
@@ -19,11 +19,11 @@ part 'server_private_info.g.dart';
|
||||
/// Some params named as `spi` in the codebase which is the abbreviation of `ServerPrivateInfo`.
|
||||
///
|
||||
/// Nowaday, more fields are added to this class, and it's renamed to `Spi`.
|
||||
@Freezed(fromJson: false)
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class Spi with _$Spi {
|
||||
@freezed
|
||||
abstract class Spi with _$Spi {
|
||||
const Spi._();
|
||||
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
const factory Spi({
|
||||
required String name,
|
||||
required String ip,
|
||||
@@ -35,7 +35,7 @@ class Spi with _$Spi {
|
||||
@JsonKey(name: 'pubKeyId') String? keyId,
|
||||
List<String>? tags,
|
||||
String? alterUrl,
|
||||
@Default(true) @JsonKey(defaultValue: true) bool autoConnect,
|
||||
@Default(true) bool autoConnect,
|
||||
|
||||
/// [id] of the jump server
|
||||
String? jumpId,
|
||||
@@ -44,7 +44,13 @@ class Spi with _$Spi {
|
||||
|
||||
/// It only applies to SSH terminal.
|
||||
Map<String, String>? envs,
|
||||
@JsonKey(fromJson: Spi.parseId) @HiveField(13, defaultValue: '') required 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,
|
||||
|
||||
/// Disabled command types for this server
|
||||
@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes,
|
||||
}) = _Spi;
|
||||
|
||||
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||
@@ -120,26 +126,25 @@ extension Spix on Spi {
|
||||
///
|
||||
/// **NOT** the default value.
|
||||
static final example = Spi(
|
||||
name: 'name',
|
||||
ip: 'ip',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
pwd: 'pwd',
|
||||
keyId: 'private_key_id',
|
||||
tags: ['tag1', 'tag2'],
|
||||
alterUrl: 'user@ip:port',
|
||||
autoConnect: true,
|
||||
jumpId: 'jump_server_id',
|
||||
custom: ServerCustom(
|
||||
pveAddr: 'http://localhost:8006',
|
||||
pveIgnoreCert: false,
|
||||
cmds: {
|
||||
'echo': 'echo hello',
|
||||
},
|
||||
preferTempDev: 'nvme-pci-0400',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
),
|
||||
id: 'id');
|
||||
name: 'name',
|
||||
ip: 'ip',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
pwd: 'pwd',
|
||||
keyId: 'private_key_id',
|
||||
tags: ['tag1', 'tag2'],
|
||||
alterUrl: 'user@ip:port',
|
||||
autoConnect: true,
|
||||
jumpId: 'jump_server_id',
|
||||
custom: ServerCustom(
|
||||
pveAddr: 'http://localhost:8006',
|
||||
pveIgnoreCert: false,
|
||||
cmds: {'echo': 'echo hello'},
|
||||
preferTempDev: 'nvme-pci-0400',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
),
|
||||
id: 'id',
|
||||
);
|
||||
|
||||
bool get isRoot => user == 'root';
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
@@ -9,479 +10,212 @@ part of 'server_private_info.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Spi {
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get ip => throw _privateConstructorUsedError;
|
||||
int get port => throw _privateConstructorUsedError;
|
||||
String get user => throw _privateConstructorUsedError;
|
||||
String? get pwd => throw _privateConstructorUsedError;
|
||||
|
||||
/// [id] of private key
|
||||
@JsonKey(name: 'pubKeyId')
|
||||
String? get keyId => throw _privateConstructorUsedError;
|
||||
List<String>? get tags => throw _privateConstructorUsedError;
|
||||
String? get alterUrl => throw _privateConstructorUsedError;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool get autoConnect => throw _privateConstructorUsedError;
|
||||
|
||||
/// [id] of the jump server
|
||||
String? get jumpId => throw _privateConstructorUsedError;
|
||||
ServerCustom? get custom => throw _privateConstructorUsedError;
|
||||
WakeOnLanCfg? get wolCfg => throw _privateConstructorUsedError;
|
||||
|
||||
/// It only applies to SSH terminal.
|
||||
Map<String, String>? get envs => throw _privateConstructorUsedError;
|
||||
@JsonKey(fromJson: Spi.parseId)
|
||||
@HiveField(13, defaultValue: '')
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
|
||||
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
|
||||
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
||||
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
|
||||
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity);
|
||||
|
||||
/// Serializes this Spi to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
|
||||
|
||||
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SpiCopyWith<Spi> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SpiCopyWith<$Res> {
|
||||
factory $SpiCopyWith(Spi value, $Res Function(Spi) then) =
|
||||
_$SpiCopyWithImpl<$Res, Spi>;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name,
|
||||
String ip,
|
||||
int port,
|
||||
String user,
|
||||
String? pwd,
|
||||
@JsonKey(name: 'pubKeyId') String? keyId,
|
||||
List<String>? tags,
|
||||
String? alterUrl,
|
||||
@JsonKey(defaultValue: true) bool autoConnect,
|
||||
String? jumpId,
|
||||
ServerCustom? custom,
|
||||
WakeOnLanCfg? wolCfg,
|
||||
Map<String, String>? envs,
|
||||
@JsonKey(fromJson: Spi.parseId) @HiveField(13, defaultValue: '') String id,
|
||||
});
|
||||
abstract mixin class $SpiCopyWith<$Res> {
|
||||
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SpiCopyWithImpl<$Res>
|
||||
implements $SpiCopyWith<$Res> {
|
||||
_$SpiCopyWithImpl(this._self, this._then);
|
||||
|
||||
final Spi _self;
|
||||
final $Res Function(Spi) _then;
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
|
||||
as int,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
|
||||
as String,pwd: freezed == pwd ? _self.pwd : pwd // ignore: cast_nullable_to_non_nullable
|
||||
as String?,keyId: freezed == keyId ? _self.keyId : keyId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
||||
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,custom: freezed == custom ? _self.custom : custom // 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 Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
|
||||
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self.disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class _Spi extends Spi {
|
||||
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
||||
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||
|
||||
@override final String name;
|
||||
@override final String ip;
|
||||
@override final int port;
|
||||
@override final String user;
|
||||
@override final String? pwd;
|
||||
/// [id] of private key
|
||||
@override@JsonKey(name: 'pubKeyId') final String? keyId;
|
||||
final List<String>? _tags;
|
||||
@override List<String>? get tags {
|
||||
final value = _tags;
|
||||
if (value == null) return null;
|
||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override final String? alterUrl;
|
||||
@override@JsonKey() final bool autoConnect;
|
||||
/// [id] of the jump server
|
||||
@override final String? jumpId;
|
||||
@override final ServerCustom? custom;
|
||||
@override final WakeOnLanCfg? wolCfg;
|
||||
/// It only applies to SSH terminal.
|
||||
final Map<String, String>? _envs;
|
||||
/// It only applies to SSH terminal.
|
||||
@override Map<String, String>? get envs {
|
||||
final value = _envs;
|
||||
if (value == null) return null;
|
||||
if (_envs is EqualUnmodifiableMapView) return _envs;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@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;
|
||||
/// Disabled command types for this server
|
||||
final List<String>? _disabledCmdTypes;
|
||||
/// Disabled command types for this server
|
||||
@override@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes {
|
||||
final value = _disabledCmdTypes;
|
||||
if (value == null) return null;
|
||||
if (_disabledCmdTypes is EqualUnmodifiableListView) return _disabledCmdTypes;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SpiCopyWith<_Spi> get copyWith => __$SpiCopyWithImpl<_Spi>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SpiToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SpiCopyWithImpl<$Res, $Val extends Spi> implements $SpiCopyWith<$Res> {
|
||||
_$SpiCopyWithImpl(this._value, this._then);
|
||||
abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
|
||||
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? name = null,
|
||||
Object? ip = null,
|
||||
Object? port = null,
|
||||
Object? user = null,
|
||||
Object? pwd = freezed,
|
||||
Object? keyId = freezed,
|
||||
Object? tags = freezed,
|
||||
Object? alterUrl = freezed,
|
||||
Object? autoConnect = null,
|
||||
Object? jumpId = freezed,
|
||||
Object? custom = freezed,
|
||||
Object? wolCfg = freezed,
|
||||
Object? envs = freezed,
|
||||
Object? id = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
ip: null == ip
|
||||
? _value.ip
|
||||
: ip // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
port: null == port
|
||||
? _value.port
|
||||
: port // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
user: null == user
|
||||
? _value.user
|
||||
: user // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
pwd: freezed == pwd
|
||||
? _value.pwd
|
||||
: pwd // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
keyId: freezed == keyId
|
||||
? _value.keyId
|
||||
: keyId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
tags: freezed == tags
|
||||
? _value.tags
|
||||
: tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
alterUrl: freezed == alterUrl
|
||||
? _value.alterUrl
|
||||
: alterUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
autoConnect: null == autoConnect
|
||||
? _value.autoConnect
|
||||
: autoConnect // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
jumpId: freezed == jumpId
|
||||
? _value.jumpId
|
||||
: jumpId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
custom: freezed == custom
|
||||
? _value.custom
|
||||
: custom // ignore: cast_nullable_to_non_nullable
|
||||
as ServerCustom?,
|
||||
wolCfg: freezed == wolCfg
|
||||
? _value.wolCfg
|
||||
: wolCfg // ignore: cast_nullable_to_non_nullable
|
||||
as WakeOnLanCfg?,
|
||||
envs: freezed == envs
|
||||
? _value.envs
|
||||
: envs // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>?,
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SpiImplCopyWith<$Res> implements $SpiCopyWith<$Res> {
|
||||
factory _$$SpiImplCopyWith(_$SpiImpl value, $Res Function(_$SpiImpl) then) =
|
||||
__$$SpiImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
String name,
|
||||
String ip,
|
||||
int port,
|
||||
String user,
|
||||
String? pwd,
|
||||
@JsonKey(name: 'pubKeyId') String? keyId,
|
||||
List<String>? tags,
|
||||
String? alterUrl,
|
||||
@JsonKey(defaultValue: true) bool autoConnect,
|
||||
String? jumpId,
|
||||
ServerCustom? custom,
|
||||
WakeOnLanCfg? wolCfg,
|
||||
Map<String, String>? envs,
|
||||
@JsonKey(fromJson: Spi.parseId) @HiveField(13, defaultValue: '') String id,
|
||||
});
|
||||
class __$SpiCopyWithImpl<$Res>
|
||||
implements _$SpiCopyWith<$Res> {
|
||||
__$SpiCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _Spi _self;
|
||||
final $Res Function(_Spi) _then;
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||
return _then(_Spi(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
|
||||
as int,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
|
||||
as String,pwd: freezed == pwd ? _self.pwd : pwd // ignore: cast_nullable_to_non_nullable
|
||||
as String?,keyId: freezed == keyId ? _self.keyId : keyId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
||||
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,custom: freezed == custom ? _self.custom : custom // 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 Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
|
||||
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self._disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SpiImplCopyWithImpl<$Res> extends _$SpiCopyWithImpl<$Res, _$SpiImpl>
|
||||
implements _$$SpiImplCopyWith<$Res> {
|
||||
__$$SpiImplCopyWithImpl(_$SpiImpl _value, $Res Function(_$SpiImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? name = null,
|
||||
Object? ip = null,
|
||||
Object? port = null,
|
||||
Object? user = null,
|
||||
Object? pwd = freezed,
|
||||
Object? keyId = freezed,
|
||||
Object? tags = freezed,
|
||||
Object? alterUrl = freezed,
|
||||
Object? autoConnect = null,
|
||||
Object? jumpId = freezed,
|
||||
Object? custom = freezed,
|
||||
Object? wolCfg = freezed,
|
||||
Object? envs = freezed,
|
||||
Object? id = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$SpiImpl(
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
ip: null == ip
|
||||
? _value.ip
|
||||
: ip // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
port: null == port
|
||||
? _value.port
|
||||
: port // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
user: null == user
|
||||
? _value.user
|
||||
: user // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
pwd: freezed == pwd
|
||||
? _value.pwd
|
||||
: pwd // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
keyId: freezed == keyId
|
||||
? _value.keyId
|
||||
: keyId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
tags: freezed == tags
|
||||
? _value._tags
|
||||
: tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
alterUrl: freezed == alterUrl
|
||||
? _value.alterUrl
|
||||
: alterUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
autoConnect: null == autoConnect
|
||||
? _value.autoConnect
|
||||
: autoConnect // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
jumpId: freezed == jumpId
|
||||
? _value.jumpId
|
||||
: jumpId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
custom: freezed == custom
|
||||
? _value.custom
|
||||
: custom // ignore: cast_nullable_to_non_nullable
|
||||
as ServerCustom?,
|
||||
wolCfg: freezed == wolCfg
|
||||
? _value.wolCfg
|
||||
: wolCfg // ignore: cast_nullable_to_non_nullable
|
||||
as WakeOnLanCfg?,
|
||||
envs: freezed == envs
|
||||
? _value._envs
|
||||
: envs // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>?,
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable(createFactory: false)
|
||||
class _$SpiImpl extends _Spi {
|
||||
const _$SpiImpl({
|
||||
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,
|
||||
@JsonKey(defaultValue: true) this.autoConnect = true,
|
||||
this.jumpId,
|
||||
this.custom,
|
||||
this.wolCfg,
|
||||
final Map<String, String>? envs,
|
||||
@JsonKey(fromJson: Spi.parseId)
|
||||
@HiveField(13, defaultValue: '')
|
||||
required this.id,
|
||||
}) : _tags = tags,
|
||||
_envs = envs,
|
||||
super._();
|
||||
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String ip;
|
||||
@override
|
||||
final int port;
|
||||
@override
|
||||
final String user;
|
||||
@override
|
||||
final String? pwd;
|
||||
|
||||
/// [id] of private key
|
||||
@override
|
||||
@JsonKey(name: 'pubKeyId')
|
||||
final String? keyId;
|
||||
final List<String>? _tags;
|
||||
@override
|
||||
List<String>? get tags {
|
||||
final value = _tags;
|
||||
if (value == null) return null;
|
||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final String? alterUrl;
|
||||
@override
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool autoConnect;
|
||||
|
||||
/// [id] of the jump server
|
||||
@override
|
||||
final String? jumpId;
|
||||
@override
|
||||
final ServerCustom? custom;
|
||||
@override
|
||||
final WakeOnLanCfg? wolCfg;
|
||||
|
||||
/// It only applies to SSH terminal.
|
||||
final Map<String, String>? _envs;
|
||||
|
||||
/// It only applies to SSH terminal.
|
||||
@override
|
||||
Map<String, String>? get envs {
|
||||
final value = _envs;
|
||||
if (value == null) return null;
|
||||
if (_envs is EqualUnmodifiableMapView) return _envs;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey(fromJson: Spi.parseId)
|
||||
@HiveField(13, defaultValue: '')
|
||||
final String id;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SpiImpl &&
|
||||
(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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
name,
|
||||
ip,
|
||||
port,
|
||||
user,
|
||||
pwd,
|
||||
keyId,
|
||||
const DeepCollectionEquality().hash(_tags),
|
||||
alterUrl,
|
||||
autoConnect,
|
||||
jumpId,
|
||||
custom,
|
||||
wolCfg,
|
||||
const DeepCollectionEquality().hash(_envs),
|
||||
id,
|
||||
);
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SpiImplCopyWith<_$SpiImpl> get copyWith =>
|
||||
__$$SpiImplCopyWithImpl<_$SpiImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SpiImplToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Spi extends Spi {
|
||||
const factory _Spi({
|
||||
required final String name,
|
||||
required final String ip,
|
||||
required final int port,
|
||||
required final String user,
|
||||
final String? pwd,
|
||||
@JsonKey(name: 'pubKeyId') final String? keyId,
|
||||
final List<String>? tags,
|
||||
final String? alterUrl,
|
||||
@JsonKey(defaultValue: true) final bool autoConnect,
|
||||
final String? jumpId,
|
||||
final ServerCustom? custom,
|
||||
final WakeOnLanCfg? wolCfg,
|
||||
final Map<String, String>? envs,
|
||||
@JsonKey(fromJson: Spi.parseId)
|
||||
@HiveField(13, defaultValue: '')
|
||||
required final String id,
|
||||
}) = _$SpiImpl;
|
||||
const _Spi._() : super._();
|
||||
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get ip;
|
||||
@override
|
||||
int get port;
|
||||
@override
|
||||
String get user;
|
||||
@override
|
||||
String? get pwd;
|
||||
|
||||
/// [id] of private key
|
||||
@override
|
||||
@JsonKey(name: 'pubKeyId')
|
||||
String? get keyId;
|
||||
@override
|
||||
List<String>? get tags;
|
||||
@override
|
||||
String? get alterUrl;
|
||||
@override
|
||||
@JsonKey(defaultValue: true)
|
||||
bool get autoConnect;
|
||||
|
||||
/// [id] of the jump server
|
||||
@override
|
||||
String? get jumpId;
|
||||
@override
|
||||
ServerCustom? get custom;
|
||||
@override
|
||||
WakeOnLanCfg? get wolCfg;
|
||||
|
||||
/// It only applies to SSH terminal.
|
||||
@override
|
||||
Map<String, String>? get envs;
|
||||
@override
|
||||
@JsonKey(fromJson: Spi.parseId)
|
||||
@HiveField(13, defaultValue: '')
|
||||
String get id;
|
||||
|
||||
/// Create a copy of Spi
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SpiImplCopyWith<_$SpiImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
// dart format on
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'server_private_info.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Spi _$SpiFromJson(Map<String, dynamic> json) => Spi(
|
||||
_Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
|
||||
name: json['name'] as String,
|
||||
ip: json['ip'] as String,
|
||||
port: (json['port'] as num).toInt(),
|
||||
@@ -26,10 +26,17 @@ Spi _$SpiFromJson(Map<String, dynamic> json) => Spi(
|
||||
envs: (json['envs'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
id: Spi.parseId(json['id']),
|
||||
id: json['id'] == null ? '' : Spi.parseId(json['id']),
|
||||
customSystemType: $enumDecodeNullable(
|
||||
_$SystemTypeEnumMap,
|
||||
json['customSystemType'],
|
||||
),
|
||||
disabledCmdTypes: (json['disabledCmdTypes'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SpiToJson(Spi instance) => <String, dynamic>{
|
||||
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'ip': instance.ip,
|
||||
'port': instance.port,
|
||||
@@ -44,21 +51,13 @@ Map<String, dynamic> _$SpiToJson(Spi instance) => <String, dynamic>{
|
||||
if (instance.wolCfg case final value?) 'wolCfg': value,
|
||||
if (instance.envs case final value?) 'envs': value,
|
||||
'id': instance.id,
|
||||
if (_$SystemTypeEnumMap[instance.customSystemType] case final value?)
|
||||
'customSystemType': value,
|
||||
if (instance.disabledCmdTypes case final value?) 'disabledCmdTypes': value,
|
||||
};
|
||||
|
||||
Map<String, dynamic> _$$SpiImplToJson(_$SpiImpl instance) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'ip': instance.ip,
|
||||
'port': instance.port,
|
||||
'user': instance.user,
|
||||
'pwd': instance.pwd,
|
||||
'pubKeyId': instance.keyId,
|
||||
'tags': instance.tags,
|
||||
'alterUrl': instance.alterUrl,
|
||||
'autoConnect': instance.autoConnect,
|
||||
'jumpId': instance.jumpId,
|
||||
'custom': instance.custom,
|
||||
'wolCfg': instance.wolCfg,
|
||||
'envs': instance.envs,
|
||||
'id': instance.id,
|
||||
const _$SystemTypeEnumMap = {
|
||||
SystemType.linux: 'linux',
|
||||
SystemType.bsd: 'bsd',
|
||||
SystemType.windows: 'windows',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/server/amd.dart';
|
||||
import 'package:server_box/data/model/server/battery.dart';
|
||||
import 'package:server_box/data/model/server/conn.dart';
|
||||
import 'package:server_box/data/model/server/cpu.dart';
|
||||
@@ -11,17 +15,19 @@ import 'package:server_box/data/model/server/nvdia.dart';
|
||||
import 'package:server_box/data/model/server/sensors.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/temp.dart';
|
||||
import 'package:server_box/data/model/server/windows_parser.dart';
|
||||
|
||||
class ServerStatusUpdateReq {
|
||||
final ServerStatus ss;
|
||||
final List<String> segments;
|
||||
final Map<String, String> parsedOutput;
|
||||
final SystemType system;
|
||||
final Map<String, String> customCmds;
|
||||
|
||||
const ServerStatusUpdateReq({
|
||||
required this.system,
|
||||
required this.ss,
|
||||
required this.segments,
|
||||
required this.parsedOutput,
|
||||
required this.customCmds,
|
||||
});
|
||||
}
|
||||
@@ -30,27 +36,27 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
|
||||
return switch (req.system) {
|
||||
SystemType.linux => _getLinuxStatus(req),
|
||||
SystemType.bsd => _getBsdStatus(req),
|
||||
SystemType.windows => _getWindowsStatus(req),
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap each operation with a try-catch, so that if one operation fails,
|
||||
// the following operations can still be executed.
|
||||
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
final segments = req.segments;
|
||||
final parsedOutput = req.parsedOutput;
|
||||
|
||||
final time =
|
||||
int.tryParse(StatusCmdType.time.find(segments)) ??
|
||||
final time = int.tryParse(StatusCmdType.time.findInMap(parsedOutput)) ??
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
try {
|
||||
final net = NetSpeed.parse(StatusCmdType.net.find(segments), time);
|
||||
final net = NetSpeed.parse(StatusCmdType.net.findInMap(parsedOutput), time);
|
||||
req.ss.netSpeed.update(net);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final sys = _parseSysVer(StatusCmdType.sys.find(segments));
|
||||
final sys = _parseSysVer(StatusCmdType.sys.findInMap(parsedOutput));
|
||||
if (sys != null) {
|
||||
req.ss.more[StatusCmdType.sys] = sys;
|
||||
}
|
||||
@@ -59,7 +65,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
}
|
||||
|
||||
try {
|
||||
final host = _parseHostName(StatusCmdType.host.find(segments));
|
||||
final host = _parseHostName(StatusCmdType.host.findInMap(parsedOutput));
|
||||
if (host != null) {
|
||||
req.ss.more[StatusCmdType.host] = host;
|
||||
}
|
||||
@@ -68,9 +74,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
}
|
||||
|
||||
try {
|
||||
final cpus = SingleCpuCore.parse(StatusCmdType.cpu.find(segments));
|
||||
final cpus = SingleCpuCore.parse(StatusCmdType.cpu.findInMap(parsedOutput));
|
||||
req.ss.cpu.update(cpus);
|
||||
final brand = CpuBrand.parse(StatusCmdType.cpuBrand.find(segments));
|
||||
final brand = CpuBrand.parse(StatusCmdType.cpuBrand.findInMap(parsedOutput));
|
||||
req.ss.cpu.brand.clear();
|
||||
req.ss.cpu.brand.addAll(brand);
|
||||
} catch (e, s) {
|
||||
@@ -79,15 +85,15 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
|
||||
try {
|
||||
req.ss.temps.parse(
|
||||
StatusCmdType.tempType.find(segments),
|
||||
StatusCmdType.tempVal.find(segments),
|
||||
StatusCmdType.tempType.findInMap(parsedOutput),
|
||||
StatusCmdType.tempVal.findInMap(parsedOutput),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final tcp = Conn.parse(StatusCmdType.conn.find(segments));
|
||||
final tcp = Conn.parse(StatusCmdType.conn.findInMap(parsedOutput));
|
||||
if (tcp != null) {
|
||||
req.ss.tcp = tcp;
|
||||
}
|
||||
@@ -96,20 +102,20 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.disk = Disk.parse(StatusCmdType.disk.find(segments));
|
||||
req.ss.disk = Disk.parse(StatusCmdType.disk.findInMap(parsedOutput));
|
||||
req.ss.diskUsage = DiskUsage.parse(req.ss.disk);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.mem = Memory.parse(StatusCmdType.mem.find(segments));
|
||||
req.ss.mem = Memory.parse(StatusCmdType.mem.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final uptime = _parseUpTime(StatusCmdType.uptime.find(segments));
|
||||
final uptime = _parseUpTime(StatusCmdType.uptime.findInMap(parsedOutput));
|
||||
if (uptime != null) {
|
||||
req.ss.more[StatusCmdType.uptime] = uptime;
|
||||
}
|
||||
@@ -118,33 +124,39 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.swap = Swap.parse(StatusCmdType.mem.find(segments));
|
||||
req.ss.swap = Swap.parse(StatusCmdType.mem.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final diskio = DiskIO.parse(StatusCmdType.diskio.find(segments), time);
|
||||
final diskio = DiskIO.parse(StatusCmdType.diskio.findInMap(parsedOutput), time);
|
||||
req.ss.diskIO.update(diskio);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.find(segments));
|
||||
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.findInMap(parsedOutput));
|
||||
req.ss.diskSmart = smarts;
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments));
|
||||
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final battery = StatusCmdType.battery.find(segments);
|
||||
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final battery = StatusCmdType.battery.findInMap(parsedOutput);
|
||||
|
||||
/// Only collect li-poly batteries
|
||||
final batteries = Batteries.parse(battery, true);
|
||||
@@ -157,7 +169,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
}
|
||||
|
||||
try {
|
||||
final sensors = SensorItem.parse(StatusCmdType.sensors.find(segments));
|
||||
final sensors = SensorItem.parse(StatusCmdType.sensors.findInMap(parsedOutput));
|
||||
if (sensors.isNotEmpty) {
|
||||
req.ss.sensors.clear();
|
||||
req.ss.sensors.addAll(sensors);
|
||||
@@ -167,9 +179,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
}
|
||||
|
||||
try {
|
||||
for (int idx = 0; idx < req.customCmds.length; idx++) {
|
||||
final key = req.customCmds.keys.elementAt(idx);
|
||||
final value = req.segments[idx + req.system.segmentsLen];
|
||||
for (final entry in req.customCmds.entries) {
|
||||
final key = entry.key;
|
||||
final value = req.parsedOutput[key] ?? '';
|
||||
req.ss.customCmds[key] = value;
|
||||
}
|
||||
} catch (e, s) {
|
||||
@@ -181,36 +193,36 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
|
||||
// Same as above, wrap with try-catch
|
||||
Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
|
||||
final segments = req.segments;
|
||||
final parsedOutput = req.parsedOutput;
|
||||
|
||||
try {
|
||||
final time = int.parse(BSDStatusCmdType.time.find(segments));
|
||||
final net = NetSpeed.parseBsd(BSDStatusCmdType.net.find(segments), time);
|
||||
final time = int.parse(BSDStatusCmdType.time.findInMap(parsedOutput));
|
||||
final net = NetSpeed.parseBsd(BSDStatusCmdType.net.findInMap(parsedOutput), time);
|
||||
req.ss.netSpeed.update(net);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.find(segments);
|
||||
req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.findInMap(parsedOutput);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.find(segments));
|
||||
req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
// try {
|
||||
// req.ss.mem = parseBsdMem(BSDStatusCmdType.mem.find(segments));
|
||||
// } catch (e, s) {
|
||||
// Loggers.app.warning(e, s);
|
||||
// }
|
||||
try {
|
||||
req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final uptime = _parseUpTime(BSDStatusCmdType.uptime.find(segments));
|
||||
final uptime = _parseUpTime(BSDStatusCmdType.uptime.findInMap(parsedOutput));
|
||||
if (uptime != null) {
|
||||
req.ss.more[StatusCmdType.uptime] = uptime;
|
||||
}
|
||||
@@ -219,7 +231,7 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.disk = Disk.parse(BSDStatusCmdType.disk.find(segments));
|
||||
req.ss.disk = Disk.parse(BSDStatusCmdType.disk.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
@@ -228,13 +240,48 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
|
||||
|
||||
// raw:
|
||||
// 19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00
|
||||
// 19:39:15 up 1 day, 2:34, 1 user, load average: 0.00, 0.00, 0.00
|
||||
// 19:39:15 up 2:34, 1 user, load average: 0.00, 0.00, 0.00
|
||||
// 19:39:15 up 34 min, 1 user, load average: 0.00, 0.00, 0.00
|
||||
String? _parseUpTime(String raw) {
|
||||
final splitedUp = raw.split('up ');
|
||||
if (splitedUp.length == 2) {
|
||||
final splitedComma = splitedUp[1].split(', ');
|
||||
if (splitedComma.length >= 2) {
|
||||
return splitedComma[0];
|
||||
final uptimePart = splitedUp[1];
|
||||
final splitedComma = uptimePart.split(', ');
|
||||
|
||||
if (splitedComma.isEmpty) return null;
|
||||
|
||||
// Handle different uptime formats
|
||||
final firstPart = splitedComma[0].trim();
|
||||
|
||||
// Case 1: "61 days" or "1 day" - need to get the time part from next segment
|
||||
if (firstPart.contains('day')) {
|
||||
if (splitedComma.length >= 2) {
|
||||
final timePart = splitedComma[1].trim();
|
||||
// Check if it's in HH:MM format
|
||||
if (timePart.contains(':') &&
|
||||
!timePart.contains('user') &&
|
||||
!timePart.contains('load')) {
|
||||
return '$firstPart, $timePart';
|
||||
}
|
||||
}
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Case 2: "2:34" (hours:minutes) - already in good format
|
||||
if (firstPart.contains(':') &&
|
||||
!firstPart.contains('user') &&
|
||||
!firstPart.contains('load')) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Case 3: "34 min" - already in good format
|
||||
if (firstPart.contains('min')) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Fallback: return first part
|
||||
return firstPart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -249,6 +296,409 @@ String? _parseSysVer(String raw) {
|
||||
|
||||
String? _parseHostName(String raw) {
|
||||
if (raw.isEmpty) return null;
|
||||
if (raw.contains(ShellFunc.scriptFile)) return null;
|
||||
if (raw.contains(ScriptConstants.scriptFile)) return null;
|
||||
return raw;
|
||||
}
|
||||
|
||||
// Windows status parsing implementation
|
||||
Future<ServerStatus> _getWindowsStatus(ServerStatusUpdateReq req) async {
|
||||
final parsedOutput = req.parsedOutput;
|
||||
final time = int.tryParse(WindowsStatusCmdType.time.findInMap(parsedOutput)) ??
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
// Parse all different resource types using helper methods
|
||||
_parseWindowsNetworkData(req, parsedOutput, time);
|
||||
_parseWindowsSystemData(req, parsedOutput);
|
||||
_parseWindowsHostData(req, parsedOutput);
|
||||
_parseWindowsCpuData(req, parsedOutput);
|
||||
_parseWindowsMemoryData(req, parsedOutput);
|
||||
_parseWindowsDiskData(req, parsedOutput);
|
||||
_parseWindowsUptimeData(req, parsedOutput);
|
||||
_parseWindowsDiskIOData(req, parsedOutput, time);
|
||||
_parseWindowsConnectionData(req, parsedOutput);
|
||||
_parseWindowsBatteryData(req, parsedOutput);
|
||||
_parseWindowsTemperatureData(req, parsedOutput);
|
||||
_parseWindowsGpuData(req, parsedOutput);
|
||||
WindowsParser.parseCustomCommands(req.ss, req.parsedOutput, req.customCmds);
|
||||
|
||||
return req.ss;
|
||||
}
|
||||
|
||||
/// Parse Windows network data
|
||||
void _parseWindowsNetworkData(ServerStatusUpdateReq req, Map<String, String> parsedOutput, int time) {
|
||||
try {
|
||||
final netRaw = WindowsStatusCmdType.net.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final sys = WindowsStatusCmdType.sys.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final host = _parseHostName(WindowsStatusCmdType.host.findInMap(parsedOutput));
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
// Windows CPU parsing - JSON format from PowerShell
|
||||
final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
|
||||
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.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final memRaw = WindowsStatusCmdType.mem.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final diskRaw = WindowsStatusCmdType.disk.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.findInMap(parsedOutput));
|
||||
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, Map<String, String> parsedOutput, int time) {
|
||||
try {
|
||||
final diskIOraw = WindowsStatusCmdType.diskio.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final connStr = WindowsStatusCmdType.conn.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final batteryRaw = WindowsStatusCmdType.battery.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final tempRaw = WindowsStatusCmdType.temp.findInMap(parsedOutput);
|
||||
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, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows NVIDIA GPU parsing failed: $e', s);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.findInMap(parsedOutput));
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ part 'snippet.g.dart';
|
||||
part 'snippet.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class Snippet with _$Snippet {
|
||||
abstract class Snippet with _$Snippet {
|
||||
const factory Snippet({
|
||||
required String name,
|
||||
required String script,
|
||||
@@ -35,23 +35,16 @@ extension SnippetX on Snippet {
|
||||
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
||||
|
||||
String fmtWithSpi(Spi spi) {
|
||||
return script.replaceAllMapped(
|
||||
fmtFinder,
|
||||
(match) {
|
||||
final key = match.group(0);
|
||||
final func = fmtArgs[key];
|
||||
if (func != null) return func(spi);
|
||||
// If not found, return the original content for further processing
|
||||
return key ?? '';
|
||||
},
|
||||
);
|
||||
return script.replaceAllMapped(fmtFinder, (match) {
|
||||
final key = match.group(0);
|
||||
final func = fmtArgs[key];
|
||||
if (func != null) return func(spi);
|
||||
// If not found, return the original content for further processing
|
||||
return key ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> runInTerm(
|
||||
Terminal terminal,
|
||||
Spi spi, {
|
||||
bool autoEnter = false,
|
||||
}) async {
|
||||
Future<void> runInTerm(Terminal terminal, Spi spi, {bool autoEnter = false}) async {
|
||||
final argsFmted = fmtWithSpi(spi);
|
||||
final matches = fmtFinder.allMatches(argsFmted);
|
||||
|
||||
@@ -119,11 +112,7 @@ extension SnippetX on Snippet {
|
||||
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
||||
}
|
||||
|
||||
Future<void> _doTermKeys(
|
||||
Terminal terminal,
|
||||
MapEntry<String, TerminalKey> termKey,
|
||||
String key,
|
||||
) async {
|
||||
Future<void> _doTermKeys(Terminal terminal, MapEntry<String, TerminalKey> termKey, String key) async {
|
||||
// if (termKey.value == TerminalKey.enter) {
|
||||
// terminal.keyInput(TerminalKey.enter);
|
||||
// return;
|
||||
@@ -140,11 +129,7 @@ extension SnippetX on Snippet {
|
||||
// `${ctrl+ad}` -> `ctrla + d`
|
||||
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
||||
if (chars.isEmpty) return;
|
||||
final ok = terminal.charInput(
|
||||
chars.codeUnitAt(0),
|
||||
ctrl: ctrlAlt.ctrl,
|
||||
alt: ctrlAlt.alt,
|
||||
);
|
||||
final ok = terminal.charInput(chars.codeUnitAt(0), ctrl: ctrlAlt.ctrl, alt: ctrlAlt.alt);
|
||||
if (!ok) {
|
||||
Loggers.app.warning('Failed to input: $key');
|
||||
}
|
||||
@@ -166,10 +151,7 @@ extension SnippetX on Snippet {
|
||||
};
|
||||
|
||||
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
|
||||
static final fmtTermKeys = {
|
||||
r'${ctrl': TerminalKey.control,
|
||||
r'${alt': TerminalKey.alt,
|
||||
};
|
||||
static final fmtTermKeys = {r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt};
|
||||
}
|
||||
|
||||
class SnippetResult {
|
||||
@@ -177,11 +159,7 @@ class SnippetResult {
|
||||
final String result;
|
||||
final Duration time;
|
||||
|
||||
SnippetResult({
|
||||
required this.dest,
|
||||
required this.result,
|
||||
required this.time,
|
||||
});
|
||||
SnippetResult({required this.dest, required this.result, required this.time});
|
||||
}
|
||||
|
||||
typedef SnippetFuncCtx = ({Terminal term, String raw});
|
||||
@@ -193,10 +171,7 @@ abstract final class SnippetFuncs {
|
||||
r'${enter': SnippetFuncs.enter,
|
||||
};
|
||||
|
||||
static const help = {
|
||||
'sleep': 'Sleep for a few seconds',
|
||||
'enter': 'Enter a few times',
|
||||
};
|
||||
static const help = {'sleep': 'Sleep for a few seconds', 'enter': 'Enter a few times'};
|
||||
|
||||
static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
|
||||
final seconds = int.tryParse(ctx.raw);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
@@ -9,280 +10,170 @@ part of 'snippet.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
Snippet _$SnippetFromJson(Map<String, dynamic> json) {
|
||||
return _Snippet.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Snippet {
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get script => throw _privateConstructorUsedError;
|
||||
List<String>? get tags => throw _privateConstructorUsedError;
|
||||
String? get note => throw _privateConstructorUsedError;
|
||||
|
||||
/// List of server id that this snippet should be auto run on
|
||||
List<String>? get autoRunOn => throw _privateConstructorUsedError;
|
||||
String get name; String get script; List<String>? get tags; String? get note;/// List of server id that this snippet should be auto run on
|
||||
List<String>? get autoRunOn;
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnippetCopyWith<Snippet> get copyWith => _$SnippetCopyWithImpl<Snippet>(this as Snippet, _$identity);
|
||||
|
||||
/// Serializes this Snippet to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Snippet&&(identical(other.name, name) || other.name == name)&&(identical(other.script, script) || other.script == script)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.note, note) || other.note == note)&&const DeepCollectionEquality().equals(other.autoRunOn, autoRunOn));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,script,const DeepCollectionEquality().hash(tags),note,const DeepCollectionEquality().hash(autoRunOn));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Snippet(name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)';
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnippetCopyWith<Snippet> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnippetCopyWith<$Res> {
|
||||
factory $SnippetCopyWith(Snippet value, $Res Function(Snippet) then) =
|
||||
_$SnippetCopyWithImpl<$Res, Snippet>;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name,
|
||||
String script,
|
||||
List<String>? tags,
|
||||
String? note,
|
||||
List<String>? autoRunOn,
|
||||
});
|
||||
}
|
||||
abstract mixin class $SnippetCopyWith<$Res> {
|
||||
factory $SnippetCopyWith(Snippet value, $Res Function(Snippet) _then) = _$SnippetCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name, String script, List<String>? tags, String? note, List<String>? autoRunOn
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnippetCopyWithImpl<$Res, $Val extends Snippet>
|
||||
class _$SnippetCopyWithImpl<$Res>
|
||||
implements $SnippetCopyWith<$Res> {
|
||||
_$SnippetCopyWithImpl(this._value, this._then);
|
||||
_$SnippetCopyWithImpl(this._self, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
final Snippet _self;
|
||||
final $Res Function(Snippet) _then;
|
||||
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? name = null,
|
||||
Object? script = null,
|
||||
Object? tags = freezed,
|
||||
Object? note = freezed,
|
||||
Object? autoRunOn = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
script: null == script
|
||||
? _value.script
|
||||
: script // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
tags: freezed == tags
|
||||
? _value.tags
|
||||
: tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
note: freezed == note
|
||||
? _value.note
|
||||
: note // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
autoRunOn: freezed == autoRunOn
|
||||
? _value.autoRunOn
|
||||
: autoRunOn // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? script = null,Object? tags = freezed,Object? note = freezed,Object? autoRunOn = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,script: null == script ? _self.script : script // ignore: cast_nullable_to_non_nullable
|
||||
as String,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,note: freezed == note ? _self.note : note // ignore: cast_nullable_to_non_nullable
|
||||
as String?,autoRunOn: freezed == autoRunOn ? _self.autoRunOn : autoRunOn // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnippetImplCopyWith<$Res> implements $SnippetCopyWith<$Res> {
|
||||
factory _$$SnippetImplCopyWith(
|
||||
_$SnippetImpl value,
|
||||
$Res Function(_$SnippetImpl) then,
|
||||
) = __$$SnippetImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
String name,
|
||||
String script,
|
||||
List<String>? tags,
|
||||
String? note,
|
||||
List<String>? autoRunOn,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnippetImplCopyWithImpl<$Res>
|
||||
extends _$SnippetCopyWithImpl<$Res, _$SnippetImpl>
|
||||
implements _$$SnippetImplCopyWith<$Res> {
|
||||
__$$SnippetImplCopyWithImpl(
|
||||
_$SnippetImpl _value,
|
||||
$Res Function(_$SnippetImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? name = null,
|
||||
Object? script = null,
|
||||
Object? tags = freezed,
|
||||
Object? note = freezed,
|
||||
Object? autoRunOn = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$SnippetImpl(
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
script: null == script
|
||||
? _value.script
|
||||
: script // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
tags: freezed == tags
|
||||
? _value._tags
|
||||
: tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
note: freezed == note
|
||||
? _value.note
|
||||
: note // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
autoRunOn: freezed == autoRunOn
|
||||
? _value._autoRunOn
|
||||
: autoRunOn // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnippetImpl implements _Snippet {
|
||||
const _$SnippetImpl({
|
||||
required this.name,
|
||||
required this.script,
|
||||
final List<String>? tags,
|
||||
this.note,
|
||||
final List<String>? autoRunOn,
|
||||
}) : _tags = tags,
|
||||
_autoRunOn = autoRunOn;
|
||||
|
||||
factory _$SnippetImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnippetImplFromJson(json);
|
||||
class _Snippet implements Snippet {
|
||||
const _Snippet({required this.name, required this.script, final List<String>? tags, this.note, final List<String>? autoRunOn}): _tags = tags,_autoRunOn = autoRunOn;
|
||||
factory _Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);
|
||||
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String script;
|
||||
final List<String>? _tags;
|
||||
@override
|
||||
List<String>? get tags {
|
||||
final value = _tags;
|
||||
if (value == null) return null;
|
||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final String? note;
|
||||
|
||||
/// List of server id that this snippet should be auto run on
|
||||
final List<String>? _autoRunOn;
|
||||
|
||||
/// List of server id that this snippet should be auto run on
|
||||
@override
|
||||
List<String>? get autoRunOn {
|
||||
final value = _autoRunOn;
|
||||
if (value == null) return null;
|
||||
if (_autoRunOn is EqualUnmodifiableListView) return _autoRunOn;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Snippet(name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnippetImpl &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.script, script) || other.script == script) &&
|
||||
const DeepCollectionEquality().equals(other._tags, _tags) &&
|
||||
(identical(other.note, note) || other.note == note) &&
|
||||
const DeepCollectionEquality().equals(
|
||||
other._autoRunOn,
|
||||
_autoRunOn,
|
||||
));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
name,
|
||||
script,
|
||||
const DeepCollectionEquality().hash(_tags),
|
||||
note,
|
||||
const DeepCollectionEquality().hash(_autoRunOn),
|
||||
);
|
||||
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnippetImplCopyWith<_$SnippetImpl> get copyWith =>
|
||||
__$$SnippetImplCopyWithImpl<_$SnippetImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnippetImplToJson(this);
|
||||
}
|
||||
@override final String name;
|
||||
@override final String script;
|
||||
final List<String>? _tags;
|
||||
@override List<String>? get tags {
|
||||
final value = _tags;
|
||||
if (value == null) return null;
|
||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
abstract class _Snippet implements Snippet {
|
||||
const factory _Snippet({
|
||||
required final String name,
|
||||
required final String script,
|
||||
final List<String>? tags,
|
||||
final String? note,
|
||||
final List<String>? autoRunOn,
|
||||
}) = _$SnippetImpl;
|
||||
|
||||
factory _Snippet.fromJson(Map<String, dynamic> json) = _$SnippetImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get script;
|
||||
@override
|
||||
List<String>? get tags;
|
||||
@override
|
||||
String? get note;
|
||||
|
||||
/// List of server id that this snippet should be auto run on
|
||||
@override
|
||||
List<String>? get autoRunOn;
|
||||
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnippetImplCopyWith<_$SnippetImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
@override final String? note;
|
||||
/// List of server id that this snippet should be auto run on
|
||||
final List<String>? _autoRunOn;
|
||||
/// List of server id that this snippet should be auto run on
|
||||
@override List<String>? get autoRunOn {
|
||||
final value = _autoRunOn;
|
||||
if (value == null) return null;
|
||||
if (_autoRunOn is EqualUnmodifiableListView) return _autoRunOn;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnippetCopyWith<_Snippet> get copyWith => __$SnippetCopyWithImpl<_Snippet>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnippetToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Snippet&&(identical(other.name, name) || other.name == name)&&(identical(other.script, script) || other.script == script)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.note, note) || other.note == note)&&const DeepCollectionEquality().equals(other._autoRunOn, _autoRunOn));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,script,const DeepCollectionEquality().hash(_tags),note,const DeepCollectionEquality().hash(_autoRunOn));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Snippet(name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnippetCopyWith<$Res> implements $SnippetCopyWith<$Res> {
|
||||
factory _$SnippetCopyWith(_Snippet value, $Res Function(_Snippet) _then) = __$SnippetCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String name, String script, List<String>? tags, String? note, List<String>? autoRunOn
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnippetCopyWithImpl<$Res>
|
||||
implements _$SnippetCopyWith<$Res> {
|
||||
__$SnippetCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _Snippet _self;
|
||||
final $Res Function(_Snippet) _then;
|
||||
|
||||
/// Create a copy of Snippet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? script = null,Object? tags = freezed,Object? note = freezed,Object? autoRunOn = freezed,}) {
|
||||
return _then(_Snippet(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,script: null == script ? _self.script : script // ignore: cast_nullable_to_non_nullable
|
||||
as String,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,note: freezed == note ? _self.note : note // ignore: cast_nullable_to_non_nullable
|
||||
as String?,autoRunOn: freezed == autoRunOn ? _self._autoRunOn : autoRunOn // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -6,22 +6,20 @@ part of 'snippet.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnippetImpl _$$SnippetImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnippetImpl(
|
||||
name: json['name'] as String,
|
||||
script: json['script'] as String,
|
||||
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
note: json['note'] as String?,
|
||||
autoRunOn: (json['autoRunOn'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
_Snippet _$SnippetFromJson(Map<String, dynamic> json) => _Snippet(
|
||||
name: json['name'] as String,
|
||||
script: json['script'] as String,
|
||||
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
note: json['note'] as String?,
|
||||
autoRunOn: (json['autoRunOn'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnippetImplToJson(_$SnippetImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'script': instance.script,
|
||||
'tags': instance.tags,
|
||||
'note': instance.note,
|
||||
'autoRunOn': instance.autoRunOn,
|
||||
};
|
||||
Map<String, dynamic> _$SnippetToJson(_Snippet instance) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'script': instance.script,
|
||||
'tags': instance.tags,
|
||||
'note': instance.note,
|
||||
'autoRunOn': instance.autoRunOn,
|
||||
};
|
||||
|
||||
@@ -1,32 +1,55 @@
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
|
||||
enum SystemType {
|
||||
linux._(linuxSign),
|
||||
bsd._(bsdSign),
|
||||
;
|
||||
linux(linuxSign),
|
||||
bsd(bsdSign),
|
||||
windows(windowsSign);
|
||||
|
||||
final String value;
|
||||
final String? value;
|
||||
|
||||
const SystemType._(this.value);
|
||||
const SystemType([this.value]);
|
||||
|
||||
static const linuxSign = '__linux';
|
||||
static const bsdSign = '__bsd';
|
||||
static const windowsSign = '__windows';
|
||||
|
||||
/// Used for parsing system types from shell output.
|
||||
///
|
||||
/// This method looks for specific system signatures in the shell output
|
||||
/// and returns the corresponding SystemType. If no signature is found,
|
||||
/// it defaults to Linux but logs the detection failure for debugging.
|
||||
static SystemType parse(String value) {
|
||||
// Log the raw value for debugging purposes (truncated to avoid spam)
|
||||
final truncatedValue = value.length > 100
|
||||
? '${value.substring(0, 100)}...'
|
||||
: value;
|
||||
|
||||
if (value.contains(windowsSign)) {
|
||||
Loggers.app.info('System detected as Windows from signature in: $truncatedValue');
|
||||
return SystemType.windows;
|
||||
}
|
||||
if (value.contains(bsdSign)) {
|
||||
Loggers.app.info('System detected as BSD from signature in: $truncatedValue');
|
||||
return SystemType.bsd;
|
||||
}
|
||||
|
||||
// Log when falling back to Linux detection
|
||||
if (value.trim().isEmpty) {
|
||||
Loggers.app.warning(
|
||||
'System detection received empty input, defaulting to Linux. '
|
||||
'This may indicate a script execution issue.'
|
||||
);
|
||||
} else if (!value.contains(linuxSign)) {
|
||||
Loggers.app.warning(
|
||||
'System detection could not find any known signatures (Windows: $windowsSign, '
|
||||
'BSD: $bsdSign, Linux: $linuxSign) in output: "$truncatedValue". '
|
||||
'Defaulting to Linux, but this may cause incorrect parsing.'
|
||||
);
|
||||
} else {
|
||||
Loggers.app.info('System detected as Linux from signature in: $truncatedValue');
|
||||
}
|
||||
|
||||
return SystemType.linux;
|
||||
}
|
||||
|
||||
bool isSegmentsLenMatch(int len) => len == segmentsLen;
|
||||
|
||||
int get segmentsLen {
|
||||
switch (this) {
|
||||
case SystemType.linux:
|
||||
return StatusCmdType.values.length;
|
||||
case SystemType.bsd:
|
||||
return BSDStatusCmdType.values.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,24 @@ enum SystemdUnitFunc {
|
||||
reload,
|
||||
enable,
|
||||
disable,
|
||||
status,
|
||||
;
|
||||
status;
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
start => Icons.play_arrow,
|
||||
stop => Icons.stop,
|
||||
restart => Icons.refresh,
|
||||
reload => Icons.refresh,
|
||||
enable => Icons.check,
|
||||
disable => Icons.close,
|
||||
status => Icons.info,
|
||||
};
|
||||
start => Icons.play_arrow,
|
||||
stop => Icons.stop,
|
||||
restart => Icons.refresh,
|
||||
reload => Icons.refresh,
|
||||
enable => Icons.check,
|
||||
disable => Icons.close,
|
||||
status => Icons.info,
|
||||
};
|
||||
}
|
||||
|
||||
enum SystemdUnitType {
|
||||
service,
|
||||
socket,
|
||||
mount,
|
||||
timer,
|
||||
;
|
||||
timer;
|
||||
|
||||
static SystemdUnitType? fromString(String? value) {
|
||||
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
|
||||
@@ -36,13 +34,12 @@ enum SystemdUnitType {
|
||||
|
||||
enum SystemdUnitScope {
|
||||
system,
|
||||
user,
|
||||
;
|
||||
user;
|
||||
|
||||
Color? get color => switch (this) {
|
||||
system => Colors.red,
|
||||
_ => null,
|
||||
};
|
||||
system => Colors.red,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
String getCmdPrefix(bool isRoot) {
|
||||
if (this == system) {
|
||||
@@ -57,17 +54,16 @@ enum SystemdUnitState {
|
||||
inactive,
|
||||
failed,
|
||||
activating,
|
||||
deactivating,
|
||||
;
|
||||
deactivating;
|
||||
|
||||
static SystemdUnitState? fromString(String? value) {
|
||||
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
|
||||
}
|
||||
|
||||
Color? get color => switch (this) {
|
||||
failed => Colors.red,
|
||||
_ => null,
|
||||
};
|
||||
failed => Colors.red,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
final class SystemdUnit {
|
||||
@@ -85,10 +81,7 @@ final class SystemdUnit {
|
||||
required this.state,
|
||||
});
|
||||
|
||||
String getCmd({
|
||||
required SystemdUnitFunc func,
|
||||
required bool isRoot,
|
||||
}) {
|
||||
String getCmd({required SystemdUnitFunc func, required bool isRoot}) {
|
||||
final prefix = scope.getCmdPrefix(isRoot);
|
||||
return '$prefix ${func.name} $name';
|
||||
}
|
||||
|
||||
@@ -40,11 +40,7 @@ class Fifo<T> extends ListBase<T> {
|
||||
abstract class TimeSeq<T extends List<TimeSeqIface>> extends Fifo<T> {
|
||||
/// Due to the design, at least two elements are required, otherwise [pre] /
|
||||
/// [now] will throw.
|
||||
TimeSeq(
|
||||
T init1,
|
||||
T init2, {
|
||||
super.capacity,
|
||||
}) : super(list: [init1, init2]);
|
||||
TimeSeq(T init1, T init2, {super.capacity}) : super(list: [init1, init2]);
|
||||
|
||||
T get pre {
|
||||
return _list[length - 2];
|
||||
|
||||
248
lib/data/model/server/windows_parser.dart
Normal file
248
lib/data/model/server/windows_parser.dart
Normal file
@@ -0,0 +1,248 @@
|
||||
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 parsed output
|
||||
static void parseCustomCommands(
|
||||
ServerStatus serverStatus,
|
||||
Map<String, String> parsedOutput,
|
||||
Map<String, String> customCmds,
|
||||
) {
|
||||
try {
|
||||
for (final entry in customCmds.entries) {
|
||||
final key = entry.key;
|
||||
final value = parsedOutput[key] ?? '';
|
||||
serverStatus.customCmds[key] = value;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows custom commands parsing failed: $e', s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows uptime from PowerShell output
|
||||
static String? parseUpTime(String raw) {
|
||||
try {
|
||||
// Clean the input - trim whitespace and get the first non-empty line
|
||||
final cleanedInput = raw.trim().split('\n')
|
||||
.where((line) => line.trim().isNotEmpty)
|
||||
.firstOrNull;
|
||||
|
||||
if (cleanedInput == null || cleanedInput.isEmpty) {
|
||||
Loggers.app.warning('Windows uptime parsing: empty or null input');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try multiple date formats to handle different Windows locale/version outputs
|
||||
final formatters = [
|
||||
DateFormat('EEEE, MMMM d, yyyy h:mm:ss a', 'en_US'), // Original format
|
||||
DateFormat('EEEE, MMMM dd, yyyy h:mm:ss a', 'en_US'), // Double-digit day
|
||||
DateFormat('EEE, MMM d, yyyy h:mm:ss a', 'en_US'), // Shortened format
|
||||
DateFormat('EEE, MMM dd, yyyy h:mm:ss a', 'en_US'), // Shortened with double-digit day
|
||||
DateFormat('M/d/yyyy h:mm:ss a', 'en_US'), // Short US format
|
||||
DateFormat('MM/dd/yyyy h:mm:ss a', 'en_US'), // Short US format with zero padding
|
||||
DateFormat('d/M/yyyy h:mm:ss a', 'en_US'), // Short European format
|
||||
DateFormat('dd/MM/yyyy h:mm:ss a', 'en_US'), // Short European format with zero padding
|
||||
];
|
||||
|
||||
DateTime? dateTime;
|
||||
for (final formatter in formatters) {
|
||||
dateTime = formatter.tryParseLoose(cleanedInput);
|
||||
if (dateTime != null) break;
|
||||
}
|
||||
|
||||
if (dateTime == null) {
|
||||
Loggers.app.warning('Windows uptime parsing: could not parse date format for: $cleanedInput');
|
||||
return null;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final uptime = now.difference(dateTime);
|
||||
|
||||
// Validate that the uptime is reasonable (not negative, not too far in the future)
|
||||
if (uptime.isNegative || uptime.inDays > 3650) { // More than 10 years seems unreasonable
|
||||
Loggers.app.warning('Windows uptime parsing: unreasonable uptime calculated: ${uptime.inDays} days for date: $cleanedInput');
|
||||
return null;
|
||||
}
|
||||
|
||||
final days = uptime.inDays;
|
||||
final hours = uptime.inHours % 24;
|
||||
final minutes = uptime.inMinutes % 60;
|
||||
|
||||
if (days > 0) {
|
||||
return '$days days, $hours:${minutes.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '$hours:${minutes.toString().padLeft(2, '0')}';
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Windows uptime parsing failed: $e for input: $raw', s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows CPU information from PowerShell output
|
||||
static List<SingleCpuCore> parseCpu(String raw, ServerStatus serverStatus) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<SingleCpuCore> cpus = [];
|
||||
|
||||
if (jsonData is List) {
|
||||
for (int i = 0; i < jsonData.length; i++) {
|
||||
final cpu = jsonData[i];
|
||||
final loadPercentage = cpu['LoadPercentage'] ?? 0;
|
||||
final usage = loadPercentage as int;
|
||||
final idle = 100 - usage;
|
||||
|
||||
// Get previous CPU data to calculate cumulative values
|
||||
final prevCpus = serverStatus.cpu.now;
|
||||
final prevCpu = i < prevCpus.length ? prevCpus[i] : null;
|
||||
|
||||
// LIMITATION: Windows CPU counters approach
|
||||
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
|
||||
// We simulate cumulative counters by adding current percentages to previous totals.
|
||||
// This approach has limitations:
|
||||
// 1. Not as accurate as true cumulative time counters (Linux /proc/stat)
|
||||
// 2. May drift over time with variable polling intervals
|
||||
// 3. Results depend on consistent polling frequency
|
||||
// However, this allows compatibility with existing delta-based CPU calculation logic.
|
||||
final newUser = (prevCpu?.user ?? 0) + usage;
|
||||
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
||||
|
||||
cpus.add(
|
||||
SingleCpuCore(
|
||||
'cpu$i',
|
||||
newUser, // cumulative user time
|
||||
0, // sys (not available)
|
||||
0, // nice (not available)
|
||||
newIdle, // cumulative idle time
|
||||
0, // iowait (not available)
|
||||
0, // irq (not available)
|
||||
0, // softirq (not available)
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (jsonData is Map) {
|
||||
// Single CPU core
|
||||
final loadPercentage = jsonData['LoadPercentage'] ?? 0;
|
||||
final usage = loadPercentage as int;
|
||||
final idle = 100 - usage;
|
||||
|
||||
// Get previous CPU data to calculate cumulative values
|
||||
final prevCpus = serverStatus.cpu.now;
|
||||
final prevCpu = prevCpus.isNotEmpty ? prevCpus[0] : null;
|
||||
|
||||
// LIMITATION: See comment above for Windows CPU counter limitations
|
||||
final newUser = (prevCpu?.user ?? 0) + usage;
|
||||
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
||||
|
||||
cpus.add(
|
||||
SingleCpuCore(
|
||||
'cpu0',
|
||||
newUser, // cumulative user time
|
||||
0, // sys
|
||||
0, // nice
|
||||
newIdle, // cumulative idle time
|
||||
0, // iowait
|
||||
0, // irq
|
||||
0, // softirq
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return cpus;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows memory information from PowerShell output
|
||||
///
|
||||
/// NOTE: Windows Win32_OperatingSystem properties TotalVisibleMemorySize
|
||||
/// and FreePhysicalMemory are returned in KB units.
|
||||
static Memory? parseMemory(String raw) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final data = jsonData is List ? jsonData.first : jsonData;
|
||||
|
||||
// Win32_OperatingSystem properties are in KB
|
||||
final totalKB = data['TotalVisibleMemorySize'] as int? ?? 0;
|
||||
final freeKB = data['FreePhysicalMemory'] as int? ?? 0;
|
||||
|
||||
return Memory(
|
||||
total: totalKB,
|
||||
free: freeKB,
|
||||
avail: freeKB, // Windows doesn't distinguish between free and available
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Windows disk information from PowerShell output
|
||||
static List<Disk> parseDisks(String raw) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<Disk> disks = [];
|
||||
|
||||
final diskList = jsonData is List ? jsonData : [jsonData];
|
||||
|
||||
for (final diskData in diskList) {
|
||||
final deviceId = diskData['DeviceID']?.toString() ?? '';
|
||||
final size =
|
||||
BigInt.tryParse(diskData['Size']?.toString() ?? '0') ?? BigInt.zero;
|
||||
final freeSpace =
|
||||
BigInt.tryParse(diskData['FreeSpace']?.toString() ?? '0') ??
|
||||
BigInt.zero;
|
||||
final fileSystem = diskData['FileSystem']?.toString() ?? '';
|
||||
|
||||
// Validate all required fields
|
||||
final hasRequiredFields = deviceId.isNotEmpty &&
|
||||
size != BigInt.zero &&
|
||||
freeSpace != BigInt.zero &&
|
||||
fileSystem.isNotEmpty;
|
||||
|
||||
if (!hasRequiredFields) {
|
||||
Loggers.app.warning('Windows disk parsing: skipping disk with missing required fields. '
|
||||
'DeviceID: $deviceId, Size: $size, FreeSpace: $freeSpace, FileSystem: $fileSystem');
|
||||
continue;
|
||||
}
|
||||
|
||||
final sizeKB = size ~/ BigInt.from(1024);
|
||||
final freeKB = freeSpace ~/ BigInt.from(1024);
|
||||
final usedKB = sizeKB - freeKB;
|
||||
final usedPercent = sizeKB > BigInt.zero
|
||||
? ((usedKB * BigInt.from(100)) ~/ sizeKB).toInt()
|
||||
: 0;
|
||||
|
||||
disks.add(
|
||||
Disk(
|
||||
path: deviceId,
|
||||
fsTyp: fileSystem,
|
||||
size: sizeKB,
|
||||
avail: freeKB,
|
||||
used: usedKB,
|
||||
usedPercent: usedPercent,
|
||||
mount: deviceId, // Windows uses drive letters as mount points
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return disks;
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Windows disk parsing failed: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,7 @@ final class WakeOnLanCfg {
|
||||
final String ip;
|
||||
final String? pwd;
|
||||
|
||||
const WakeOnLanCfg({
|
||||
required this.mac,
|
||||
required this.ip,
|
||||
this.pwd,
|
||||
});
|
||||
const WakeOnLanCfg({required this.mac, required this.ip, this.pwd});
|
||||
|
||||
(Object?, bool) validate() {
|
||||
final macValidation = MACAddress.validate(mac);
|
||||
@@ -39,10 +35,7 @@ final class WakeOnLanCfg {
|
||||
final mac_ = MACAddress(mac);
|
||||
final pwd_ = pwd != null ? SecureONPassword(pwd!) : null;
|
||||
final obj = WakeOnLAN(ip_, mac_, password: pwd_);
|
||||
return obj.wake(
|
||||
repeat: 3,
|
||||
repeatDelay: const Duration(milliseconds: 500),
|
||||
);
|
||||
return obj.wake(repeat: 3, repeatDelay: const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) => _$WakeOnLanCfgFromJson(json);
|
||||
|
||||
@@ -9,12 +9,7 @@ class SftpReq {
|
||||
Spi? jumpSpi;
|
||||
String? jumpPrivateKey;
|
||||
|
||||
SftpReq(
|
||||
this.spi,
|
||||
this.remotePath,
|
||||
this.localPath,
|
||||
this.type,
|
||||
) {
|
||||
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
||||
final keyId = spi.keyId;
|
||||
if (keyId != null) {
|
||||
privateKey = getPrivateKey(keyId);
|
||||
@@ -44,15 +39,9 @@ class SftpReqStatus {
|
||||
Exception? error;
|
||||
Duration? spentTime;
|
||||
|
||||
SftpReqStatus({
|
||||
required this.req,
|
||||
required this.notifyListeners,
|
||||
this.completer,
|
||||
}) : id = DateTime.now().microsecondsSinceEpoch {
|
||||
worker = SftpWorker(
|
||||
onNotify: onNotify,
|
||||
req: req,
|
||||
)..init();
|
||||
SftpReqStatus({required this.req, required this.notifyListeners, this.completer})
|
||||
: id = DateTime.now().microsecondsSinceEpoch {
|
||||
worker = SftpWorker(onNotify: onNotify, req: req)..init();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -18,10 +18,7 @@ class SftpWorker {
|
||||
|
||||
final worker = Worker();
|
||||
|
||||
SftpWorker({
|
||||
required this.onNotify,
|
||||
required this.req,
|
||||
});
|
||||
SftpWorker({required this.onNotify, required this.req});
|
||||
|
||||
void _dispose() {
|
||||
worker.dispose();
|
||||
@@ -31,11 +28,7 @@ class SftpWorker {
|
||||
/// the threads
|
||||
Future<void> init() async {
|
||||
if (worker.isInitialized) worker.dispose();
|
||||
await worker.init(
|
||||
mainMessageHandler,
|
||||
isolateMessageHandler,
|
||||
errorHandler: print,
|
||||
);
|
||||
await worker.init(mainMessageHandler, isolateMessageHandler, errorHandler: print);
|
||||
worker.sendMessage(req);
|
||||
}
|
||||
|
||||
@@ -46,11 +39,7 @@ class SftpWorker {
|
||||
}
|
||||
|
||||
/// Handle the messages coming from the main
|
||||
Future<void> isolateMessageHandler(
|
||||
dynamic data,
|
||||
SendPort mainSendPort,
|
||||
SendErrorFunction sendError,
|
||||
) async {
|
||||
Future<void> isolateMessageHandler(dynamic data, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||
switch (data) {
|
||||
case final SftpReq val:
|
||||
switch (val.type) {
|
||||
@@ -67,11 +56,7 @@ Future<void> isolateMessageHandler(
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _download(
|
||||
SftpReq req,
|
||||
SendPort mainSendPort,
|
||||
SendErrorFunction sendError,
|
||||
) async {
|
||||
Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||
try {
|
||||
mainSendPort.send(SftpWorkerStatus.preparing);
|
||||
final watch = Stopwatch()..start();
|
||||
@@ -99,16 +84,21 @@ Future<void> _download(
|
||||
mainSendPort.send(size);
|
||||
mainSendPort.send(SftpWorkerStatus.loading);
|
||||
|
||||
// Read 2m each time
|
||||
// Issue #161
|
||||
// The download speed is about 2m/s may due to single core performance
|
||||
const defaultChunkSize = 1024 * 1024 * 2;
|
||||
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
|
||||
for (var i = 0; i < size; i += chunkSize) {
|
||||
final fileData = file.read(length: chunkSize);
|
||||
await for (var form in fileData) {
|
||||
localFile.add(form);
|
||||
mainSendPort.send((i + form.length) / size * 100);
|
||||
// Due to single core performance, limit the chunk size
|
||||
const defaultChunkSize = 1024 * 1024 * 5;
|
||||
var totalRead = 0;
|
||||
|
||||
while (totalRead < size) {
|
||||
final remaining = size - totalRead;
|
||||
final chunkSize = remaining > defaultChunkSize ? defaultChunkSize : remaining;
|
||||
dprint('Size: $size, Total Read: $totalRead, Chunk Size: $chunkSize');
|
||||
|
||||
final fileData = file.read(offset: totalRead, length: chunkSize);
|
||||
await for (var chunk in fileData) {
|
||||
localFile.add(chunk);
|
||||
totalRead += chunk.length;
|
||||
mainSendPort.send(totalRead / size * 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,11 +112,7 @@ Future<void> _download(
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _upload(
|
||||
SftpReq req,
|
||||
SendPort mainSendPort,
|
||||
SendErrorFunction sendError,
|
||||
) async {
|
||||
Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||
try {
|
||||
mainSendPort.send(SftpWorkerStatus.preparing);
|
||||
final watch = Stopwatch()..start();
|
||||
@@ -151,9 +137,7 @@ Future<void> _upload(
|
||||
// If remote exists, overwrite it
|
||||
final file = await sftp.open(
|
||||
req.remotePath,
|
||||
mode: SftpFileOpenMode.truncate |
|
||||
SftpFileOpenMode.create |
|
||||
SftpFileOpenMode.write,
|
||||
mode: SftpFileOpenMode.truncate | SftpFileOpenMode.create | SftpFileOpenMode.write,
|
||||
);
|
||||
final writer = file.write(
|
||||
localFile,
|
||||
|
||||
@@ -21,6 +21,7 @@ enum VirtKey {
|
||||
right,
|
||||
clipboard,
|
||||
ime,
|
||||
shift,
|
||||
pgup,
|
||||
pgdn,
|
||||
slash,
|
||||
@@ -50,30 +51,30 @@ enum VirtKey {
|
||||
f9,
|
||||
f10,
|
||||
f11,
|
||||
f12;
|
||||
f12,
|
||||
}
|
||||
|
||||
extension VirtKeyX on VirtKey {
|
||||
/// Used for input to terminal
|
||||
String? get inputRaw => switch (this) {
|
||||
VirtKey.slash => '/',
|
||||
VirtKey.backSlash => '\\',
|
||||
VirtKey.underscore => '_',
|
||||
VirtKey.plus => '+',
|
||||
VirtKey.equal => '=',
|
||||
VirtKey.minus => '-',
|
||||
VirtKey.parenLeft => '(',
|
||||
VirtKey.parenRight => ')',
|
||||
VirtKey.bracketLeft => '[',
|
||||
VirtKey.bracketRight => ']',
|
||||
VirtKey.braceLeft => '{',
|
||||
VirtKey.braceRight => '}',
|
||||
VirtKey.chevronLeft => '<',
|
||||
VirtKey.chevronRight => '>',
|
||||
VirtKey.colon => ':',
|
||||
VirtKey.semicolon => ';',
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.slash => '/',
|
||||
VirtKey.backSlash => '\\',
|
||||
VirtKey.underscore => '_',
|
||||
VirtKey.plus => '+',
|
||||
VirtKey.equal => '=',
|
||||
VirtKey.minus => '-',
|
||||
VirtKey.parenLeft => '(',
|
||||
VirtKey.parenRight => ')',
|
||||
VirtKey.bracketLeft => '[',
|
||||
VirtKey.bracketRight => ']',
|
||||
VirtKey.braceLeft => '{',
|
||||
VirtKey.braceRight => '}',
|
||||
VirtKey.chevronLeft => '<',
|
||||
VirtKey.chevronRight => '>',
|
||||
VirtKey.colon => ':',
|
||||
VirtKey.semicolon => ';',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// Used for displaying on UI
|
||||
String get text {
|
||||
@@ -105,77 +106,79 @@ extension VirtKeyX on VirtKey {
|
||||
VirtKey.right,
|
||||
VirtKey.clipboard,
|
||||
VirtKey.ime,
|
||||
VirtKey.shift,
|
||||
];
|
||||
|
||||
/// Corresponding [TerminalKey]
|
||||
TerminalKey? get key => switch (this) {
|
||||
VirtKey.esc => TerminalKey.escape,
|
||||
VirtKey.alt => TerminalKey.alt,
|
||||
VirtKey.home => TerminalKey.home,
|
||||
VirtKey.up => TerminalKey.arrowUp,
|
||||
VirtKey.end => TerminalKey.end,
|
||||
VirtKey.tab => TerminalKey.tab,
|
||||
VirtKey.ctrl => TerminalKey.control,
|
||||
VirtKey.left => TerminalKey.arrowLeft,
|
||||
VirtKey.down => TerminalKey.arrowDown,
|
||||
VirtKey.right => TerminalKey.arrowRight,
|
||||
VirtKey.pgup => TerminalKey.pageUp,
|
||||
VirtKey.pgdn => TerminalKey.pageDown,
|
||||
VirtKey.f1 => TerminalKey.f1,
|
||||
VirtKey.f2 => TerminalKey.f2,
|
||||
VirtKey.f3 => TerminalKey.f3,
|
||||
VirtKey.f4 => TerminalKey.f4,
|
||||
VirtKey.f5 => TerminalKey.f5,
|
||||
VirtKey.f6 => TerminalKey.f6,
|
||||
VirtKey.f7 => TerminalKey.f7,
|
||||
VirtKey.f8 => TerminalKey.f8,
|
||||
VirtKey.f9 => TerminalKey.f9,
|
||||
VirtKey.f10 => TerminalKey.f10,
|
||||
VirtKey.f11 => TerminalKey.f11,
|
||||
VirtKey.f12 => TerminalKey.f12,
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.esc => TerminalKey.escape,
|
||||
VirtKey.alt => TerminalKey.alt,
|
||||
VirtKey.home => TerminalKey.home,
|
||||
VirtKey.up => TerminalKey.arrowUp,
|
||||
VirtKey.end => TerminalKey.end,
|
||||
VirtKey.tab => TerminalKey.tab,
|
||||
VirtKey.ctrl => TerminalKey.control,
|
||||
VirtKey.left => TerminalKey.arrowLeft,
|
||||
VirtKey.down => TerminalKey.arrowDown,
|
||||
VirtKey.right => TerminalKey.arrowRight,
|
||||
VirtKey.shift => TerminalKey.shift,
|
||||
VirtKey.pgup => TerminalKey.pageUp,
|
||||
VirtKey.pgdn => TerminalKey.pageDown,
|
||||
VirtKey.f1 => TerminalKey.f1,
|
||||
VirtKey.f2 => TerminalKey.f2,
|
||||
VirtKey.f3 => TerminalKey.f3,
|
||||
VirtKey.f4 => TerminalKey.f4,
|
||||
VirtKey.f5 => TerminalKey.f5,
|
||||
VirtKey.f6 => TerminalKey.f6,
|
||||
VirtKey.f7 => TerminalKey.f7,
|
||||
VirtKey.f8 => TerminalKey.f8,
|
||||
VirtKey.f9 => TerminalKey.f9,
|
||||
VirtKey.f10 => TerminalKey.f10,
|
||||
VirtKey.f11 => TerminalKey.f11,
|
||||
VirtKey.f12 => TerminalKey.f12,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// Icons for virtual keys
|
||||
IconData? get icon => switch (this) {
|
||||
VirtKey.up => Icons.arrow_upward,
|
||||
VirtKey.left => Icons.arrow_back,
|
||||
VirtKey.down => Icons.arrow_downward,
|
||||
VirtKey.right => Icons.arrow_forward,
|
||||
VirtKey.sftp => Icons.file_open,
|
||||
VirtKey.snippet => Icons.code,
|
||||
VirtKey.clipboard => Icons.paste,
|
||||
VirtKey.ime => Icons.keyboard,
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.up => Icons.arrow_upward,
|
||||
VirtKey.left => Icons.arrow_back,
|
||||
VirtKey.down => Icons.arrow_downward,
|
||||
VirtKey.right => Icons.arrow_forward,
|
||||
VirtKey.sftp => Icons.file_open,
|
||||
VirtKey.snippet => Icons.code,
|
||||
VirtKey.clipboard => Icons.paste,
|
||||
VirtKey.ime => Icons.keyboard,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
// Use [VirtualKeyFunc] instead of [VirtKey]
|
||||
// This can help linter to enum all [VirtualKeyFunc]
|
||||
// and make sure all [VirtualKeyFunc] are handled
|
||||
VirtualKeyFunc? get func => switch (this) {
|
||||
VirtKey.sftp => VirtualKeyFunc.file,
|
||||
VirtKey.snippet => VirtualKeyFunc.snippet,
|
||||
VirtKey.clipboard => VirtualKeyFunc.clipboard,
|
||||
VirtKey.ime => VirtualKeyFunc.toggleIME,
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.sftp => VirtualKeyFunc.file,
|
||||
VirtKey.snippet => VirtualKeyFunc.snippet,
|
||||
VirtKey.clipboard => VirtualKeyFunc.clipboard,
|
||||
VirtKey.ime => VirtualKeyFunc.toggleIME,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
bool get toggleable => switch (this) {
|
||||
VirtKey.alt || VirtKey.ctrl => true,
|
||||
_ => false,
|
||||
};
|
||||
VirtKey.alt || VirtKey.ctrl || VirtKey.shift => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
bool get canLongPress => switch (this) {
|
||||
VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,
|
||||
_ => false,
|
||||
};
|
||||
VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
String? get help => switch (this) {
|
||||
VirtKey.sftp => l10n.virtKeyHelpSFTP,
|
||||
VirtKey.clipboard => l10n.virtKeyHelpClipboard,
|
||||
VirtKey.ime => l10n.virtKeyHelpIME,
|
||||
_ => null,
|
||||
};
|
||||
VirtKey.sftp => l10n.virtKeyHelpSFTP,
|
||||
VirtKey.clipboard => l10n.virtKeyHelpClipboard,
|
||||
VirtKey.ime => l10n.virtKeyHelpIME,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// - [saveDefaultIfErr] if the stored raw values is invalid, save default order to store
|
||||
static List<VirtKey> loadFromStore({bool saveDefaultIfErr = true}) {
|
||||
|
||||
@@ -6,10 +6,8 @@ part 'app.g.dart';
|
||||
part 'app.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class AppState with _$AppState {
|
||||
const factory AppState({
|
||||
@Default(false) bool desktopMode,
|
||||
}) = _AppState;
|
||||
abstract class AppState with _$AppState {
|
||||
const factory AppState({@Default(false) bool desktopMode}) = _AppState;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
@@ -9,140 +10,133 @@ part of 'app.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AppState {
|
||||
bool get desktopMode => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$AppStateCopyWith<AppState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
bool get desktopMode;
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AppStateCopyWith<AppState> get copyWith => _$AppStateCopyWithImpl<AppState>(this as AppState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppState&&(identical(other.desktopMode, desktopMode) || other.desktopMode == desktopMode));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,desktopMode);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppState(desktopMode: $desktopMode)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AppStateCopyWith<$Res> {
|
||||
factory $AppStateCopyWith(AppState value, $Res Function(AppState) then) =
|
||||
_$AppStateCopyWithImpl<$Res, AppState>;
|
||||
@useResult
|
||||
$Res call({bool desktopMode});
|
||||
}
|
||||
abstract mixin class $AppStateCopyWith<$Res> {
|
||||
factory $AppStateCopyWith(AppState value, $Res Function(AppState) _then) = _$AppStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool desktopMode
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AppStateCopyWithImpl<$Res, $Val extends AppState>
|
||||
class _$AppStateCopyWithImpl<$Res>
|
||||
implements $AppStateCopyWith<$Res> {
|
||||
_$AppStateCopyWithImpl(this._value, this._then);
|
||||
_$AppStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
final AppState _self;
|
||||
final $Res Function(AppState) _then;
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({Object? desktopMode = null}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
desktopMode: null == desktopMode
|
||||
? _value.desktopMode
|
||||
: desktopMode // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? desktopMode = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
desktopMode: null == desktopMode ? _self.desktopMode : desktopMode // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$AppStateImplCopyWith<$Res>
|
||||
implements $AppStateCopyWith<$Res> {
|
||||
factory _$$AppStateImplCopyWith(
|
||||
_$AppStateImpl value,
|
||||
$Res Function(_$AppStateImpl) then,
|
||||
) = __$$AppStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({bool desktopMode});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$AppStateImplCopyWithImpl<$Res>
|
||||
extends _$AppStateCopyWithImpl<$Res, _$AppStateImpl>
|
||||
implements _$$AppStateImplCopyWith<$Res> {
|
||||
__$$AppStateImplCopyWithImpl(
|
||||
_$AppStateImpl _value,
|
||||
$Res Function(_$AppStateImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({Object? desktopMode = null}) {
|
||||
return _then(
|
||||
_$AppStateImpl(
|
||||
desktopMode: null == desktopMode
|
||||
? _value.desktopMode
|
||||
: desktopMode // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$AppStateImpl implements _AppState {
|
||||
const _$AppStateImpl({this.desktopMode = false});
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool desktopMode;
|
||||
class _AppState implements AppState {
|
||||
const _AppState({this.desktopMode = false});
|
||||
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppState(desktopMode: $desktopMode)';
|
||||
}
|
||||
@override@JsonKey() final bool desktopMode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$AppStateImpl &&
|
||||
(identical(other.desktopMode, desktopMode) ||
|
||||
other.desktopMode == desktopMode));
|
||||
}
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$AppStateCopyWith<_AppState> get copyWith => __$AppStateCopyWithImpl<_AppState>(this, _$identity);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, desktopMode);
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$AppStateImplCopyWith<_$AppStateImpl> get copyWith =>
|
||||
__$$AppStateImplCopyWithImpl<_$AppStateImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppState&&(identical(other.desktopMode, desktopMode) || other.desktopMode == desktopMode));
|
||||
}
|
||||
|
||||
abstract class _AppState implements AppState {
|
||||
const factory _AppState({final bool desktopMode}) = _$AppStateImpl;
|
||||
|
||||
@override
|
||||
bool get desktopMode;
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,desktopMode);
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$AppStateImplCopyWith<_$AppStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppState(desktopMode: $desktopMode)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$AppStateCopyWith<$Res> implements $AppStateCopyWith<$Res> {
|
||||
factory _$AppStateCopyWith(_AppState value, $Res Function(_AppState) _then) = __$AppStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool desktopMode
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$AppStateCopyWithImpl<$Res>
|
||||
implements _$AppStateCopyWith<$Res> {
|
||||
__$AppStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _AppState _self;
|
||||
final $Res Function(_AppState) _then;
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? desktopMode = null,}) {
|
||||
return _then(_AppState(
|
||||
desktopMode: null == desktopMode ? _self.desktopMode : desktopMode // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -6,14 +6,13 @@ import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.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/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/container/image.dart';
|
||||
import 'package:server_box/data/model/container/ps.dart';
|
||||
import 'package:server_box/data/model/container/type.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
final _dockerNotFound =
|
||||
RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
||||
final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
||||
|
||||
class ContainerProvider extends ChangeNotifier {
|
||||
final SSHClient? client;
|
||||
@@ -90,11 +89,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
final includeStats = Stores.setting.containerParseStat.fetch();
|
||||
|
||||
var raw = '';
|
||||
final cmd = _wrap(ContainerCmdType.execAll(
|
||||
type,
|
||||
sudo: sudo,
|
||||
includeStats: includeStats,
|
||||
));
|
||||
final cmd = _wrap(ContainerCmdType.execAll(type, sudo: sudo, includeStats: includeStats));
|
||||
final code = await client?.execWithPwd(
|
||||
cmd,
|
||||
context: context,
|
||||
@@ -114,7 +109,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Check result segments count
|
||||
final segments = raw.split(ShellFunc.seperator);
|
||||
final segments = raw.split(ScriptConstants.separator);
|
||||
if (segments.length != ContainerCmdType.values.length) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.segmentsNotMatch,
|
||||
@@ -130,10 +125,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
try {
|
||||
version = json.decode(verRaw)['Client']['Version'];
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.invalidVersion,
|
||||
message: '$e',
|
||||
);
|
||||
error = ContainerErr(type: ContainerErrType.invalidVersion, message: '$e');
|
||||
Loggers.app.warning('Container version failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -150,10 +142,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parsePs,
|
||||
message: '$e',
|
||||
);
|
||||
error = ContainerErr(type: ContainerErrType.parsePs, message: '$e');
|
||||
Loggers.app.warning('Container ps failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -173,10 +162,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
|
||||
}
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parseImages,
|
||||
message: '$e',
|
||||
);
|
||||
error = ContainerErr(type: ContainerErrType.parseImages, message: '$e');
|
||||
Loggers.app.warning('Container images failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -199,10 +185,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
item.parseStats(statsLine);
|
||||
}
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parseStats,
|
||||
message: '$e',
|
||||
);
|
||||
error = ContainerErr(type: ContainerErrType.parseStats, message: '$e');
|
||||
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -222,6 +205,23 @@ class ContainerProvider extends ChangeNotifier {
|
||||
|
||||
Future<ContainerErr?> restart(String id) async => await run('restart $id');
|
||||
|
||||
Future<ContainerErr?> pruneImages({bool all = true}) async {
|
||||
final cmd = 'image prune${all ? " -a" : ""} -f';
|
||||
return await run(cmd);
|
||||
}
|
||||
|
||||
Future<ContainerErr?> pruneContainers() async {
|
||||
return await run('container prune -f');
|
||||
}
|
||||
|
||||
Future<ContainerErr?> pruneVolumes() async {
|
||||
return await run('volume prune -f');
|
||||
}
|
||||
|
||||
Future<ContainerErr?> pruneSystem() async {
|
||||
return await run('system prune -a -f --volumes');
|
||||
}
|
||||
|
||||
Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
|
||||
cmd = switch (type) {
|
||||
ContainerType.docker => 'docker $cmd',
|
||||
@@ -244,10 +244,7 @@ class ContainerProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
if (code != 0) {
|
||||
return ContainerErr(
|
||||
type: ContainerErrType.unknown,
|
||||
message: errs.join('\n').trim(),
|
||||
);
|
||||
return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim());
|
||||
}
|
||||
if (autoRefresh) await refresh();
|
||||
return null;
|
||||
@@ -271,40 +268,39 @@ enum ContainerCmdType {
|
||||
version,
|
||||
ps,
|
||||
stats,
|
||||
images,
|
||||
images
|
||||
// No specific commands needed for prune actions as they are simple
|
||||
// and don't require splitting output with ScriptConstants.separator
|
||||
;
|
||||
|
||||
String exec(
|
||||
ContainerType type, {
|
||||
bool sudo = false,
|
||||
bool includeStats = false,
|
||||
}) {
|
||||
String exec(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
||||
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
|
||||
return switch (this) {
|
||||
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
||||
ContainerCmdType.ps => switch (type) {
|
||||
/// TODO: Rollback to json format when permformance recovers.
|
||||
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
||||
ContainerType.docker => '$prefix ps -a --format "table {{printf \\"'
|
||||
/// TODO: Rollback to json format when permformance recovers.
|
||||
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
||||
ContainerType.docker =>
|
||||
'$prefix ps -a --format "table {{printf \\"'
|
||||
'%-15.15s '
|
||||
'%-30.30s '
|
||||
'${"%-50.50s " * 2}\\"'
|
||||
' .ID .Status .Names .Image}}"',
|
||||
ContainerType.podman => '$prefix ps -a $_jsonFmt',
|
||||
},
|
||||
ContainerCmdType.stats =>
|
||||
includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
|
||||
ContainerType.podman => '$prefix ps -a $_jsonFmt',
|
||||
},
|
||||
ContainerCmdType.stats => includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
|
||||
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
|
||||
};
|
||||
}
|
||||
|
||||
static String execAll(
|
||||
ContainerType type, {
|
||||
bool sudo = false,
|
||||
bool includeStats = false,
|
||||
}) {
|
||||
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
||||
return ContainerCmdType.values
|
||||
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
|
||||
.join('\necho ${ShellFunc.seperator}\n');
|
||||
.join('\necho ${ScriptConstants.separator}\n');
|
||||
}
|
||||
|
||||
/// Find out the required segment from [segments]
|
||||
String find(List<String> segments) {
|
||||
return segments[index];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,15 +86,18 @@ final class PveProvider extends ChangeNotifier {
|
||||
forward.stream.cast<List<int>>().pipe(socket);
|
||||
socket.cast<List<int>>().pipe(forward.sink);
|
||||
});
|
||||
final newUrl = Uri.parse(addr)
|
||||
.replace(host: 'localhost', port: _localPort)
|
||||
.toString();
|
||||
final newUrl = Uri.parse(
|
||||
addr,
|
||||
).replace(host: 'localhost', port: _localPort).toString();
|
||||
debugPrint('Forwarding $newUrl to $addr');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConnectionTask<Socket>> cf(
|
||||
Uri url, String? proxyHost, int? proxyPort) async {
|
||||
Uri url,
|
||||
String? proxyHost,
|
||||
int? proxyPort,
|
||||
) async {
|
||||
/* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
final _localPort = serverSocket.port;
|
||||
serverSocket.listen((socket) async {
|
||||
@@ -105,8 +108,11 @@ final class PveProvider extends ChangeNotifier {
|
||||
});*/
|
||||
|
||||
if (url.isScheme('https')) {
|
||||
return SecureSocket.startConnect('localhost', _localPort,
|
||||
onBadCertificate: (_) => true);
|
||||
return SecureSocket.startConnect(
|
||||
'localhost',
|
||||
_localPort,
|
||||
onBadCertificate: (_) => true,
|
||||
);
|
||||
} else {
|
||||
return Socket.startConnect('localhost', _localPort);
|
||||
}
|
||||
@@ -119,7 +125,7 @@ final class PveProvider extends ChangeNotifier {
|
||||
'username': spi.user,
|
||||
'password': spi.pwd,
|
||||
'realm': 'pam',
|
||||
'new-format': '1'
|
||||
'new-format': '1',
|
||||
},
|
||||
options: Options(
|
||||
headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType},
|
||||
@@ -151,8 +157,10 @@ final class PveProvider extends ChangeNotifier {
|
||||
try {
|
||||
final resp = await session.get('$addr/api2/json/cluster/resources');
|
||||
final res = resp.data['data'] as List;
|
||||
final result =
|
||||
await Computer.shared.start(PveRes.parse, (res, data.value));
|
||||
final result = await Computer.shared.start(PveRes.parse, (
|
||||
res,
|
||||
data.value,
|
||||
));
|
||||
data.value = result;
|
||||
} catch (e) {
|
||||
Loggers.app.warning('PVE list failed', e);
|
||||
@@ -164,29 +172,33 @@ final class PveProvider extends ChangeNotifier {
|
||||
|
||||
Future<bool> reboot(String node, String id) async {
|
||||
await connected.future;
|
||||
final resp =
|
||||
await session.post('$addr/api2/json/nodes/$node/$id/status/reboot');
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/reboot',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
}
|
||||
|
||||
Future<bool> start(String node, String id) async {
|
||||
await connected.future;
|
||||
final resp =
|
||||
await session.post('$addr/api2/json/nodes/$node/$id/status/start');
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/start',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
}
|
||||
|
||||
Future<bool> stop(String node, String id) async {
|
||||
await connected.future;
|
||||
final resp =
|
||||
await session.post('$addr/api2/json/nodes/$node/$id/status/stop');
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/stop',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
}
|
||||
|
||||
Future<bool> shutdown(String node, String id) async {
|
||||
await connected.future;
|
||||
final resp =
|
||||
await session.post('$addr/api2/json/nodes/$node/$id/status/shutdown');
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/shutdown',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/core/sync.dart';
|
||||
import 'package:server_box/core/utils/server.dart';
|
||||
import 'package:server_box/core/utils/ssh_auth.dart';
|
||||
import 'package:server_box/data/helper/system_detector.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
||||
@@ -32,6 +34,8 @@ class ServerProvider extends Provider {
|
||||
|
||||
static final _manualDisconnectedIds = <String>{};
|
||||
|
||||
static final _serverIdsUpdating = <String, Future<void>?>{};
|
||||
|
||||
@override
|
||||
Future<void> load() async {
|
||||
super.load();
|
||||
@@ -124,11 +128,35 @@ class ServerProvider extends Provider {
|
||||
return;
|
||||
}
|
||||
|
||||
return await _getData(s.spi);
|
||||
// Check if already updating, and if so, wait for it to complete
|
||||
final existingUpdate = _serverIdsUpdating[s.spi.id];
|
||||
if (existingUpdate != null) {
|
||||
// Already updating, wait for the existing update to complete
|
||||
try {
|
||||
await existingUpdate;
|
||||
} catch (e) {
|
||||
// Ignore errors from the existing update, we'll try our own
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Start a new update operation
|
||||
final updateFuture = _updateServer(s.spi);
|
||||
_serverIdsUpdating[s.spi.id] = updateFuture;
|
||||
|
||||
try {
|
||||
await updateFuture;
|
||||
} finally {
|
||||
_serverIdsUpdating.remove(s.spi.id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _updateServer(Spi spi) async {
|
||||
await _getData(spi);
|
||||
}
|
||||
|
||||
static Future<void> startAutoRefresh() async {
|
||||
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
||||
stopAutoRefresh();
|
||||
@@ -305,13 +333,17 @@ class ServerProvider extends Provider {
|
||||
_setServerState(s, ServerConn.connected);
|
||||
|
||||
try {
|
||||
// Detect system type using helper
|
||||
final detectedSystemType = await SystemDetector.detect(sv.client!, spi);
|
||||
sv.status.system = detectedSystemType;
|
||||
|
||||
final (_, writeScriptResult) = await sv.client!.exec((session) async {
|
||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
||||
final scriptRaw = ShellFuncManager.allScript(spi.custom?.cmds, systemType: detectedSystemType, disabledCmdTypes: spi.disabledCmdTypes).uint8List;
|
||||
session.stdin.add(scriptRaw);
|
||||
session.stdin.close();
|
||||
}, entry: ShellFunc.getInstallShellCmd(spi.id));
|
||||
if (writeScriptResult.isNotEmpty) {
|
||||
ShellFunc.switchScriptDir(spi.id);
|
||||
}, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType));
|
||||
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
|
||||
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
|
||||
throw writeScriptResult;
|
||||
}
|
||||
} on SSHAuthAbortError catch (e) {
|
||||
@@ -351,8 +383,9 @@ class ServerProvider extends Provider {
|
||||
String? raw;
|
||||
|
||||
try {
|
||||
raw = await sv.client?.run(ShellFunc.status.exec(spi.id)).string;
|
||||
segments = raw?.split(ShellFunc.seperator).map((e) => e.trim()).toList();
|
||||
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(ScriptConstants.separator).map((e) => e.trim()).toList();
|
||||
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
|
||||
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
||||
// Keep previous server status when err occurs
|
||||
@@ -373,31 +406,14 @@ class ServerProvider extends Provider {
|
||||
return;
|
||||
}
|
||||
|
||||
final systemType = SystemType.parse(segments[0]);
|
||||
final customCmdLen = spi.custom?.cmds?.length ?? 0;
|
||||
if (!systemType.isSegmentsLenMatch(segments.length - customCmdLen)) {
|
||||
TryLimiter.inc(sid);
|
||||
if (raw.contains('Could not chdir to home directory /var/services/')) {
|
||||
sv.status.err = SSHErr(type: SSHErrType.chdir, message: raw);
|
||||
_setServerState(s, ServerConn.failed);
|
||||
return;
|
||||
}
|
||||
final expected = systemType.segmentsLen;
|
||||
final actual = segments.length;
|
||||
sv.status.err = SSHErr(
|
||||
type: SSHErrType.segements,
|
||||
message: 'Segments: expect $expected, got $actual, raw:\n\n$raw',
|
||||
);
|
||||
_setServerState(s, ServerConn.failed);
|
||||
return;
|
||||
}
|
||||
sv.status.system = systemType;
|
||||
|
||||
try {
|
||||
// Parse script output into command-specific map
|
||||
final parsedOutput = ScriptConstants.parseScriptOutput(raw);
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
ss: sv.status,
|
||||
segments: segments,
|
||||
system: systemType,
|
||||
parsedOutput: parsedOutput,
|
||||
system: sv.status.system,
|
||||
customCmds: spi.custom?.cmds ?? {},
|
||||
);
|
||||
sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.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/systemd.dart';
|
||||
@@ -44,10 +44,8 @@ final class SystemdProvider {
|
||||
}
|
||||
}
|
||||
|
||||
final parsedUserUnits =
|
||||
await _parseUnitObj(userUnits, SystemdUnitScope.user);
|
||||
final parsedSystemUnits =
|
||||
await _parseUnitObj(systemUnits, SystemdUnitScope.system);
|
||||
final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);
|
||||
final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);
|
||||
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Parse systemd', e, s);
|
||||
@@ -56,22 +54,18 @@ final class SystemdProvider {
|
||||
isBusy.value = false;
|
||||
}
|
||||
|
||||
Future<List<SystemdUnit>> _parseUnitObj(
|
||||
List<String> unitNames,
|
||||
SystemdUnitScope scope,
|
||||
) async {
|
||||
final unitNames_ = unitNames
|
||||
.map((e) => e.trim().split('/').last.split('.').first)
|
||||
.toList();
|
||||
final script = '''
|
||||
Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {
|
||||
final unitNames_ = unitNames.map((e) => e.trim().split('/').last.split('.').first).toList();
|
||||
final script =
|
||||
'''
|
||||
for unit in ${unitNames_.join(' ')}; do
|
||||
state=\$(systemctl show --no-pager \$unit)
|
||||
echo -n "${ShellFunc.seperator}\n\$state"
|
||||
echo -n "${ScriptConstants.separator}\n\$state"
|
||||
done
|
||||
''';
|
||||
final client = _si.value.client!;
|
||||
final result = await client.execForOutput(script);
|
||||
final units = result.split(ShellFunc.seperator);
|
||||
final units = result.split(ScriptConstants.separator);
|
||||
|
||||
final parsedUnits = <SystemdUnit>[];
|
||||
for (final unit in units) {
|
||||
@@ -108,13 +102,9 @@ done
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedUnits.add(SystemdUnit(
|
||||
name: name,
|
||||
type: unitType,
|
||||
scope: scope,
|
||||
state: unitState,
|
||||
description: description,
|
||||
));
|
||||
parsedUnits.add(
|
||||
SystemdUnit(name: name, type: unitType, scope: scope, state: unitState, description: description),
|
||||
);
|
||||
}
|
||||
|
||||
parsedUnits.sort((a, b) {
|
||||
@@ -131,7 +121,8 @@ done
|
||||
return parsedUnits;
|
||||
}
|
||||
|
||||
late final _getUnitsCmd = '''
|
||||
late final _getUnitsCmd =
|
||||
'''
|
||||
get_files() {
|
||||
unit_type=\$1
|
||||
base_dir=\$2
|
||||
|
||||
@@ -23,6 +23,15 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool _shift = false;
|
||||
bool get shift => _shift;
|
||||
set shift(bool value) {
|
||||
if (value != _shift) {
|
||||
_shift = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void reset(TerminalKeyboardEvent e) {
|
||||
if (e.ctrl) {
|
||||
ctrl = false;
|
||||
@@ -30,6 +39,9 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
|
||||
if (e.alt) {
|
||||
alt = false;
|
||||
}
|
||||
if (e.shift) {
|
||||
shift = false;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -38,6 +50,7 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
|
||||
final e = event.copyWith(
|
||||
ctrl: event.ctrl || ctrl,
|
||||
alt: event.alt || alt,
|
||||
shift: event.shift || shift,
|
||||
);
|
||||
if (Stores.setting.sshVirtualKeyAutoOff.fetch()) {
|
||||
reset(e);
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
abstract class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 1184;
|
||||
static const int script = 63;
|
||||
static const int build = 1206;
|
||||
static const int script = 67;
|
||||
}
|
||||
|
||||
@@ -118,6 +118,12 @@ abstract final class GithubIds {
|
||||
'rhwong',
|
||||
'AstroEngineeer',
|
||||
'mochasweet',
|
||||
'back-lacking',
|
||||
'cainiaojr',
|
||||
'MisterMunkerz',
|
||||
'CreeperKong',
|
||||
'zxf945',
|
||||
'cnen2018',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ class ContainerStore extends HiveStore {
|
||||
ContainerType getType([String id = '']) {
|
||||
final cfg = box.get(_keyConfig + id);
|
||||
if (cfg != null) {
|
||||
final type =
|
||||
ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);
|
||||
final type = ContainerType.values.firstWhereOrNull((e) => e.toString() == cfg);
|
||||
if (type != null) return type;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,10 @@ class _ListHistory {
|
||||
final String _name;
|
||||
final Box _box;
|
||||
|
||||
_ListHistory({
|
||||
required Box box,
|
||||
required String name,
|
||||
}) : _box = box,
|
||||
_name = name,
|
||||
_history = box.get(name, defaultValue: [])!;
|
||||
_ListHistory({required Box box, required String name})
|
||||
: _box = box,
|
||||
_name = name,
|
||||
_history = box.get(name, defaultValue: [])!;
|
||||
|
||||
void add(String path) {
|
||||
_history.remove(path);
|
||||
@@ -28,12 +26,10 @@ class _MapHistory {
|
||||
final String _name;
|
||||
final Box _box;
|
||||
|
||||
_MapHistory({
|
||||
required Box box,
|
||||
required String name,
|
||||
}) : _box = box,
|
||||
_name = name,
|
||||
_history = box.get(name, defaultValue: <dynamic, dynamic>{})!;
|
||||
_MapHistory({required Box box, required String name})
|
||||
: _box = box,
|
||||
_name = name,
|
||||
_history = box.get(name, defaultValue: <dynamic, dynamic>{})!;
|
||||
|
||||
void put(String id, String val) {
|
||||
_history[id] = val;
|
||||
@@ -56,6 +52,5 @@ class HistoryStore extends HiveStore {
|
||||
late final sshCmds = _ListHistory(box: box, name: 'sshCmds');
|
||||
|
||||
/// Notify users that this app will write script to server to works properly
|
||||
late final writeScriptTipShown =
|
||||
propertyDefault('writeScriptTipShown', false);
|
||||
late final writeScriptTipShown = propertyDefault('writeScriptTipShown', false);
|
||||
}
|
||||
|
||||
@@ -176,8 +176,8 @@ class SettingStore extends HiveStore {
|
||||
late final containerParseStat = propertyDefault('containerParseStat', true);
|
||||
|
||||
/// Auto refresh container status
|
||||
late final contaienrAutoRefresh = propertyDefault(
|
||||
'contaienrAutoRefresh',
|
||||
late final containerAutoRefresh = propertyDefault(
|
||||
'containerAutoRefresh',
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -270,4 +270,7 @@ class SettingStore extends HiveStore {
|
||||
|
||||
/// Have notified user for notificaiton permission or not
|
||||
late final noNotiPerm = propertyDefault('noNotiPerm', false);
|
||||
|
||||
/// The backup password
|
||||
late final backupasswd = SecureProp('bakPasswd');
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @backupTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The exported data is weakly encrypted. \nPlease keep it safe.'**
|
||||
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
|
||||
String get backupTip;
|
||||
|
||||
/// No description provided for @backupVersionNotMatch.
|
||||
@@ -197,6 +197,48 @@ abstract class AppLocalizations {
|
||||
/// **'Backup version is not match.'**
|
||||
String get backupVersionNotMatch;
|
||||
|
||||
/// No description provided for @backupPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup password'**
|
||||
String get backupPassword;
|
||||
|
||||
/// No description provided for @backupPasswordTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set a password to encrypt backup files. Leave empty to disable encryption.'**
|
||||
String get backupPasswordTip;
|
||||
|
||||
/// No description provided for @backupPasswordWrong.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Incorrect backup password'**
|
||||
String get backupPasswordWrong;
|
||||
|
||||
/// No description provided for @backupEncrypted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup is encrypted'**
|
||||
String get backupEncrypted;
|
||||
|
||||
/// No description provided for @backupNotEncrypted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup is not encrypted'**
|
||||
String get backupNotEncrypted;
|
||||
|
||||
/// No description provided for @backupPasswordSet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup password set'**
|
||||
String get backupPasswordSet;
|
||||
|
||||
/// No description provided for @backupPasswordRemoved.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup password removed'**
|
||||
String get backupPasswordRemoved;
|
||||
|
||||
/// No description provided for @battery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -914,6 +956,12 @@ abstract class AppLocalizations {
|
||||
/// **'Process'**
|
||||
String get process;
|
||||
|
||||
/// No description provided for @prune.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Prune'**
|
||||
String get prune;
|
||||
|
||||
/// No description provided for @pushToken.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -938,12 +986,6 @@ abstract class AppLocalizations {
|
||||
/// **'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.'**
|
||||
String get pveVersionLow;
|
||||
|
||||
/// No description provided for @pwd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password'**
|
||||
String get pwd;
|
||||
|
||||
/// No description provided for @read.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1499,7 +1541,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @writeScriptTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'After connecting to the server, a script will be written to ~/.config/server_box to monitor the system status. You can review the script content.'**
|
||||
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
||||
String get writeScriptTip;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,12 +47,34 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Das Backup wird nur einfach verschlüsselt.\nBitte bewahre die Datei sicher auf.';
|
||||
'Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'Die Backup-Version stimmt nicht überein.';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Backup-Passwort';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Falsches Backup-Passwort';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Backup ist verschlüsselt';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Backup ist nicht verschlüsselt';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Backup-Passwort gesetzt';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Backup-Passwort entfernt';
|
||||
|
||||
@override
|
||||
String get battery => 'Batterie';
|
||||
|
||||
@@ -452,6 +474,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Prozess';
|
||||
|
||||
@override
|
||||
String get prune => 'Beschneiden';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Push Token';
|
||||
|
||||
@@ -467,9 +492,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Diese Funktion befindet sich derzeit in der Testphase und wurde nur auf PVE 8+ getestet. Bitte verwenden Sie sie mit Vorsicht.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Passwort';
|
||||
|
||||
@override
|
||||
String get read => 'Lesen';
|
||||
|
||||
@@ -772,5 +794,5 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'Nach der Verbindung mit dem Server wird ein Skript in ~/.config/server_box geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
||||
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
||||
}
|
||||
|
||||
@@ -47,11 +47,33 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'The exported data is weakly encrypted. \nPlease keep it safe.';
|
||||
'The exported data can be encrypted with password. \nPlease keep it safe.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'Backup version is not match.';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Backup password';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Set a password to encrypt backup files. Leave empty to disable encryption.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Incorrect backup password';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Backup is encrypted';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Backup is not encrypted';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Backup password set';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Backup password removed';
|
||||
|
||||
@override
|
||||
String get battery => 'Battery';
|
||||
|
||||
@@ -450,6 +472,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Process';
|
||||
|
||||
@override
|
||||
String get prune => 'Prune';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Push token';
|
||||
|
||||
@@ -465,9 +490,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Password';
|
||||
|
||||
@override
|
||||
String get read => 'Read';
|
||||
|
||||
@@ -766,5 +788,5 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'After connecting to the server, a script will be written to ~/.config/server_box to monitor the system status. You can review the script content.';
|
||||
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
||||
}
|
||||
|
||||
@@ -47,12 +47,34 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Los datos exportados solo están encriptados de manera básica, por favor guárdalos en un lugar seguro.';
|
||||
'Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'La versión de la copia de seguridad no coincide, no se puede restaurar';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Contraseña de respaldo';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Contraseña de respaldo incorrecta';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'El respaldo está encriptado';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'El respaldo no está encriptado';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Contraseña de respaldo establecida';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Contraseña de respaldo eliminada';
|
||||
|
||||
@override
|
||||
String get battery => 'Batería';
|
||||
|
||||
@@ -454,6 +476,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Proceso';
|
||||
|
||||
@override
|
||||
String get prune => 'Podar';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Token de notificaciones';
|
||||
|
||||
@@ -469,9 +494,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Esta función está actualmente en fase de prueba y solo se ha probado en PVE 8+. Úsela con precaución.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Contraseña';
|
||||
|
||||
@override
|
||||
String get read => 'Leer';
|
||||
|
||||
@@ -774,5 +796,5 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'Después de conectarse al servidor, se escribirá un script en ~/.config/server_box para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
||||
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
||||
}
|
||||
|
||||
@@ -47,12 +47,34 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Les données exportées sont simplement chiffrées. \nVeuillez les garder en sécurité.';
|
||||
'Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'La version de sauvegarde ne correspond pas.';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Mot de passe de sauvegarde';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Mot de passe de sauvegarde incorrect';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'La sauvegarde est chiffrée';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'La sauvegarde n\'est pas chiffrée';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Mot de passe de sauvegarde défini';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Mot de passe de sauvegarde supprimé';
|
||||
|
||||
@override
|
||||
String get battery => 'Batterie';
|
||||
|
||||
@@ -455,6 +477,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Processus';
|
||||
|
||||
@override
|
||||
String get prune => 'Élaguer';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Jeton d\'identification';
|
||||
|
||||
@@ -470,9 +495,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Cette fonctionnalité est actuellement en phase de test et n\'a été testée que sur PVE 8+. Veuillez l\'utiliser avec prudence.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Mot de passe';
|
||||
|
||||
@override
|
||||
String get read => 'Lire';
|
||||
|
||||
@@ -776,5 +798,5 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'Après la connexion au serveur, un script sera écrit dans ~/.config/server_box pour surveiller l’état du système. Vous pouvez examiner le contenu du script.';
|
||||
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l’état du système. Vous pouvez examiner le contenu du script.';
|
||||
}
|
||||
|
||||
@@ -47,11 +47,33 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Data yang diekspor hanya dienkripsi.\nTolong jaga keamanannya.';
|
||||
'Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'Versi cadangan tidak cocok.';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Kata sandi cadangan';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Kata sandi cadangan salah';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Cadangan telah dienkripsi';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Cadangan tidak dienkripsi';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Kata sandi cadangan ditetapkan';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Kata sandi cadangan dihapus';
|
||||
|
||||
@override
|
||||
String get battery => 'Baterai';
|
||||
|
||||
@@ -450,6 +472,9 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Proses';
|
||||
|
||||
@override
|
||||
String get prune => 'Pangkas';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Dorong token';
|
||||
|
||||
@@ -465,9 +490,6 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Fitur ini saat ini sedang dalam tahap pengujian dan hanya diuji pada PVE 8+. Gunakan dengan hati-hati.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Kata sandi';
|
||||
|
||||
@override
|
||||
String get read => 'Baca';
|
||||
|
||||
@@ -765,5 +787,5 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'Setelah terhubung ke server, sebuah skrip akan ditulis ke ~/.config/server_box untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
||||
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
||||
}
|
||||
|
||||
@@ -43,11 +43,33 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => 'ホームウィジェットを自動更新';
|
||||
|
||||
@override
|
||||
String get backupTip => 'エクスポートされたデータは簡単に暗号化されています。適切に保管してください。';
|
||||
String get backupTip => 'エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'バックアップバージョンが一致しないため、復元できません';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'バックアップパスワード';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'バックアップパスワードが間違っています';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'バックアップは暗号化されています';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'バックアップは暗号化されていません';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'バックアップパスワードが設定されました';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'バックアップパスワードが削除されました';
|
||||
|
||||
@override
|
||||
String get battery => 'バッテリー';
|
||||
|
||||
@@ -436,6 +458,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'プロセス';
|
||||
|
||||
@override
|
||||
String get prune => '剪定する';
|
||||
|
||||
@override
|
||||
String get pushToken => 'プッシュトークン';
|
||||
|
||||
@@ -450,9 +475,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get pveVersionLow => 'この機能は現在テスト段階にあり、PVE 8+でのみテストされています。ご利用の際は慎重に。';
|
||||
|
||||
@override
|
||||
String get pwd => 'パスワード';
|
||||
|
||||
@override
|
||||
String get read => '読み取り';
|
||||
|
||||
@@ -745,5 +767,5 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'サーバーに接続すると、システムの状態を監視するためのスクリプトが ~/.config/server_box に書き込まれます。スクリプトの内容を確認できます。';
|
||||
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
||||
}
|
||||
|
||||
@@ -47,11 +47,33 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'De geëxporteerde gegevens zijn simpelweg versleuteld. \nBewaar deze aub veilig.';
|
||||
'De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'Back-upversie komt niet overeen.';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Back-up wachtwoord';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Onjuist back-up wachtwoord';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Back-up is versleuteld';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Back-up is niet versleuteld';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Back-up wachtwoord ingesteld';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Back-up wachtwoord verwijderd';
|
||||
|
||||
@override
|
||||
String get battery => 'Batterij';
|
||||
|
||||
@@ -451,6 +473,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Proces';
|
||||
|
||||
@override
|
||||
String get prune => 'Snoeien';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Push-token';
|
||||
|
||||
@@ -466,9 +491,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Deze functie bevindt zich momenteel in de testfase en is alleen getest op PVE 8+. Gebruik het met voorzichtigheid.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Wachtwoord';
|
||||
|
||||
@override
|
||||
String get read => 'Lezen';
|
||||
|
||||
@@ -771,5 +793,5 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'Na het verbinden met de server wordt een script geschreven naar ~/.config/server_box om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
||||
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
||||
}
|
||||
|
||||
@@ -47,12 +47,34 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Os dados exportados são criptografados de forma simples, por favor, guarde-os com segurança.';
|
||||
'Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'Versão de backup não compatível, não é possível restaurar';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Senha de backup';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Senha de backup incorreta';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Backup está criptografado';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Backup não está criptografado';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Senha de backup definida';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Senha de backup removida';
|
||||
|
||||
@override
|
||||
String get battery => 'Bateria';
|
||||
|
||||
@@ -451,6 +473,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Processo';
|
||||
|
||||
@override
|
||||
String get prune => 'Podar';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Token de notificação push';
|
||||
|
||||
@@ -466,9 +491,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Esta funcionalidade está atualmente em fase de teste e foi testada apenas no PVE 8+. Por favor, use com cautela.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Senha';
|
||||
|
||||
@override
|
||||
String get read => 'Leitura';
|
||||
|
||||
@@ -768,5 +790,5 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'Após conectar ao servidor, um script será escrito em ~/.config/server_box para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
||||
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
||||
}
|
||||
|
||||
@@ -47,12 +47,34 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Экспортированные данные зашифрованы простым способом \nПожалуйста, храните их в безопасности.';
|
||||
'Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'Версия резервной копии не совпадает, восстановление невозможно';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Пароль резервной копии';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Неверный пароль резервной копии';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Резервная копия зашифрована';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Резервная копия не зашифрована';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Пароль резервной копии установлен';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Пароль резервной копии удален';
|
||||
|
||||
@override
|
||||
String get battery => 'Батарея';
|
||||
|
||||
@@ -452,6 +474,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Процесс';
|
||||
|
||||
@override
|
||||
String get prune => 'Обрезать';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Токен уведомлений';
|
||||
|
||||
@@ -467,9 +492,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Пароль';
|
||||
|
||||
@override
|
||||
String get read => 'Чтение';
|
||||
|
||||
@@ -771,5 +793,5 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'После подключения к серверу скрипт будет записан в ~/.config/server_box для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
||||
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
||||
}
|
||||
|
||||
@@ -46,11 +46,33 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Dışa aktarılan veriler zayıf bir şekilde şifrelenmiştir. \nLütfen güvenli bir şekilde saklayın.';
|
||||
'Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'Yedekleme sürümü eşleşmiyor.';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Yedekleme parolası';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Yanlış yedekleme parolası';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Yedekleme şifrelenmiş';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Yedekleme şifreli değil';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Yedekleme parolası ayarlandı';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Yedekleme parolası kaldırıldı';
|
||||
|
||||
@override
|
||||
String get battery => 'Pil';
|
||||
|
||||
@@ -449,6 +471,9 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'İşlem';
|
||||
|
||||
@override
|
||||
String get prune => 'Budamak';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Push belirteci';
|
||||
|
||||
@@ -464,9 +489,6 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Bu özellik şu anda test aşamasında ve yalnızca PVE 8+ üzerinde test edildi. Lütfen dikkatli kullanın.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Şifre';
|
||||
|
||||
@override
|
||||
String get read => 'Oku';
|
||||
|
||||
@@ -766,5 +788,5 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için ~/.config/server_box dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
||||
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
||||
}
|
||||
|
||||
@@ -47,12 +47,34 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Експортовані дані слабо зашифровані. \nБудь ласка, зберігайте їх у безпеці.';
|
||||
'Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'Версія резервного копіювання не збіглася.';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Пароль резервного копіювання';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => 'Неправильний пароль резервного копіювання';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Резервна копія зашифрована';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Резервна копія не зашифрована';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Пароль резервного копіювання встановлено';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Пароль резервного копіювання видалено';
|
||||
|
||||
@override
|
||||
String get battery => 'Акумулятор';
|
||||
|
||||
@@ -454,6 +476,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get process => 'Процес';
|
||||
|
||||
@override
|
||||
String get prune => 'Обрізати';
|
||||
|
||||
@override
|
||||
String get pushToken => 'Надіслати токен';
|
||||
|
||||
@@ -469,9 +494,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get pveVersionLow =>
|
||||
'Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.';
|
||||
|
||||
@override
|
||||
String get pwd => 'Пароль';
|
||||
|
||||
@override
|
||||
String get read => 'Читати';
|
||||
|
||||
@@ -772,5 +794,5 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'Після підключення до сервера скрипт буде записано у ~/.config/server_box для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
||||
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get acceptBeta => '接受测试版更新推送';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => '当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?';
|
||||
String get addSystemPrivateKeyTip => '检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => '已添加至任务列表';
|
||||
@@ -24,13 +24,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get addr => '地址';
|
||||
|
||||
@override
|
||||
String get alreadyLastDir => '已经是最上层目录了';
|
||||
String get alreadyLastDir => '已是顶级目录';
|
||||
|
||||
@override
|
||||
String get authFailTip => '认证失败,请检查密码/密钥/主机/用户等是否错误';
|
||||
String get authFailTip => '认证失败,请检查连接信息是否正确';
|
||||
|
||||
@override
|
||||
String get autoBackupConflict => '只能同时开启一个自动备份';
|
||||
String get autoBackupConflict => '仅可启用一个自动备份任务';
|
||||
|
||||
@override
|
||||
String get autoConnect => '自动连接';
|
||||
@@ -42,10 +42,31 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => '自动更新桌面小部件';
|
||||
|
||||
@override
|
||||
String get backupTip => '导出的数据仅进行了简单加密,请妥善保管。';
|
||||
String get backupTip => '导出数据可通过密码加密,请妥善保管。';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => '备份版本不匹配,无法恢复';
|
||||
String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
|
||||
|
||||
@override
|
||||
String get backupPassword => '备份密码';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip => '设置密码以加密备份文件。留空则禁用加密。';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => '备份密码错误';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => '备份已加密';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => '备份未加密';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => '备份密码已设置';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => '备份密码已移除';
|
||||
|
||||
@override
|
||||
String get battery => '电池';
|
||||
@@ -55,7 +76,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get bgRunTip =>
|
||||
'此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请修改省电策略为“无限制”。';
|
||||
'此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请将省电策略改为“无限制”。';
|
||||
|
||||
@override
|
||||
String get closeAfterSave => '保存后关闭';
|
||||
@@ -111,10 +132,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get desktopTerminalTip => '启动 SSH 连接所用的终端模拟器命令';
|
||||
|
||||
@override
|
||||
String get dirEmpty => '请确保文件夹为空';
|
||||
String get dirEmpty => '请确保目录为空';
|
||||
|
||||
@override
|
||||
String get disconnected => '连接断开';
|
||||
String get disconnected => '已断开连接';
|
||||
|
||||
@override
|
||||
String get disk => '磁盘';
|
||||
@@ -139,11 +160,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String dockerImagesFmt(Object count) {
|
||||
return '共 $count 个镜像';
|
||||
return '$count 个镜像';
|
||||
}
|
||||
|
||||
@override
|
||||
String get dockerNotInstalled => 'Docker 未安装';
|
||||
String get dockerNotInstalled => '未安装 Docker';
|
||||
|
||||
@override
|
||||
String dockerStatusRunningAndStoppedFmt(
|
||||
@@ -162,7 +183,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get doubleColumnMode => '双列模式';
|
||||
|
||||
@override
|
||||
String get doubleColumnTip => '此选项仅开启功能,实际是否能开启还取决于设备的宽度';
|
||||
String get doubleColumnTip => '此选项仅用于启用该功能,是否生效取决于设备宽度';
|
||||
|
||||
@override
|
||||
String get editVirtKeys => '编辑虚拟按键';
|
||||
@@ -171,7 +192,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get editor => '编辑器';
|
||||
|
||||
@override
|
||||
String get editorHighlightTip => '目前的代码高亮性能较为糟糕,可以选择关闭以改善。';
|
||||
String get editorHighlightTip => '代码高亮功能可能影响性能,可选择关闭。';
|
||||
|
||||
@override
|
||||
String get emulator => '模拟器';
|
||||
@@ -225,7 +246,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get fullScreenJitter => '全屏模式抖动';
|
||||
|
||||
@override
|
||||
String get fullScreenJitterHelp => '防止烧屏';
|
||||
String get fullScreenJitterHelp => '用于防止屏幕烧屏';
|
||||
|
||||
@override
|
||||
String get fullScreenTip => '当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器 Tab 页。';
|
||||
@@ -250,7 +271,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String httpFailedWithCode(Object code) {
|
||||
return '请求失败, 状态码: $code';
|
||||
return '请求失败,状态码: $code';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -273,7 +294,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get installDockerWithUrl =>
|
||||
'请先 https://docs.docker.com/engine/install docker';
|
||||
'请先前往 https://docs.docker.com/engine/install 安装 Docker';
|
||||
|
||||
@override
|
||||
String get invalid => '无效';
|
||||
@@ -282,7 +303,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get jumpServer => '跳板服务器';
|
||||
|
||||
@override
|
||||
String get keepForeground => '请保持应用处于前台!';
|
||||
String get keepForeground => '请将应用保持在前台运行';
|
||||
|
||||
@override
|
||||
String get keepStatusWhenErr => '保留上次的服务器状态';
|
||||
@@ -323,7 +344,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get maxRetryCount => '服务器尝试重连次数';
|
||||
|
||||
@override
|
||||
String get maxRetryCountEqual0 => '会无限重试';
|
||||
String get maxRetryCountEqual0 => '将无限次重试';
|
||||
|
||||
@override
|
||||
String get min => '最小';
|
||||
@@ -388,7 +409,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get openLastPath => '打开上次的路径';
|
||||
|
||||
@override
|
||||
String get openLastPathTip => '不同的服务器会有不同的记录,且记录的是退出时的路径';
|
||||
String get openLastPathTip => '将为每台服务器记录其最后访问路径';
|
||||
|
||||
@override
|
||||
String get parseContainerStatsTip => 'Docker 解析占用状态较为缓慢';
|
||||
@@ -431,6 +452,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get process => '进程';
|
||||
|
||||
@override
|
||||
String get prune => '修剪';
|
||||
|
||||
@override
|
||||
String get pushToken => '消息推送 Token';
|
||||
|
||||
@@ -438,14 +462,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get pveIgnoreCertTip => '不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项';
|
||||
|
||||
@override
|
||||
String get pveLoginFailed => '登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。';
|
||||
String get pveLoginFailed => '登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。';
|
||||
|
||||
@override
|
||||
String get pveVersionLow => '当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用';
|
||||
|
||||
@override
|
||||
String get pwd => '密码';
|
||||
|
||||
@override
|
||||
String get read => '读';
|
||||
|
||||
@@ -523,7 +544,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'SFTP 已连接...';
|
||||
String get sftpSSHConnected => 'SFTP 已连接';
|
||||
|
||||
@override
|
||||
String get sftpShowFoldersFirst => '文件夹显示在前';
|
||||
@@ -554,7 +575,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String spentTime(Object time) {
|
||||
return '耗时: $time';
|
||||
return '耗时:$time';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -662,7 +683,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get update => '更新';
|
||||
|
||||
@override
|
||||
String get updateIntervalEqual0 => '你设置为 0,服务器状态不会自动刷新。\n且不能计算 CPU 使用情况。';
|
||||
String get updateIntervalEqual0 => '设置为 0 将不自动刷新服务器状态。\n且无法计算 CPU 使用率。';
|
||||
|
||||
@override
|
||||
String get updateServerStatusInterval => '服务器状态刷新间隔';
|
||||
@@ -722,7 +743,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get whenOpenApp => '当打开 App 时';
|
||||
|
||||
@override
|
||||
String get wolTip => '在配置 WOL 后,每次连接服务器都会先发送一次 WOL 请求';
|
||||
String get wolTip => '配置 WOL 后,每次连接服务器时将自动发送唤醒请求';
|
||||
|
||||
@override
|
||||
String get write => '写';
|
||||
@@ -732,7 +753,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'在连接服务器后,会向 ~/.config/server_box 写入脚本来监测系统状态,你可以审查脚本内容。';
|
||||
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||
@@ -746,66 +767,87 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get acceptBeta => '接受測試版更新推送';
|
||||
|
||||
@override
|
||||
String get addSystemPrivateKeyTip => '當前沒有任何私鑰,是否添加系統自帶的 (~/.ssh/id_rsa)?';
|
||||
String get addSystemPrivateKeyTip => '偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?';
|
||||
|
||||
@override
|
||||
String get added2List => '已添加至任務列表';
|
||||
String get added2List => '已新增至任務清單';
|
||||
|
||||
@override
|
||||
String get addr => '位址';
|
||||
|
||||
@override
|
||||
String get alreadyLastDir => '已經是最上層目錄了';
|
||||
String get alreadyLastDir => '已是頂層目錄';
|
||||
|
||||
@override
|
||||
String get authFailTip => '認證失敗,請檢查密碼/密鑰/主機/用戶等是否錯誤。';
|
||||
String get authFailTip => '認證失敗,請檢查連線資訊是否正確';
|
||||
|
||||
@override
|
||||
String get autoBackupConflict => '只能同時開啓一個自動備份';
|
||||
String get autoBackupConflict => '僅能啟用一項自動備份任務';
|
||||
|
||||
@override
|
||||
String get autoConnect => '自動連接';
|
||||
String get autoConnect => '自動連線';
|
||||
|
||||
@override
|
||||
String get autoRun => '自動運行';
|
||||
String get autoRun => '自動執行';
|
||||
|
||||
@override
|
||||
String get autoUpdateHomeWidget => '自動更新桌面小部件';
|
||||
String get autoUpdateHomeWidget => '自動更新桌面小工具';
|
||||
|
||||
@override
|
||||
String get backupTip => '匯出的資料僅進行了簡單加密,請妥善保管。';
|
||||
String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => '備份版本不匹配,無法還原';
|
||||
String get backupVersionNotMatch => '備份版本不相容,無法還原';
|
||||
|
||||
@override
|
||||
String get backupPassword => '備份密碼';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip => '設定密碼來加密備份檔案。留空則停用加密。';
|
||||
|
||||
@override
|
||||
String get backupPasswordWrong => '備份密碼錯誤';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => '備份已加密';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => '備份未加密';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => '備份密碼已設定';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => '備份密碼已移除';
|
||||
|
||||
@override
|
||||
String get battery => '電池';
|
||||
|
||||
@override
|
||||
String get bgRun => '後台運行';
|
||||
String get bgRun => '背景執行';
|
||||
|
||||
@override
|
||||
String get bgRunTip =>
|
||||
'此開關只代表程式會嘗試在後台運行,具體能否在後臺運行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池優化”,MIUI / HyperOS 請修改省電策略為“無限制”。';
|
||||
'此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。';
|
||||
|
||||
@override
|
||||
String get closeAfterSave => '儲存後關閉';
|
||||
|
||||
@override
|
||||
String get cmd => '命令';
|
||||
String get cmd => '指令';
|
||||
|
||||
@override
|
||||
String get collapseUITip => '是否預設折疊 UI 中存在的長列表';
|
||||
|
||||
@override
|
||||
String get conn => '連接';
|
||||
String get conn => '連線';
|
||||
|
||||
@override
|
||||
String get container => '容器';
|
||||
|
||||
@override
|
||||
String get containerTrySudoTip =>
|
||||
'例如:App 內設置使用者為 aaa,但是 Docker 安裝在 root 使用者,這時就需要開啟此選項';
|
||||
'例如:App 內設定使用者為 aaa,但是 Docker 安裝在 root 使用者,這時就需要開啟此選項';
|
||||
|
||||
@override
|
||||
String get convert => '轉換';
|
||||
@@ -820,14 +862,14 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get cursorType => '游標類型';
|
||||
|
||||
@override
|
||||
String get customCmd => '自訂命令';
|
||||
String get customCmd => '自訂指令';
|
||||
|
||||
@override
|
||||
String get customCmdDocUrl =>
|
||||
'https://github.com/lollipopkit/flutter_server_box/wiki/主页#自定义命令';
|
||||
'https://github.com/lollipopkit/flutter_server_box/wiki/主页#自定义指令';
|
||||
|
||||
@override
|
||||
String get customCmdHint => '\"命令名稱\": \"命令\"';
|
||||
String get customCmdHint => '\"指令名稱\": \"指令\"';
|
||||
|
||||
@override
|
||||
String get decode => '解碼';
|
||||
@@ -836,16 +878,16 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get decompress => '解壓縮';
|
||||
|
||||
@override
|
||||
String get deleteServers => '批量刪除伺服器';
|
||||
String get deleteServers => '大量刪除伺服器';
|
||||
|
||||
@override
|
||||
String get desktopTerminalTip => '啟動 SSH 連線時用於打開終端機模擬器的指令。';
|
||||
|
||||
@override
|
||||
String get dirEmpty => '請確保資料夾為空';
|
||||
String get dirEmpty => '請確保目錄為空';
|
||||
|
||||
@override
|
||||
String get disconnected => '連接斷開';
|
||||
String get disconnected => '已中斷連線';
|
||||
|
||||
@override
|
||||
String get disk => '磁碟';
|
||||
@@ -866,34 +908,34 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get dockerEmptyRunningItems =>
|
||||
'沒有正在運行的容器。\n這可能是因為:\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變量 DOCKER_HOST 沒有被正確讀取。你可以通過在終端內運行 `echo \$DOCKER_HOST` 來獲取。';
|
||||
'沒有正在執行的容器。\n這可能是因為:\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變數 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo \$DOCKER_HOST` 來獲取。';
|
||||
|
||||
@override
|
||||
String dockerImagesFmt(Object count) {
|
||||
return '共 $count 個鏡像';
|
||||
return '$count 個映像檔';
|
||||
}
|
||||
|
||||
@override
|
||||
String get dockerNotInstalled => 'Docker 未安裝';
|
||||
String get dockerNotInstalled => '未安裝 Docker';
|
||||
|
||||
@override
|
||||
String dockerStatusRunningAndStoppedFmt(
|
||||
Object runningCount,
|
||||
Object stoppedCount,
|
||||
) {
|
||||
return '$runningCount 個正在運行, $stoppedCount 個已停止';
|
||||
return '$runningCount 個正在執行, $stoppedCount 個已停止';
|
||||
}
|
||||
|
||||
@override
|
||||
String dockerStatusRunningFmt(Object count) {
|
||||
return '$count 個容器正在運行';
|
||||
return '$count 個容器正在執行';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doubleColumnMode => '雙列模式';
|
||||
|
||||
@override
|
||||
String get doubleColumnTip => '此選項僅開啟功能,實際是否能開啟還取決於設備的寬度';
|
||||
String get doubleColumnTip => '此選項僅用於啟用此功能,是否生效取決於裝置寬度';
|
||||
|
||||
@override
|
||||
String get editVirtKeys => '編輯虛擬按鍵';
|
||||
@@ -902,7 +944,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get editor => '編輯器';
|
||||
|
||||
@override
|
||||
String get editorHighlightTip => '目前的代碼高亮性能較為糟糕,可以選擇關閉以改善。';
|
||||
String get editorHighlightTip => '程式碼高亮功能可能影響效能,可選擇性關閉。';
|
||||
|
||||
@override
|
||||
String get emulator => '模擬器';
|
||||
@@ -911,7 +953,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get encode => '編碼';
|
||||
|
||||
@override
|
||||
String get envVars => '環境變量';
|
||||
String get envVars => '環境變數';
|
||||
|
||||
@override
|
||||
String get experimentalFeature => '實驗性功能';
|
||||
@@ -923,18 +965,18 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get fallbackSshDest => '備選 SSH 目標';
|
||||
|
||||
@override
|
||||
String get fdroidReleaseTip => '如果你是從 F-Droid 下載的本應用,推薦關閉此選項';
|
||||
String get fdroidReleaseTip => '如果你是從 F-Droid 下載的本App,推薦關閉此選項';
|
||||
|
||||
@override
|
||||
String get fgService => '前台服務';
|
||||
|
||||
@override
|
||||
String get fgServiceTip =>
|
||||
'開啟後,可能會導致部分機型閃退。關閉可能導致部分機型無法後台保持 SSH 連接。請在系統設置內允許 ServerBox 通知權限、後台運行、自我喚醒。';
|
||||
'開啟後,可能會導致部分機型閃退。關閉可能導致部分機型無法背景保持 SSH 連線。請在系統設定內允許 ServerBox 通知權限、背景執行、自我喚醒。';
|
||||
|
||||
@override
|
||||
String fileTooLarge(Object file, Object size, Object sizeMax) {
|
||||
return '文件 \'$file\' 過大 \'$size\',超過了 $sizeMax';
|
||||
return '檔案 \'$file\' 過大 \'$size\',超過了 $sizeMax';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -956,10 +998,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get fullScreenJitter => '全螢幕模式抖動';
|
||||
|
||||
@override
|
||||
String get fullScreenJitterHelp => '防止燒屏';
|
||||
String get fullScreenJitterHelp => '防止螢幕烙印';
|
||||
|
||||
@override
|
||||
String get fullScreenTip => '當設備旋轉為橫屏時,是否開啟全熒幕模式?此選項僅適用於伺服器選項卡。';
|
||||
String get fullScreenTip => '當設備旋轉為橫向時,是否開啟全螢幕模式?此選項僅適用於伺服器分頁。';
|
||||
|
||||
@override
|
||||
String get goBackQ => '返回?';
|
||||
@@ -971,27 +1013,27 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get hideTitleBar => '隱藏標題欄';
|
||||
|
||||
@override
|
||||
String get highlight => '代碼高亮';
|
||||
String get highlight => '程式碼標記';
|
||||
|
||||
@override
|
||||
String get homeWidgetUrlConfig => '桌面部件鏈接配置';
|
||||
String get homeWidgetUrlConfig => '桌面小工具連結配置';
|
||||
|
||||
@override
|
||||
String get host => '主機';
|
||||
|
||||
@override
|
||||
String httpFailedWithCode(Object code) {
|
||||
return '請求失敗, 狀態碼: $code';
|
||||
return '請求失敗,狀態碼:$code';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ignoreCert => '忽略證書';
|
||||
String get ignoreCert => '忽略憑證';
|
||||
|
||||
@override
|
||||
String get image => '鏡像';
|
||||
String get image => '映像檔';
|
||||
|
||||
@override
|
||||
String get imagesList => '鏡像列表';
|
||||
String get imagesList => '映像檔列表';
|
||||
|
||||
@override
|
||||
String get init => '初始化';
|
||||
@@ -1004,7 +1046,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get installDockerWithUrl =>
|
||||
'請先 https://docs.docker.com/engine/install docker';
|
||||
'請先前往 https://docs.docker.com/engine/install 安裝 Docker';
|
||||
|
||||
@override
|
||||
String get invalid => '無效';
|
||||
@@ -1013,7 +1055,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get jumpServer => '跳板伺服器';
|
||||
|
||||
@override
|
||||
String get keepForeground => '請保持應用處於前台!';
|
||||
String get keepForeground => '請讓 App 保持在前景執行';
|
||||
|
||||
@override
|
||||
String get keepStatusWhenErr => '保留上次的伺服器狀態';
|
||||
@@ -1022,22 +1064,22 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get keepStatusWhenErrTip => '僅在執行腳本出錯時';
|
||||
|
||||
@override
|
||||
String get keyAuth => '密鑰認證';
|
||||
String get keyAuth => '金鑰認證';
|
||||
|
||||
@override
|
||||
String get letterCache => '输入法字符緩存';
|
||||
String get letterCache => '輸入法字符快取';
|
||||
|
||||
@override
|
||||
String get letterCacheTip => '建議關閉,但關閉後將無法輸入 CJK 等文字。';
|
||||
|
||||
@override
|
||||
String get license => '證書';
|
||||
String get license => '憑證';
|
||||
|
||||
@override
|
||||
String get location => '位置';
|
||||
|
||||
@override
|
||||
String get loss => '丟包率';
|
||||
String get loss => '逾時';
|
||||
|
||||
@override
|
||||
String madeWithLove(Object myGithub) {
|
||||
@@ -1054,7 +1096,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get maxRetryCount => '伺服器嘗試重連次數';
|
||||
|
||||
@override
|
||||
String get maxRetryCountEqual0 => '會無限重試';
|
||||
String get maxRetryCountEqual0 => '將無限次重試';
|
||||
|
||||
@override
|
||||
String get min => '最小';
|
||||
@@ -1074,16 +1116,16 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get needHomeDir =>
|
||||
'如果你是群暉用戶,[看這裡](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。其他系統用戶,需搜索如何創建家目錄(home directory)。';
|
||||
'如果你是群暉使用者,[看這裡](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。其他系統使用者,需搜尋如何建立家目錄(home directory)。';
|
||||
|
||||
@override
|
||||
String get needRestart => '需要重啓 App';
|
||||
String get needRestart => '需要重開 App';
|
||||
|
||||
@override
|
||||
String get net => '網路';
|
||||
|
||||
@override
|
||||
String get netViewType => '網路視圖類型';
|
||||
String get netViewType => '網路檢視類型';
|
||||
|
||||
@override
|
||||
String get newContainer => '新建容器';
|
||||
@@ -1110,7 +1152,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get onServerDetailPage => '在伺服器詳情頁';
|
||||
|
||||
@override
|
||||
String get onlyOneLine => '僅顯示為一行(可滾動)';
|
||||
String get onlyOneLine => '僅顯示為一行(可捲動)';
|
||||
|
||||
@override
|
||||
String get onlyWhenCoreBiggerThan8 => '僅當核心數大於 8 時生效';
|
||||
@@ -1119,10 +1161,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get openLastPath => '打開上次的路徑';
|
||||
|
||||
@override
|
||||
String get openLastPathTip => '不同的伺服器會有不同的記錄,且記錄的是退出時的路徑';
|
||||
String get openLastPathTip => '將為每台伺服器紀錄其最後存取路徑';
|
||||
|
||||
@override
|
||||
String get parseContainerStatsTip => 'Docker 解析佔用狀態較為緩慢';
|
||||
String get parseContainerStatsTip => 'Docker 解析消耗狀態較為緩慢';
|
||||
|
||||
@override
|
||||
String percentOfSize(Object percent, Object size) {
|
||||
@@ -1142,7 +1184,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get pingNoServer => '沒有伺服器可用於 Ping\n請在伺服器 Tab 新增伺服器後再試';
|
||||
|
||||
@override
|
||||
String get pkg => '包管理';
|
||||
String get pkg => '套件管理';
|
||||
|
||||
@override
|
||||
String get plugInType => '插入類型';
|
||||
@@ -1160,28 +1202,28 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get privateKey => '私鑰';
|
||||
|
||||
@override
|
||||
String get process => '行程';
|
||||
String get process => '處理程序';
|
||||
|
||||
@override
|
||||
String get prune => '修剪';
|
||||
|
||||
@override
|
||||
String get pushToken => '消息推送 Token';
|
||||
|
||||
@override
|
||||
String get pveIgnoreCertTip => '不建議啟用,請注意安全風險!如果您使用的是 PVE 的默認證書,則需要啟用此選項。';
|
||||
String get pveIgnoreCertTip => '不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。';
|
||||
|
||||
@override
|
||||
String get pveLoginFailed => '登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。';
|
||||
String get pveLoginFailed => '登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。';
|
||||
|
||||
@override
|
||||
String get pveVersionLow => '此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。';
|
||||
|
||||
@override
|
||||
String get pwd => '密碼';
|
||||
String get read => '讀取';
|
||||
|
||||
@override
|
||||
String get read => '读';
|
||||
|
||||
@override
|
||||
String get reboot => '重启';
|
||||
String get reboot => '重開';
|
||||
|
||||
@override
|
||||
String get rememberPwdInMem => '在記憶體中記住密碼';
|
||||
@@ -1190,13 +1232,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get rememberPwdInMemTip => '用於容器、暫停等';
|
||||
|
||||
@override
|
||||
String get rememberWindowSize => '記住窗口大小';
|
||||
String get rememberWindowSize => '記住視窗大小';
|
||||
|
||||
@override
|
||||
String get remotePath => '遠端路徑';
|
||||
|
||||
@override
|
||||
String get restart => '重啓';
|
||||
String get restart => '重開';
|
||||
|
||||
@override
|
||||
String get result => '結果';
|
||||
@@ -1208,7 +1250,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get route => '路由';
|
||||
|
||||
@override
|
||||
String get run => '運行';
|
||||
String get run => '執行';
|
||||
|
||||
@override
|
||||
String get running => '運作中';
|
||||
@@ -1217,16 +1259,16 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get sameIdServerExist => '已存在相同 ID 的伺服器';
|
||||
|
||||
@override
|
||||
String get save => '保存';
|
||||
String get save => '儲存';
|
||||
|
||||
@override
|
||||
String get saved => '已保存';
|
||||
String get saved => '已儲存';
|
||||
|
||||
@override
|
||||
String get second => '秒';
|
||||
|
||||
@override
|
||||
String get sensors => '感測器';
|
||||
String get sensors => '感應器';
|
||||
|
||||
@override
|
||||
String get sequence => '順序';
|
||||
@@ -1244,17 +1286,17 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get serverOrder => '伺服器順序';
|
||||
|
||||
@override
|
||||
String get sftpDlPrepare => '準備連接至伺服器...';
|
||||
String get sftpDlPrepare => '準備連線至伺服器...';
|
||||
|
||||
@override
|
||||
String get sftpEditorTip =>
|
||||
'如果為空, 使用App內置的文件編輯器。如果有值, 則使用遠程伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。';
|
||||
'如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠端伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。';
|
||||
|
||||
@override
|
||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除文件夾';
|
||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除檔案夾';
|
||||
|
||||
@override
|
||||
String get sftpSSHConnected => 'SFTP 已連接...';
|
||||
String get sftpSSHConnected => 'SFTP 已連線';
|
||||
|
||||
@override
|
||||
String get sftpShowFoldersFirst => '資料夾顯示在前';
|
||||
@@ -1285,16 +1327,16 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String spentTime(Object time) {
|
||||
return '耗時: $time';
|
||||
return '耗時:$time';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'在終端可滾動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。文件圖標會打開當前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容,在未選中並且剪貼簿有內容時貼上內容到終端。代碼圖標會貼上代碼片段到終端並執行。';
|
||||
'在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容,在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。';
|
||||
|
||||
@override
|
||||
String sshTip(Object url) {
|
||||
return '該功能目前處於測試階段。\n\n請在 $url 反饋問題,或者加入我們開發。';
|
||||
return '該功能目前處於測試階段。\n\n請在 $url 回饋問題,或者加入我們開發。';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1322,10 +1364,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get supportFmtArgs => '支援以下格式化參數:';
|
||||
|
||||
@override
|
||||
String get suspend => '挂起';
|
||||
String get suspend => '當機';
|
||||
|
||||
@override
|
||||
String get suspendTip => 'suspend 功能需要 root 權限及 systemd 支持。';
|
||||
String get suspendTip => 'suspend 功能需要 root 權限及 systemd 支援。';
|
||||
|
||||
@override
|
||||
String switchTo(Object val) {
|
||||
@@ -1342,13 +1384,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get system => '系統';
|
||||
|
||||
@override
|
||||
String get tag => '标签';
|
||||
String get tag => '標籤';
|
||||
|
||||
@override
|
||||
String get temperature => '溫度';
|
||||
|
||||
@override
|
||||
String get termFontSizeTip => '此設置將影響終端大小(寬度和高度)。您可以在終端頁面縮放,來調整當前會話的字型大小。';
|
||||
String get termFontSizeTip => '此設定將影響終端機大小(寬度和高度)。您可以在終端機頁面縮放,來調整目前會話的字型大小。';
|
||||
|
||||
@override
|
||||
String get terminal => '终端機';
|
||||
@@ -1393,7 +1435,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get update => '更新';
|
||||
|
||||
@override
|
||||
String get updateIntervalEqual0 => '你設置為 0,伺服器狀態不會自動更新。\n且不能計算CPU使用情況。';
|
||||
String get updateIntervalEqual0 => '設定為 0 將不自動刷新伺服器狀態,\n也無法計算 CPU 使用率。';
|
||||
|
||||
@override
|
||||
String get updateServerStatusInterval => '伺服器狀態更新間隔';
|
||||
@@ -1405,40 +1447,40 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get upsideDown => '上下交換';
|
||||
|
||||
@override
|
||||
String get uptime => '啟動時長';
|
||||
String get uptime => '運作時間';
|
||||
|
||||
@override
|
||||
String get useCdn => '使用 CDN';
|
||||
|
||||
@override
|
||||
String get useCdnTip => '非中國大陸用戶建議使用 CDN,是否使用?';
|
||||
String get useCdnTip => '非中國使用者建議使用 CDN,是否使用?';
|
||||
|
||||
@override
|
||||
String get useNoPwd => '将使用無密碼';
|
||||
String get useNoPwd => '將使用無密碼';
|
||||
|
||||
@override
|
||||
String get usePodmanByDefault => '默認使用 Podman';
|
||||
String get usePodmanByDefault => '預設使用 Podman';
|
||||
|
||||
@override
|
||||
String get used => '已用';
|
||||
String get used => '已使用';
|
||||
|
||||
@override
|
||||
String get view => '視圖';
|
||||
String get view => '檢視';
|
||||
|
||||
@override
|
||||
String get viewErr => '查看錯誤';
|
||||
|
||||
@override
|
||||
String get virtKeyHelpClipboard => '如果終端有選中字元,則復製選中字元至剪貼簿,否則粘貼剪貼簿內容至終端。';
|
||||
String get virtKeyHelpClipboard => '如果終端機有選中字元,則復製選中字元至剪貼簿,否則貼上剪貼簿內容至終端機。';
|
||||
|
||||
@override
|
||||
String get virtKeyHelpIME => '打開/關閉鍵盤';
|
||||
|
||||
@override
|
||||
String get virtKeyHelpSFTP => '在 SFTP 中打開當前路徑。';
|
||||
String get virtKeyHelpSFTP => '在 SFTP 中打開目前路徑。';
|
||||
|
||||
@override
|
||||
String get waitConnection => '請等待連接建立';
|
||||
String get waitConnection => '請等待連線建立';
|
||||
|
||||
@override
|
||||
String get wakeLock => '保持喚醒';
|
||||
@@ -1447,21 +1489,21 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get watchNotPaired => '沒有已配對的 Apple Watch';
|
||||
|
||||
@override
|
||||
String get webdavSettingEmpty => 'WebDav 設置項爲空';
|
||||
String get webdavSettingEmpty => 'WebDav 設定項爲空';
|
||||
|
||||
@override
|
||||
String get whenOpenApp => '當打開 App 時';
|
||||
|
||||
@override
|
||||
String get wolTip => '在配置 WOL(網絡喚醒)後,每次連接伺服器都會先發送一次 WOL 請求。';
|
||||
String get wolTip => '設定 WOL 後,每次連線伺服器時將自動發送喚醒請求';
|
||||
|
||||
@override
|
||||
String get write => '写';
|
||||
String get write => '寫入';
|
||||
|
||||
@override
|
||||
String get writeScriptFailTip => '寫入腳本失敗,可能是沒有權限/目錄不存在等。';
|
||||
|
||||
@override
|
||||
String get writeScriptTip =>
|
||||
'連接到伺服器後,將會在 ~/.config/server_box 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
||||
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/snippet.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||
|
||||
@@ -17,5 +18,6 @@ import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||
AdapterSpec<ServerFuncBtn>(),
|
||||
AdapterSpec<ServerCustom>(),
|
||||
AdapterSpec<WakeOnLanCfg>(),
|
||||
AdapterSpec<SystemType>(),
|
||||
])
|
||||
part 'hive_adapters.g.dart';
|
||||
|
||||
@@ -111,13 +111,15 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
||||
wolCfg: fields[11] as WakeOnLanCfg?,
|
||||
envs: (fields[12] as Map?)?.cast<String, String>(),
|
||||
id: fields[13] == null ? '' : fields[13] as String,
|
||||
customSystemType: fields[14] as SystemType?,
|
||||
disabledCmdTypes: (fields[15] as List?)?.cast<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Spi obj) {
|
||||
writer
|
||||
..writeByte(14)
|
||||
..writeByte(16)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
@@ -145,7 +147,11 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
||||
..writeByte(12)
|
||||
..write(obj.envs)
|
||||
..writeByte(13)
|
||||
..write(obj.id);
|
||||
..write(obj.id)
|
||||
..writeByte(14)
|
||||
..write(obj.customSystemType)
|
||||
..writeByte(15)
|
||||
..write(obj.disabledCmdTypes);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -254,6 +260,8 @@ class VirtKeyAdapter extends TypeAdapter<VirtKey> {
|
||||
return VirtKey.f11;
|
||||
case 43:
|
||||
return VirtKey.f12;
|
||||
case 44:
|
||||
return VirtKey.shift;
|
||||
default:
|
||||
return VirtKey.esc;
|
||||
}
|
||||
@@ -350,6 +358,8 @@ class VirtKeyAdapter extends TypeAdapter<VirtKey> {
|
||||
writer.writeByte(42);
|
||||
case VirtKey.f12:
|
||||
writer.writeByte(43);
|
||||
case VirtKey.shift:
|
||||
writer.writeByte(44);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,3 +563,44 @@ class WakeOnLanCfgAdapter extends TypeAdapter<WakeOnLanCfg> {
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class SystemTypeAdapter extends TypeAdapter<SystemType> {
|
||||
@override
|
||||
final typeId = 9;
|
||||
|
||||
@override
|
||||
SystemType read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return SystemType.linux;
|
||||
case 1:
|
||||
return SystemType.bsd;
|
||||
case 2:
|
||||
return SystemType.windows;
|
||||
default:
|
||||
return SystemType.linux;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SystemType obj) {
|
||||
switch (obj) {
|
||||
case SystemType.linux:
|
||||
writer.writeByte(0);
|
||||
case SystemType.bsd:
|
||||
writer.writeByte(1);
|
||||
case SystemType.windows:
|
||||
writer.writeByte(2);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SystemTypeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Hive CE
|
||||
# Manual modifications may be necessary for certain migrations
|
||||
# Check in to version control
|
||||
nextTypeId: 9
|
||||
nextTypeId: 10
|
||||
types:
|
||||
PrivateKeyInfo:
|
||||
typeId: 1
|
||||
@@ -27,7 +27,7 @@ types:
|
||||
index: 4
|
||||
Spi:
|
||||
typeId: 3
|
||||
nextIndex: 14
|
||||
nextIndex: 16
|
||||
fields:
|
||||
name:
|
||||
index: 0
|
||||
@@ -57,9 +57,13 @@ types:
|
||||
index: 12
|
||||
id:
|
||||
index: 13
|
||||
customSystemType:
|
||||
index: 14
|
||||
disabledCmdTypes:
|
||||
index: 15
|
||||
VirtKey:
|
||||
typeId: 4
|
||||
nextIndex: 44
|
||||
nextIndex: 45
|
||||
fields:
|
||||
esc:
|
||||
index: 0
|
||||
@@ -149,6 +153,8 @@ types:
|
||||
index: 42
|
||||
f12:
|
||||
index: 43
|
||||
shift:
|
||||
index: 44
|
||||
NetViewType:
|
||||
typeId: 5
|
||||
nextIndex: 3
|
||||
@@ -205,3 +211,13 @@ types:
|
||||
index: 1
|
||||
pwd:
|
||||
index: 2
|
||||
SystemType:
|
||||
typeId: 9
|
||||
nextIndex: 3
|
||||
fields:
|
||||
linux:
|
||||
index: 0
|
||||
bsd:
|
||||
index: 1
|
||||
windows:
|
||||
index: 2
|
||||
|
||||
@@ -13,6 +13,7 @@ extension HiveRegistrar on HiveInterface {
|
||||
registerAdapter(ServerFuncBtnAdapter());
|
||||
registerAdapter(SnippetAdapter());
|
||||
registerAdapter(SpiAdapter());
|
||||
registerAdapter(SystemTypeAdapter());
|
||||
registerAdapter(VirtKeyAdapter());
|
||||
registerAdapter(WakeOnLanCfgAdapter());
|
||||
}
|
||||
@@ -26,6 +27,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
registerAdapter(ServerFuncBtnAdapter());
|
||||
registerAdapter(SnippetAdapter());
|
||||
registerAdapter(SpiAdapter());
|
||||
registerAdapter(SystemTypeAdapter());
|
||||
registerAdapter(VirtKeyAdapter());
|
||||
registerAdapter(WakeOnLanCfgAdapter());
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user