Compare commits

...

97 Commits

Author SHA1 Message Date
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
Noo6
7dda63af8a fix: add file on windows 2024-11-14 11:20:45 +08:00
lollipopkit🏳️‍⚧️
00d303ac36 fix: editing pref store (#618) 2024-10-29 19:37:19 +08:00
216 changed files with 25059 additions and 8518 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 provide charts to display <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> server status and tools to manage server.
<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>
## 📥 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**!
## 🔖 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,33 @@ 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).
## 🧱 Contribution
Any positive contribution is welcome.
### 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 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
<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,30 @@ 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更好看的反馈不一定会接受
## 🧱 贡献
任何正面的贡献都欢迎。
### 开发
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"

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

@@ -1,68 +1,20 @@
PODS:
- app_links (0.0.2):
- Flutter
- camera_avfoundation (0.0.1):
- Flutter
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_background_service_ios (0.0.3):
- flutter_native_splash (2.4.3):
- Flutter
- flutter_native_splash (0.0.1):
- flutter_secure_storage (6.0.0):
- Flutter
- GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleMLKit/BarcodeScanning (6.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 5.0.0)
- GoogleMLKit/MLKitCore (6.0.0):
- MLKitCommon (~> 11.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (7.13.3)
- GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilitiesComponents (1.1.0):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (3.5.0)
- icloud_storage (0.0.1):
- Flutter
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- MLImage (1.0.0-beta5)
- MLKitBarcodeScanning (5.0.0):
- MLKitCommon (~> 11.0)
- MLKitVision (~> 7.0)
- MLKitCommon (11.0.0):
- GoogleDataTransport (< 10.0, >= 9.4.1)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0)
- GoogleUtilitiesComponents (~> 1.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (7.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta5)
- MLKitCommon (~> 11.0)
- mobile_scanner (5.1.1):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 6.0.0)
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
@@ -70,7 +22,6 @@ PODS:
- FlutterMacOS
- plain_notification_token (0.0.1):
- Flutter
- PromisesObjC (2.4.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -82,17 +33,16 @@ PODS:
- Flutter
- watch_connectivity (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
- 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`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- plain_notification_token (from `.symlinks/plugins/plain_notification_token/ios`)
@@ -101,38 +51,24 @@ 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`)
SPEC REPOS:
trunk:
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GoogleUtilitiesComponents
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_background_service_ios:
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
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:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
@@ -149,39 +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:
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
mobile_scanner: 8564358885a9253c43f822435b70f9345c87224f
nanopb: 438bc412db1928dac798aa6fd75726007be04262
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
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 = 1201;
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.1201;
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 = 1201;
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.1201;
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 = 1201;
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.1201;
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 = 1201;
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.1201;
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 = 1201;
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.1201;
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 = 1201;
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.1201;
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 = 1201;
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.1201;
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 = 1201;
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.1201;
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 = 1201;
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.1201;
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,67 @@ 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,6 +94,14 @@ class MyApp extends StatelessWidget {
return MaterialApp(
key: ValueKey(locale),
builder: (context, child) => ResponsiveBreakpoints.builder(
child: child ?? UIs.placeholder,
breakpoints: const [
Breakpoint(start: 0, end: 450, name: MOBILE),
Breakpoint(start: 451, end: 800, name: TABLET),
Breakpoint(start: 801, end: 1920, name: DESKTOP),
],
),
locale: locale,
localizationsDelegates: const [
LibLocalizations.delegate,
@@ -87,21 +114,25 @@ 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,17 +12,17 @@ extension SftpFileX on SftpFileMode {
UnixPerm toUnixPerm() {
return UnixPerm(
user: RWX(
user: UnixPermOp(
r: userRead,
w: userWrite,
x: userExecute,
),
group: RWX(
group: UnixPermOp(
r: groupRead,
w: groupWrite,
x: groupExecute,
),
other: RWX(
other: UnixPermOp(
r: otherRead,
w: otherWrite,
x: otherExecute,

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

@@ -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.
///
@@ -32,7 +31,7 @@ enum GenSSHClientStatus {
}
String getPrivateKey(String id) {
final pki = Stores.key.get(id);
final pki = Stores.key.fetchOne(id);
if (pki == null) {
throw SSHErr(
type: SSHErrType.noPrivateKey,
@@ -59,7 +58,7 @@ Future<SSHClient> genClient(
Spi? jumpSpi,
/// Handle keyboard-interactive authentication
FutureOr<List<String>?> Function(SSHUserInfoRequest)? onKeyboardInteractive,
SSHUserInfoRequestHandler? onKeyboardInteractive,
}) async {
onStatus?.call(GenSSHClientStatus.socket);

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

@@ -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) {
@@ -230,3 +234,4 @@ String _diyDecrypt(String raw) {
rethrow;
}
}

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,27 +1,6 @@
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,
@@ -35,7 +14,7 @@ enum SSHErrType {
}
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) {
@@ -45,11 +24,6 @@ class SSHErr extends Err<SSHErrType> {
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
_ => null,
};
@override
String toString() {
return 'SSHErr<$type>: $message';
}
}
enum ContainerErrType {
@@ -65,16 +39,10 @@ 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 {
@@ -84,15 +52,10 @@ enum ICloudErrType {
}
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 {
@@ -102,15 +65,10 @@ enum WebdavErrType {
}
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 {
@@ -121,13 +79,8 @@ enum PveErrType {
}
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

@@ -1,36 +1,23 @@
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!) {

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,17 +1,10 @@
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) {

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

@@ -1,10 +1,12 @@
import 'package:fl_lib/fl_lib.dart';
final _seperator = Pfs.seperator;
/// It's used on platform's file system.
/// So use [Platform.pathSeparator] to join path.
class LocalPath {
final String _prefixPath;
String _path = '/';
String _path = _seperator;
String? _prePath;
String get path => _prefixPath + _path;
@@ -13,20 +15,20 @@ class LocalPath {
void update(String newPath) {
_prePath = _path;
if (newPath == '..') {
_path = _path.substring(0, _path.lastIndexOf('/'));
_path = _path.substring(0, _path.lastIndexOf(_seperator));
if (_path == '') {
_path = '/';
_path = _seperator;
}
return;
}
if (newPath == '/') {
_path = '/';
if (newPath == _seperator) {
_path = _seperator;
return;
}
_path = _path.joinPath(newPath);
}
bool get canBack => path != '$_prefixPath/';
bool get canBack => path != '$_prefixPath$_seperator';
bool undo() {
if (_prePath == null || _path == _prePath) {
@@ -38,7 +40,7 @@ class LocalPath {
}
String _trimSuffix(String prefixPath) {
if (prefixPath.endsWith('/')) {
if (prefixPath.endsWith(_seperator)) {
return prefixPath.substring(0, prefixPath.length - 1);
}
return prefixPath;

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,8 +1,7 @@
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';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/build_data.dart';
enum ShellFunc {
status,
@@ -10,14 +9,19 @@ enum ShellFunc {
process,
shutdown,
reboot,
suspend,
;
suspend;
static const seperator = 'SrvBoxSep';
/// The suffix `\t` is for formatting
static const cmdDivider = '\necho $seperator\n\t';
/// Cached Linux status commands string
static final _linuxStatusCmds = StatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
/// Cached BSD status commands string
static final _bsdStatusCmds = BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
/// srvboxm -> ServerBox Mobile
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
static const scriptDirHome = '~/.config/server_box';
@@ -30,19 +34,17 @@ enum ShellFunc {
/// 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;
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
return _scriptDirMap.putIfAbsent(id, () {
return scriptDirTmp;
});
_scriptDirMap[id] ??= scriptDirTmp;
return _scriptDirMap[id]!;
}
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
_ => _scriptDirMap[id] = scriptDirHome,
};
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
_ => _scriptDirMap[id] = scriptDirHome,
};
static String getScriptPath(String id) {
return '${getScriptDir(id)}/$scriptFile';
@@ -59,53 +61,34 @@ 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',
};
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 name => switch (this) {
ShellFunc.status => 'status',
ShellFunc.process => 'process',
ShellFunc.shutdown => 'ShutDown',
ShellFunc.reboot => 'Reboot',
ShellFunc.suspend => 'Suspend',
};
String get _cmd {
switch (this) {
case ShellFunc.status:
return '''
String get _cmd => switch (this) {
ShellFunc.status =>
'''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\t${StatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
\t$_linuxStatusCmds
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 '''
\t$_bsdStatusCmds
fi''',
ShellFunc.process =>
'''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\tif [ "\$isBusybox" != "" ]; then
\t\tps w
@@ -115,30 +98,29 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
else
\tps -ax
fi
''';
case ShellFunc.shutdown:
return '''
''',
ShellFunc.shutdown =>
'''
if [ "\$userId" = "0" ]; then
\tshutdown -h now
else
\tsudo -S shutdown -h now
fi''';
case ShellFunc.reboot:
return '''
fi''',
ShellFunc.reboot =>
'''
if [ "\$userId" = "0" ]; then
\treboot
else
\tsudo -S reboot
fi''';
case ShellFunc.suspend:
return '''
fi''',
ShellFunc.suspend =>
'''
if [ "\$userId" = "0" ]; then
\tsystemctl suspend
else
\tsudo -S systemctl suspend
fi''';
}
}
fi''',
};
static String allScript(Map<String, String>? customCmds) {
final sb = StringBuffer();
@@ -164,9 +146,7 @@ exec 2>/dev/null
// Write each func
for (final func in values) {
final customCmdsStr = () {
if (func == ShellFunc.status &&
customCmds != null &&
customCmds.isNotEmpty) {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
}
return '';
@@ -209,22 +189,22 @@ enum StatusCmdType {
echo._('echo ${SystemType.linuxSign}'),
time._('date +%s'),
net._('cat /proc/net/dev'),
sys._('cat /etc/*-release | grep PRETTY_NAME'),
sys._('cat /etc/*-release | grep ^PRETTY_NAME'),
cpu._('cat /proc/stat | grep cpu'),
uptime._('uptime'),
conn._('cat /proc/net/snmp'),
disk._('df'),
disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'),
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
host._('cat /etc/hostname'),
diskio._('cat /proc/diskstats'),
battery._(
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
nvidia._('nvidia-smi -q -x'),
amd._('if command -v amd-smi >/dev/null 2>&1; then amd-smi list --json && amd-smi metric --json; elif command -v rocm-smi >/dev/null 2>&1; then rocm-smi --json || rocm-smi --showunique --showuse --showtemp --showfan --showclocks --showmemuse --showpower; elif command -v radeontop >/dev/null 2>&1; then timeout 2s radeontop -d - -l 1 | tail -n +2; else echo "No AMD GPU monitoring tools found"; fi'),
sensors._('sensors'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"'),
;
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
final String cmd;
@@ -238,12 +218,12 @@ enum BSDStatusCmdType {
sys._('uname -or'),
cpu._('top -l 1 | grep "CPU usage"'),
uptime._('uptime'),
// Keep df -k for BSD systems as lsblk is not available on macOS/BSD
disk._('df -k'),
mem._('top -l 1 | grep PhysMem'),
//temp,
host._('hostname'),
cpuBrand._('sysctl -n machdep.cpu.brand_string'),
;
cpuBrand._('sysctl -n machdep.cpu.brand_string');
final String cmd;
@@ -252,10 +232,12 @@ enum BSDStatusCmdType {
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,
};
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,
};
}

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,11 +1,11 @@
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 {
@@ -13,13 +13,13 @@ enum AppTab {
ssh,
file,
snippet,
settings,
//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(),
@@ -33,11 +33,11 @@ enum AppTab {
label: l10n.server,
selectedIcon: const Icon(BoxIcons.bxs_server),
),
settings => NavigationDestination(
icon: const Icon(Icons.settings),
label: libL10n.setting,
selectedIcon: const Icon(Icons.settings),
),
// 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',
@@ -56,7 +56,41 @@ enum AppTab {
};
}
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

@@ -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;
}
}
@@ -109,6 +105,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

@@ -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,208 @@
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) {
@@ -43,12 +222,12 @@ class Disk {
final mount = vals[5];
if (!_shouldCalc(fs, mount)) continue;
list.add(Disk(
fs: fs,
path: fs,
mount: mount,
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
used: BigInt.parse(vals[2]),
size: BigInt.parse(vals[1]),
avail: BigInt.parse(vals[3]),
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 +237,8 @@ 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 +250,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 +289,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);
@@ -166,7 +354,11 @@ class DiskUsage {
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 +366,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 +380,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,6 +11,7 @@ enum Dist {
alpine,
rocky,
deepin,
coreelec,
;
}

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

View File

@@ -1,15 +1,11 @@
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({
@@ -17,8 +13,7 @@ class PrivateKeyInfo {
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

@@ -1,10 +1,12 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/amd.dart';
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,15 @@
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/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 +18,75 @@ 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.
@HiveField(12)
final Map<String, String>? envs;
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';
/// It only applies to SSH terminal.
Map<String, String>? envs,
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
}) = _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,27 +118,27 @@ extension Spix on Spi {
/// Just for showing the struct of the class.
///
/// **NOT** the default value.
static const example = Spi(
name: 'name',
ip: 'ip',
port: 22,
user: 'root',
pwd: 'pwd',
keyId: 'private_key_id',
tags: ['tag1', 'tag2'],
alterUrl: 'user@ip:port',
autoConnect: true,
jumpId: 'jump_server_id',
custom: ServerCustom(
pveAddr: 'http://localhost:8006',
pveIgnoreCert: false,
cmds: {
'echo': 'echo hello',
},
preferTempDev: 'nvme-pci-0400',
logoUrl: 'https://example.com/logo.png',
),
);
static final example = Spi(
name: 'name',
ip: 'ip',
port: 22,
user: 'root',
pwd: 'pwd',
keyId: 'private_key_id',
tags: ['tag1', 'tag2'],
alterUrl: 'user@ip:port',
autoConnect: true,
jumpId: 'jump_server_id',
custom: ServerCustom(
pveAddr: 'http://localhost:8006',
pveIgnoreCert: false,
cmds: {
'echo': 'echo hello',
},
preferTempDev: 'nvme-pci-0400',
logoUrl: 'https://example.com/logo.png',
),
id: 'id');
bool get isRoot => user == 'root';
}

View File

@@ -0,0 +1,202 @@
// 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;
/// 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));
}
@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);
}
/// @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
});
}
/// @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,}) {
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,
));
}
}
/// @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 = ''}): _tags = tags,_envs = envs,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;
/// 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));
}
@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);
}
/// @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
});
}
/// @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,}) {
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,
));
}
}
// dart format on

View File

@@ -2,118 +2,46 @@
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']),
);
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,
};

View File

@@ -1,17 +1,18 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/amd.dart';
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';
class ServerStatusUpdateReq {
final ServerStatus ss;
final List<String> segments;
@@ -38,7 +39,8 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
final segments = req.segments;
final time = int.tryParse(StatusCmdType.time.find(segments)) ??
final time =
int.tryParse(StatusCmdType.time.find(segments)) ??
DateTime.now().millisecondsSinceEpoch ~/ 1000;
try {
@@ -49,9 +51,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
}
try {
final sys = _parseSysVer(
StatusCmdType.sys.find(segments),
);
final sys = _parseSysVer(StatusCmdType.sys.find(segments));
if (sys != null) {
req.ss.more[StatusCmdType.sys] = sys;
}
@@ -131,12 +131,25 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
Loggers.app.warning(e, s);
}
try {
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.find(segments));
req.ss.diskSmart = smarts;
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.find(segments));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final battery = StatusCmdType.battery.find(segments);

View File

@@ -1,42 +1,37 @@
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) {
@@ -175,14 +170,6 @@ class Snippet {
r'${ctrl': TerminalKey.control,
r'${alt': TerminalKey.alt,
};
static const example = Snippet(
name: 'example',
script: 'echo hello',
tags: ['tag'],
note: 'note',
autoRunOn: ['server_id'],
);
}
class SnippetResult {

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,19 +1,14 @@
import 'dart:io';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:wake_on_lan/wake_on_lan.dart';
part 'wol_cfg.g.dart';
@JsonSerializable()
@HiveType(typeId: 8)
@JsonSerializable(includeIfNull: false)
final class WakeOnLanCfg {
@HiveField(0)
final String mac;
@HiveField(1)
final String ip;
@HiveField(2)
final String? pwd;
const WakeOnLanCfg({
@@ -26,18 +21,12 @@ final class WakeOnLanCfg {
final macValidation = MACAddress.validate(mac);
final ipValidation = IPAddress.validate(
ip,
type: ip.contains(':')
? InternetAddressType.IPv6
: InternetAddressType.IPv4,
type: ip.contains(':') ? InternetAddressType.IPv6 : InternetAddressType.IPv4,
);
final pwdValidation = pwd != null
? SecureONPassword.validate(pwd)
: (state: true, error: null);
final pwdValidation = pwd != null ? SecureONPassword.validate(pwd) : (state: true, error: null);
final valid =
macValidation.state && ipValidation.state && pwdValidation.state;
final err =
macValidation.error ?? ipValidation.error ?? pwdValidation.error;
final valid = macValidation.state && ipValidation.state && pwdValidation.state;
final err = macValidation.error ?? ipValidation.error ?? pwdValidation.error;
return (err, valid);
}
@@ -56,8 +45,7 @@ final class WakeOnLanCfg {
);
}
factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) =>
_$WakeOnLanCfgFromJson(json);
factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) => _$WakeOnLanCfgFromJson(json);
Map<String, dynamic> toJson() => _$WakeOnLanCfgToJson(this);
}

View File

@@ -2,63 +2,19 @@
part of 'wol_cfg.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class WakeOnLanCfgAdapter extends TypeAdapter<WakeOnLanCfg> {
@override
final int typeId = 8;
@override
WakeOnLanCfg read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return WakeOnLanCfg(
mac: fields[0] as String,
ip: fields[1] as String,
pwd: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, WakeOnLanCfg obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.mac)
..writeByte(1)
..write(obj.ip)
..writeByte(2)
..write(obj.pwd);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is WakeOnLanCfgAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
WakeOnLanCfg _$WakeOnLanCfgFromJson(Map<String, dynamic> json) => WakeOnLanCfg(
mac: json['mac'] as String,
ip: json['ip'] as String,
pwd: json['pwd'] as String?,
);
mac: json['mac'] as String,
ip: json['ip'] as String,
pwd: json['pwd'] as String?,
);
Map<String, dynamic> _$WakeOnLanCfgToJson(WakeOnLanCfg instance) =>
<String, dynamic>{
'mac': instance.mac,
'ip': instance.ip,
'pwd': instance.pwd,
if (instance.pwd case final value?) 'pwd': value,
};

View File

@@ -36,7 +36,7 @@ class _AbsolutePath {
_path = newPath;
return;
}
_path = _path.joinPath(newPath, seperator: _sep);
_path = _path.joinPath(newPath, separator: _sep);
}
bool undo() {

View File

@@ -21,7 +21,7 @@ class SftpReq {
}
if (spi.jumpId != null) {
jumpSpi = Stores.server.box.get(spi.jumpId);
jumpPrivateKey = Stores.key.get(jumpSpi?.keyId)?.key;
jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key;
}
}
}

View File

@@ -60,8 +60,6 @@ Future<void> isolateMessageHandler(
case SftpReqType.upload:
await _upload(data, mainSendPort, sendError);
break;
default:
sendError(Exception('unknown type'));
}
break;
default:
@@ -101,16 +99,21 @@ Future<void> _download(
mainSendPort.send(size);
mainSendPort.send(SftpWorkerStatus.loading);
// Read 2m each time
// Issue #161
// The download speed is about 2m/s may due to single core performance
const defaultChunkSize = 1024 * 1024 * 2;
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
for (var i = 0; i < size; i += chunkSize) {
final fileData = file.read(length: chunkSize);
await for (var form in fileData) {
localFile.add(form);
mainSendPort.send((i + form.length) / size * 100);
// Due to single core performance, limit the chunk size
const defaultChunkSize = 1024 * 1024 * 5;
var totalRead = 0;
while (totalRead < size) {
final remaining = size - totalRead;
final chunkSize = remaining > defaultChunkSize ? defaultChunkSize : remaining;
dprint('Size: $size, Total Read: $totalRead, Chunk Size: $chunkSize');
final fileData = file.read(offset: totalRead, length: chunkSize);
await for (var chunk in fileData) {
localFile.add(chunk);
totalRead += chunk.length;
mainSendPort.send(totalRead / size * 100);
}
}

View File

@@ -1,81 +1,57 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
import 'package:xterm/core.dart';
part 'virtual_key.g.dart';
enum VirtualKeyFunc { toggleIME, backspace, clipboard, snippet, file }
@HiveType(typeId: 4)
enum VirtKey {
@HiveField(0)
esc,
@HiveField(1)
alt,
@HiveField(2)
home,
@HiveField(3)
up,
@HiveField(4)
end,
@HiveField(5)
sftp,
@HiveField(6)
snippet,
@HiveField(7)
tab,
@HiveField(8)
ctrl,
@HiveField(9)
left,
@HiveField(10)
down,
@HiveField(11)
right,
@HiveField(12)
clipboard,
@HiveField(13)
ime,
@HiveField(14)
shift,
pgup,
@HiveField(15)
pgdn,
@HiveField(16)
slash,
@HiveField(17)
backSlash,
@HiveField(18)
underscore,
@HiveField(19)
plus,
@HiveField(20)
equal,
@HiveField(21)
minus,
@HiveField(22)
parenLeft,
@HiveField(23)
parenRight,
@HiveField(24)
bracketLeft,
@HiveField(25)
bracketRight,
@HiveField(26)
braceLeft,
@HiveField(27)
braceRight,
@HiveField(28)
chevronLeft,
@HiveField(29)
chevronRight,
@HiveField(30)
colon,
@HiveField(31)
semicolon,
;
f1,
f2,
f3,
f4,
f5,
f6,
f7,
f8,
f9,
f10,
f11,
f12;
}
extension VirtKeyX on VirtKey {
@@ -130,6 +106,7 @@ extension VirtKeyX on VirtKey {
VirtKey.right,
VirtKey.clipboard,
VirtKey.ime,
VirtKey.shift,
];
/// Corresponding [TerminalKey]
@@ -144,8 +121,21 @@ extension VirtKeyX on VirtKey {
VirtKey.left => TerminalKey.arrowLeft,
VirtKey.down => TerminalKey.arrowDown,
VirtKey.right => TerminalKey.arrowRight,
VirtKey.shift => TerminalKey.shift,
VirtKey.pgup => TerminalKey.pageUp,
VirtKey.pgdn => TerminalKey.pageDown,
VirtKey.f1 => TerminalKey.f1,
VirtKey.f2 => TerminalKey.f2,
VirtKey.f3 => TerminalKey.f3,
VirtKey.f4 => TerminalKey.f4,
VirtKey.f5 => TerminalKey.f5,
VirtKey.f6 => TerminalKey.f6,
VirtKey.f7 => TerminalKey.f7,
VirtKey.f8 => TerminalKey.f8,
VirtKey.f9 => TerminalKey.f9,
VirtKey.f10 => TerminalKey.f10,
VirtKey.f11 => TerminalKey.f11,
VirtKey.f12 => TerminalKey.f12,
_ => null,
};
@@ -174,7 +164,7 @@ extension VirtKeyX on VirtKey {
};
bool get toggleable => switch (this) {
VirtKey.alt || VirtKey.ctrl => true,
VirtKey.alt || VirtKey.ctrl || VirtKey.shift => true,
_ => false,
};

View File

@@ -1,196 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'virtual_key.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class VirtKeyAdapter extends TypeAdapter<VirtKey> {
@override
final int typeId = 4;
@override
VirtKey read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return VirtKey.esc;
case 1:
return VirtKey.alt;
case 2:
return VirtKey.home;
case 3:
return VirtKey.up;
case 4:
return VirtKey.end;
case 5:
return VirtKey.sftp;
case 6:
return VirtKey.snippet;
case 7:
return VirtKey.tab;
case 8:
return VirtKey.ctrl;
case 9:
return VirtKey.left;
case 10:
return VirtKey.down;
case 11:
return VirtKey.right;
case 12:
return VirtKey.clipboard;
case 13:
return VirtKey.ime;
case 14:
return VirtKey.pgup;
case 15:
return VirtKey.pgdn;
case 16:
return VirtKey.slash;
case 17:
return VirtKey.backSlash;
case 18:
return VirtKey.underscore;
case 19:
return VirtKey.plus;
case 20:
return VirtKey.equal;
case 21:
return VirtKey.minus;
case 22:
return VirtKey.parenLeft;
case 23:
return VirtKey.parenRight;
case 24:
return VirtKey.bracketLeft;
case 25:
return VirtKey.bracketRight;
case 26:
return VirtKey.braceLeft;
case 27:
return VirtKey.braceRight;
case 28:
return VirtKey.chevronLeft;
case 29:
return VirtKey.chevronRight;
case 30:
return VirtKey.colon;
case 31:
return VirtKey.semicolon;
default:
return VirtKey.esc;
}
}
@override
void write(BinaryWriter writer, VirtKey obj) {
switch (obj) {
case VirtKey.esc:
writer.writeByte(0);
break;
case VirtKey.alt:
writer.writeByte(1);
break;
case VirtKey.home:
writer.writeByte(2);
break;
case VirtKey.up:
writer.writeByte(3);
break;
case VirtKey.end:
writer.writeByte(4);
break;
case VirtKey.sftp:
writer.writeByte(5);
break;
case VirtKey.snippet:
writer.writeByte(6);
break;
case VirtKey.tab:
writer.writeByte(7);
break;
case VirtKey.ctrl:
writer.writeByte(8);
break;
case VirtKey.left:
writer.writeByte(9);
break;
case VirtKey.down:
writer.writeByte(10);
break;
case VirtKey.right:
writer.writeByte(11);
break;
case VirtKey.clipboard:
writer.writeByte(12);
break;
case VirtKey.ime:
writer.writeByte(13);
break;
case VirtKey.pgup:
writer.writeByte(14);
break;
case VirtKey.pgdn:
writer.writeByte(15);
break;
case VirtKey.slash:
writer.writeByte(16);
break;
case VirtKey.backSlash:
writer.writeByte(17);
break;
case VirtKey.underscore:
writer.writeByte(18);
break;
case VirtKey.plus:
writer.writeByte(19);
break;
case VirtKey.equal:
writer.writeByte(20);
break;
case VirtKey.minus:
writer.writeByte(21);
break;
case VirtKey.parenLeft:
writer.writeByte(22);
break;
case VirtKey.parenRight:
writer.writeByte(23);
break;
case VirtKey.bracketLeft:
writer.writeByte(24);
break;
case VirtKey.bracketRight:
writer.writeByte(25);
break;
case VirtKey.braceLeft:
writer.writeByte(26);
break;
case VirtKey.braceRight:
writer.writeByte(27);
break;
case VirtKey.chevronLeft:
writer.writeByte(28);
break;
case VirtKey.chevronRight:
writer.writeByte(29);
break;
case VirtKey.colon:
writer.writeByte(30);
break;
case VirtKey.semicolon:
writer.writeByte(31);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is VirtKeyAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,7 +1,27 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
final class AppProvider {
const AppProvider._();
part 'app.g.dart';
part 'app.freezed.dart';
static BuildContext? ctx;
@freezed
abstract class AppState with _$AppState {
const factory AppState({
@Default(false) bool desktopMode,
}) = _AppState;
}
@Riverpod(keepAlive: true)
class AppProvider extends _$AppProvider {
static BuildContext? ctx;
@override
AppState build() {
return const AppState();
}
void setDesktop(bool desktopMode) {
state = state.copyWith(desktopMode: desktopMode);
}
}

View File

@@ -0,0 +1,142 @@
// 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 'app.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppState {
bool get desktopMode;
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AppStateCopyWith<AppState> get copyWith => _$AppStateCopyWithImpl<AppState>(this as AppState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppState&&(identical(other.desktopMode, desktopMode) || other.desktopMode == desktopMode));
}
@override
int get hashCode => Object.hash(runtimeType,desktopMode);
@override
String toString() {
return 'AppState(desktopMode: $desktopMode)';
}
}
/// @nodoc
abstract mixin class $AppStateCopyWith<$Res> {
factory $AppStateCopyWith(AppState value, $Res Function(AppState) _then) = _$AppStateCopyWithImpl;
@useResult
$Res call({
bool desktopMode
});
}
/// @nodoc
class _$AppStateCopyWithImpl<$Res>
implements $AppStateCopyWith<$Res> {
_$AppStateCopyWithImpl(this._self, this._then);
final AppState _self;
final $Res Function(AppState) _then;
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? desktopMode = null,}) {
return _then(_self.copyWith(
desktopMode: null == desktopMode ? _self.desktopMode : desktopMode // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _AppState implements AppState {
const _AppState({this.desktopMode = false});
@override@JsonKey() final bool desktopMode;
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AppStateCopyWith<_AppState> get copyWith => __$AppStateCopyWithImpl<_AppState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppState&&(identical(other.desktopMode, desktopMode) || other.desktopMode == desktopMode));
}
@override
int get hashCode => Object.hash(runtimeType,desktopMode);
@override
String toString() {
return 'AppState(desktopMode: $desktopMode)';
}
}
/// @nodoc
abstract mixin class _$AppStateCopyWith<$Res> implements $AppStateCopyWith<$Res> {
factory _$AppStateCopyWith(_AppState value, $Res Function(_AppState) _then) = __$AppStateCopyWithImpl;
@override @useResult
$Res call({
bool desktopMode
});
}
/// @nodoc
class __$AppStateCopyWithImpl<$Res>
implements _$AppStateCopyWith<$Res> {
__$AppStateCopyWithImpl(this._self, this._then);
final _AppState _self;
final $Res Function(_AppState) _then;
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? desktopMode = null,}) {
return _then(_AppState(
desktopMode: null == desktopMode ? _self.desktopMode : desktopMode // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$appProviderHash() => r'8378ec9d0a9c8d99cc05805047cd2d52ac4dbb56';
/// See also [AppProvider].
@ProviderFor(AppProvider)
final appProviderProvider = NotifierProvider<AppProvider, AppState>.internal(
AppProvider.new,
name: r'appProviderProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appProviderHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppProvider = Notifier<AppState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -5,10 +5,10 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/container/image.dart';
import 'package:server_box/data/model/container/ps.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/store.dart';
@@ -222,6 +222,23 @@ class ContainerProvider extends ChangeNotifier {
Future<ContainerErr?> restart(String id) async => await run('restart $id');
Future<ContainerErr?> pruneImages({bool all = true}) async {
final cmd = 'image prune${all ? " -a" : ""} -f';
return await run(cmd);
}
Future<ContainerErr?> pruneContainers() async {
return await run('container prune -f');
}
Future<ContainerErr?> pruneVolumes() async {
return await run('volume prune -f');
}
Future<ContainerErr?> pruneSystem() async {
return await run('system prune -a -f --volumes');
}
Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
cmd = switch (type) {
ContainerType.docker => 'docker $cmd',
@@ -272,6 +289,8 @@ enum ContainerCmdType {
ps,
stats,
images,
// No specific commands needed for prune actions as they are simple
// and don't require splitting output with ShellFunc.seperator
;
String exec(

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:computer/computer.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_lib/fl_lib.dart';
@@ -10,7 +11,6 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/pve.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:dartssh2/dartssh2.dart';
typedef PveCtrlFunc = Future<bool> Function(String node, String id);
@@ -47,11 +47,11 @@ final class PveProvider extends ChangeNotifier {
final client = HttpClient();
client.connectionFactory = cf;
if (_ignoreCert) {
client.badCertificateCallback = (_, __, ___) => true;
client.badCertificateCallback = (_, _, _) => true;
}
return client;
},
validateCertificate: _ignoreCert ? (_, __, ___) => true : null,
validateCertificate: _ignoreCert ? (_, _, _) => true : null,
);
final data = ValueNotifier<PveRes?>(null);

View File

@@ -1,4 +1,5 @@
import 'dart:async';
// import 'dart:io';
import 'package:computer/computer.dart';
@@ -6,18 +7,17 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/sync.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_status_update_req.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/res/status.dart';
import 'package:server_box/data/res/store.dart';
class ServerProvider extends Provider {
const ServerProvider._();
@@ -45,22 +45,20 @@ class ServerProvider extends Provider {
for (int idx = 0; idx < spis.length; idx++) {
final spi = spis[idx];
final originServer = oldServers[spi.id];
final newServer = genServer(spi);
/// #258
/// If not [shouldReconnect], then keep the old state.
if (originServer != null &&
!originServer.value.spi.shouldReconnect(spi)) {
newServer.conn = originServer.value.conn;
if (originServer != null && !originServer.value.spi.shouldReconnect(spi)) {
originServer.value.spi = spi;
servers[spi.id] = originServer;
} else {
final newServer = genServer(spi);
servers[spi.id] = newServer.vn;
}
servers[spi.id] = newServer.vn;
}
final serverOrder_ = Stores.setting.serverOrder.fetch();
if (serverOrder_.isNotEmpty) {
spis.reorder(
order: serverOrder_,
finder: (n, id) => n.id == id,
);
spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id);
serverOrder.value.addAll(spis.map((e) => e.id));
} else {
serverOrder.value.addAll(servers.keys);
@@ -105,31 +103,30 @@ class ServerProvider extends Provider {
/// if [spi] is specificed then only refresh this server
/// [onlyFailed] only refresh failed servers
static Future<void> refresh({
Spi? spi,
bool onlyFailed = false,
}) async {
static Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {
if (spi != null) {
_manualDisconnectedIds.remove(spi.id);
await _getData(spi);
return;
}
await Future.wait(servers.values.map((val) async {
final s = val.value;
if (onlyFailed) {
if (s.conn != ServerConn.failed) return;
TryLimiter.reset(s.spi.id);
}
await Future.wait(
servers.values.map((val) async {
final s = val.value;
if (onlyFailed) {
if (s.conn != ServerConn.failed) return;
TryLimiter.reset(s.spi.id);
}
if (_manualDisconnectedIds.contains(s.spi.id)) return;
if (_manualDisconnectedIds.contains(s.spi.id)) return;
if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) {
return;
}
if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) {
return;
}
return await _getData(s.spi);
}));
return await _getData(s.spi);
}),
);
}
static Future<void> startAutoRefresh() async {
@@ -174,12 +171,16 @@ class ServerProvider extends Provider {
static void _closeOneServer(String id) {
final s = servers[id];
final item = s?.value;
item?.client?.close();
item?.client = null;
item?.conn = ServerConn.disconnected;
if (s == null) {
Loggers.app.warning('Server with id $id not found');
return;
}
final item = s.value;
item.client?.close();
item.client = null;
item.conn = ServerConn.disconnected;
_manualDisconnectedIds.add(id);
s?.notify();
s.notify();
}
static void addServer(Spi spi) {
@@ -208,14 +209,12 @@ class ServerProvider extends Provider {
serverOrder.value.clear();
serverOrder.notify();
Stores.setting.serverOrder.put(serverOrder.value);
Stores.server.deleteAll();
Stores.server.clear();
_updateTags();
bakSync.sync(milliDelay: 1000);
}
static Future<void> updateServer(
Spi old,
Spi newSpi,
) async {
static Future<void> updateServer(Spi old, Spi newSpi) async {
if (old != newSpi) {
Stores.server.update(old, newSpi);
servers[old.id]?.value.spi = newSpi;
@@ -236,7 +235,7 @@ class ServerProvider extends Provider {
}
}
_updateTags();
bakSync.sync();
bakSync.sync(milliDelay: 1000);
}
static void _setServerState(VNode<Server> s, ServerConn ss) {
@@ -306,14 +305,11 @@ class ServerProvider extends Provider {
_setServerState(s, ServerConn.connected);
try {
final (_, writeScriptResult) = await sv.client!.exec(
(session) async {
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
session.stdin.add(scriptRaw);
session.stdin.close();
},
entry: ShellFunc.getInstallShellCmd(spi.id),
);
final (_, writeScriptResult) = await sv.client!.exec((session) async {
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
session.stdin.add(scriptRaw);
session.stdin.close();
}, entry: ShellFunc.getInstallShellCmd(spi.id));
if (writeScriptResult.isNotEmpty) {
ShellFunc.switchScriptDir(spi.id);
throw writeScriptResult;
@@ -365,10 +361,7 @@ class ServerProvider extends Provider {
}
}
TryLimiter.inc(sid);
sv.status.err = SSHErr(
type: SSHErrType.segements,
message: 'Seperate segments failed, raw:\n$raw',
);
sv.status.err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
_setServerState(s, ServerConn.failed);
return;
}
@@ -407,17 +400,10 @@ class ServerProvider extends Provider {
system: systemType,
customCmds: spi.custom?.cmds ?? {},
);
sv.status = await Computer.shared.start(
getStatus,
req,
taskName: 'StatusUpdateReq<${sv.id}>',
);
sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>');
} catch (e, trace) {
TryLimiter.inc(sid);
sv.status.err = SSHErr(
type: SSHErrType.getStatus,
message: 'Parse failed: $e\n\n$raw',
);
sv.status.err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
_setServerState(s, ServerConn.failed);
Loggers.app.warning('Server status', e, trace);
return;

View File

@@ -14,11 +14,7 @@ class SftpProvider extends Provider {
}
static int add(SftpReq req, {Completer? completer}) {
final reqStat = SftpReqStatus(
notifyListeners: status.notify,
completer: completer,
req: req,
);
final reqStat = SftpReqStatus(notifyListeners: status.notify, completer: completer, req: req);
status.value.add(reqStat);
status.notify();
return reqStat.id;
@@ -34,6 +30,10 @@ class SftpProvider extends Provider {
static void cancel(int id) {
final idx = status.value.indexWhere((e) => e.id == id);
if (idx < 0 || idx >= status.value.length) {
dprint('SftpProvider.cancel: id $id not found');
return;
}
status.value[idx].dispose();
status.value.removeAt(idx);
status.notify();

View File

@@ -23,6 +23,15 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
}
}
bool _shift = false;
bool get shift => _shift;
set shift(bool value) {
if (value != _shift) {
_shift = value;
notifyListeners();
}
}
void reset(TerminalKeyboardEvent e) {
if (e.ctrl) {
ctrl = false;
@@ -30,6 +39,9 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
if (e.alt) {
alt = false;
}
if (e.shift) {
shift = false;
}
notifyListeners();
}
@@ -38,6 +50,7 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
final e = event.copyWith(
ctrl: event.ctrl || ctrl,
alt: event.alt || alt,
shift: event.shift || shift,
);
if (Stores.setting.sshVirtualKeyAutoOff.fetch()) {
reset(e);

View File

@@ -3,6 +3,6 @@
abstract class BuildData {
static const String name = "ServerBox";
static const int build = 1104;
static const int script = 58;
static const int build = 1201;
static const int script = 65;
}

View File

@@ -17,6 +17,11 @@ abstract final class GithubIds {
'dccif',
'mikropsoft',
'CakesTwix',
'dsvf',
'fei1025',
'MasedMSD',
'GitGitro',
'Shin-suechtig',
};
static const participants = <GhId>{
@@ -99,6 +104,21 @@ abstract final class GithubIds {
'88484396',
'honggeigei',
'likecreep',
'axlrose',
'immortal521',
'PRO-2684',
'Xiaobao-Yang',
'Mrhs121',
'Fudiautobi',
'papaj-na-wrotkach',
'kid1412621',
'smanx',
'xuanyue1024',
'RuofengX',
'rhwong',
'AstroEngineeer',
'mochasweet',
'back-lacking',
};
}

View File

@@ -1,6 +0,0 @@
import 'package:fl_lib/fl_lib.dart';
abstract final class RNodes {
static final app = RNode();
static final dark = false.vn;
}

View File

@@ -1,64 +1,40 @@
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/temp.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/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/server.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/temp.dart';
abstract final class InitStatus {
static SingleCpuCore get _initOneTimeCpuStatus => SingleCpuCore(
'cpu',
0,
0,
0,
0,
0,
0,
0,
);
static Cpus get cpus => Cpus(
[_initOneTimeCpuStatus],
[_initOneTimeCpuStatus],
);
static NetSpeedPart get _initNetSpeedPart => NetSpeedPart(
'',
BigInt.zero,
BigInt.zero,
0,
);
static NetSpeed get netSpeed => NetSpeed(
[_initNetSpeedPart],
[_initNetSpeedPart],
);
static SingleCpuCore get _initOneTimeCpuStatus =>
SingleCpuCore('cpu', 0, 0, 0, 0, 0, 0, 0);
static Cpus get cpus =>
Cpus([_initOneTimeCpuStatus], [_initOneTimeCpuStatus]);
static NetSpeedPart get _initNetSpeedPart =>
NetSpeedPart('', BigInt.zero, BigInt.zero, 0);
static NetSpeed get netSpeed =>
NetSpeed([_initNetSpeedPart], [_initNetSpeedPart]);
static ServerStatus get status => ServerStatus(
cpu: cpus,
mem: const Memory(
total: 1,
free: 1,
avail: 1,
),
disk: [
Disk(
fs: '/',
mount: '/',
usedPercent: 0,
used: BigInt.zero,
size: BigInt.one,
avail: BigInt.zero,
)
],
tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0),
netSpeed: netSpeed,
swap: const Swap(
total: 0,
free: 0,
cached: 0,
),
system: SystemType.linux,
temps: Temperatures(),
diskIO: DiskIO([], []),
);
cpu: cpus,
mem: const Memory(total: 1, free: 1, avail: 1),
disk: [
Disk(
path: '/',
mount: '/',
usedPercent: 0,
used: BigInt.zero,
size: BigInt.one,
avail: BigInt.zero,
),
],
tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0),
netSpeed: netSpeed,
swap: const Swap(total: 0, free: 0, cached: 0),
system: SystemType.linux,
temps: Temperatures(),
diskIO: DiskIO([], []),
diskSmart: const [],
);
}

View File

@@ -1,7 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/store/container.dart';
import 'package:server_box/data/store/history.dart';
import 'package:server_box/data/store/no_backup.dart';
import 'package:server_box/data/store/private_key.dart';
import 'package:server_box/data/store/server.dart';
import 'package:server_box/data/store/setting.dart';
@@ -16,7 +15,7 @@ abstract final class Stores {
static final history = HistoryStore.instance;
/// All stores that need backup
static final List<PersistentStore> _allBackup = [
static final List<HiveStore> _allBackup = [
SettingStore.instance,
ServerStore.instance,
ContainerStore.instance,
@@ -27,15 +26,24 @@ abstract final class Stores {
static Future<void> init() async {
await Future.wait(_allBackup.map((store) => store.init()));
await NoBackupStore.instance.init();
}
static int? get lastModTime {
int? lastModTime = 0;
static int get lastModTime {
var lastModTime = 0;
for (final store in _allBackup) {
final last = store.box.lastModified ?? 0;
if (last > (lastModTime ?? 0)) {
lastModTime = last;
final last = store.lastUpdateTs;
if (last == null) {
continue;
}
var lastModTimeTs = 0;
for (final item in last.entries) {
final ts = item.value;
if (ts > lastModTimeTs) {
lastModTimeTs = ts;
}
}
if (lastModTimeTs > lastModTime) {
lastModTime = lastModTimeTs;
}
}
return lastModTime;

View File

@@ -4,7 +4,7 @@ import 'package:server_box/data/res/store.dart';
const _keyConfig = 'providerConfig';
class ContainerStore extends PersistentStore {
class ContainerStore extends HiveStore {
ContainerStore._() : super('docker');
static final instance = ContainerStore._();
@@ -14,8 +14,7 @@ class ContainerStore extends PersistentStore {
}
void put(String id, String host) {
box.put(id, host);
box.updateLastModified();
set(id, host);
}
ContainerType getType([String id = '']) {
@@ -30,16 +29,17 @@ class ContainerStore extends PersistentStore {
}
ContainerType get defaultType {
if (Stores.setting.usePodman.fetch()) return ContainerType.podman;
if (Stores.setting.usePodman.get()) return ContainerType.podman;
return ContainerType.docker;
}
void setType(ContainerType type, [String id = '']) {
if (type == defaultType) {
box.delete(_keyConfig + id);
// box.delete(_keyConfig + id);
remove(_keyConfig + id);
} else {
box.put(_keyConfig + id, type.toString());
// box.put(_keyConfig + id, type.toString());
set(_keyConfig + id, type.toString());
}
box.updateLastModified();
}
}

View File

@@ -1,5 +1,5 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
/// index from 0 -> n : latest -> oldest
class _ListHistory {
@@ -18,7 +18,6 @@ class _ListHistory {
_history.remove(path);
_history.insert(0, path);
_box.put(_name, _history);
_box.updateLastModified();
}
List get all => _history;
@@ -39,13 +38,12 @@ class _MapHistory {
void put(String id, String val) {
_history[id] = val;
_box.put(_name, _history);
_box.updateLastModified();
}
String? fetch(String id) => _history[id];
}
class HistoryStore extends PersistentStore {
class HistoryStore extends HiveStore {
HistoryStore._() : super('history');
static final instance = HistoryStore._();
@@ -58,5 +56,6 @@ class HistoryStore extends PersistentStore {
late final sshCmds = _ListHistory(box: box, name: 'sshCmds');
/// Notify users that this app will write script to server to works properly
late final writeScriptTipShown = property('writeScriptTipShown', false);
late final writeScriptTipShown =
propertyDefault('writeScriptTipShown', false);
}

View File

@@ -1,49 +0,0 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/store.dart';
final class NoBackupStore extends PersistentStore {
NoBackupStore._() : super('no_backup');
static final instance = NoBackupStore._();
/// Only valid on iOS and macOS
late final icloudSync = property('icloudSync', false);
/// Webdav sync
late final webdavSync = property('webdavSync', false);
late final webdavUrl = property('webdavUrl', '');
late final webdavUser = property('webdavUser', '');
late final webdavPwd = property('webdavPwd', '');
void migrate() {
if (BuildData.build > 1076) return;
final settings = Stores.setting;
final icloudSync_ = settings.box.get('icloudSync');
if (icloudSync_ is bool) {
icloudSync.put(icloudSync_);
settings.box.delete('icloudSync');
}
final webdavSync_ = settings.box.get('webdavSync');
if (webdavSync_ is bool) {
webdavSync.put(webdavSync_);
settings.box.delete('webdavSync');
}
final webdavUrl_ = settings.box.get('webdavUrl');
if (webdavUrl_ is String) {
webdavUrl.put(webdavUrl_);
settings.box.delete('webdavUrl');
}
final webdavUser_ = settings.box.get('webdavUser');
if (webdavUser_ is String) {
webdavUser.put(webdavUser_);
settings.box.delete('webdavUser');
}
final webdavPwd_ = settings.box.get('webdavPwd');
if (webdavPwd_ is String) {
webdavPwd.put(webdavPwd_);
settings.box.delete('webdavPwd');
}
}
}

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