Compare commits

..

100 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
584af5423a bump: v1206 2025-08-09 12:45:45 +08:00
lollipopkit🏳️‍⚧️
95f8e571c1 feat: ability to disable monitoring cmds (#840) 2025-08-09 12:37:30 +08:00
lollipopkit🏳️‍⚧️
9c9648656d fix: macOS ssh term unusable (#838) 2025-08-08 18:59:25 +08:00
lollipopkit🏳️‍⚧️
6880bcc192 opt.: m3 layout breakpoints (#837) 2025-08-08 17:12:13 +08:00
lollipopkit🏳️‍⚧️
3a615449e3 feat: Windows compatibility (#836)
* feat: win compatibility

* fix

* fix: uptime parse

* opt.: linux uptime accuracy

* fix: windows temperature fetching

* opt.

* opt.: powershell exec

* refactor: address PR review feedback and improve code quality

### Major Improvements:
- **Refactored Windows status parsing**: Broke down large `_getWindowsStatus` method into 13 smaller, focused helper methods for better maintainability and readability
- **Extracted system detection logic**: Created dedicated `SystemDetector` helper class to separate OS detection concerns from ServerProvider
- **Improved concurrency handling**: Implemented proper synchronization for server updates using Future-based locks to prevent race conditions

### Bug Fixes:
- **Fixed CPU percentage parsing**: Removed incorrect '*100' multiplication in BSD CPU parsing (values were already percentages)
- **Enhanced memory parsing**: Added validation and error handling to BSD memory fallback parsing with proper logging
- **Improved uptime parsing**: Added support for multiple Windows date formats and robust error handling with validation
- **Fixed division by zero**: Added safety checks in Swap.usedPercent getter

### Code Quality Enhancements:
- **Added comprehensive documentation**: Documented Windows CPU counter limitations and approach
- **Strengthened error handling**: Added detailed logging and validation throughout parsing methods
- **Improved robustness**: Enhanced BSD CPU parsing with percentage validation and warnings
- **Better separation of concerns**: Each parsing method now has single responsibility

### Files Changed:
- `lib/data/helper/system_detector.dart` (new): System detection helper
- `lib/data/model/server/cpu.dart`: Fixed percentage parsing and added validation
- `lib/data/model/server/memory.dart`: Enhanced fallback parsing and division-by-zero protection
- `lib/data/model/server/server_status_update_req.dart`: Refactored into 13 focused parsing methods
- `lib/data/provider/server.dart`: Improved synchronization and extracted system detection

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: parse & shell fn struct

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-08 16:56:36 +08:00
lollipopkit🏳️‍⚧️
46a12bc844 bump: v1201 2025-07-28 22:27:56 +08:00
lollipopkit🏳️‍⚧️
8d597294a4 feat: amd gpu (#831) 2025-07-28 22:26:29 +08:00
lollipopkit🏳️‍⚧️
682a6e4f2d feat: custom pwd of bak (#827) 2025-07-25 16:38:28 +08:00
lollipopkit🏳️‍⚧️
8c3302cf0d chore: update script location in Attention notice (#825)
Fixes #824
2025-07-21 16:42:16 +08:00
lollipopkit🏳️‍⚧️
ec4bf3df24 opt.: sftp dl 2025-07-21 16:20:27 +08:00
lollipopkit🏳️‍⚧️
263d4eabb4 feat: store critical data in secure store (#821) 2025-07-17 18:26:34 +08:00
lollipopkit🏳️‍⚧️
c6439673b8 feat: shift key in ssh term (#819) 2025-07-17 18:18:18 +08:00
lollipopkit🏳️‍⚧️
a35d21981b opt.: watch sync mechanism (#817)
* opt.: watch sync mechanism
Fixes #816

* opt.
2025-07-17 16:55:56 +08:00
Tom
dbc873c0c0 feat: enhance server card layout and add logo display functionality (#804) 2025-06-27 18:55:48 +08:00
Integral
e69808a2f6 fix: disable APK signing block to resolve F-Droid build issues (#793)
Thanks for the patch from @linsui.
2025-06-16 01:51:30 +08:00
lollipopkit🏳️‍⚧️
55b3ba63ec opt.: ui 2025-06-12 22:04:03 +08:00
ИEØ_ΙΙØZ
006e66d825 update: app_zh_tw.arb (#790) 2025-06-11 17:07:22 +08:00
lollipopkit🏳️‍⚧️
c556c0f1b5 bump: v1189 2025-06-11 17:02:47 +08:00
lollipopkit🏳️‍⚧️
c42c701ffc bug: incorrect disk smart info (#789) 2025-06-11 16:45:25 +08:00
lollipopkit🏳️‍⚧️
e6db2db320 fix: container not working (#787) 2025-06-11 14:53:43 +08:00
lollipopkit🏳️‍⚧️
66ecb02d9e migrate: freezed v3 2025-06-10 14:27:27 +08:00
lollipopkit🏳️‍⚧️
8e7de604ee fix: linux build 2025-06-09 18:50:01 +08:00
lollipopkit🏳️‍⚧️
6f2a58ce18 bump: v1184 2025-06-09 16:01:39 +08:00
lollipopkit🏳️‍⚧️
066629d7e0 fix: android build 2025-06-08 20:59:58 +08:00
lollipopkit🏳️‍⚧️
4b3953e0d2 readd: serverTabPreferDiskAmount (#780)
Fixes #643
2025-06-08 11:15:54 +08:00
lollipopkit🏳️‍⚧️
b5aec55106 fix android reload when physical keyboard changes (#779) 2025-06-08 10:46:47 +08:00
lollipopkit🏳️‍⚧️
ba686db847 fix: ssh terminal ui 2025-06-07 17:18:42 +08:00
lollipopkit🏳️‍⚧️
4d52023982 opt.: ssh terminal ux (#778) 2025-06-07 17:07:13 +08:00
lollipopkit🏳️‍⚧️
7a71a96442 fix: examples UI of importing (#777)
Fixes #601
2025-06-05 09:22:54 +08:00
lollipopkit🏳️‍⚧️
79c515c903 new: bio_auth -> local_auth (#776)
Fixes #722
2025-06-05 09:07:28 +08:00
lollipopkit🏳️‍⚧️
4701757857 feat: SSH page background (#775) 2025-06-05 08:53:00 +08:00
lollipopkit🏳️‍⚧️
176cb7da03 feat: disk smart info (#773) 2025-06-05 07:31:45 +08:00
lollipopkit🏳️‍⚧️
741a6442e0 fix: batch delete servers (#772) 2025-06-04 19:28:58 +08:00
lollipopkit🏳️‍⚧️
f6d394c71e opt.: custom terminal emulator (#771) 2025-06-04 19:13:31 +08:00
lollipopkit🏳️‍⚧️
7127c960f7 opt.: server detail page columns 2025-06-04 17:29:03 +08:00
lollipopkit🏳️‍⚧️
1084c49a5f opt.: ui 2025-06-04 01:52:27 +08:00
lollipopkit🏳️‍⚧️
bc824691e0 opt.: server card loading UI 2025-06-04 00:47:18 +08:00
lollipopkit🏳️‍⚧️
0c1ada0067 fix: cloud sync (#769) 2025-06-04 00:11:31 +08:00
lollipopkit🏳️‍⚧️
9547d92ac5 migrate: flutter 3.32 2025-05-25 17:05:46 +08:00
lollipopkit🏳️‍⚧️
7e16d2f159 new: parse disk info via lsblk output Fixes #709 (#760) 2025-05-17 00:45:38 +08:00
lollipopkit🏳️‍⚧️
d88e97e699 new: use generated ids for servers (#765)
* new: use generated ids for servers
Fixes #743

* fix: deps.

* fix: migrate related settings

* fix: restore servers from json
2025-05-16 21:50:44 +08:00
lollipopkit🏳️‍⚧️
d29bd1d806 opt.: ssh page sftp path checking (#763) 2025-05-15 21:01:10 +08:00
lollipopkit🏳️‍⚧️
2b2f1ddb60 opt.: handle esc btn in ssh page (#761) 2025-05-15 20:35:45 +08:00
lollipopkit🏳️‍⚧️
4f16d510c8 fix: code editor page popping (#759)
Fixes #713
2025-05-14 18:58:58 +08:00
lollipopkit🏳️‍⚧️
94cded39a6 fix: horizontal ssh virt keys ui (#758)
Fixes #737
2025-05-14 18:21:18 +08:00
lollipopkit🏳️‍⚧️
12082e1235 chore: README 2025-05-14 16:54:28 +08:00
lollipopkit🏳️‍⚧️
28e34e2183 opt.: editor lang parse 2025-05-14 16:19:26 +08:00
lollipopkit🏳️‍⚧️
4d45d01074 feat: searching in editor page (#756)
* feat: searching in editor page
Fixes #734

* opt.: editor searching ui
2025-05-14 05:09:32 +08:00
lollipopkit🏳️‍⚧️
f6b3ec2a62 opt.: editor (#755)
Fixes #753
2025-05-14 04:07:21 +08:00
lollipopkit🏳️‍⚧️
d6cf33fb70 bug: can't select file on macOS (#754)
Fixes #750
2025-05-14 04:04:19 +08:00
lollipopkit🏳️‍⚧️
1eea133b69 opt.: appbar scrolledUnderElevation (#752)
Fixes #751
2025-05-14 04:02:30 +08:00
lollipopkit🏳️‍⚧️
2b46cb6dcc fix: android monochrome icon (#749)
Fixes #732
2025-05-14 03:22:02 +08:00
lollipopkit🏳️‍⚧️
8627ff823f optimization: desktop UI (#747) 2025-05-13 04:57:37 +08:00
lollipopkit🏳️‍⚧️
e520929411 chore: migrate fl_lib 2025-04-28 23:15:54 +08:00
lollipopkit🏳️‍⚧️
8f09085cf3 Merge remote-tracking branch 'origin/lollipopkit/issue727' 2025-04-25 18:33:36 +08:00
lollipopkit🏳️‍⚧️
9e66071cb0 opt. 2025-04-25 18:32:29 +08:00
Noo6
fa90c1ef31 opt: navigation bar (#740) 2025-04-22 11:54:29 +08:00
Noo6
ede238c647 feat: adaptive navigation bar (#739) 2025-04-22 11:19:19 +08:00
lollipopkit🏳️‍⚧️
6e7fee20b8 opt.: routes 2025-04-10 15:28:47 +08:00
lollipopkit🏳️‍⚧️
391e4f6b65 opt.: page struct 2025-04-09 12:15:42 +08:00
lollipopkit🏳️‍⚧️
e185414355 fix: logo url dist null check 2025-04-08 14:49:26 +08:00
lollipopkit🏳️‍⚧️
2a2f348063 migrate: fl_lib 2025-03-24 23:07:52 +08:00
moli765
95ca6bcfc9 reslove issue 717 about logo url and add coreelec support (#718) 2025-03-22 23:19:54 +08:00
lollipopkit🏳️‍⚧️
275041247a migrate: webdav_client_plus (#729)
Fixes #723
2025-03-22 01:27:17 +08:00
lollipopkit🏳️‍⚧️
24d64b835d opt.: app bar
Fixes #727
2025-03-20 20:20:13 +08:00
lollipopkit🏳️‍⚧️
dd5fea09b1 opt.: skip updating home widget on desktop 2025-03-13 16:07:45 +08:00
𝗛𝗼𝗹𝗶
0a404e035e Improve Turkish Language (#721) 2025-03-10 15:21:17 +08:00
Noo6
b5ab5b1cab fix: window title bar might not be displayed (#701) 2025-02-12 13:33:49 +08:00
Noo6
5cb83001c6 opt: windows app icon (#700) 2025-02-12 13:17:54 +08:00
Noo6
20a39f0292 feat: record window position (#692) 2025-02-05 20:59:04 +08:00
Noo6
900686f955 chore: Fns & FnRes (#690) 2025-02-04 22:41:03 +08:00
Calvin lin
a10321e3de Update release.yml
It may work now :)
2025-02-03 16:22:18 +08:00
Calvin lin
0691ab2213 Update release.yml 2025-02-03 16:16:45 +08:00
Calvin lin
ef05203ea3 Update release.yml 2025-02-03 16:08:31 +08:00
Calvin lin
28410707a8 Update release.yml 2025-02-03 16:03:49 +08:00
Calvin lin
06b966caa8 Merge pull request #687 from lollipopkit/fix_action_linux
fix github action build linux
2025-02-03 16:01:36 +08:00
calvin
11b0806083 fix github action build linux 2025-02-03 15:48:19 +08:00
lollipopkit🏳️‍⚧️
749fd4d800 fix: ci 2025-01-29 23:56:27 +08:00
lollipopkit🏳️‍⚧️
bec4a3b314 chore: bump version 2025-01-29 23:44:44 +08:00
lollipopkit🏳️‍⚧️
9e5babec76 opt.: close after saving (#684) 2025-01-29 15:10:50 +08:00
lollipopkit🏳️‍⚧️
dbbb10364b fix: webdav settings (#683) 2025-01-29 13:13:12 +08:00
lollipopkit🏳️‍⚧️
16948c3e0f new: provide .deb & .rpm 2025-01-29 12:56:03 +08:00
lollipopkit🏳️‍⚧️
e39fb23b66 bug: unix perm switcher (#674) 2025-01-14 11:19:47 +08:00
lollipopkit🏳️‍⚧️
4777166dd9 migrate: fl_lib v235 2025-01-13 21:57:12 +08:00
Noo6
0ae0241800 opt: window title bar (#672)
* opt: window title bar

* rm: `VirtualWindowFrame` on `SettingsPage`
2025-01-10 15:19:03 +08:00
lollipopkit🏳️‍⚧️
e7a5f43cc4 fix: disabled android service related (#670)
Fixes #662
2025-01-07 20:36:12 +08:00
lollipopkit🏳️‍⚧️
7f58237589 fix: catch crash of fg service (#669) 2025-01-04 16:22:20 +08:00
lollipopkit🏳️‍⚧️
0bbd0b43b3 opt.: display settings btn in fullscreen mode (#660) 2024-12-15 23:53:31 +08:00
lollipopkit🏳️‍⚧️
aaa1eddeaf opt.: display err if home widget fails (#659) 2024-12-15 23:39:38 +08:00
lollipopkit🏳️‍⚧️
2f6db2961f fix: crash while opening terminal (#658)
Fixes #639
2024-12-14 21:06:37 +08:00
lollipopkit
831efa833b fix: file_picker err 2024-12-14 16:21:25 +08:00
lollipopkit
867fcbfc0d chore: migrate to flutter 3.27 2024-12-14 14:45:04 +08:00
lollipopkit
41886be649 opt.: home top bar 2024-12-14 14:11:26 +08:00
lollipopkit
029b4e0dba chore: README 2024-12-03 00:13:58 +08:00
lollipopkit🏳️‍⚧️
3a3c29764a bug: can't share server via qr_code (#651)
Fixes #650
2024-12-02 22:22:14 +08:00
lollipopkit
4ace4af7da opt.: home ui
- new: top left settings btn
- opt.: top logo
2024-12-02 21:41:17 +08:00
lollipopkit🏳️‍⚧️
ddd32e82d4 opt.: migrate to new fl_lib (#649)
Fixes #648
2024-12-02 21:06:44 +08:00
dsvf
b882baeafa Added Function keys (F1-F12) to SSH virtual keyboard options (#641) 2024-11-24 13:19:20 +08:00
fei1025
046f2c06d0 修复路径在windos下读取不到的问题 (#630)
* 修复路径在windos下读取不到的问题

* opt: `local.dart` fmt

---------

Co-authored-by: Noo6 <72285529+No06@users.noreply.github.com>
2024-11-14 14:42:15 +08:00
Noo6
d706886343 fix: sftp open file on windows (#633) 2024-11-14 14:24:57 +08:00
250 changed files with 34554 additions and 9997 deletions

View File

@@ -1,6 +1,7 @@
name: Flutter Release
on:
workflow_dispatch:
push:
tags:
- "v*"
@@ -18,12 +19,12 @@ jobs:
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.24.1'
channel: "stable"
flutter-version: "3.32.2"
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
distribution: "zulu"
java-version: "17"
- name: Fetch secrets
run: |
curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key
@@ -53,14 +54,28 @@ jobs:
uses: actions/checkout@v4
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Install dependencies
run: |
sudo apt update
# Basic
sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev libvulkan-dev desktop-file-utils wget
# App Specific
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev
# Packaging
sudo wget https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage -O /bin/appimagetool
sudo chmod +x /bin/appimagetool
- name: Build
run: |
dart run fl_build -p linux
- name: Rename artifacts
run: |
appimage_name=$(ls dist/*/*.AppImage)
mv $appimage_name ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -90,9 +105,9 @@ jobs:
# uses: actions/checkout@v4
# - name: Install Flutter
# uses: subosito/flutter-action@v2
# with:
# channel: 'stable'
# flutter-version: '3.22.2'
# with:
# channel: 'stable'
# flutter-version: '3.32.1'
# - name: Build
# run: dart run fl_build -p ios,mac
# - name: Create Release

2
.gitignore vendored
View File

@@ -46,6 +46,7 @@ app.*.map.json
/android/app/release
/android/app/fjy.androidstudio.key
/android/app/app.key
/release
test.dart
@@ -64,3 +65,4 @@ untranlated.json
.vscode/settings.json
more_build_data.json
trans.txt
android/app/.cxx

View File

@@ -6,16 +6,17 @@ English | [简体中文](README_zh.md)
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
<img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow">
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</div>
<p align="center">
A Flutter project which provide charts to display <a href="../../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>
## 🏙️ Screenshots
<table>
<tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
@@ -25,27 +26,26 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
</tr>
</table>
## 📥 Installation
## 📥 Install
Platform | From
--- | ---
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?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.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
|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**!
## 🔖 Features
## 🔖 Feature
- `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>
@@ -54,30 +54,35 @@ Please only download pkgs from the source that **you trust**!
- **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki).
Before you open an issue, please read the following:
1. Paste the **entire log** (click the top right of the home page) in the issue template.
2. Make sure whether the issue is caused by ServerBox app.
3. Welcome all valid and positive feedback, subjective feedback (such as you think other UI is better) may not be accepted.
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
## 🧱 Contributions
## 🧱 Contribution
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.
2. Clone this repo, run `flutter run` to start the app.
3. Run `dart run fl_build -p PLATFORM` to build the app.
### Translation
- [Guide](https://blog.lpkt.cn/posts/faq/) can be found in my blog.
- We need your help! Just feel free to open a PR.
## 💡 My other apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms.
- [More](https://github.com/lollipopkit) - Tools & etc.
## 📝 License
`GPL v3 lollipopkit`

View File

@@ -6,16 +6,17 @@
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
<img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow">
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</div>
<p align="center">
使用 Flutter 开发的 <a href="../../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>
@@ -25,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.lolli.tech/serverbox/?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.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
平台|下载
--|--
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, 简体中文
@@ -46,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>
@@ -58,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.lolli.tech/faq/) 可在我的博客中找到。
[指南](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`

View File

@@ -11,11 +11,13 @@ include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- '**/*.g.dart'
- "**/*.g.dart"
language:
# strict-casts: true
# strict-inference: true
# strict-raw-types: true
errors:
invalid_annotation_target: ignore
linter:
# The lint rules applied to this project can be customized in the
@@ -41,8 +43,9 @@ linter:
annotate_overrides: true
avoid_empty_else: true
# avoid_print: false # Uncomment to disable the `avoid_print` rule
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
avoid_return_types_on_setters: true
directives_ordering: true # Enable sorting of imports
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -85,13 +85,20 @@ android {
}
debug {
applicationIdSuffix '.debug'
// No applicationIdSuffix or resValue here
}
profile {
applicationIdSuffix '.debug'
// 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 {

View File

@@ -11,10 +11,11 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="ServerBox"
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">
@@ -23,7 +24,7 @@
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|layoutDirection|fontScale|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as

View File

@@ -4,85 +4,158 @@ import android.app.*
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import java.io.File
import java.util.*
class ForegroundService : Service() {
private val chanId = "ForegroundServiceChannel"
private fun logError(message: String, error: Throwable? = null) {
Log.e("ForegroundService", message, error)
try {
val logFile = File(getExternalFilesDir(null), "server_box.log")
val timestamp = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
val logMessage = "$timestamp [ForegroundService] ERROR: $message\n${error?.stackTraceToString() ?: ""}\n"
logFile.appendText(logMessage)
} catch (e: Exception) {
Log.e("ForegroundService", "Failed to write log", e)
}
}
override fun onCreate() {
super.onCreate()
Log.d("ForegroundService", "Service onCreate")
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
"ACTION_STOP_FOREGROUND" -> {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
androidx.core.content.ContextCompat.checkSelfPermission(
this, android.Manifest.permission.POST_NOTIFICATIONS
) != android.content.pm.PackageManager.PERMISSION_GRANTED
) {
Log.w("ForegroundService", "Notification permission denied. Stopping service.")
stopForegroundService()
return START_NOT_STICKY
}
else -> {
val notification = createNotification()
startForeground(1, notification)
return START_STICKY
if (intent == null) {
Log.w("ForegroundService", "onStartCommand called with null intent")
stopForegroundService()
return START_NOT_STICKY
}
val action = intent.action
Log.d("ForegroundService", "onStartCommand action=$action")
// Create notification before starting foreground
val notification = createNotification()
// Use try-catch for startForeground
try {
startForeground(1, notification)
} catch (e: Exception) {
logError("Failed to start foreground", e)
stopSelf()
return START_NOT_STICKY
}
return when (action) {
"ACTION_STOP_FOREGROUND" -> {
stopForegroundService()
START_NOT_STICKY
}
else -> {
START_STICKY
}
}
} catch (e: Exception) {
logError("Error in onStartCommand", e)
stopSelf()
return START_NOT_STICKY
}
}
override fun onBind(intent: Intent): IBinder? {
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
if (manager == null) {
Log.e("ForegroundService", "Failed to get NotificationManager")
return
}
val serviceChannel = NotificationChannel(
chanId,
chanId,
"ForegroundServiceChannel",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
).apply {
description = "For foreground service"
}
manager.createNotificationChannel(serviceChannel)
}
}
private fun createNotification(): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE
)
try {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE
)
val deleteIntent = Intent(this, ForegroundService::class.java).apply {
action = "ACTION_STOP_FOREGROUND"
}
val deletePendingIntent = PendingIntent.getService(
this,
0,
deleteIntent,
PendingIntent.FLAG_IMMUTABLE
)
val deleteIntent = Intent(this, ForegroundService::class.java).apply {
action = "ACTION_STOP_FOREGROUND"
}
val deletePendingIntent = PendingIntent.getService(
this,
0,
deleteIntent,
PendingIntent.FLAG_IMMUTABLE
)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
} else {
Notification.Builder(this)
}
return builder
.setContentTitle("Server Box")
.setContentText("Open the app")
.setContentText("Running in background")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
.build()
} else {
Notification.Builder(this)
} catch (e: Exception) {
logError("Error creating notification", e)
// Return a basic notification as fallback
return Notification.Builder(this)
.setContentTitle("Server Box")
.setContentText("Open the app")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
.build()
}
}
fun stopForegroundService() {
stopForeground(true)
private fun stopForegroundService() {
try {
stopForeground(true)
} catch (e: Exception) {
logError("Error stopping foreground", e)
}
stopSelf()
Log.d("ForegroundService", "ForegroundService stopped")
}
override fun onDestroy() {
super.onDestroy()
Log.d("ForegroundService", "Service onDestroy")
}
}

View File

@@ -9,13 +9,15 @@ import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.appwidget.AppWidgetManager
import tech.lolli.toolbox.widget.HomeWidget
class MainActivity: FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger
MethodChannel(binaryMessenger, "tech.lolli.toolbox/app_retain").apply {
MethodChannel(binaryMessenger, "tech.lolli.toolbox/main_chan").apply {
setMethodCallHandler { method, result ->
when (method.method) {
"sendToBackground" -> {
@@ -23,12 +25,19 @@ class MainActivity: FlutterFragmentActivity() {
result.success(null)
}
"startService" -> {
reqPerm()
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
try {
reqPerm()
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
result.success(null)
} catch (e: Exception) {
// Log error but don't crash
android.util.Log.e("MainActivity", "Failed to start service: ${e.message}")
result.error("SERVICE_ERROR", e.message, null)
}
}
"stopService" -> {
@@ -36,6 +45,12 @@ class MainActivity: FlutterFragmentActivity() {
stopService(serviceIntent)
result.success(null)
}
"updateHomeWidget" -> {
val intent = Intent(this@MainActivity, HomeWidget::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
sendBroadcast(intent)
result.success(null)
}
else -> {
result.notImplemented()
}
@@ -46,13 +61,21 @@ class MainActivity: FlutterFragmentActivity() {
private fun reqPerm() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
// Check if we already have the permission to avoid unnecessary prompts
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
123,
)
try {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
123,
)
} catch (e: Exception) {
// Log error but don't crash
android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}")
}
}
}
}

View File

@@ -6,15 +6,18 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import tech.lolli.toolbox.R
import java.net.URL
import java.net.HttpURLConnection
import java.io.FileNotFoundException
class HomeWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
@@ -23,7 +26,6 @@ class HomeWidget : AppWidgetProvider() {
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.home_widget)
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
@@ -36,6 +38,10 @@ class HomeWidget : AppWidgetProvider() {
url = gUrl
}
if (url.isNullOrEmpty()) {
Log.e("HomeWidget", "URL not found")
}
val intentUpdate = Intent(context, HomeWidget::class.java)
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = intArrayOf(appWidgetId)
@@ -54,11 +60,13 @@ class HomeWidget : AppWidgetProvider() {
views.setOnClickPendingIntent(R.id.widget_container, pendingUpdate)
if (url.isNullOrEmpty()) {
views.setViewVisibility(R.id.widget_cpu_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_mem_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_disk_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_net_label, View.INVISIBLE)
views.setTextViewText(R.id.widget_name, "ID: $appWidgetId")
views.setTextViewText(R.id.widget_name, "No URL")
// Update the widget to display a message for missing URL
views.setViewVisibility(R.id.error_message, View.VISIBLE)
views.setTextViewText(R.id.error_message, "Please configure the widget URL.")
views.setViewVisibility(R.id.widget_content, View.GONE)
views.setFloat(R.id.widget_name, "setAlpha", 1f)
views.setFloat(R.id.error_message, "setAlpha", 1f)
appWidgetManager.updateAppWidget(appWidgetId, views)
return
} else {
@@ -68,44 +76,53 @@ class HomeWidget : AppWidgetProvider() {
views.setViewVisibility(R.id.widget_net_label, View.VISIBLE)
}
GlobalScope.launch(Dispatchers.IO) {
CoroutineScope(Dispatchers.IO).launch {
try {
val jsonStr = URL(url).readText()
val jsonObject = JSONObject(jsonStr)
val data = jsonObject.getJSONObject("data")
val server = data.getString("name")
val cpu = data.getString("cpu")
val mem = data.getString("mem")
val disk = data.getString("disk")
val net = data.getString("net")
GlobalScope.launch(Dispatchers.Main) main@ {
// mem or disk is empty -> get status failed
// (cpu | net) isEmpty -> data is not ready
if (mem.isEmpty() || disk.isEmpty()) {
return@main
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
val jsonObject = JSONObject(jsonStr)
val data = jsonObject.getJSONObject("data")
val server = data.getString("name")
val cpu = data.getString("cpu")
val mem = data.getString("mem")
val disk = data.getString("disk")
val net = data.getString("net")
withContext(Dispatchers.Main) {
if (mem.isEmpty() || disk.isEmpty()) {
Log.e("HomeWidget", "Failed to retrieve status: Memory or disk information is empty")
return@withContext
}
views.setTextViewText(R.id.widget_name, server)
views.setTextViewText(R.id.widget_cpu, cpu)
views.setTextViewText(R.id.widget_mem, mem)
views.setTextViewText(R.id.widget_disk, disk)
views.setTextViewText(R.id.widget_net, net)
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
views.setTextViewText(R.id.widget_time, timeStr)
views.setFloat(R.id.widget_name, "setAlpha", 1f)
views.setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
views.setFloat(R.id.widget_mem_label, "setAlpha", 1f)
views.setFloat(R.id.widget_disk_label, "setAlpha", 1f)
views.setFloat(R.id.widget_net_label, "setAlpha", 1f)
views.setFloat(R.id.widget_time, "setAlpha", 1f)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
views.setTextViewText(R.id.widget_name, server)
views.setTextViewText(R.id.widget_cpu, cpu)
views.setTextViewText(R.id.widget_mem, mem)
views.setTextViewText(R.id.widget_disk, disk)
views.setTextViewText(R.id.widget_net, net)
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
views.setTextViewText(R.id.widget_time, timeStr)
appWidgetManager.updateAppWidget(appWidgetId, views)
} else {
throw FileNotFoundException("HTTP response code: $responseCode")
}
} catch (e: Exception) {
println("ServerBoxHomeWidget: ${e.localizedMessage}")
GlobalScope.launch(Dispatchers.Main) main@ {
views.setViewVisibility(R.id.widget_cpu_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_mem_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_disk_label, View.INVISIBLE)
views.setViewVisibility(R.id.widget_net_label, View.INVISIBLE)
views.setTextViewText(R.id.widget_name, "ID: $appWidgetId")
views.setTextViewText(R.id.widget_mem, e.localizedMessage)
Log.e("HomeWidget", "Error updating widget: ${e.localizedMessage}", e)
withContext(Dispatchers.Main) {
views.setTextViewText(R.id.widget_name, "Error")
// Update the widget to display a message for data retrieval failure
views.setViewVisibility(R.id.error_message, View.VISIBLE)
views.setTextViewText(R.id.error_message, "Failed to retrieve data.")
views.setViewVisibility(R.id.widget_content, View.GONE)
views.setFloat(R.id.widget_name, "setAlpha", 1f)
views.setFloat(R.id.error_message, "setAlpha", 1f)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}

View File

@@ -16,124 +16,151 @@
android:textSize="23sp"
android:textStyle="bold"
android:maxLines="1"
android:alpha="0"
android:animateLayoutChanges="true"
tools:text="Server Name" />
<RelativeLayout
android:id="@+id/widget_container_inner"
<!-- Wrap the content in a LinearLayout for easy visibility management -->
<LinearLayout
android:id="@+id/widget_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@id/widget_name"
android:paddingTop="13dp">
<LinearLayout
android:id="@+id/widget_cpu_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="2.7dp"
<RelativeLayout
android:id="@+id/widget_container_inner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal">
android:paddingTop="13dp"
android:animateLayoutChanges="true">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/speed_24">
</ImageView>
<TextView
android:id="@+id/widget_cpu"
android:layout_width="match_parent"
<LinearLayout
android:id="@+id/widget_cpu_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:singleLine="true"
android:ellipsize = "marquee"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="CPU" />
android:paddingBottom="2.7dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/speed_24">
</ImageView>
<TextView
android:id="@+id/widget_cpu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:singleLine="true"
android:ellipsize = "marquee"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="CPU" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/widget_mem_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="2.7dp"
android:layout_below="@id/widget_cpu_label"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/memory_24">
</ImageView>
<TextView
android:id="@+id/widget_mem"
android:layout_width="match_parent"
<LinearLayout
android:id="@+id/widget_mem_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:maxLines="1"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Mem" />
android:paddingBottom="2.7dp"
android:layout_below="@id/widget_cpu_label"
android:gravity="center_vertical"
android:orientation="horizontal">
</LinearLayout>
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/memory_24">
</ImageView>
<LinearLayout
android:id="@+id/widget_disk_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="2.7dp"
android:layout_below="@id/widget_mem_label"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/widget_mem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:maxLines="1"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Mem" />
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/storage_24">
</ImageView>
</LinearLayout>
<TextView
android:id="@+id/widget_disk"
android:layout_width="match_parent"
<LinearLayout
android:id="@+id/widget_disk_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:maxLines="1"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Disk" />
android:paddingBottom="2.7dp"
android:layout_below="@id/widget_mem_label"
android:gravity="center_vertical"
android:orientation="horizontal">
</LinearLayout>
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/storage_24">
</ImageView>
<LinearLayout
android:id="@+id/widget_net_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/widget_disk_label"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/widget_disk"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:maxLines="1"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Disk" />
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/net_24">
</ImageView>
</LinearLayout>
<TextView
android:id="@+id/widget_net"
android:layout_width="match_parent"
<LinearLayout
android:id="@+id/widget_net_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:maxLines="1"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Net" />
android:layout_below="@id/widget_disk_label"
android:gravity="center_vertical"
android:orientation="horizontal">
</LinearLayout>
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/net_24">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/widget_net"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:maxLines="1"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Net" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<!-- Add a TextView for error messages -->
<TextView
android:id="@+id/error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/widget_name"
android:textColor="@color/widgetSummaryText"
android:textSize="12sp"
android:visibility="gone"
android:alpha="0"
android:animateLayoutChanges="true"
tools:text="Error message" />
<TextView
android:id="@+id/widget_time"
@@ -143,6 +170,8 @@
android:maxLines="2"
android:textColor="@color/widgetSummaryText"
android:textSize="11sp"
android:alpha="0"
android:animateLayoutChanges="true"
tools:text="UpdateTime" />
</RelativeLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ServerBox</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>

View File

@@ -9,6 +9,23 @@ rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects { subproject ->
// Only works on com.android.application(the main app module)
if (subproject.plugins.hasPlugin('com.android.application')) {
subproject.afterEvaluate {
android.buildTypes.matching { it.name == 'profile' }.all { buildType ->
buildType.applicationIdSuffix = ".profile"
buildTypes.profile.resValue 'string', 'app_name', 'SrvBxP'
}
android.buildTypes.matching { it.name == 'debug' }.all { buildType ->
buildType.applicationIdSuffix = ".debug"
buildTypes.debug.resValue 'string', 'app_name', 'SrvBxD'
}
}
}
}
subprojects {
project.evaluationDependsOn(':app')
}

View File

@@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -2,5 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@@ -19,8 +19,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.4.2" apply false
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
id "com.android.application" version '8.6.0' apply false
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
}
include ":app"

6505
coverage/lcov.info Normal file

File diff suppressed because it is too large Load Diff

13
distribute_options.yaml Normal file
View File

@@ -0,0 +1,13 @@
variables:
output: dist/
releases:
- name: linux
jobs:
- name: release-linux-deb
package:
platform: linux
target: deb
- name: release-linux-rpm
package:
platform: linux
target: rpm

View File

@@ -6,7 +6,9 @@ PODS:
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_native_splash (0.0.1):
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- icloud_storage (0.0.1):
- Flutter
@@ -31,8 +33,6 @@ PODS:
- Flutter
- watch_connectivity (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
@@ -40,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`)
@@ -50,7 +51,6 @@ DEPENDENCIES:
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
EXTERNAL SOURCES:
app_links:
@@ -63,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:
@@ -83,27 +85,25 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
watch_connectivity:
:path: ".symlinks/plugins/watch_connectivity/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
watch_connectivity: 715eb484685e05846eab74795348a44bb2809b82
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
plain_notification_token: 047876b9d80a5b93565ddcc13a487a7e7b906f7d
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2

View File

@@ -672,7 +672,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1104;
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.1104;
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 = 1104;
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.1104;
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 = 1104;
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.1104;
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 = 1104;
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.1104;
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 = 1104;
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.1104;
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 = 1104;
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.1104;
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 = 1104;
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.1104;
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 = 1104;
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.1104;
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 = 1104;
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.1104;
MARKETING_VERSION = 1.0.1206;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;

View File

@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@@ -43,11 +44,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@@ -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()
}
}

View File

@@ -1,4 +1,6 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: l10n.dart
output-dir: lib/generated/l10n
synthetic-package: false
untranslated-messages-file: untranlated.json

View File

@@ -1,14 +1,14 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:fl_lib/l10n/gen_l10n/lib_l10n.dart';
import 'package:fl_lib/generated/l10n/lib_l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/home/home.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/generated/l10n/l10n.dart';
import 'package:server_box/view/page/home.dart';
part 'intro.dart';
@@ -22,48 +22,52 @@ class MyApp extends StatelessWidget {
listenable: RNodes.app,
builder: (context, _) {
if (!Stores.setting.useSystemPrimaryColor.fetch()) {
final colorSeed = Color(Stores.setting.colorSeed.fetch());
UIs.colorSeed = colorSeed;
// Past code uses [UIs.primaryColor] as the primary color
UIs.primaryColor = colorSeed;
return _buildApp(
context,
light: ThemeData(
useMaterial3: true,
colorSchemeSeed: UIs.colorSeed,
),
dark: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: UIs.colorSeed,
),
);
return _build(context);
}
return DynamicColorBuilder(
builder: (light, 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) {
UIs.primaryColor = light.primary;
}
return _buildApp(context, light: lightTheme, dark: darkTheme);
},
);
return _buildDynamicColor(context);
},
);
}
Widget _buildApp(BuildContext ctx,
{required ThemeData light, required ThemeData dark}) {
Widget _build(BuildContext context) {
final colorSeed = Color(Stores.setting.colorSeed.fetch());
UIs.colorSeed = colorSeed;
UIs.primaryColor = colorSeed;
return _buildApp(
context,
light: ThemeData(
useMaterial3: true,
colorSchemeSeed: UIs.colorSeed,
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
),
dark: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: UIs.colorSeed,
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
),
);
}
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);
if (context.isDark && dark != null) {
UIs.primaryColor = dark.primary;
} else if (!context.isDark && light != null) {
UIs.primaryColor = light.primary;
}
return _buildApp(context, light: lightTheme, dark: darkTheme);
},
);
}
Widget _buildApp(BuildContext ctx, {required ThemeData light, required ThemeData dark}) {
final tMode = Stores.setting.themeMode.fetch();
// Issue #57
final themeMode = switch (tMode) {
@@ -75,11 +79,16 @@ class MyApp extends StatelessWidget {
return MaterialApp(
key: ValueKey(locale),
builder: (context, child) => ResponsiveBreakpoints.builder(
child: child ?? UIs.placeholder,
breakpoints: const [
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],
@@ -87,21 +96,22 @@ class MyApp extends StatelessWidget {
themeMode: themeMode,
theme: light.fixWindowsFont,
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
home: VirtualWindowFrame(
child: Builder(
builder: (context) {
context.setLibL10n();
final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
home: Builder(
builder: (context) {
context.setLibL10n();
final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
final intros = _IntroPage.builders;
if (intros.isNotEmpty) {
return _IntroPage(intros);
}
Widget child;
final intros = _IntroPage.builders;
if (intros.isNotEmpty) {
child = _IntroPage(intros);
}
return const HomePage();
},
),
child = const HomePage();
return VirtualWindowFrame(title: BuildData.name, child: child);
},
),
);
}

30
lib/core/chan.dart Normal file
View File

@@ -0,0 +1,30 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/services.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
abstract final class MethodChans {
static const _channel = MethodChannel('${Miscs.pkgName}/main_chan');
static void moveToBg() {
_channel.invokeMethod('sendToBackground');
}
/// Issue #662
static void startService() {
// if (Stores.setting.fgService.fetch() != true) return;
// _channel.invokeMethod('startService');
}
/// Issue #662
static void stopService() {
// if (Stores.setting.fgService.fetch() != true) return;
// _channel.invokeMethod('stopService');
}
static void updateHomeWidget() async {
if (!isIOS || !isAndroid) return;
if (!Stores.setting.autoUpdateHomeWidget.fetch()) return;
await _channel.invokeMethod('updateHomeWidget');
}
}

View File

@@ -1,18 +0,0 @@
import 'package:flutter/services.dart';
import 'package:server_box/data/res/misc.dart';
abstract final class BgRunMC {
static const _channel = MethodChannel('${Miscs.pkgName}/app_retain');
static void moveToBg() {
_channel.invokeMethod('sendToBackground');
}
static void startService() {
_channel.invokeMethod('startService');
}
static void stopService() {
_channel.invokeMethod('stopService');
}
}

View File

@@ -1,12 +0,0 @@
import 'package:flutter/services.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
abstract final class HomeWidgetMC {
static const _channel = MethodChannel('${Miscs.pkgName}/home_widget');
static void update() {
if (!Stores.setting.autoUpdateHomeWidget.fetch()) return;
_channel.invokeMethod('update');
}
}

View File

@@ -1,4 +1,9 @@
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_gen/gen_l10n/l10n_en.dart';
import 'package:flutter/widgets.dart';
import 'package:server_box/generated/l10n/l10n.dart';
import 'package:server_box/generated/l10n/l10n_en.dart';
AppLocalizations l10n = AppLocalizationsEn();
extension LocaleX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
}

View File

@@ -12,21 +12,9 @@ extension SftpFileX on SftpFileMode {
UnixPerm toUnixPerm() {
return UnixPerm(
user: RWX(
r: userRead,
w: userWrite,
x: userExecute,
),
group: RWX(
r: groupRead,
w: groupWrite,
x: groupExecute,
),
other: RWX(
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),
);
}
}

View File

@@ -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 {

View File

@@ -1,169 +1,9 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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/res/store.dart';
import 'package:server_box/view/page/container.dart';
import 'package:server_box/view/page/home/home.dart';
import 'package:server_box/view/page/iperf.dart';
import 'package:server_box/view/page/ping.dart';
import 'package:server_box/view/page/private_key/edit.dart';
import 'package:server_box/view/page/pve.dart';
import 'package:server_box/view/page/server/detail/view.dart';
import 'package:server_box/view/page/setting/platform/android.dart';
import 'package:server_box/view/page/setting/platform/ios.dart';
import 'package:server_box/view/page/setting/seq/srv_func_seq.dart';
import 'package:server_box/view/page/snippet/result.dart';
import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/setting/seq/virt_key.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/view/page/process.dart';
import 'package:server_box/view/page/server/tab.dart';
import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
import 'package:server_box/view/page/setting/seq/srv_seq.dart';
import 'package:server_box/view/page/snippet/edit.dart';
import 'package:server_box/view/page/storage/sftp.dart';
import 'package:server_box/view/page/storage/sftp_mission.dart';
class AppRoutes {
final Widget page;
final String title;
/// The args class for [AppRoute].
final class SpiRequiredArgs {
/// The only required argument for this class.
final Spi spi;
AppRoutes(this.page, this.title);
Future<T?> go<T>(BuildContext context) {
return Navigator.push<T>(
context,
Stores.setting.cupertinoRoute.fetch()
? CupertinoPageRoute(builder: (context) => page)
: MaterialPageRoute(builder: (context) => page),
);
}
Future<T?> checkGo<T>({
required BuildContext context,
required bool Function() check,
}) {
if (check()) {
return go(context);
}
return Future.value(null);
}
static AppRoutes serverDetail({Key? key, required Spi spi}) {
return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail');
}
static AppRoutes serverTab({Key? key}) {
return AppRoutes(ServerPage(key: key), 'server_tab');
}
static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) {
return AppRoutes(
PrivateKeyEditPage(pki: pki),
'key_${pki == null ? 'add' : 'edit'}',
);
}
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
return AppRoutes(
SnippetEditPage(snippet: snippet),
'snippet_${snippet == null ? 'add' : 'edit'}',
);
}
static AppRoutes ssh({
Key? key,
required Spi spi,
String? initCmd,
Snippet? initSnippet,
}) {
return AppRoutes(
SSHPage(
key: key,
spi: spi,
initCmd: initCmd,
initSnippet: initSnippet,
),
'ssh_term',
);
}
static AppRoutes sshVirtKeySetting({Key? key}) {
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
}
static AppRoutes sftpMission({Key? key}) {
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
}
static AppRoutes sftp(
{Key? key, required Spi spi, String? initPath, bool isSelect = false}) {
return AppRoutes(
SftpPage(
key: key,
spi: spi,
initPath: initPath,
isSelect: isSelect,
),
'sftp');
}
static AppRoutes docker({Key? key, required Spi spi}) {
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
}
// static AppRoutes fullscreen({Key? key}) {
// return AppRoutes(FullScreenPage(key: key), 'fullscreen');
// }
static AppRoutes home({Key? key}) {
return AppRoutes(HomePage(key: key), 'home');
}
static AppRoutes ping({Key? key}) {
return AppRoutes(PingPage(key: key), 'ping');
}
static AppRoutes process({Key? key, required Spi spi}) {
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
}
static AppRoutes serverOrder({Key? key}) {
return AppRoutes(ServerOrderPage(key: key), 'server_order');
}
static AppRoutes serverDetailOrder({Key? key}) {
return AppRoutes(ServerDetailOrderPage(key: key), 'server_detail_order');
}
static AppRoutes iosSettings({Key? key}) {
return AppRoutes(IOSSettingsPage(key: key), 'ios_setting');
}
static AppRoutes androidSettings({Key? key}) {
return AppRoutes(AndroidSettingsPage(key: key), 'android_setting');
}
static AppRoutes snippetResult(
{Key? key, required List<SnippetResult?> results}) {
return AppRoutes(
SnippetResultPage(
key: key,
results: results,
),
'snippet_result');
}
static AppRoutes iperf({Key? key, required Spi spi}) {
return AppRoutes(IPerfPage(key: key, spi: spi), 'iperf');
}
static AppRoutes serverFuncBtnsOrder({Key? key}) {
return AppRoutes(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
}
static AppRoutes pve({Key? key, required Spi spi}) {
return AppRoutes(PvePage(key: key, spi: spi), 'pve');
}
const SpiRequiredArgs(this.spi);
}

View File

@@ -1,39 +1,32 @@
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/backup.dart';
import 'package:server_box/data/store/no_backup.dart';
import 'package:server_box/data/model/app/bak/backup2.dart';
import 'package:server_box/data/model/app/bak/utils.dart';
const bakSync = BakSyncer._();
final class BakSyncer extends SyncIface<Backup> {
final icloud = ICloud(containerId: 'iCloud.tech.lolli.serverbox');
final class BakSyncer extends SyncIface {
const BakSyncer._() : super();
@override
Future<void> saveToFile() => Backup.backup();
Future<void> saveToFile() => BackupV2.backup();
@override
Future<Backup> fromFile(String path) async {
Future<Mergeable> fromFile(String path) async {
final content = await File(path).readAsString();
return Backup.fromJsonString(content);
return MergeableUtils.fromJsonString(content).$1;
}
@override
Future<RemoteStorage?> get remoteStorage async {
if (isMacOS || isIOS) await icloud.init('iCloud.tech.lolli.serverbox');
final settings = NoBackupStore.instance;
await webdav.init(WebdavInitArgs(
url: settings.webdavUrl.fetch(),
user: settings.webdavUser.fetch(),
pwd: settings.webdavPwd.fetch(),
prefix: 'serverbox/',
));
final icloudEnabled = settings.icloudSync.fetch();
RemoteStorage? get remoteStorage {
final icloudEnabled = PrefProps.icloudSync.get();
if (icloudEnabled) return icloud;
final webdavEnabled = settings.webdavSync.fetch();
if (webdavEnabled) return webdav;
final webdavEnabled = PrefProps.webdavSync.get();
if (webdavEnabled) return Webdav.shared;
return null;
}

View File

@@ -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;

View File

@@ -4,9 +4,8 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/foundation.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/store.dart';
/// Must put this func out of any Class.
///
@@ -25,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.get(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;
}
@@ -59,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);
@@ -74,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;

View File

@@ -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,
);

View 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;
}
}

View File

@@ -1,37 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Backup _$BackupFromJson(Map<String, dynamic> json) => Backup(
version: (json['version'] as num).toInt(),
date: json['date'] as String,
spis: (json['spis'] as List<dynamic>)
.map((e) => Spi.fromJson(e as Map<String, dynamic>))
.toList(),
snippets: (json['snippets'] as List<dynamic>)
.map((e) => Snippet.fromJson(e as Map<String, dynamic>))
.toList(),
keys: (json['keys'] as List<dynamic>)
.map((e) => PrivateKeyInfo.fromJson(e as Map<String, dynamic>))
.toList(),
container: json['container'] as Map<String, dynamic>,
history: json['history'] as Map<String, dynamic>,
settings: json['settings'] as Map<String, dynamic>?,
lastModTime: (json['lastModTime'] as num?)?.toInt(),
);
Map<String, dynamic> _$BackupToJson(Backup instance) => <String, dynamic>{
'version': instance.version,
'date': instance.date,
'spis': instance.spis,
'snippets': instance.snippets,
'keys': instance.keys,
'container': instance.container,
'history': instance.history,
'lastModTime': instance.lastModTime,
'settings': instance.settings,
};

View File

@@ -8,7 +8,6 @@ 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/res/misc.dart';
import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart';
part 'backup.g.dart';
@@ -46,19 +45,24 @@ class Backup implements Mergeable {
Map<String, dynamic> toJson() => _$BackupToJson(this);
Backup.loadFromStore()
: version = backupFormatVersion,
date = DateTime.now().toString().split('.').firstOrNull ?? '',
spis = Stores.server.fetch(),
snippets = Stores.snippet.fetch(),
keys = Stores.key.fetch(),
container = Stores.container.box.toJson(),
lastModTime = Stores.lastModTime,
history = Stores.history.box.toJson(),
settings = Stores.setting.box.toJson();
static Future<Backup> loadFromStore() async {
final lastModTime = Stores.lastModTime;
return Backup(
version: backupFormatVersion,
date: DateTime.now().toString().split('.').firstOrNull ?? '',
spis: Stores.server.fetch(),
snippets: Stores.snippet.fetch(),
keys: Stores.key.fetch(),
container: Stores.container.getAllMap(),
lastModTime: lastModTime,
history: Stores.history.getAllMap(),
settings: Stores.setting.getAllMap(),
);
}
static Future<String> backup([String? name]) async {
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson()));
final bak = await Backup.loadFromStore();
final result = _diyEncrypt(json.encode(bak.toJson()));
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result);
return path;
@@ -66,7 +70,7 @@ class Backup implements Mergeable {
@override
Future<void> merge({bool force = false}) async {
final curTime = Stores.lastModTime ?? 0;
final curTime = Stores.lastModTime;
final bakTime = lastModTime ?? 0;
final shouldRestore = force || curTime < bakTime;
if (!shouldRestore) {
@@ -209,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 {

View File

@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Backup _$BackupFromJson(Map<String, dynamic> json) => Backup(
version: (json['version'] as num).toInt(),
date: json['date'] as String,
spis: (json['spis'] as List<dynamic>)
.map((e) => Spi.fromJson(e as Map<String, dynamic>))
.toList(),
snippets: (json['snippets'] as List<dynamic>)
.map((e) => Snippet.fromJson(e as Map<String, dynamic>))
.toList(),
keys: (json['keys'] as List<dynamic>)
.map((e) => PrivateKeyInfo.fromJson(e as Map<String, dynamic>))
.toList(),
container: json['container'] as Map<String, dynamic>,
history: json['history'] as Map<String, dynamic>,
settings: json['settings'] as Map<String, dynamic>?,
lastModTime: (json['lastModTime'] as num?)?.toInt(),
);
Map<String, dynamic> _$BackupToJson(Backup instance) => <String, dynamic>{
'version': instance.version,
'date': instance.date,
'spis': instance.spis,
'snippets': instance.snippets,
'keys': instance.keys,
'container': instance.container,
'history': instance.history,
'lastModTime': instance.lastModTime,
'settings': instance.settings,
};

View File

@@ -0,0 +1,101 @@
import 'dart:convert';
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:logging/logging.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
part 'backup2.freezed.dart';
part 'backup2.g.dart';
final _loggerV2 = Logger('BackupV2');
@freezed
abstract class BackupV2 with _$BackupV2 implements Mergeable {
const BackupV2._();
/// Construct a backup with the latest format (v2).
///
/// All `Map<String, dynamic>` are:
/// ```json
/// {
/// "key1": Model{},
/// "_lastModTime": {
/// "key1": 1234567890,
/// },
/// }
/// ```
const factory BackupV2({
required int version,
required int date,
required Map<String, Object?> spis,
required Map<String, Object?> snippets,
required Map<String, Object?> keys,
required Map<String, Object?> container,
required Map<String, Object?> history,
required Map<String, Object?> settings,
}) = _BackupV2;
factory BackupV2.fromJson(Map<String, dynamic> json) => _$BackupV2FromJson(json);
@override
Future<void> merge({bool force = false}) async {
_loggerV2.info('Merging...');
// Merge each store
await Mergeable.mergeStore(backupData: spis, store: Stores.server, force: force);
await Mergeable.mergeStore(backupData: snippets, store: Stores.snippet, force: force);
await Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force);
await Mergeable.mergeStore(backupData: container, store: Stores.container, force: force);
await Mergeable.mergeStore(backupData: history, store: Stores.history, force: force);
await Mergeable.mergeStore(backupData: settings, store: Stores.setting, force: force);
// Reload providers and notify listeners
Provider.reload();
RNodes.app.notify();
_loggerV2.info('Merge completed');
}
static const formatVer = 2;
static Future<BackupV2> loadFromStore() async {
return BackupV2(
version: formatVer,
date: DateTimeX.timestamp,
spis: Stores.server.getAllMap(includeInternalKeys: true),
snippets: Stores.snippet.getAllMap(includeInternalKeys: true),
keys: Stores.key.getAllMap(includeInternalKeys: true),
container: Stores.container.getAllMap(includeInternalKeys: true),
history: Stores.history.getAllMap(includeInternalKeys: true),
settings: Stores.setting.getAllMap(includeInternalKeys: true),
);
}
static Future<String> backup([String? name, String? password]) async {
final bak = await BackupV2.loadFromStore();
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, [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);
}
}

View File

@@ -0,0 +1,205 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'backup2.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BackupV2 {
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();
@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> {
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>
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.
@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
@JsonSerializable()
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);
}
/// 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

View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup2.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_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> _$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,
};

View 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;
}
}

View 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;
}

View File

@@ -0,0 +1,15 @@
import 'package:fl_lib/fl_lib.dart';
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, [String? password]) {
try {
final bak = BackupV2.fromJsonString(json, password);
return (bak, DateTime.fromMillisecondsSinceEpoch(bak.date).hms());
} catch (e) {
final bak = Backup.fromJsonString(json);
return (bak, bak.date);
}
}
}

View File

@@ -1,55 +1,19 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
enum ErrFrom {
unknown,
apt,
docker,
sftp,
ssh,
status,
icloud,
webdav,
;
}
abstract class Err<T> {
final ErrFrom from;
final T type;
final String? message;
String? get solution;
Err({required this.from, required this.type, this.message});
}
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}) : super(from: ErrFrom.ssh);
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,
};
@override
String toString() {
return 'SSHErr<$type>: $message';
}
SSHErrType.chdir => l10n.needHomeDir,
SSHErrType.auth => l10n.authFailTip,
SSHErrType.writeScript => l10n.writeScriptFailTip,
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
_ => null,
};
}
enum ContainerErrType {
@@ -65,69 +29,35 @@ enum ContainerErrType {
}
class ContainerErr extends Err<ContainerErrType> {
ContainerErr({required super.type, super.message})
: super(from: ErrFrom.docker);
ContainerErr({required super.type, super.message});
@override
String? get solution => null;
@override
String toString() {
return 'ContainerErr<$type>: $message';
}
}
enum ICloudErrType {
generic,
notFound,
multipleFiles,
}
enum ICloudErrType { generic, notFound, multipleFiles }
class ICloudErr extends Err<ICloudErrType> {
ICloudErr({required super.type, super.message}) : super(from: ErrFrom.icloud);
ICloudErr({required super.type, super.message});
@override
String? get solution => null;
@override
String toString() {
return 'ICloudErr<$type>: $message';
}
}
enum WebdavErrType {
generic,
notFound,
;
}
enum WebdavErrType { generic, notFound }
class WebdavErr extends Err<WebdavErrType> {
WebdavErr({required super.type, super.message}) : super(from: ErrFrom.webdav);
WebdavErr({required super.type, super.message});
@override
String? get solution => null;
@override
String toString() {
return 'WebdavErr<$type>: $message';
}
}
enum PveErrType {
unknown,
net,
loginFailed,
;
}
enum PveErrType { unknown, net, loginFailed }
class PveErr extends Err<PveErrType> {
PveErr({required super.type, super.message}) : super(from: ErrFrom.status);
PveErr({required super.type, super.message});
@override
String? get solution => null;
@override
String toString() {
return 'PveErr<$type>: $message';
}
}

View File

@@ -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,
};
}

View File

@@ -1,36 +1,22 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
part 'server_func.g.dart';
@HiveType(typeId: 6)
enum ServerFuncBtn {
@HiveField(0)
terminal._(),
@HiveField(1)
sftp._(),
@HiveField(2)
container._(),
@HiveField(3)
process._(),
//@HiveField(4)
//pkg,
@HiveField(5)
snippet._(),
@HiveField(6)
iperf._(),
// @HiveField(7)
// pve,
@HiveField(8)
systemd._(1058),
;
terminal(),
sftp(),
container(),
process(),
//pkg(),
snippet(),
iperf(),
// pve(),
systemd(1058);
final int? addedVersion;
const ServerFuncBtn._([this.addedVersion]);
const ServerFuncBtn([this.addedVersion]);
static void autoAddNewFuncs(int cur) {
if (cur >= systemd.addedVersion!) {
@@ -54,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',
};
}

View File

@@ -1,71 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'server_func.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
@override
final int typeId = 6;
@override
ServerFuncBtn read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return ServerFuncBtn.terminal;
case 1:
return ServerFuncBtn.sftp;
case 2:
return ServerFuncBtn.container;
case 3:
return ServerFuncBtn.process;
case 5:
return ServerFuncBtn.snippet;
case 6:
return ServerFuncBtn.iperf;
case 8:
return ServerFuncBtn.systemd;
default:
return ServerFuncBtn.terminal;
}
}
@override
void write(BinaryWriter writer, ServerFuncBtn obj) {
switch (obj) {
case ServerFuncBtn.terminal:
writer.writeByte(0);
break;
case ServerFuncBtn.sftp:
writer.writeByte(1);
break;
case ServerFuncBtn.container:
writer.writeByte(2);
break;
case ServerFuncBtn.process:
writer.writeByte(3);
break;
case ServerFuncBtn.snippet:
writer.writeByte(5);
break;
case ServerFuncBtn.iperf:
writer.writeByte(6);
break;
case ServerFuncBtn.systemd:
writer.writeByte(8);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerFuncBtnAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,30 +1,23 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/server.dart';
part 'net_view.g.dart';
@HiveType(typeId: 5)
enum NetViewType {
@HiveField(0)
conn,
@HiveField(1)
speed,
@HiveField(2)
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...).
@@ -33,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);
@@ -67,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,
};
}

View File

@@ -1,51 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'net_view.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class NetViewTypeAdapter extends TypeAdapter<NetViewType> {
@override
final int typeId = 5;
@override
NetViewType read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return NetViewType.conn;
case 1:
return NetViewType.speed;
case 2:
return NetViewType.traffic;
default:
return NetViewType.conn;
}
}
@override
void write(BinaryWriter writer, NetViewType obj) {
switch (obj) {
case NetViewType.conn:
writer.writeByte(0);
break;
case NetViewType.speed:
writer.writeByte(1);
break;
case NetViewType.traffic:
writer.writeByte(2);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NetViewTypeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View 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] ?? '';
}
}

View 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()];
}
}

View 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();
}
}

View 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);
}
}

View File

@@ -11,13 +11,13 @@ enum ServerDetailCards {
swap(Icons.swap_horiz),
gpu(Bootstrap.gpu_card),
disk(Bootstrap.device_hdd_fill),
smart(Icons.health_and_safety, sinceBuild: 1174),
net(ZondIcons.network),
sensor(MingCute.dashboard_4_line),
temp(FontAwesome.temperature_empty_solid),
battery(Icons.battery_full),
pve(BoxIcons.bxs_dashboard, sinceBuild: 818),
custom(Icons.code, sinceBuild: 825),
;
custom(Icons.code, sinceBuild: 825);
final int? sinceBuild;
@@ -31,19 +31,20 @@ enum ServerDetailCards {
static final names = values.map((e) => e.name).toList();
String get toStr => switch (this) {
about => libL10n.about,
cpu => 'CPU',
mem => 'RAM',
swap => 'Swap',
gpu => 'GPU',
disk => l10n.disk,
net => l10n.net,
sensor => l10n.sensors,
temp => l10n.temperature,
battery => l10n.battery,
pve => 'PVE',
custom => l10n.cmd,
};
about => libL10n.about,
cpu => 'CPU',
mem => 'RAM',
swap => 'Swap',
gpu => 'GPU',
disk => l10n.disk,
smart => l10n.diskHealth,
net => l10n.net,
sensor => l10n.sensors,
temp => l10n.temperature,
battery => l10n.battery,
pve => 'PVE',
custom => l10n.cmd,
};
/// If:
/// Version 1 => user set [about], default is [about, cpu]

View File

@@ -1,261 +0,0 @@
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/model/server/system.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._('df'),
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'),
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'),
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,
};
}

View File

@@ -1,24 +0,0 @@
import 'dart:async';
class SyncResult<T, E> {
final List<T> up;
final List<T> down;
final Map<T, E> err;
const SyncResult({
required this.up,
required this.down,
required this.err,
});
@override
String toString() {
return 'SyncResult{up: $up, down: $down, err: $err}';
}
}
abstract class SyncIface<T> {
/// Merge [other] into [this], return [this] after merge.
/// Data in [other] has higher priority than [this].
FutureOr<void> sync(T other);
}

View File

@@ -1,25 +1,25 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/view/page/server/tab.dart';
import 'package:server_box/view/page/setting/entry.dart';
import 'package:server_box/view/page/server/tab/tab.dart';
// import 'package:server_box/view/page/setting/entry.dart';
import 'package:server_box/view/page/snippet/list.dart';
import 'package:server_box/view/page/ssh/tab.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/view/page/storage/local.dart';
enum AppTab {
server,
ssh,
file,
snippet,
settings,
snippet
//settings,
;
Widget get page {
return switch (this) {
server => const ServerPage(),
settings => const SettingsPage(),
//settings => const SettingsPage(),
ssh => const SSHTabPage(),
file => const LocalFilePage(),
snippet => const SnippetListPage(),
@@ -29,34 +29,68 @@ 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),
),
settings => NavigationDestination(
icon: const Icon(Icons.settings),
label: libL10n.setting,
selectedIcon: const Icon(Icons.settings),
),
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),
),
// 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),
),
snippet => NavigationRailDestination(
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),
),
};
}
static List<NavigationDestination> get navDestinations {
return AppTab.values.map((e) => e.navDestination).toList();
}
static List<NavigationRailDestination> get navRailDestinations {
return AppTab.values.map((e) => e.navRailDestination).toList();
}
}

View File

@@ -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,
};
}

View File

@@ -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());
}
}

View File

@@ -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,
};
}

View File

@@ -22,8 +22,6 @@ enum PkgManager {
return 'opkg list-upgradable';
case PkgManager.apk:
return 'apk list --upgradable';
default:
return null;
}
}
@@ -56,8 +54,6 @@ enum PkgManager {
return 'opkg upgrade $args';
case PkgManager.apk:
return 'apk upgrade';
default:
return null;
}
}
@@ -66,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);
}
@@ -75,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 [];
}
@@ -109,6 +103,7 @@ enum PkgManager {
return PkgManager.apt;
case Dist.opensuse:
return PkgManager.zypper;
case Dist.coreelec:
case Dist.wrt:
return PkgManager.opkg;
case Dist.arch:

View 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}';
}
}

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -1,32 +1,27 @@
import 'package:hive_flutter/adapters.dart';
import 'package:json_annotation/json_annotation.dart';
part 'custom.g.dart';
@JsonSerializable()
@HiveType(typeId: 7)
@JsonSerializable(includeIfNull: false)
final class ServerCustom {
// @HiveField(0)
// final String? temperature;
@HiveField(1)
final String? pveAddr;
@HiveField(2, defaultValue: false)
final bool pveIgnoreCert;
/// {"title": "cmd"}
@HiveField(3)
final Map<String, String>? cmds;
@HiveField(4)
final String? preferTempDev;
@HiveField(5)
final String? logoUrl;
/// The device name of the network interface displayed in the home server card.
@HiveField(6)
final String? netDev;
/// The directory where the script is stored.
@HiveField(7)
final String? scriptDir;
const ServerCustom({
@@ -40,8 +35,7 @@ final class ServerCustom {
this.scriptDir,
});
factory ServerCustom.fromJson(Map<String, dynamic> json) =>
_$ServerCustomFromJson(json);
factory ServerCustom.fromJson(Map<String, dynamic> json) => _$ServerCustomFromJson(json);
Map<String, dynamic> toJson() => _$ServerCustomToJson(this);

View File

@@ -2,85 +2,29 @@
part of 'custom.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
@override
final int typeId = 7;
@override
ServerCustom read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ServerCustom(
pveAddr: fields[1] as String?,
pveIgnoreCert: fields[2] == null ? false : fields[2] as bool,
cmds: (fields[3] as Map?)?.cast<String, String>(),
preferTempDev: fields[4] as String?,
logoUrl: fields[5] as String?,
netDev: fields[6] as String?,
scriptDir: fields[7] as String?,
);
}
@override
void write(BinaryWriter writer, ServerCustom obj) {
writer
..writeByte(7)
..writeByte(1)
..write(obj.pveAddr)
..writeByte(2)
..write(obj.pveIgnoreCert)
..writeByte(3)
..write(obj.cmds)
..writeByte(4)
..write(obj.preferTempDev)
..writeByte(5)
..write(obj.logoUrl)
..writeByte(6)
..write(obj.netDev)
..writeByte(7)
..write(obj.scriptDir);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerCustomAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ServerCustom _$ServerCustomFromJson(Map<String, dynamic> json) => ServerCustom(
pveAddr: json['pveAddr'] as String?,
pveIgnoreCert: json['pveIgnoreCert'] as bool? ?? false,
cmds: (json['cmds'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
preferTempDev: json['preferTempDev'] as String?,
logoUrl: json['logoUrl'] as String?,
netDev: json['netDev'] as String?,
scriptDir: json['scriptDir'] as String?,
);
pveAddr: json['pveAddr'] as String?,
pveIgnoreCert: json['pveIgnoreCert'] as bool? ?? false,
cmds: (json['cmds'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
preferTempDev: json['preferTempDev'] as String?,
logoUrl: json['logoUrl'] as String?,
netDev: json['netDev'] as String?,
scriptDir: json['scriptDir'] as String?,
);
Map<String, dynamic> _$ServerCustomToJson(ServerCustom instance) =>
<String, dynamic>{
'pveAddr': instance.pveAddr,
if (instance.pveAddr case final value?) 'pveAddr': value,
'pveIgnoreCert': instance.pveIgnoreCert,
'cmds': instance.cmds,
'preferTempDev': instance.preferTempDev,
'logoUrl': instance.logoUrl,
'netDev': instance.netDev,
'scriptDir': instance.scriptDir,
if (instance.cmds case final value?) 'cmds': value,
if (instance.preferTempDev case final value?) 'preferTempDev': value,
if (instance.logoUrl case final value?) 'logoUrl': value,
if (instance.netDev case final value?) 'netDev': value,
if (instance.scriptDir case final value?) 'scriptDir': value,
};

View File

@@ -1,29 +1,207 @@
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart';
import 'package:server_box/data/res/misc.dart';
class Disk {
final String fs;
class Disk with EquatableMixin {
final String path;
final String? fsTyp;
final String mount;
final int usedPercent;
final BigInt used;
final BigInt size;
final BigInt avail;
/// Device name (e.g., sda1, nvme0n1p1)
final String? name;
/// Internal kernel device name
final String? kname;
/// Filesystem UUID
final String? uuid;
/// Child disks (partitions)
final List<Disk> children;
const Disk({
required this.fs,
required this.path,
this.fsTyp,
required this.mount,
required this.usedPercent,
required this.used,
required this.size,
required this.avail,
this.name,
this.kname,
this.uuid,
this.children = const [],
});
static List<Disk> parse(String raw) {
final list = <Disk>[];
raw = raw.trim();
try {
if (raw.startsWith('{')) {
// Parse JSON output from lsblk command
final Map<String, dynamic> jsonData = json.decode(raw);
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
for (final device in blockdevices) {
// Process each device
_processTopLevelDevice(device, list);
}
} else {
// Fallback to the old parsing method in case of non-JSON output
return _parseWithOldMethod(raw);
}
} catch (e) {
Loggers.app.warning('Failed to parse disk info: $e', e);
}
return list;
}
/// Process a top-level device and add all valid disks to the list
static void _processTopLevelDevice(Map<String, dynamic> device, List<Disk> list) {
final disk = _processDiskDevice(device);
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);
if (childDisk != null) {
list.add(childDisk);
}
}
}
}
/// Process a single device without recursively processing its children
static Disk? _processSingleDevice(Map<String, dynamic> device) {
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;
}
final sizeStr = device['fssize']?.toString() ?? '0';
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final usedStr = device['fsused']?.toString() ?? '0';
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final availStr = device['fsavail']?.toString() ?? '0';
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
// Parse fsuse% which is usually in the format "45%"
String usePercentStr = device['fsuse%']?.toString() ?? '0';
usePercentStr = usePercentStr.replaceAll('%', '');
final usedPercent = int.tryParse(usePercentStr) ?? 0;
final name = device['name']?.toString();
final kname = device['kname']?.toString();
final uuid = device['uuid']?.toString();
return Disk(
path: path,
fsTyp: fstype,
mount: mountpoint,
usedPercent: usedPercent,
used: used,
size: size,
avail: avail,
name: name,
kname: kname,
uuid: uuid,
children: const [], // No children for direct device
);
}
static Disk? _processDiskDevice(Map<String, dynamic> device) {
final fstype = device['fstype']?.toString();
final String mountpoint = device['mountpoint']?.toString() ?? '';
// For parent devices that don't have a mountpoint themselves
final String path = device['path']?.toString() ?? '';
final String mount = mountpoint;
final List<Disk> childDisks = [];
// Process children devices recursively
final List<dynamic> childDevices = device['children'] ?? [];
for (final childDevice in childDevices) {
final childDisk = _processDiskDevice(childDevice);
if (childDisk != null) {
childDisks.add(childDisk);
}
}
// Handle common filesystem cases or parent devices with children
if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
final sizeStr = device['fssize']?.toString() ?? '0';
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final usedStr = device['fsused']?.toString() ?? '0';
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final availStr = device['fsavail']?.toString() ?? '0';
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
// Parse fsuse% which is usually in the format "45%"
String usePercentStr = device['fsuse%']?.toString() ?? '0';
usePercentStr = usePercentStr.replaceAll('%', '');
final usedPercent = int.tryParse(usePercentStr) ?? 0;
final name = device['name']?.toString();
final kname = device['kname']?.toString();
final uuid = device['uuid']?.toString();
return Disk(
path: path,
fsTyp: fstype,
mount: mount,
usedPercent: usedPercent,
used: used,
size: size,
avail: avail,
name: name,
kname: kname,
uuid: uuid,
children: childDisks,
);
} else if (childDisks.isNotEmpty) {
// If this is a parent device with no filesystem but has children,
// return the first valid child instead
if (childDisks.isNotEmpty) {
return childDisks.first;
}
}
return null;
}
// Fallback to the old parsing method in case JSON parsing fails
static List<Disk> _parseWithOldMethod(String raw) {
final list = <Disk>[];
final items = raw.split('\n');
items.removeAt(0);
if (items.isNotEmpty) items.removeAt(0);
var pathCache = '';
for (var item in items) {
if (item.isEmpty) {
@@ -42,14 +220,16 @@ class Disk {
final fs = vals[0];
final mount = vals[5];
if (!_shouldCalc(fs, mount)) continue;
list.add(Disk(
fs: fs,
mount: mount,
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
used: BigInt.parse(vals[2]),
size: BigInt.parse(vals[1]),
avail: BigInt.parse(vals[3]),
));
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;
}
@@ -58,9 +238,19 @@ class Disk {
}
@override
String toString() {
return 'Disk{dev: $fs, mount: $mount, usedPercent: $usedPercent, used: $used, size: $size, avail: $avail}';
}
List<Object?> get props => [
path,
name,
kname,
fsTyp,
mount,
usedPercent,
used,
size,
avail,
uuid,
children,
];
}
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
@@ -72,9 +262,16 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
}
(double?, double?) _getSpeed(String dev) {
if (dev.startsWith('/dev/')) dev = dev.substring(5);
final old = pre.firstWhereOrNull((e) => e.dev == dev);
final new_ = now.firstWhereOrNull((e) => e.dev == dev);
// Extract the device name from path if needed
String searchDev = dev;
if (dev.startsWith('/dev/')) {
searchDev = dev.substring(5);
}
// Try to find by exact device name first
final old = pre.firstWhereOrNull((e) => e.dev == searchDev);
final new_ = now.firstWhereOrNull((e) => e.dev == searchDev);
if (old == null || new_ == null) return (null, null);
final sectorsRead = new_.sectorsRead - old.sectorsRead;
final sectorsWrite = new_.sectorsWrite - old.sectorsWrite;
@@ -104,11 +301,14 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
!item.dev.startsWith('vd') &&
!item.dev.startsWith('hd') &&
!item.dev.startsWith('mmcblk') &&
!item.dev.startsWith('sr')) continue;
!item.dev.startsWith('sr')) {
continue;
}
final (read_, write_) = _getSpeed(item.dev);
read += read_ ?? 0;
write += write_ ?? 0;
}
final readStr = '${read.bytes2Str}/s';
final writeStr = '${write.bytes2Str}/s';
return (readStr, writeStr);
@@ -126,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;
}
@@ -146,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;
@@ -161,12 +358,13 @@ 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 => used / size * 100;
double get usedPercent {
// Avoid division by zero
if (size == BigInt.zero) return 0;
return used / size * 100;
}
/// Find all devs, add their used and size
static DiskUsage parse(List<Disk> disks) {
@@ -174,9 +372,12 @@ class DiskUsage {
var used = BigInt.zero;
var size = BigInt.zero;
for (var disk in disks) {
if (!_shouldCalc(disk.fs, disk.mount)) continue;
if (devs.contains(disk.fs)) continue;
devs.add(disk.fs);
if (!_shouldCalc(disk.path, disk.mount)) continue;
// Use a combination of path and kernel name to uniquely identify disks
// This helps distinguish between multiple physical disks in BTRFS RAID setups
final uniqueId = '${disk.path}:${disk.kname ?? "unknown"}';
if (devs.contains(uniqueId)) continue;
devs.add(uniqueId);
used += disk.used;
size += disk.size;
}
@@ -185,12 +386,24 @@ class DiskUsage {
}
bool _shouldCalc(String fs, String mount) {
// Skip swap partitions
// if (mount == '[SWAP]') return false;
// Include standard filesystems
if (fs.startsWith('/dev')) return true;
// Some NAS may have mounted path like this `//192.168.1.2/`
if (fs.startsWith('//')) return true;
if (mount.startsWith('/mnt')) return true;
// if (fs.startsWith('shm') ||
// fs.startsWith('overlay') ||
// fs.startsWith('tmpfs')) return false;
return false;
// Include common filesystem types
// final commonFsTypes = ['ext2', 'ext3', 'ext4', 'xfs', 'btrfs', 'zfs', 'ntfs', 'fat', 'vfat'];
// if (commonFsTypes.any((type) => fs.toLowerCase() == type)) return true;
// Skip special filesystems
// if (fs == 'LVM2_member' || fs == 'crypto_LUKS') return false;
if (fs.startsWith('shm') || fs.startsWith('overlay') || fs.startsWith('tmpfs')) {
return false;
}
return true;
}

View File

@@ -0,0 +1,293 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'disk_smart.freezed.dart';
part 'disk_smart.g.dart';
@freezed
abstract class DiskSmart with _$DiskSmart {
const DiskSmart._();
const factory DiskSmart({
required String device,
bool? healthy,
double? temperature,
String? model,
String? serial,
int? powerOnHours,
int? powerCycleCount,
required Map<String, dynamic> rawData,
required Map<String, SmartAttribute> smartAttributes,
}) = _DiskSmart;
factory DiskSmart.fromJson(Map<String, dynamic> json) => _$DiskSmartFromJson(json);
static List<DiskSmart> parse(String raw) {
final results = <DiskSmart>[];
final jsonBlocks = raw.split('\n\n').where((s) => s.trim().isNotEmpty);
for (final jsonStr in jsonBlocks) {
try {
final data = json.decode(jsonStr.trim()) as Map<String, dynamic>;
// Basic
final device = data['device']?['name']?.toString() ?? '';
if (!_isPhysicalDisk(device)) continue;
final healthy = _parseHealthStatus(data);
// Model and Serial
final model =
data['model_name']?.toString() ??
data['model_family']?.toString() ??
data['device']?['model_name']?.toString();
final serial = data['serial_number']?.toString() ?? data['device']?['serial_number']?.toString();
// SMART Attrs
final smartAttributes = _parseSmartAttributes(data);
final temperature = _extractTemperature(data, smartAttributes);
final powerOnHours =
data['power_on_time']?['hours'] as int? ?? smartAttributes['Power_On_Hours']?.rawValue as int?;
final powerCycleCount =
data['power_cycle_count'] as int? ?? smartAttributes['Power_Cycle_Count']?.rawValue as int?;
results.add(
DiskSmart(
device: device,
healthy: healthy,
temperature: temperature,
model: model,
serial: serial,
powerOnHours: powerOnHours,
powerCycleCount: powerCycleCount,
rawData: data,
smartAttributes: smartAttributes,
),
);
} catch (e, s) {
Loggers.app.warning('DiskSmart parse', e, s);
}
}
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>{};
final attrTable = data['ata_smart_attributes']?['table'] as List?;
if (attrTable == null) return attributes;
for (final attr in attrTable) {
if (attr is Map<String, dynamic>) {
final name = attr['name']?.toString();
if (name != null) {
attributes[name] = SmartAttribute(
id: attr['id'] as int?,
name: name,
value: attr['value'] as int?,
worst: attr['worst'] as int?,
thresh: attr['thresh'] as int?,
whenFailed: attr['when_failed']?.toString(),
rawValue: attr['raw']?['value'],
rawString: attr['raw']?['string']?.toString(),
flags: SmartAttributeFlags.fromMap(attr['flags'] as Map<String, dynamic>? ?? {}),
);
}
}
}
return attributes;
}
static final _tempReg = RegExp(r'^(\d+(?:\.\d+)?)');
/// Extract temperature from the data
static double? _extractTemperature(Map<String, dynamic> data, Map<String, SmartAttribute> attrs) {
// Directly
final directTemp = data['temperature']?['current'];
if (directTemp is num) return directTemp.toDouble();
// SMART attribute
final tempAttr = attrs['Temperature_Celsius'];
if (tempAttr != null) {
// "35 (Min/Max 14/61)"
final rawString = tempAttr.rawString;
if (rawString != null) {
final match = _tempReg.firstMatch(rawString);
if (match != null) {
return double.tryParse(match.group(1)!);
}
}
// Simple numeric value
if (tempAttr.rawValue is num && tempAttr.rawValue! < 150) {
return tempAttr.rawValue!.toDouble();
}
}
return null;
}
/// Get the specific SMART attribute by name
SmartAttribute? getAttribute(String name) => smartAttributes[name];
int? get ssdLifeLeft => smartAttributes['SSD_Life_Left']?.rawValue as int?;
int? get lifetimeWritesGiB => smartAttributes['Lifetime_Writes_GiB']?.rawValue as int?;
int? get lifetimeReadsGiB => smartAttributes['Lifetime_Reads_GiB']?.rawValue as int?;
int? get unsafeShutdownCount => smartAttributes['Unsafe_Shutdown_Count']?.rawValue as int?;
int? get averageEraseCount => smartAttributes['Average_Erase_Count']?.rawValue as int?;
int? get maxEraseCount => smartAttributes['Max_Erase_Count']?.rawValue as int?;
@override
String toString() => 'DiskSmart($device)';
}
@freezed
abstract class SmartAttribute with _$SmartAttribute {
const SmartAttribute._();
const factory SmartAttribute({
int? id,
required String name,
int? value,
int? worst,
int? thresh,
String? whenFailed,
dynamic rawValue,
String? rawString,
required SmartAttributeFlags flags,
}) = _SmartAttribute;
factory SmartAttribute.fromJson(Map<String, dynamic> json) => _$SmartAttributeFromJson(json);
@override
String toString() {
return 'SmartAttribute(id: $id, name: $name)';
}
}
@freezed
abstract class SmartAttributeFlags with _$SmartAttributeFlags {
const SmartAttributeFlags._();
const factory SmartAttributeFlags({
int? value,
String? string,
@Default(false) bool prefailure,
@Default(false) bool updatedOnline,
@Default(false) bool performance,
@Default(false) bool errorRate,
@Default(false) bool eventCount,
@Default(false) bool autoKeep,
}) = _SmartAttributeFlags;
factory SmartAttributeFlags.fromJson(Map<String, dynamic> json) => _$SmartAttributeFlagsFromJson(json);
factory SmartAttributeFlags.fromMap(Map<String, dynamic> map) {
return SmartAttributeFlags(
value: map['value'] as int?,
string: map['string']?.toString(),
prefailure: map['prefailure'] == true,
updatedOnline: map['updated_online'] == true,
performance: map['performance'] == true,
errorRate: map['error_rate'] == true,
eventCount: map['event_count'] == true,
autoKeep: map['auto_keep'] == true,
);
}
@override
String toString() {
return 'SmartAttributeFlags(value: $value, string: $string)';
}
}

View File

@@ -0,0 +1,489 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'disk_smart.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$DiskSmart {
String get device; bool? get healthy; double? get temperature; String? get model; String? get serial; int? get powerOnHours; int? get powerCycleCount; Map<String, dynamic> get rawData; Map<String, SmartAttribute> get smartAttributes;
/// Create a copy of DiskSmart
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$DiskSmartCopyWith<DiskSmart> get copyWith => _$DiskSmartCopyWithImpl<DiskSmart>(this as DiskSmart, _$identity);
/// Serializes this DiskSmart to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is DiskSmart&&(identical(other.device, device) || other.device == device)&&(identical(other.healthy, healthy) || other.healthy == healthy)&&(identical(other.temperature, temperature) || other.temperature == temperature)&&(identical(other.model, model) || other.model == model)&&(identical(other.serial, serial) || other.serial == serial)&&(identical(other.powerOnHours, powerOnHours) || other.powerOnHours == powerOnHours)&&(identical(other.powerCycleCount, powerCycleCount) || other.powerCycleCount == powerCycleCount)&&const DeepCollectionEquality().equals(other.rawData, rawData)&&const DeepCollectionEquality().equals(other.smartAttributes, smartAttributes));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,device,healthy,temperature,model,serial,powerOnHours,powerCycleCount,const DeepCollectionEquality().hash(rawData),const DeepCollectionEquality().hash(smartAttributes));
}
/// @nodoc
abstract mixin class $DiskSmartCopyWith<$Res> {
factory $DiskSmartCopyWith(DiskSmart value, $Res Function(DiskSmart) _then) = _$DiskSmartCopyWithImpl;
@useResult
$Res call({
String device, bool? healthy, double? temperature, String? model, String? serial, int? powerOnHours, int? powerCycleCount, Map<String, dynamic> rawData, Map<String, SmartAttribute> smartAttributes
});
}
/// @nodoc
class _$DiskSmartCopyWithImpl<$Res>
implements $DiskSmartCopyWith<$Res> {
_$DiskSmartCopyWithImpl(this._self, this._then);
final DiskSmart _self;
final $Res Function(DiskSmart) _then;
/// Create a copy of DiskSmart
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? device = null,Object? healthy = freezed,Object? temperature = freezed,Object? model = freezed,Object? serial = freezed,Object? powerOnHours = freezed,Object? powerCycleCount = freezed,Object? rawData = null,Object? smartAttributes = null,}) {
return _then(_self.copyWith(
device: null == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as String,healthy: freezed == healthy ? _self.healthy : healthy // ignore: cast_nullable_to_non_nullable
as bool?,temperature: freezed == temperature ? _self.temperature : temperature // ignore: cast_nullable_to_non_nullable
as double?,model: freezed == model ? _self.model : model // ignore: cast_nullable_to_non_nullable
as String?,serial: freezed == serial ? _self.serial : serial // ignore: cast_nullable_to_non_nullable
as String?,powerOnHours: freezed == powerOnHours ? _self.powerOnHours : powerOnHours // ignore: cast_nullable_to_non_nullable
as int?,powerCycleCount: freezed == powerCycleCount ? _self.powerCycleCount : powerCycleCount // ignore: cast_nullable_to_non_nullable
as int?,rawData: null == rawData ? _self.rawData : rawData // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,smartAttributes: null == smartAttributes ? _self.smartAttributes : smartAttributes // ignore: cast_nullable_to_non_nullable
as Map<String, SmartAttribute>,
));
}
}
/// @nodoc
@JsonSerializable()
class _DiskSmart extends DiskSmart {
const _DiskSmart({required this.device, this.healthy, this.temperature, this.model, this.serial, this.powerOnHours, this.powerCycleCount, required final Map<String, dynamic> rawData, required final Map<String, SmartAttribute> smartAttributes}): _rawData = rawData,_smartAttributes = smartAttributes,super._();
factory _DiskSmart.fromJson(Map<String, dynamic> json) => _$DiskSmartFromJson(json);
@override final String device;
@override final bool? healthy;
@override final double? temperature;
@override final String? model;
@override final String? serial;
@override final int? powerOnHours;
@override final int? powerCycleCount;
final Map<String, dynamic> _rawData;
@override Map<String, dynamic> get rawData {
if (_rawData is EqualUnmodifiableMapView) return _rawData;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_rawData);
}
final Map<String, SmartAttribute> _smartAttributes;
@override Map<String, SmartAttribute> get smartAttributes {
if (_smartAttributes is EqualUnmodifiableMapView) return _smartAttributes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_smartAttributes);
}
/// Create a copy of DiskSmart
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$DiskSmartCopyWith<_DiskSmart> get copyWith => __$DiskSmartCopyWithImpl<_DiskSmart>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$DiskSmartToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DiskSmart&&(identical(other.device, device) || other.device == device)&&(identical(other.healthy, healthy) || other.healthy == healthy)&&(identical(other.temperature, temperature) || other.temperature == temperature)&&(identical(other.model, model) || other.model == model)&&(identical(other.serial, serial) || other.serial == serial)&&(identical(other.powerOnHours, powerOnHours) || other.powerOnHours == powerOnHours)&&(identical(other.powerCycleCount, powerCycleCount) || other.powerCycleCount == powerCycleCount)&&const DeepCollectionEquality().equals(other._rawData, _rawData)&&const DeepCollectionEquality().equals(other._smartAttributes, _smartAttributes));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,device,healthy,temperature,model,serial,powerOnHours,powerCycleCount,const DeepCollectionEquality().hash(_rawData),const DeepCollectionEquality().hash(_smartAttributes));
}
/// @nodoc
abstract mixin class _$DiskSmartCopyWith<$Res> implements $DiskSmartCopyWith<$Res> {
factory _$DiskSmartCopyWith(_DiskSmart value, $Res Function(_DiskSmart) _then) = __$DiskSmartCopyWithImpl;
@override @useResult
$Res call({
String device, bool? healthy, double? temperature, String? model, String? serial, int? powerOnHours, int? powerCycleCount, Map<String, dynamic> rawData, Map<String, SmartAttribute> smartAttributes
});
}
/// @nodoc
class __$DiskSmartCopyWithImpl<$Res>
implements _$DiskSmartCopyWith<$Res> {
__$DiskSmartCopyWithImpl(this._self, this._then);
final _DiskSmart _self;
final $Res Function(_DiskSmart) _then;
/// Create a copy of DiskSmart
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? device = null,Object? healthy = freezed,Object? temperature = freezed,Object? model = freezed,Object? serial = freezed,Object? powerOnHours = freezed,Object? powerCycleCount = freezed,Object? rawData = null,Object? smartAttributes = null,}) {
return _then(_DiskSmart(
device: null == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as String,healthy: freezed == healthy ? _self.healthy : healthy // ignore: cast_nullable_to_non_nullable
as bool?,temperature: freezed == temperature ? _self.temperature : temperature // ignore: cast_nullable_to_non_nullable
as double?,model: freezed == model ? _self.model : model // ignore: cast_nullable_to_non_nullable
as String?,serial: freezed == serial ? _self.serial : serial // ignore: cast_nullable_to_non_nullable
as String?,powerOnHours: freezed == powerOnHours ? _self.powerOnHours : powerOnHours // ignore: cast_nullable_to_non_nullable
as int?,powerCycleCount: freezed == powerCycleCount ? _self.powerCycleCount : powerCycleCount // ignore: cast_nullable_to_non_nullable
as int?,rawData: null == rawData ? _self._rawData : rawData // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,smartAttributes: null == smartAttributes ? _self._smartAttributes : smartAttributes // ignore: cast_nullable_to_non_nullable
as Map<String, SmartAttribute>,
));
}
}
/// @nodoc
mixin _$SmartAttribute {
int? get id; String get name; int? get value; int? get worst; int? get thresh; String? get whenFailed; dynamic get rawValue; String? get rawString; SmartAttributeFlags get flags;
/// Create a copy of SmartAttribute
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SmartAttributeCopyWith<SmartAttribute> get copyWith => _$SmartAttributeCopyWithImpl<SmartAttribute>(this as SmartAttribute, _$identity);
/// Serializes this SmartAttribute to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SmartAttribute&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.value, value) || other.value == value)&&(identical(other.worst, worst) || other.worst == worst)&&(identical(other.thresh, thresh) || other.thresh == thresh)&&(identical(other.whenFailed, whenFailed) || other.whenFailed == whenFailed)&&const DeepCollectionEquality().equals(other.rawValue, rawValue)&&(identical(other.rawString, rawString) || other.rawString == rawString)&&(identical(other.flags, flags) || other.flags == flags));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,value,worst,thresh,whenFailed,const DeepCollectionEquality().hash(rawValue),rawString,flags);
}
/// @nodoc
abstract mixin class $SmartAttributeCopyWith<$Res> {
factory $SmartAttributeCopyWith(SmartAttribute value, $Res Function(SmartAttribute) _then) = _$SmartAttributeCopyWithImpl;
@useResult
$Res call({
int? id, String name, int? value, int? worst, int? thresh, String? whenFailed, dynamic rawValue, String? rawString, SmartAttributeFlags flags
});
$SmartAttributeFlagsCopyWith<$Res> get flags;
}
/// @nodoc
class _$SmartAttributeCopyWithImpl<$Res>
implements $SmartAttributeCopyWith<$Res> {
_$SmartAttributeCopyWithImpl(this._self, this._then);
final SmartAttribute _self;
final $Res Function(SmartAttribute) _then;
/// Create a copy of SmartAttribute
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? name = null,Object? value = freezed,Object? worst = freezed,Object? thresh = freezed,Object? whenFailed = freezed,Object? rawValue = freezed,Object? rawString = freezed,Object? flags = null,}) {
return _then(_self.copyWith(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as int?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
as int?,worst: freezed == worst ? _self.worst : worst // ignore: cast_nullable_to_non_nullable
as int?,thresh: freezed == thresh ? _self.thresh : thresh // ignore: cast_nullable_to_non_nullable
as int?,whenFailed: freezed == whenFailed ? _self.whenFailed : whenFailed // ignore: cast_nullable_to_non_nullable
as String?,rawValue: freezed == rawValue ? _self.rawValue : rawValue // ignore: cast_nullable_to_non_nullable
as dynamic,rawString: freezed == rawString ? _self.rawString : rawString // ignore: cast_nullable_to_non_nullable
as String?,flags: null == flags ? _self.flags : flags // ignore: cast_nullable_to_non_nullable
as SmartAttributeFlags,
));
}
/// Create a copy of SmartAttribute
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SmartAttributeFlagsCopyWith<$Res> get flags {
return $SmartAttributeFlagsCopyWith<$Res>(_self.flags, (value) {
return _then(_self.copyWith(flags: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SmartAttribute extends SmartAttribute {
const _SmartAttribute({this.id, required this.name, this.value, this.worst, this.thresh, this.whenFailed, this.rawValue, this.rawString, required this.flags}): super._();
factory _SmartAttribute.fromJson(Map<String, dynamic> json) => _$SmartAttributeFromJson(json);
@override final int? id;
@override final String name;
@override final int? value;
@override final int? worst;
@override final int? thresh;
@override final String? whenFailed;
@override final dynamic rawValue;
@override final String? rawString;
@override final SmartAttributeFlags flags;
/// Create a copy of SmartAttribute
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SmartAttributeCopyWith<_SmartAttribute> get copyWith => __$SmartAttributeCopyWithImpl<_SmartAttribute>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SmartAttributeToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SmartAttribute&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.value, value) || other.value == value)&&(identical(other.worst, worst) || other.worst == worst)&&(identical(other.thresh, thresh) || other.thresh == thresh)&&(identical(other.whenFailed, whenFailed) || other.whenFailed == whenFailed)&&const DeepCollectionEquality().equals(other.rawValue, rawValue)&&(identical(other.rawString, rawString) || other.rawString == rawString)&&(identical(other.flags, flags) || other.flags == flags));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,value,worst,thresh,whenFailed,const DeepCollectionEquality().hash(rawValue),rawString,flags);
}
/// @nodoc
abstract mixin class _$SmartAttributeCopyWith<$Res> implements $SmartAttributeCopyWith<$Res> {
factory _$SmartAttributeCopyWith(_SmartAttribute value, $Res Function(_SmartAttribute) _then) = __$SmartAttributeCopyWithImpl;
@override @useResult
$Res call({
int? id, String name, int? value, int? worst, int? thresh, String? whenFailed, dynamic rawValue, String? rawString, SmartAttributeFlags flags
});
@override $SmartAttributeFlagsCopyWith<$Res> get flags;
}
/// @nodoc
class __$SmartAttributeCopyWithImpl<$Res>
implements _$SmartAttributeCopyWith<$Res> {
__$SmartAttributeCopyWithImpl(this._self, this._then);
final _SmartAttribute _self;
final $Res Function(_SmartAttribute) _then;
/// Create a copy of SmartAttribute
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? name = null,Object? value = freezed,Object? worst = freezed,Object? thresh = freezed,Object? whenFailed = freezed,Object? rawValue = freezed,Object? rawString = freezed,Object? flags = null,}) {
return _then(_SmartAttribute(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as int?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
as int?,worst: freezed == worst ? _self.worst : worst // ignore: cast_nullable_to_non_nullable
as int?,thresh: freezed == thresh ? _self.thresh : thresh // ignore: cast_nullable_to_non_nullable
as int?,whenFailed: freezed == whenFailed ? _self.whenFailed : whenFailed // ignore: cast_nullable_to_non_nullable
as String?,rawValue: freezed == rawValue ? _self.rawValue : rawValue // ignore: cast_nullable_to_non_nullable
as dynamic,rawString: freezed == rawString ? _self.rawString : rawString // ignore: cast_nullable_to_non_nullable
as String?,flags: null == flags ? _self.flags : flags // ignore: cast_nullable_to_non_nullable
as SmartAttributeFlags,
));
}
/// Create a copy of SmartAttribute
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SmartAttributeFlagsCopyWith<$Res> get flags {
return $SmartAttributeFlagsCopyWith<$Res>(_self.flags, (value) {
return _then(_self.copyWith(flags: value));
});
}
}
/// @nodoc
mixin _$SmartAttributeFlags {
int? get value; String? get string; bool get prefailure; bool get updatedOnline; bool get performance; bool get errorRate; bool get eventCount; bool get autoKeep;
/// Create a copy of SmartAttributeFlags
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SmartAttributeFlagsCopyWith<SmartAttributeFlags> get copyWith => _$SmartAttributeFlagsCopyWithImpl<SmartAttributeFlags>(this as SmartAttributeFlags, _$identity);
/// Serializes this SmartAttributeFlags to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SmartAttributeFlags&&(identical(other.value, value) || other.value == value)&&(identical(other.string, string) || other.string == string)&&(identical(other.prefailure, prefailure) || other.prefailure == prefailure)&&(identical(other.updatedOnline, updatedOnline) || other.updatedOnline == updatedOnline)&&(identical(other.performance, performance) || other.performance == performance)&&(identical(other.errorRate, errorRate) || other.errorRate == errorRate)&&(identical(other.eventCount, eventCount) || other.eventCount == eventCount)&&(identical(other.autoKeep, autoKeep) || other.autoKeep == autoKeep));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,value,string,prefailure,updatedOnline,performance,errorRate,eventCount,autoKeep);
}
/// @nodoc
abstract mixin class $SmartAttributeFlagsCopyWith<$Res> {
factory $SmartAttributeFlagsCopyWith(SmartAttributeFlags value, $Res Function(SmartAttributeFlags) _then) = _$SmartAttributeFlagsCopyWithImpl;
@useResult
$Res call({
int? value, String? string, bool prefailure, bool updatedOnline, bool performance, bool errorRate, bool eventCount, bool autoKeep
});
}
/// @nodoc
class _$SmartAttributeFlagsCopyWithImpl<$Res>
implements $SmartAttributeFlagsCopyWith<$Res> {
_$SmartAttributeFlagsCopyWithImpl(this._self, this._then);
final SmartAttributeFlags _self;
final $Res Function(SmartAttributeFlags) _then;
/// Create a copy of SmartAttributeFlags
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? value = freezed,Object? string = freezed,Object? prefailure = null,Object? updatedOnline = null,Object? performance = null,Object? errorRate = null,Object? eventCount = null,Object? autoKeep = null,}) {
return _then(_self.copyWith(
value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
as int?,string: freezed == string ? _self.string : string // ignore: cast_nullable_to_non_nullable
as String?,prefailure: null == prefailure ? _self.prefailure : prefailure // ignore: cast_nullable_to_non_nullable
as bool,updatedOnline: null == updatedOnline ? _self.updatedOnline : updatedOnline // ignore: cast_nullable_to_non_nullable
as bool,performance: null == performance ? _self.performance : performance // ignore: cast_nullable_to_non_nullable
as bool,errorRate: null == errorRate ? _self.errorRate : errorRate // ignore: cast_nullable_to_non_nullable
as bool,eventCount: null == eventCount ? _self.eventCount : eventCount // ignore: cast_nullable_to_non_nullable
as bool,autoKeep: null == autoKeep ? _self.autoKeep : autoKeep // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _SmartAttributeFlags extends SmartAttributeFlags {
const _SmartAttributeFlags({this.value, this.string, this.prefailure = false, this.updatedOnline = false, this.performance = false, this.errorRate = false, this.eventCount = false, this.autoKeep = false}): super._();
factory _SmartAttributeFlags.fromJson(Map<String, dynamic> json) => _$SmartAttributeFlagsFromJson(json);
@override final int? value;
@override final String? string;
@override@JsonKey() final bool prefailure;
@override@JsonKey() final bool updatedOnline;
@override@JsonKey() final bool performance;
@override@JsonKey() final bool errorRate;
@override@JsonKey() final bool eventCount;
@override@JsonKey() final bool autoKeep;
/// Create a copy of SmartAttributeFlags
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SmartAttributeFlagsCopyWith<_SmartAttributeFlags> get copyWith => __$SmartAttributeFlagsCopyWithImpl<_SmartAttributeFlags>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SmartAttributeFlagsToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SmartAttributeFlags&&(identical(other.value, value) || other.value == value)&&(identical(other.string, string) || other.string == string)&&(identical(other.prefailure, prefailure) || other.prefailure == prefailure)&&(identical(other.updatedOnline, updatedOnline) || other.updatedOnline == updatedOnline)&&(identical(other.performance, performance) || other.performance == performance)&&(identical(other.errorRate, errorRate) || other.errorRate == errorRate)&&(identical(other.eventCount, eventCount) || other.eventCount == eventCount)&&(identical(other.autoKeep, autoKeep) || other.autoKeep == autoKeep));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,value,string,prefailure,updatedOnline,performance,errorRate,eventCount,autoKeep);
}
/// @nodoc
abstract mixin class _$SmartAttributeFlagsCopyWith<$Res> implements $SmartAttributeFlagsCopyWith<$Res> {
factory _$SmartAttributeFlagsCopyWith(_SmartAttributeFlags value, $Res Function(_SmartAttributeFlags) _then) = __$SmartAttributeFlagsCopyWithImpl;
@override @useResult
$Res call({
int? value, String? string, bool prefailure, bool updatedOnline, bool performance, bool errorRate, bool eventCount, bool autoKeep
});
}
/// @nodoc
class __$SmartAttributeFlagsCopyWithImpl<$Res>
implements _$SmartAttributeFlagsCopyWith<$Res> {
__$SmartAttributeFlagsCopyWithImpl(this._self, this._then);
final _SmartAttributeFlags _self;
final $Res Function(_SmartAttributeFlags) _then;
/// Create a copy of SmartAttributeFlags
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? value = freezed,Object? string = freezed,Object? prefailure = null,Object? updatedOnline = null,Object? performance = null,Object? errorRate = null,Object? eventCount = null,Object? autoKeep = null,}) {
return _then(_SmartAttributeFlags(
value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
as int?,string: freezed == string ? _self.string : string // ignore: cast_nullable_to_non_nullable
as String?,prefailure: null == prefailure ? _self.prefailure : prefailure // ignore: cast_nullable_to_non_nullable
as bool,updatedOnline: null == updatedOnline ? _self.updatedOnline : updatedOnline // ignore: cast_nullable_to_non_nullable
as bool,performance: null == performance ? _self.performance : performance // ignore: cast_nullable_to_non_nullable
as bool,errorRate: null == errorRate ? _self.errorRate : errorRate // ignore: cast_nullable_to_non_nullable
as bool,eventCount: null == eventCount ? _self.eventCount : eventCount // ignore: cast_nullable_to_non_nullable
as bool,autoKeep: null == autoKeep ? _self.autoKeep : autoKeep // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -0,0 +1,87 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'disk_smart.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_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> _$DiskSmartToJson(_DiskSmart instance) =>
<String, dynamic>{
'device': instance.device,
'healthy': instance.healthy,
'temperature': instance.temperature,
'model': instance.model,
'serial': instance.serial,
'powerOnHours': instance.powerOnHours,
'powerCycleCount': instance.powerCycleCount,
'rawData': instance.rawData,
'smartAttributes': instance.smartAttributes,
};
_SmartAttribute _$SmartAttributeFromJson(Map<String, dynamic> json) =>
_SmartAttribute(
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String,
value: (json['value'] as num?)?.toInt(),
worst: (json['worst'] as num?)?.toInt(),
thresh: (json['thresh'] as num?)?.toInt(),
whenFailed: json['whenFailed'] as String?,
rawValue: json['rawValue'],
rawString: json['rawString'] as String?,
flags: SmartAttributeFlags.fromJson(
json['flags'] as Map<String, dynamic>,
),
);
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,
};
_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> _$SmartAttributeFlagsToJson(
_SmartAttributeFlags instance,
) => <String, dynamic>{
'value': instance.value,
'string': instance.string,
'prefailure': instance.prefailure,
'updatedOnline': instance.updatedOnline,
'performance': instance.performance,
'errorRate': instance.errorRate,
'eventCount': instance.eventCount,
'autoKeep': instance.autoKeep,
};

View File

@@ -11,7 +11,7 @@ enum Dist {
alpine,
rocky,
deepin,
;
coreelec,
}
extension StringX on String {
@@ -33,6 +33,4 @@ extension StringX on String {
// Special rules
const _wrts = [
'istoreos',
];
const _wrts = ['istoreos'];

View File

@@ -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);
}
}

View File

@@ -1,3 +1,5 @@
// ignore_for_file: unintended_html_in_doc_comment
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart';
@@ -14,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);
@@ -30,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
@@ -56,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;

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -1,24 +1,16 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
part 'private_key_info.g.dart';
@JsonSerializable()
@HiveType(typeId: 1)
class PrivateKeyInfo {
@HiveField(0)
final String id;
@JsonKey(name: 'private_key')
@HiveField(1)
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);
factory PrivateKeyInfo.fromJson(Map<String, dynamic> json) => _$PrivateKeyInfoFromJson(json);
Map<String, dynamic> toJson() => _$PrivateKeyInfoToJson(this);

View File

@@ -2,47 +2,6 @@
part of 'private_key_info.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class PrivateKeyInfoAdapter extends TypeAdapter<PrivateKeyInfo> {
@override
final int typeId = 1;
@override
PrivateKeyInfo read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PrivateKeyInfo(
id: fields[0] as String,
key: fields[1] as String,
);
}
@override
void write(BinaryWriter writer, PrivateKeyInfo obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.key);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PrivateKeyInfoAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
@@ -54,7 +13,4 @@ PrivateKeyInfo _$PrivateKeyInfoFromJson(Map<String, dynamic> json) =>
);
Map<String, dynamic> _$PrivateKeyInfoToJson(PrivateKeyInfo instance) =>
<String, dynamic>{
'id': instance.id,
'private_key': instance.key,
};
<String, dynamic>{'id': instance.id, 'private_key': instance.key};

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -1,10 +1,12 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:fl_lib/fl_lib.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';
import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/disk_smart.dart';
import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/net_speed.dart';
import 'package:server_box/data/model/server/nvdia.dart';
@@ -19,12 +21,7 @@ class Server {
SSHClient? client;
ServerConn conn;
Server(
this.spi,
this.status,
this.conn, {
this.client,
});
Server(this.spi, this.status, this.conn, {this.client});
bool get needGenClient => conn < ServerConn.connecting;
@@ -44,7 +41,9 @@ class ServerStatus {
SystemType system;
Err? err;
DiskIO diskIO;
List<DiskSmart> diskSmart;
List<NvidiaSmiItem>? nvidia;
List<AmdSmiItem>? amd;
final List<Battery> batteries = [];
final Map<StatusCmdType, String> more = {};
final List<SensorItem> sensors = [];
@@ -61,6 +60,7 @@ class ServerStatus {
required this.temps,
required this.system,
required this.diskIO,
this.diskSmart = const [],
this.err,
this.nvidia,
this.diskUsage,

View File

@@ -1,15 +1,16 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/store/server.dart';
import 'package:server_box/data/model/app/error.dart';
part 'server_private_info.freezed.dart';
part 'server_private_info.g.dart';
/// In the first version, it's called `ServerPrivateInfo` which was designed to
@@ -18,79 +19,81 @@ 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`.
@JsonSerializable()
@HiveType(typeId: 3)
class Spi {
@HiveField(0)
final String name;
@HiveField(1)
final String ip;
@HiveField(2)
final int port;
@HiveField(3)
final String user;
@HiveField(4)
final String? pwd;
@freezed
abstract class Spi with _$Spi {
const Spi._();
/// [id] of private key
@JsonKey(name: 'pubKeyId')
@HiveField(5)
final String? keyId;
@HiveField(6)
final List<String>? tags;
@HiveField(7)
final String? alterUrl;
@HiveField(8, defaultValue: true)
final bool autoConnect;
@JsonSerializable(includeIfNull: false)
const factory Spi({
required String name,
required String ip,
required int port,
required String user,
String? pwd,
/// [id] of the jump server
@HiveField(9)
final String? jumpId;
/// [id] of private key
@JsonKey(name: 'pubKeyId') String? keyId,
List<String>? tags,
String? alterUrl,
@Default(true) bool autoConnect,
@HiveField(10)
final ServerCustom? custom;
/// [id] of the jump server
String? jumpId,
ServerCustom? custom,
WakeOnLanCfg? wolCfg,
@HiveField(11)
final WakeOnLanCfg? wolCfg;
/// It only applies to SSH terminal.
Map<String, String>? envs,
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
/// It only applies to SSH terminal.
@HiveField(12)
final Map<String, String>? envs;
/// Custom system type (unix or windows). If set, skip auto-detection.
@JsonKey(includeIfNull: false) SystemType? customSystemType,
final String id;
const Spi({
required this.name,
required this.ip,
required this.port,
required this.user,
required this.pwd,
this.keyId,
this.tags,
this.alterUrl,
this.autoConnect = true,
this.jumpId,
this.custom,
this.wolCfg,
this.envs,
}) : id = '$user@$ip:$port';
/// Disabled command types for this server
@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes,
}) = _Spi;
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
Map<String, dynamic> toJson() => _$SpiToJson(this);
@override
String toString() => id;
String toString() => 'Spi<$oldId>';
static String parseId(Object? id) {
if (id == null || id is! String || id.isEmpty) return ShortId.generate();
return id;
}
}
extension Spix on Spi {
/// After upgrading to >= 1155, this field is only recommended to be used
/// for displaying the server name.
String get oldId => '$user@$ip:$port';
/// Save the [Spi] to the local storage.
void save() => ServerStore.instance.put(this);
/// Migrate the [oldId] to the new generated [id] by [ShortId.generate].
///
/// Returns:
/// - `null` if the [id] is not empty.
/// - The new [id] if the [id] is empty.
String? migrateId() {
if (id.isNotEmpty) return null;
ServerStore.instance.delete(oldId);
final newSpi = copyWith(id: ShortId.generate());
newSpi.save();
return newSpi.id;
}
String toJsonString() => json.encode(toJson());
VNode<Server>? get server => ServerProvider.pick(spi: this);
VNode<Server>? get jumpServer => ServerProvider.pick(id: jumpId);
bool shouldReconnect(Spi old) {
return id != old.id ||
return user != old.user ||
ip != old.ip ||
port != old.port ||
pwd != old.pwd ||
keyId != old.keyId ||
alterUrl != old.alterUrl ||
@@ -122,7 +125,7 @@ extension Spix on Spi {
/// Just for showing the struct of the class.
///
/// **NOT** the default value.
static const example = Spi(
static final example = Spi(
name: 'name',
ip: 'ip',
port: 22,
@@ -136,12 +139,11 @@ extension Spix on Spi {
custom: ServerCustom(
pveAddr: 'http://localhost:8006',
pveIgnoreCert: false,
cmds: {
'echo': 'echo hello',
},
cmds: {'echo': 'echo hello'},
preferTempDev: 'nvme-pci-0400',
logoUrl: 'https://example.com/logo.png',
),
id: 'id',
);
bool get isRoot => user == 'root';

View File

@@ -0,0 +1,221 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'server_private_info.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Spi {
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// 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();
@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
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
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
});
}
/// @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.
@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>?,
));
}
}
// dart format on

View File

@@ -2,118 +2,62 @@
part of 'server_private_info.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SpiAdapter extends TypeAdapter<Spi> {
@override
final int typeId = 3;
@override
Spi read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Spi(
name: fields[0] as String,
ip: fields[1] as String,
port: fields[2] as int,
user: fields[3] as String,
pwd: fields[4] as String?,
keyId: fields[5] as String?,
tags: (fields[6] as List?)?.cast<String>(),
alterUrl: fields[7] as String?,
autoConnect: fields[8] == null ? true : fields[8] as bool,
jumpId: fields[9] as String?,
custom: fields[10] as ServerCustom?,
wolCfg: fields[11] as WakeOnLanCfg?,
envs: (fields[12] as Map?)?.cast<String, String>(),
);
}
@override
void write(BinaryWriter writer, Spi obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.ip)
..writeByte(2)
..write(obj.port)
..writeByte(3)
..write(obj.user)
..writeByte(4)
..write(obj.pwd)
..writeByte(5)
..write(obj.keyId)
..writeByte(6)
..write(obj.tags)
..writeByte(7)
..write(obj.alterUrl)
..writeByte(8)
..write(obj.autoConnect)
..writeByte(9)
..write(obj.jumpId)
..writeByte(10)
..write(obj.custom)
..writeByte(11)
..write(obj.wolCfg)
..writeByte(12)
..write(obj.envs);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SpiAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Spi _$SpiFromJson(Map<String, dynamic> json) => Spi(
name: json['name'] as String,
ip: json['ip'] as String,
port: (json['port'] as num).toInt(),
user: json['user'] as String,
pwd: json['pwd'] as String?,
keyId: json['pubKeyId'] as String?,
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
alterUrl: json['alterUrl'] as String?,
autoConnect: json['autoConnect'] as bool? ?? true,
jumpId: json['jumpId'] as String?,
custom: json['custom'] == null
? null
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
wolCfg: json['wolCfg'] == null
? null
: WakeOnLanCfg.fromJson(json['wolCfg'] as Map<String, dynamic>),
envs: (json['envs'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
);
_Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
name: json['name'] as String,
ip: json['ip'] as String,
port: (json['port'] as num).toInt(),
user: json['user'] as String,
pwd: json['pwd'] as String?,
keyId: json['pubKeyId'] as String?,
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
alterUrl: json['alterUrl'] as String?,
autoConnect: json['autoConnect'] as bool? ?? true,
jumpId: json['jumpId'] as String?,
custom: json['custom'] == null
? null
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
wolCfg: json['wolCfg'] == null
? null
: WakeOnLanCfg.fromJson(json['wolCfg'] as Map<String, dynamic>),
envs: (json['envs'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
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>{
'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,
};
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
'name': instance.name,
'ip': instance.ip,
'port': instance.port,
'user': instance.user,
if (instance.pwd case final value?) 'pwd': value,
if (instance.keyId case final value?) 'pubKeyId': value,
if (instance.tags case final value?) 'tags': value,
if (instance.alterUrl case final value?) 'alterUrl': value,
'autoConnect': instance.autoConnect,
if (instance.jumpId case final value?) 'jumpId': value,
if (instance.custom case final value?) 'custom': value,
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,
};
const _$SystemTypeEnumMap = {
SystemType.linux: 'linux',
SystemType.bsd: 'bsd',
SystemType.windows: 'windows',
};

View File

@@ -1,27 +1,33 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.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';
import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/disk_smart.dart';
import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/net_speed.dart';
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/app/shell_func.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/net_speed.dart';
import 'package:server_box/data/model/server/conn.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,28 +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;
}
@@ -60,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;
}
@@ -69,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) {
@@ -80,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;
}
@@ -97,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;
}
@@ -119,26 +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 {
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments));
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.findInMap(parsedOutput));
req.ss.diskSmart = smarts;
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final battery = StatusCmdType.battery.find(segments);
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
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);
@@ -151,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);
@@ -161,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) {
@@ -175,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;
}
@@ -213,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);
}
@@ -222,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;
}
@@ -243,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
}
}

View File

@@ -1,62 +1,50 @@
import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:xterm/core.dart';
part 'snippet.g.dart';
part 'snippet.freezed.dart';
@JsonSerializable()
@HiveType(typeId: 2)
class Snippet {
@HiveField(0)
final String name;
@HiveField(1)
final String script;
@HiveField(2)
final List<String>? tags;
@HiveField(3)
final String? note;
@freezed
abstract class Snippet with _$Snippet {
const factory Snippet({
required String name,
required String script,
List<String>? tags,
String? note,
/// List of server id that this snippet should be auto run on
@HiveField(4)
final List<String>? autoRunOn;
/// List of server id that this snippet should be auto run on
List<String>? autoRunOn,
}) = _Snippet;
const Snippet({
required this.name,
required this.script,
this.tags,
this.note,
this.autoRunOn,
});
factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);
factory Snippet.fromJson(Map<String, dynamic> json) =>
_$SnippetFromJson(json);
Map<String, dynamic> toJson() => _$SnippetToJson(this);
static const example = Snippet(
name: 'example',
script: 'echo hello',
tags: ['tag'],
note: 'note',
autoRunOn: ['server_id'],
);
}
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);
@@ -124,11 +112,7 @@ class 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;
@@ -145,11 +129,7 @@ class 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');
}
@@ -171,18 +151,7 @@ class Snippet {
};
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
static final fmtTermKeys = {
r'${ctrl': TerminalKey.control,
r'${alt': TerminalKey.alt,
};
static const example = Snippet(
name: 'example',
script: 'echo hello',
tags: ['tag'],
note: 'note',
autoRunOn: ['server_id'],
);
static final fmtTermKeys = {r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt};
}
class SnippetResult {
@@ -190,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});
@@ -206,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);

View File

@@ -0,0 +1,179 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'snippet.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Snippet {
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();
@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> {
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>
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.
@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
@JsonSerializable()
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);
}
/// 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

View File

@@ -2,74 +2,24 @@
part of 'snippet.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SnippetAdapter extends TypeAdapter<Snippet> {
@override
final int typeId = 2;
@override
Snippet read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Snippet(
name: fields[0] as String,
script: fields[1] as String,
tags: (fields[2] as List?)?.cast<String>(),
note: fields[3] as String?,
autoRunOn: (fields[4] as List?)?.cast<String>(),
);
}
@override
void write(BinaryWriter writer, Snippet obj) {
writer
..writeByte(5)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.script)
..writeByte(2)
..write(obj.tags)
..writeByte(3)
..write(obj.note)
..writeByte(4)
..write(obj.autoRunOn);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SnippetAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
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(),
);
_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> _$SnippetToJson(Snippet 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,
};

View File

@@ -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;
}
}
}

View File

@@ -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';
}

View File

@@ -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];

Some files were not shown because too many files have changed in this diff Show More