Compare commits

..

1 Commits

Author SHA1 Message Date
Noo6
b56e033773 fix: sftp open file on windows 2024-11-14 14:18:32 +08:00
250 changed files with 10000 additions and 34557 deletions

View File

@@ -1,7 +1,6 @@
name: Flutter Release name: Flutter Release
on: on:
workflow_dispatch:
push: push:
tags: tags:
- "v*" - "v*"
@@ -19,12 +18,12 @@ jobs:
- name: Install Flutter - name: Install Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: 'stable'
flutter-version: "3.32.2" flutter-version: '3.24.1'
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: "zulu" distribution: 'zulu'
java-version: "17" java-version: '17'
- name: Fetch secrets - name: Fetch secrets
run: | run: |
curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key
@@ -54,28 +53,14 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Flutter - name: Install Flutter
uses: subosito/flutter-action@v2 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 - name: Build
run: | run: |
dart run fl_build -p linux 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 - name: Create Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -105,9 +90,9 @@ jobs:
# uses: actions/checkout@v4 # uses: actions/checkout@v4
# - name: Install Flutter # - name: Install Flutter
# uses: subosito/flutter-action@v2 # uses: subosito/flutter-action@v2
# with: # with:
# channel: 'stable' # channel: 'stable'
# flutter-version: '3.32.1' # flutter-version: '3.22.2'
# - name: Build # - name: Build
# run: dart run fl_build -p ios,mac # run: dart run fl_build -p ios,mac
# - name: Create Release # - name: Create Release

2
.gitignore vendored
View File

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

View File

@@ -6,17 +6,16 @@ English | [简体中文](README_zh.md)
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a> <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="lang" src="https://img.shields.io/badge/lang-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow"> <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> </div>
<p align="center"> <p align="center">
A Flutter project which provides charts to display Linux, Unix and Windows server status and tools to manage servers. A Flutter project which provide charts to display <a href="../../issues/43">Linux</a> server status and tools to manage server.
<br> <br>
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>. Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
</p> </p>
## 🏙️ Screenshots
## 🏙️ Screenshots
<table> <table>
<tr> <tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
@@ -26,26 +25,27 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
</tr> </tr>
</table> </table>
## 📥 Installation
|Platform| From| ## 📥 Install
|--|--|
| iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) | Platform | From
| 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) | 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)
Please only download pkgs from the source that **you trust**! Please only download pkgs from the source that **you trust**!
## 🔖 Features
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`, `S.M.A.R.T`... ## 🔖 Feature
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
- Platform specific: `Bio auth``Msg push``Home widget``watchOS App`... - 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) - 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 ## 🆘 Help
<div align="center"> <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://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> <a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div> </div>
@@ -54,35 +54,30 @@ 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). - **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: 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. 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. 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. 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). After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
## 🧱 Contributions
## 🧱 Contribution
Any positive contribution is welcome. Any positive contribution is welcome.
If I forgot to add your name to the contributors list, please add a comment in the issue or PR you opened to let me know, I will add it as soon as possible.
### Development ### Development
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment. 1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
2. Clone this repo, run `flutter run` to start the app. 2. Clone this repo, run `flutter run` to start the app.
3. Run `dart run fl_build -p PLATFORM` to build the app. 3. Run `dart run fl_build -p PLATFORM` to build the app.
### Translation ### Translation
- [Guide](https://blog.lpkt.cn/posts/faq/) can be found in my blog. - [Guide](https://blog.lpkt.cn/posts/faq/) can be found in my blog.
- We need your help! Just feel free to open a PR. - We need your help! Just feel free to open a PR.
## 💡 My other apps
## 💡 My other apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms. - [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. - [More](https://github.com/lollipopkit) - Tools & etc.
## 📝 License
## 📝 License
`GPL v3 lollipopkit` `GPL v3 lollipopkit`

View File

@@ -6,17 +6,16 @@
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a> <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="语言" src="https://img.shields.io/badge/语言-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow"> <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> </div>
<p align="center"> <p align="center">
使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱,提供服务器状态图表和管理工具。 使用 Flutter 开发的 <a href="../../issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
<br> <br>
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a> 特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>
</p> </p>
## 🏙️ 截屏
## 🏙️ 截屏
<table> <table>
<tr> <tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
@@ -26,19 +25,20 @@
</tr> </tr>
</table> </table>
## 📥 安装 ## 📥 安装
平台|下载 平台 | 下载
--|-- --- | ---
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) 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/) 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.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
请从 **信任** 的来源下载! 请从 **信任** 的来源下载!
## 🔖 特点
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理,`S.M.A.R.T`... ## 🔖 特点
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
- 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`... - 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`...
- 本地化 - 本地化
- English, 简体中文 - 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); - 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"> <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://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> <a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div> </div>
@@ -58,32 +58,26 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。 - **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
反馈前须知: 反馈前须知:
1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。 1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。
2. 反馈问题前请检查是否是 serverbox 的问题。 2. 反馈问题前请检查是否是 serverbox 的问题。
3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受 3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受
## 🧱 贡献
## 🧱 贡献
任何正面的贡献都欢迎。 任何正面的贡献都欢迎。
如果我忘记在贡献者列表中添加你的名字,请在你打开的 issue 或 PR 中添加评论让我知道,我会尽快添加。
### 开发 ### 开发
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install) 1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
2. 克隆这个仓库, 运行 `flutter run` 启动应用 2. 克隆这个仓库, 运行 `flutter run` 启动应用
3. 运行 `dart run fl_build -p PLATFORM` 构建应用 3. 运行 `dart run fl_build -p PLATFORM` 构建应用
### 翻译 ### 翻译
[指南](https://blog.lolli.tech/faq/) 可在我的博客中找到。
[指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。
## 💡 我的其它 Apps ## 💡 我的其它 Apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。 - [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。
- [更多](https://github.com/lollipopkit) - 工具 & etc. - [更多](https://github.com/lollipopkit) - 工具 & etc.
## 📝 协议
## 📝 协议
`GPL v3 lollipopkit` `GPL v3 lollipopkit`

View File

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

View File

@@ -85,20 +85,13 @@ android {
} }
debug { debug {
// No applicationIdSuffix or resValue here applicationIdSuffix '.debug'
} }
profile { profile {
// No applicationIdSuffix or resValue here applicationIdSuffix '.debug'
} }
} }
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
} }
flutter { flutter {

View File

@@ -11,11 +11,10 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:label="@string/app_name" android:label="ServerBox"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:hasFragileUserData="true" android:hasFragileUserData="true"
android:restoreAnyVersion="true" android:restoreAnyVersion="true"
tools:targetApi="q"> tools:targetApi="q">
@@ -24,7 +23,7 @@
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|layoutDirection|fontScale|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as

View File

@@ -4,158 +4,85 @@ import android.app.*
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log
import java.io.File
import java.util.*
class ForegroundService : Service() { class ForegroundService : Service() {
private val chanId = "ForegroundServiceChannel" 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() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d("ForegroundService", "Service onCreate")
createNotificationChannel() createNotificationChannel()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try { when (intent?.action) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && "ACTION_STOP_FOREGROUND" -> {
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() stopForegroundService()
return START_NOT_STICKY return START_NOT_STICKY
} }
else -> {
if (intent == null) { val notification = createNotification()
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) startForeground(1, notification)
} catch (e: Exception) { return START_STICKY
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 return null
} }
private fun createNotificationChannel() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 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( val serviceChannel = NotificationChannel(
chanId, chanId,
"ForegroundServiceChannel", chanId,
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
).apply { )
description = "For foreground service" val manager = getSystemService(NotificationManager::class.java)
}
manager.createNotificationChannel(serviceChannel) manager.createNotificationChannel(serviceChannel)
} }
} }
private fun createNotification(): Notification { private fun createNotification(): Notification {
try { val notificationIntent = Intent(this, MainActivity::class.java)
val notificationIntent = Intent(this, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity(
val pendingIntent = PendingIntent.getActivity( this,
this, 0,
0, notificationIntent,
notificationIntent, PendingIntent.FLAG_IMMUTABLE
PendingIntent.FLAG_IMMUTABLE )
)
val deleteIntent = Intent(this, ForegroundService::class.java).apply { val deleteIntent = Intent(this, ForegroundService::class.java).apply {
action = "ACTION_STOP_FOREGROUND" action = "ACTION_STOP_FOREGROUND"
} }
val deletePendingIntent = PendingIntent.getService( val deletePendingIntent = PendingIntent.getService(
this, this,
0, 0,
deleteIntent, deleteIntent,
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
) )
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId) Notification.Builder(this, chanId)
} else {
Notification.Builder(this)
}
return builder
.setContentTitle("Server Box") .setContentTitle("Server Box")
.setContentText("Running in background") .setContentText("Open the app")
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent) .addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
.build() .build()
} catch (e: Exception) { } else {
logError("Error creating notification", e) Notification.Builder(this)
// Return a basic notification as fallback
return Notification.Builder(this)
.setContentTitle("Server Box") .setContentTitle("Server Box")
.setContentText("Open the app")
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
.build() .build()
} }
} }
private fun stopForegroundService() { fun stopForegroundService() {
try { stopForeground(true)
stopForeground(true)
} catch (e: Exception) {
logError("Error stopping foreground", e)
}
stopSelf() stopSelf()
Log.d("ForegroundService", "ForegroundService stopped")
}
override fun onDestroy() {
super.onDestroy()
Log.d("ForegroundService", "Service onDestroy")
} }
} }

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

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

View File

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

View File

@@ -9,23 +9,6 @@ rootProject.buildDir = '../build'
subprojects { subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}" 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 { subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -672,7 +672,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -682,7 +682,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -808,7 +808,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -818,7 +818,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -836,7 +836,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -846,7 +846,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -867,7 +867,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -880,7 +880,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -906,7 +906,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -919,7 +919,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -942,7 +942,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -955,7 +955,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -978,7 +978,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -990,7 +990,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
@@ -1019,7 +1019,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1031,7 +1031,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;
@@ -1057,7 +1057,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1206; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1069,7 +1069,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1206; MARKETING_VERSION = 1.0.1104;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;

View File

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

View File

@@ -14,20 +14,13 @@ class PhoneConnMgr: NSObject, WCSessionDelegate, ObservableObject {
set { set {
Store.setCtx(newValue) Store.setCtx(newValue)
updateUrls(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 { get {
return _ctx return _ctx
} }
} }
var userInfo: [String: Any] = [:]
@Published var urls: [String] = [] @Published var urls: [String] = []
override init() { override init() {
super.init() super.init()
if !WCSession.isSupported() { if !WCSession.isSupported() {
@@ -36,85 +29,24 @@ class PhoneConnMgr: NSObject, WCSessionDelegate, ObservableObject {
session = WCSession.default session = WCSession.default
session?.delegate = self session?.delegate = self
session?.activate() session?.activate()
_ctx = Store.getCtx() ctx = Store.getCtx()
updateUrls(_ctx)
} }
func updateUrls(_ val: [String: Any]) { func updateUrls(_ val: [String: Any]) {
if let urls = val["urls"] as? [String] { if let urls = val["urls"] as? [String] {
DispatchQueue.main.async { self.urls = urls.filter { !$0.isEmpty }
self.urls = urls.filter { !$0.isEmpty }
}
} }
} }
func session( func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?
) {
// Request latest data when the session is activated
if activationState == .activated {
requestLatestData()
}
} }
// Receive realtime msgs // implement session:didReceiveApplicationContext:
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
DispatchQueue.main.async { ctx = applicationContext
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,6 +1,4 @@
arb-dir: lib/l10n arb-dir: lib/l10n
template-arb-file: app_en.arb template-arb-file: app_en.arb
output-localization-file: l10n.dart output-localization-file: l10n.dart
output-dir: lib/generated/l10n
synthetic-package: false
untranslated-messages-file: untranlated.json untranslated-messages-file: untranlated.json

View File

@@ -1,14 +1,14 @@
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:fl_lib/generated/l10n/lib_l10n.dart'; import 'package:fl_lib/l10n/gen_l10n/lib_l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:server_box/core/extension/context/locale.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/build_data.dart';
import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/generated/l10n/l10n.dart'; import 'package:server_box/view/page/home/home.dart';
import 'package:server_box/view/page/home.dart'; import 'package:icons_plus/icons_plus.dart';
part 'intro.dart'; part 'intro.dart';
@@ -22,52 +22,48 @@ class MyApp extends StatelessWidget {
listenable: RNodes.app, listenable: RNodes.app,
builder: (context, _) { builder: (context, _) {
if (!Stores.setting.useSystemPrimaryColor.fetch()) { if (!Stores.setting.useSystemPrimaryColor.fetch()) {
return _build(context); 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 DynamicColorBuilder(
return _buildDynamicColor(context); 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 _build(BuildContext context) { Widget _buildApp(BuildContext ctx,
final colorSeed = Color(Stores.setting.colorSeed.fetch()); {required ThemeData light, required ThemeData dark}) {
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(); final tMode = Stores.setting.themeMode.fetch();
// Issue #57 // Issue #57
final themeMode = switch (tMode) { final themeMode = switch (tMode) {
@@ -79,16 +75,11 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
key: ValueKey(locale), key: ValueKey(locale),
builder: (context, child) => ResponsiveBreakpoints.builder(
child: child ?? UIs.placeholder,
breakpoints: const [
Breakpoint(start: 0, end: 600, name: MOBILE),
Breakpoint(start: 600, end: 1199, name: TABLET),
Breakpoint(start: 1199, end: 3840, name: DESKTOP),
],
),
locale: locale, locale: locale,
localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates], localizationsDelegates: const [
LibLocalizations.delegate,
...AppLocalizations.localizationsDelegates,
],
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
localeListResolutionCallback: LocaleUtil.resolve, localeListResolutionCallback: LocaleUtil.resolve,
navigatorObservers: [AppRouteObserver.instance], navigatorObservers: [AppRouteObserver.instance],
@@ -96,22 +87,21 @@ class MyApp extends StatelessWidget {
themeMode: themeMode, themeMode: themeMode,
theme: light.fixWindowsFont, theme: light.fixWindowsFont,
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont, darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
home: Builder( home: VirtualWindowFrame(
builder: (context) { child: Builder(
context.setLibL10n(); builder: (context) {
final appL10n = AppLocalizations.of(context); context.setLibL10n();
if (appL10n != null) l10n = appL10n; final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
Widget child; final intros = _IntroPage.builders;
final intros = _IntroPage.builders; if (intros.isNotEmpty) {
if (intros.isNotEmpty) { return _IntroPage(intros);
child = _IntroPage(intros); }
}
child = const HomePage(); return const HomePage();
},
return VirtualWindowFrame(title: BuildData.name, child: child); ),
},
), ),
); );
} }

View File

@@ -1,30 +0,0 @@
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

@@ -0,0 +1,18 @@
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

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

View File

@@ -12,9 +12,21 @@ extension SftpFileX on SftpFileMode {
UnixPerm toUnixPerm() { UnixPerm toUnixPerm() {
return UnixPerm( return UnixPerm(
user: UnixPermOp(r: userRead, w: userWrite, x: userExecute), user: RWX(
group: UnixPermOp(r: groupRead, w: groupWrite, x: groupExecute), r: userRead,
other: UnixPermOp(r: otherRead, w: otherWrite, x: otherExecute), w: userWrite,
x: userExecute,
),
group: RWX(
r: groupRead,
w: groupWrite,
x: groupExecute,
),
other: RWX(
r: otherRead,
w: otherWrite,
x: otherExecute,
),
); );
} }
} }

View File

@@ -4,7 +4,6 @@ import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
@@ -14,52 +13,6 @@ typedef OnStdin = void Function(SSHSession session);
typedef PwdRequestFunc = Future<String?> Function(String? user); typedef PwdRequestFunc = Future<String?> Function(String? user);
extension SSHClientX on SSHClient { extension SSHClientX on SSHClient {
/// Create a persistent PowerShell session for Windows commands
Future<(SSHSession, String)> execPowerShell(
OnStdin onStdin, {
SSHPtyConfig? pty,
OnStdout? onStdout,
OnStdout? onStderr,
bool stdout = true,
bool stderr = true,
Map<String, String>? env,
}) async {
final session = await execute(
'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
pty: pty,
environment: env,
);
final result = BytesBuilder(copy: false);
final stdoutDone = Completer<void>();
final stderrDone = Completer<void>();
session.stdout.listen(
(e) {
onStdout?.call(e.string, session);
if (stdout) result.add(e);
},
onDone: stdoutDone.complete,
onError: stderrDone.completeError,
);
session.stderr.listen(
(e) {
onStderr?.call(e.string, session);
if (stderr) result.add(e);
},
onDone: stderrDone.complete,
onError: stderrDone.completeError,
);
onStdin(session);
await stdoutDone.future;
await stderrDone.future;
return (session, result.takeBytes().string);
}
Future<(SSHSession, String)> exec( Future<(SSHSession, String)> exec(
OnStdin onStdin, { OnStdin onStdin, {
String? entry, String? entry,
@@ -69,14 +22,9 @@ extension SSHClientX on SSHClient {
bool stdout = true, bool stdout = true,
bool stderr = true, bool stderr = true,
Map<String, String>? env, Map<String, String>? env,
SystemType? systemType,
}) async { }) async {
final session = await execute( final session = await execute(
entry ?? entry ?? 'cat | sh',
switch (systemType) {
SystemType.windows => 'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass',
_ => 'cat | sh',
},
pty: pty, pty: pty,
environment: env, environment: env,
); );
@@ -133,7 +81,9 @@ extension SSHClientX on SSHClient {
isRequestingPwd = true; isRequestingPwd = true;
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1); final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
if (context == null) return; if (context == null) return;
final pwd = context.mounted ? await context.showPwdDialog(title: user, id: id) : null; final pwd = context.mounted
? await context.showPwdDialog(title: user, id: id)
: null;
if (pwd == null || pwd.isEmpty) { if (pwd == null || pwd.isEmpty) {
session.stdin.close(); session.stdin.close();
} else { } else {

View File

@@ -1,9 +1,169 @@
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/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';
/// The args class for [AppRoute]. class AppRoutes {
final class SpiRequiredArgs { final Widget page;
/// The only required argument for this class. final String title;
final Spi spi;
const SpiRequiredArgs(this.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');
}
} }

View File

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

View File

@@ -6,8 +6,10 @@ class ChainComparator<T> {
ChainComparator.empty() : this._create(null, (a, b) => 0); ChainComparator.empty() : this._create(null, (a, b) => 0);
ChainComparator.create() : this._create(null, (a, b) => 0); ChainComparator.create() : this._create(null, (a, b) => 0);
static ChainComparator<T> comparing<T, F extends Comparable<F>>(F Function(T) extractor) { static ChainComparator<T> comparing<T, F extends Comparable<F>>(
return ChainComparator._create(null, (a, b) => extractor(a).compareTo(extractor(b))); F Function(T) extractor) {
return ChainComparator._create(
null, (a, b) => extractor(a).compareTo(extractor(b)));
} }
int compare(T a, T b) { int compare(T a, T b) {
@@ -24,9 +26,8 @@ class ChainComparator<T> {
} }
ChainComparator<T> thenCompareBy<F extends Comparable<F>>( ChainComparator<T> thenCompareBy<F extends Comparable<F>>(
F Function(T) extractor, { F Function(T) extractor,
bool reversed = false, {bool reversed = false}) {
}) {
return ChainComparator._create( return ChainComparator._create(
this, this,
reversed reversed
@@ -35,12 +36,18 @@ class ChainComparator<T> {
); );
} }
ChainComparator<T> thenWithComparator(Comparator<T> comparator, {bool reversed = false}) { ChainComparator<T> thenWithComparator(Comparator<T> comparator,
return ChainComparator._create(this, !reversed ? comparator : (a, b) => comparator(b, a)); {bool reversed = false}) {
return ChainComparator._create(
this,
!reversed ? comparator : (a, b) => comparator(b, a),
);
} }
ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(F Function(T) extractor) { ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(
return ChainComparator._create(this, (a, b) => -extractor(a).compareTo(extractor(b))); F Function(T) extractor) {
return ChainComparator._create(
this, (a, b) => -extractor(a).compareTo(extractor(b)));
} }
ChainComparator<T> thenTrueFirst(bool Function(T) f) { ChainComparator<T> thenTrueFirst(bool Function(T) f) {
@@ -56,7 +63,8 @@ class ChainComparator<T> {
} }
class Comparators { class Comparators {
static Comparator<String> compareStringCaseInsensitive({bool uppercaseFirst = false}) { static Comparator<String> compareStringCaseInsensitive(
{bool uppercaseFirst = false}) {
return (String a, String b) { return (String a, String b) {
final r = a.toLowerCase().compareTo(b.toLowerCase()); final r = a.toLowerCase().compareTo(b.toLowerCase());
if (r != 0) return r; if (r != 0) return r;

View File

@@ -4,9 +4,10 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
/// Must put this func out of any Class. /// Must put this func out of any Class.
/// ///
/// Because of this function is called by [compute]. /// Because of this function is called by [compute].
@@ -24,12 +25,19 @@ String decyptPem(List<String> args) {
return sshKey.first.toPem(); return sshKey.first.toPem();
} }
enum GenSSHClientStatus { socket, key, pwd } enum GenSSHClientStatus {
socket,
key,
pwd,
}
String getPrivateKey(String id) { String getPrivateKey(String id) {
final pki = Stores.key.fetchOne(id); final pki = Stores.key.get(id);
if (pki == null) { if (pki == null) {
throw SSHErr(type: SSHErrType.noPrivateKey, message: 'key [$id] not found'); throw SSHErr(
type: SSHErrType.noPrivateKey,
message: 'key [$id] not found',
);
} }
return pki.key; return pki.key;
} }
@@ -51,7 +59,7 @@ Future<SSHClient> genClient(
Spi? jumpSpi, Spi? jumpSpi,
/// Handle keyboard-interactive authentication /// Handle keyboard-interactive authentication
SSHUserInfoRequestHandler? onKeyboardInteractive, FutureOr<List<String>?> Function(SSHUserInfoRequest)? onKeyboardInteractive,
}) async { }) async {
onStatus?.call(GenSSHClientStatus.socket); onStatus?.call(GenSSHClientStatus.socket);
@@ -66,21 +74,36 @@ Future<SSHClient> genClient(
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId); if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
}(); }();
if (jumpSpi_ != null) { if (jumpSpi_ != null) {
final jumpClient = await genClient(jumpSpi_, privateKey: jumpPrivateKey, timeout: timeout); final jumpClient = await genClient(
jumpSpi_,
privateKey: jumpPrivateKey,
timeout: timeout,
);
return await jumpClient.forwardLocal(spi.ip, spi.port); return await jumpClient.forwardLocal(
spi.ip,
spi.port,
);
} }
// Direct // Direct
try { try {
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout); return await SSHSocket.connect(
spi.ip,
spi.port,
timeout: timeout,
);
} catch (e) { } catch (e) {
Loggers.app.warning('genClient', e); Loggers.app.warning('genClient', e);
if (spi.alterUrl == null) rethrow; if (spi.alterUrl == null) rethrow;
try { try {
final res = spi.fromStringUrl(); final res = spi.fromStringUrl();
alterUser = res.$2; alterUser = res.$2;
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout); return await SSHSocket.connect(
res.$1,
res.$3,
timeout: timeout,
);
} catch (e) { } catch (e) {
Loggers.app.warning('genClient alterUrl', e); Loggers.app.warning('genClient alterUrl', e);
rethrow; rethrow;

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.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/model/server/server_private_info.dart';
import 'package:server_box/data/provider/app.dart'; import 'package:server_box/data/provider/app.dart';
@@ -12,7 +13,7 @@ abstract final class KeybordInteractive {
}) async { }) async {
try { try {
final res = await (ctx ?? AppProvider.ctx)?.showPwdDialog( final res = await (ctx ?? AppProvider.ctx)?.showPwdDialog(
title: libL10n.pwd, title: l10n.pwd,
id: spi.id, id: spi.id,
label: spi.id, label: spi.id,
); );

View File

@@ -1,57 +0,0 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/system.dart';
/// Helper class for detecting remote system types
class SystemDetector {
/// Detects the system type of a remote server
///
/// First checks if a custom system type is configured in [spi].
/// If not, attempts to detect the system by running commands:
/// 1. 'ver' command to detect Windows
/// 2. 'uname -a' command to detect Linux/BSD/Darwin
///
/// Returns [SystemType.linux] as default if detection fails.
static Future<SystemType> detect(
SSHClient client,
Spi spi,
) async {
// First, check if custom system type is defined
SystemType? detectedSystemType = spi.customSystemType;
if (detectedSystemType != null) {
dprint('Using custom system type ${detectedSystemType.name} for ${spi.oldId}');
return detectedSystemType;
}
try {
// Try to detect Windows systems first (more reliable detection)
final powershellResult = await client.run('ver 2>nul').string;
if (powershellResult.isNotEmpty &&
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
detectedSystemType = SystemType.windows;
dprint('Detected Windows system type for ${spi.oldId}');
return detectedSystemType;
}
// Try to detect Unix/Linux/BSD systems
final unixResult = await client.run('uname -a').string;
if (unixResult.contains('Linux')) {
detectedSystemType = SystemType.linux;
dprint('Detected Linux system type for ${spi.oldId}');
return detectedSystemType;
} else if (unixResult.contains('Darwin') || unixResult.contains('BSD')) {
detectedSystemType = SystemType.bsd;
dprint('Detected BSD system type for ${spi.oldId}');
return detectedSystemType;
}
} catch (e) {
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
}
// Default fallback
detectedSystemType = SystemType.linux;
dprint('Defaulting to Linux system type for ${spi.oldId}');
return detectedSystemType;
}
}

View File

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

View File

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

View File

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

@@ -1,101 +0,0 @@
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

@@ -1,205 +0,0 @@
// 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

@@ -1,29 +0,0 @@
// 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

@@ -1,198 +0,0 @@
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

@@ -1,62 +0,0 @@
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

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

View File

@@ -8,7 +8,7 @@ enum ContainerMenu {
restart, restart,
rm, rm,
logs, logs,
terminal terminal,
//stats, //stats,
; ;
@@ -27,22 +27,22 @@ enum ContainerMenu {
} }
IconData get icon => switch (this) { IconData get icon => switch (this) {
ContainerMenu.start => Icons.play_arrow, ContainerMenu.start => Icons.play_arrow,
ContainerMenu.stop => Icons.stop, ContainerMenu.stop => Icons.stop,
ContainerMenu.restart => Icons.restart_alt, ContainerMenu.restart => Icons.restart_alt,
ContainerMenu.rm => Icons.delete, ContainerMenu.rm => Icons.delete,
ContainerMenu.logs => Icons.logo_dev, ContainerMenu.logs => Icons.logo_dev,
ContainerMenu.terminal => Icons.terminal, ContainerMenu.terminal => Icons.terminal,
// DockerMenuType.stats => Icons.bar_chart, // DockerMenuType.stats => Icons.bar_chart,
}; };
String get toStr => switch (this) { String get toStr => switch (this) {
ContainerMenu.start => l10n.start, ContainerMenu.start => l10n.start,
ContainerMenu.stop => l10n.stop, ContainerMenu.stop => l10n.stop,
ContainerMenu.restart => l10n.restart, ContainerMenu.restart => l10n.restart,
ContainerMenu.rm => libL10n.delete, ContainerMenu.rm => libL10n.delete,
ContainerMenu.logs => libL10n.log, ContainerMenu.logs => libL10n.log,
ContainerMenu.terminal => l10n.terminal, ContainerMenu.terminal => l10n.terminal,
// DockerMenuType.stats => s.stats, // DockerMenuType.stats => s.stats,
}; };
} }

View File

@@ -1,22 +1,36 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:icons_plus/icons_plus.dart'; import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
part 'server_func.g.dart';
@HiveType(typeId: 6)
enum ServerFuncBtn { enum ServerFuncBtn {
terminal(), @HiveField(0)
sftp(), terminal._(),
container(), @HiveField(1)
process(), sftp._(),
//pkg(), @HiveField(2)
snippet(), container._(),
iperf(), @HiveField(3)
// pve(), process._(),
systemd(1058); //@HiveField(4)
//pkg,
@HiveField(5)
snippet._(),
@HiveField(6)
iperf._(),
// @HiveField(7)
// pve,
@HiveField(8)
systemd._(1058),
;
final int? addedVersion; final int? addedVersion;
const ServerFuncBtn([this.addedVersion]); const ServerFuncBtn._([this.addedVersion]);
static void autoAddNewFuncs(int cur) { static void autoAddNewFuncs(int cur) {
if (cur >= systemd.addedVersion!) { if (cur >= systemd.addedVersion!) {
@@ -40,24 +54,24 @@ enum ServerFuncBtn {
].map((e) => e.index).toList(); ].map((e) => e.index).toList();
IconData get icon => switch (this) { IconData get icon => switch (this) {
sftp => Icons.insert_drive_file, sftp => Icons.insert_drive_file,
snippet => Icons.code, snippet => Icons.code,
//pkg => Icons.system_security_update, //pkg => Icons.system_security_update,
container => FontAwesome.docker_brand, container => FontAwesome.docker_brand,
process => Icons.list_alt_outlined, process => Icons.list_alt_outlined,
terminal => Icons.terminal, terminal => Icons.terminal,
iperf => Icons.speed, iperf => Icons.speed,
systemd => MingCute.plugin_2_fill, systemd => MingCute.plugin_2_fill,
}; };
String get toStr => switch (this) { String get toStr => switch (this) {
sftp => 'SFTP', sftp => 'SFTP',
snippet => l10n.snippet, snippet => l10n.snippet,
//pkg => l10n.pkg, //pkg => l10n.pkg,
container => l10n.container, container => l10n.container,
process => l10n.process, process => l10n.process,
terminal => l10n.terminal, terminal => l10n.terminal,
iperf => 'iperf', iperf => 'iperf',
systemd => 'Systemd', systemd => 'Systemd',
}; };
} }

View File

@@ -0,0 +1,71 @@
// 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,23 +1,30 @@
import 'package:fl_lib/fl_lib.dart'; 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/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
part 'net_view.g.dart';
@HiveType(typeId: 5)
enum NetViewType { enum NetViewType {
@HiveField(0)
conn, conn,
@HiveField(1)
speed, speed,
@HiveField(2)
traffic; traffic;
NetViewType get next => switch (this) { NetViewType get next => switch (this) {
conn => speed, conn => speed,
speed => traffic, speed => traffic,
traffic => conn, traffic => conn,
}; };
String get toStr => switch (this) { String get toStr => switch (this) {
NetViewType.conn => l10n.conn, NetViewType.conn => l10n.conn,
NetViewType.traffic => l10n.traffic, NetViewType.traffic => l10n.traffic,
NetViewType.speed => l10n.speed, NetViewType.speed => l10n.speed,
}; };
/// If no device is specified, return the cached value (only real devices, /// If no device is specified, return the cached value (only real devices,
/// such as ethX, wlanX...). /// such as ethX, wlanX...).
@@ -26,17 +33,32 @@ enum NetViewType {
try { try {
switch (this) { switch (this) {
case NetViewType.conn: case NetViewType.conn:
return ('${l10n.conn}:\n${ss.tcp.maxConn}', '${libL10n.fail}:\n${ss.tcp.fail}'); return (
'${l10n.conn}:\n${ss.tcp.maxConn}',
'${libL10n.fail}:\n${ss.tcp.fail}',
);
case NetViewType.speed: case NetViewType.speed:
if (notSepcifyDev) { if (notSepcifyDev) {
return ('↓:\n${ss.netSpeed.cachedVals.speedIn}', '↑:\n${ss.netSpeed.cachedVals.speedOut}'); return (
'↓:\n${ss.netSpeed.cachedVals.speedIn}',
'↑:\n${ss.netSpeed.cachedVals.speedOut}',
);
} }
return ('↓:\n${ss.netSpeed.speedIn(device: dev)}', '↑:\n${ss.netSpeed.speedOut(device: dev)}'); return (
'↓:\n${ss.netSpeed.speedIn(device: dev)}',
'↑:\n${ss.netSpeed.speedOut(device: dev)}',
);
case NetViewType.traffic: case NetViewType.traffic:
if (notSepcifyDev) { if (notSepcifyDev) {
return ('↓:\n${ss.netSpeed.cachedVals.sizeIn}', '↑:\n${ss.netSpeed.cachedVals.sizeOut}'); return (
'↓:\n${ss.netSpeed.cachedVals.sizeIn}',
'↑:\n${ss.netSpeed.cachedVals.sizeOut}',
);
} }
return ('↓:\n${ss.netSpeed.sizeIn(device: dev)}', '↑:\n${ss.netSpeed.sizeOut(device: dev)}'); return (
'↓:\n${ss.netSpeed.sizeIn(device: dev)}',
'↑:\n${ss.netSpeed.sizeOut(device: dev)}',
);
} }
} catch (e, s) { } catch (e, s) {
Loggers.app.warning('NetViewType.build', e, s); Loggers.app.warning('NetViewType.build', e, s);
@@ -45,14 +67,14 @@ enum NetViewType {
} }
int toJson() => switch (this) { int toJson() => switch (this) {
NetViewType.conn => 0, NetViewType.conn => 0,
NetViewType.speed => 1, NetViewType.speed => 1,
NetViewType.traffic => 2, NetViewType.traffic => 2,
}; };
static NetViewType fromJson(int json) => switch (json) { static NetViewType fromJson(int json) => switch (json) {
0 => NetViewType.conn, 0 => NetViewType.conn,
1 => NetViewType.speed, 1 => NetViewType.speed,
_ => NetViewType.traffic, _ => NetViewType.traffic,
}; };
} }

View File

@@ -0,0 +1,51 @@
// 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,274 +0,0 @@
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/server/system.dart';
/// Base class for all command type enums
abstract class CommandType implements Enum {
String get cmd;
/// Get command-specific separator
String get separator;
/// Get command-specific divider (separator with echo and formatting)
String get divider;
}
/// Linux/Unix status commands
enum StatusCmdType implements CommandType {
echo('echo ${SystemType.linuxSign}'),
time('date +%s'),
net('cat /proc/net/dev'),
sys('cat /etc/*-release | grep ^PRETTY_NAME'),
cpu('cat /proc/stat | grep cpu'),
uptime('uptime'),
conn('cat /proc/net/snmp'),
disk(
'lsblk --bytes --json --output '
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
),
mem("cat /proc/meminfo | grep -E 'Mem|Swap'"),
tempType('cat /sys/class/thermal/thermal_zone*/type'),
tempVal('cat /sys/class/thermal/thermal_zone*/temp'),
host('cat /etc/hostname'),
diskio('cat /proc/diskstats'),
/// Get battery information from Linux power supply subsystem
///
/// Reads battery data from sysfs power supply interface:
/// - Iterates through all power supply devices in /sys/class/power_supply/
/// - Each device has a uevent file with key-value pairs of power supply properties
/// - Includes battery level, status, technology type, and other attributes
/// - Works with laptops, UPS devices, and other power supplies
/// - Adds echo after each file to separate multiple power supplies
/// - Returns empty if no power supplies are detected (e.g., desktop systems)
battery('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
/// Get NVIDIA GPU information using nvidia-smi in XML format
/// Requires NVIDIA drivers and nvidia-smi utility to be installed
nvidia('nvidia-smi -q -x'),
/// Get AMD GPU information using multiple fallback methods
///
/// This command tries three different AMD monitoring tools in order of preference:
/// 1. amd-smi: Modern AMD System Management Interface (ROCm 5.0+)
/// - Uses 'amd-smi list --json' to get GPU list
/// - Uses 'amd-smi metric --json' to get performance metrics
/// 2. rocm-smi: ROCm System Management Interface (older versions)
/// - First tries '--json' output format if supported
/// - Falls back to human-readable format with comprehensive metrics
/// 3. radeontop: Real-time GPU usage monitor for older AMD cards
/// - Uses 2-second timeout to avoid hanging
/// - Skips header line with 'tail -n +2'
/// - Outputs single line of usage data
///
/// If none of these tools are available, outputs error message
amd(
'if command -v amd-smi >/dev/null 2>&1; then '
'amd-smi list --json && amd-smi metric --json; '
'elif command -v rocm-smi >/dev/null 2>&1; then '
'rocm-smi --json || rocm-smi --showunique --showuse --showtemp '
'--showfan --showclocks --showmemuse --showpower; '
'elif command -v radeontop >/dev/null 2>&1; then '
'timeout 2s radeontop -d - -l 1 | tail -n +2; '
'else echo "No AMD GPU monitoring tools found"; fi',
),
sensors('sensors'),
/// Get SMART disk health information for all storage devices
///
/// Uses a combination of lsblk and smartctl to collect disk health data:
/// - lsblk -dn -o KNAME lists all block devices (kernel names only, no dependencies)
/// - For each device, runs smartctl with -a (all info) and -j (JSON output)
/// - Targets raw device nodes in /dev/ (e.g., /dev/sda, /dev/nvme0n1)
/// - Adds echo after each device to separate output blocks
/// - May require elevated privileges for some drives
/// - smartctl must be installed (part of smartmontools package)
diskSmart('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
cpuBrand('cat /proc/cpuinfo | grep "model name"');
@override
final String cmd;
const StatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
}
/// BSD/macOS status commands
enum BSDStatusCmdType implements CommandType {
echo('echo ${SystemType.bsdSign}'),
time('date +%s'),
net('netstat -ibn'),
sys('uname -or'),
cpu('top -l 1 | grep "CPU usage"'),
uptime('uptime'),
disk('df -k'), // Keep df -k for BSD systems as lsblk is not available on macOS/BSD
mem('top -l 1 | grep PhysMem'),
host('hostname'),
cpuBrand('sysctl -n machdep.cpu.brand_string');
@override
final String cmd;
const BSDStatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
}
/// Windows PowerShell status commands
enum WindowsStatusCmdType implements CommandType {
echo('echo ${SystemType.windowsSign}'),
time('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
/// Get network interface statistics using Windows Performance Counters
///
/// Uses Get-Counter to collect network I/O metrics from all network interfaces:
/// - Collects bytes received and sent per second for all network interfaces
/// - Takes 2 samples with 1 second interval to calculate rates
/// - Outputs results in JSON format for easy parsing
/// - Counter paths use double backslashes to escape PowerShell string literals
net(
r'Get-Counter -Counter '
r'"\\NetworkInterface(*)\\Bytes Received/sec", '
r'"\\NetworkInterface(*)\\Bytes Sent/sec" '
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
),
sys('(Get-ComputerInfo).OsName'),
cpu(
'Get-WmiObject -Class Win32_Processor | '
'Select-Object Name, LoadPercentage | ConvertTo-Json',
),
uptime('(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime'),
conn('(netstat -an | findstr ESTABLISHED | Measure-Object -Line).Count'),
disk(
'Get-WmiObject -Class Win32_LogicalDisk | '
'Select-Object DeviceID, Size, FreeSpace, FileSystem | ConvertTo-Json',
),
mem(
'Get-WmiObject -Class Win32_OperatingSystem | '
'Select-Object TotalVisibleMemorySize, FreePhysicalMemory | ConvertTo-Json',
),
/// Get system temperature using Windows Management Instrumentation (WMI)
///
/// Queries the MSAcpi_ThermalZoneTemperature class from the WMI root/wmi namespace:
/// - Uses Get-CimInstance to access ACPI thermal zone data
/// - ErrorAction SilentlyContinue prevents errors on systems without thermal sensors
/// - Converts temperature from 10ths of Kelvin to Celsius: (temp - 2732) / 10
/// - Uses calculated property to perform the temperature conversion
/// - Returns JSON with InstanceName and converted Temperature values
/// - May return empty result on systems without ACPI thermal sensor support
temp(
'Get-CimInstance -ClassName MSAcpi_ThermalZoneTemperature '
'-Namespace root/wmi -ErrorAction SilentlyContinue | '
'Select-Object InstanceName, @{Name=\'Temperature\';'
'Expression={[math]::Round((\$_.CurrentTemperature - 2732) / 10, 1)}} | '
'ConvertTo-Json',
),
host(r'Write-Output $env:COMPUTERNAME'),
/// Get disk I/O statistics using Windows Performance Counters
///
/// Uses Get-Counter to collect disk I/O metrics from all physical disks:
/// - Monitors read and write bytes per second for all physical disks
/// - Takes 2 samples with 1 second interval to calculate I/O rates
/// - Physical disk counters provide hardware-level I/O statistics
/// - Outputs results in JSON format for parsing
/// - Counter names use wildcard (*) to capture all disk instances
diskio(
r'Get-Counter -Counter '
r'"\\PhysicalDisk(*)\\Disk Read Bytes/sec", '
r'"\\PhysicalDisk(*)\\Disk Write Bytes/sec" '
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
),
battery(
'Get-WmiObject -Class Win32_Battery | '
'Select-Object EstimatedChargeRemaining, BatteryStatus | ConvertTo-Json',
),
/// Get NVIDIA GPU information on Windows
///
/// Checks if nvidia-smi is available before attempting to use it:
/// - Uses Get-Command to test if nvidia-smi.exe exists in PATH
/// - ErrorAction SilentlyContinue prevents PowerShell errors if not found
/// - If available, runs nvidia-smi with -q (query) and -x (XML output) flags
/// - If not available, outputs standard error message for consistent handling
nvidia(
'if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { '
'nvidia-smi -q -x } else { echo "NVIDIA driver not found" }',
),
/// Get AMD GPU information on Windows
///
/// Checks for AMD monitoring tools using similar pattern to Linux version:
/// - Uses Get-Command to test if amd-smi.exe exists in PATH
/// - ErrorAction SilentlyContinue prevents PowerShell errors if not found
/// - If available, runs amd-smi list command with JSON output
/// - If not available, outputs standard error message for consistent handling
/// - Windows version is simpler than Linux due to fewer AMD tool variations
amd(
'if (Get-Command amd-smi -ErrorAction SilentlyContinue) { '
'amd-smi list --json } else { echo "AMD driver not found" }',
),
sensors(
'Get-CimInstance -ClassName Win32_TemperatureProbe '
'-ErrorAction SilentlyContinue | '
'Select-Object Name, CurrentReading | ConvertTo-Json',
),
/// Get SMART disk health information on Windows using Storage cmdlets
///
/// Uses Windows PowerShell storage management cmdlets:
/// - Get-PhysicalDisk retrieves all physical storage devices
/// - Get-StorageReliabilityCounter gets SMART health data via pipeline
/// - Selects key health metrics: DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours
/// - Outputs results in JSON format for consistent parsing
/// - Works with NVMe, SATA, and other storage interfaces supported by Windows
/// - May require elevated privileges on some systems
diskSmart(
'Get-PhysicalDisk | Get-StorageReliabilityCounter | '
'Select-Object DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours | '
'ConvertTo-Json',
),
cpuBrand('(Get-WmiObject -Class Win32_Processor).Name');
@override
final String cmd;
const WindowsStatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
}
/// Extensions for StatusCmdType
extension StatusCmdTypeX on StatusCmdType {
String get i18n => switch (this) {
StatusCmdType.sys => l10n.system,
StatusCmdType.host => l10n.host,
StatusCmdType.uptime => l10n.uptime,
StatusCmdType.battery => l10n.battery,
StatusCmdType.sensors => l10n.sensors,
StatusCmdType.disk => l10n.disk,
final val => val.name,
};
}
/// Extension for CommandType to find content in parsed map
extension CommandTypeX on CommandType {
/// Find the command output from the parsed script output map
String findInMap(Map<String, String> parsedOutput) {
return parsedOutput[name] ?? '';
}
}

View File

@@ -1,271 +0,0 @@
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart';
/// Abstract base class for platform-specific script builders
sealed class ScriptBuilder {
const ScriptBuilder();
/// Generate a complete script for all shell functions
String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]);
/// Get the script file name for this platform
String get scriptFileName;
/// Get the command to install the script
String getInstallCommand(String scriptDir, String scriptPath);
/// Get the execution command for a specific function
String getExecCommand(String scriptPath, ShellFunc func);
/// Get custom commands string for this platform
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds);
/// Get the script header for this platform
String get scriptHeader;
}
/// Windows PowerShell script builder
class WindowsScriptBuilder extends ScriptBuilder {
const WindowsScriptBuilder();
@override
String get scriptFileName => ScriptConstants.scriptFileWindows;
@override
String get scriptHeader => ScriptConstants.windowsScriptHeader;
@override
String getInstallCommand(String scriptDir, String scriptPath) {
return 'New-Item -ItemType Directory -Force -Path \'$scriptDir\' | Out-Null; '
'\$content = [System.Console]::In.ReadToEnd(); '
'Set-Content -Path \'$scriptPath\' -Value \$content -Encoding UTF8';
}
@override
String getExecCommand(String scriptPath, ShellFunc func) {
return 'powershell -ExecutionPolicy Bypass -File "$scriptPath" -${func.flag}';
}
@override
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
final sb = StringBuffer();
for (final e in customCmds.entries) {
final cmdDivider = ScriptConstants.getCustomCmdSeparator(e.key);
sb.writeln(' Write-Host "$cmdDivider"');
sb.writeln(' ${e.value}');
}
return '\n$sb';
}
return '';
}
@override
String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {
final sb = StringBuffer();
sb.write(scriptHeader);
// Write each function
for (final func in ShellFunc.values) {
final customCmdsStr = getCustomCmdsString(func, customCmds);
sb.write('''
function ${func.name} {
${_getWindowsCommand(func, disabledCmdTypes).split('\n').map((e) => e.isEmpty ? '' : ' $e').join('\n')}$customCmdsStr
}
''');
}
// Write switch case
sb.write('''
switch (\$args[0]) {
''');
for (final func in ShellFunc.values) {
sb.write('''
"-${func.flag}" { ${func.name} }
''');
}
sb.write('''
default { Write-Host "Invalid argument \$(\$args[0])" }
}
''');
return sb.toString();
}
/// Get Windows-specific command for a shell function
String _getWindowsCommand(ShellFunc func, [List<String>? disabledCmdTypes]) => switch (func) {
ShellFunc.status => _getWindowsStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),
ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',
ShellFunc.shutdown => 'Stop-Computer -Force',
ShellFunc.reboot => 'Restart-Computer -Force',
ShellFunc.suspend =>
'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState(\'Suspend\', \$false, \$false)',
};
/// Get Windows status command with command-specific separators
String _getWindowsStatusCommand({required List<String> disabledCmdTypes}) {
final cmdTypes = WindowsStatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
return cmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight(); // Remove trailing divider
}
}
/// Unix shell script builder
class UnixScriptBuilder extends ScriptBuilder {
const UnixScriptBuilder();
@override
String get scriptFileName => ScriptConstants.scriptFile;
@override
String get scriptHeader => ScriptConstants.unixScriptHeader;
@override
String getInstallCommand(String scriptDir, String scriptPath) {
return '''
mkdir -p $scriptDir
cat > $scriptPath
chmod 755 $scriptPath
''';
}
@override
String getExecCommand(String scriptPath, ShellFunc func) {
return 'sh $scriptPath -${func.flag}';
}
@override
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
final sb = StringBuffer();
for (final e in customCmds.entries) {
final cmdDivider = ScriptConstants.getCustomCmdSeparator(e.key);
sb.writeln('echo "$cmdDivider"');
sb.writeln(e.value);
}
return '\n$sb';
}
return '';
}
@override
String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {
final sb = StringBuffer();
sb.write(scriptHeader);
// Write each function
for (final func in ShellFunc.values) {
final customCmdsStr = getCustomCmdsString(func, customCmds);
sb.write('''
${func.name}() {
${_getUnixCommand(func, disabledCmdTypes).split('\n').map((e) => '\t$e').join('\n')}
$customCmdsStr
}
''');
}
// Write switch case
sb.write('case \$1 in\n');
for (final func in ShellFunc.values) {
sb.write('''
'-${func.flag}')
${func.name}
;;
''');
}
sb.write('''
*)
echo "Invalid argument \$1"
;;
esac''');
return sb.toString();
}
/// Get Unix-specific command for a shell function
String _getUnixCommand(ShellFunc func, [List<String>? disabledCmdTypes]) {
return switch (func) {
ShellFunc.status => _getUnixStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),
ShellFunc.process => _getUnixProcessCommand(),
ShellFunc.shutdown => _getUnixShutdownCommand(),
ShellFunc.reboot => _getUnixRebootCommand(),
ShellFunc.suspend => _getUnixSuspendCommand(),
};
}
/// Get Unix status command with OS detection
String _getUnixStatusCommand({required List<String> disabledCmdTypes}) {
// Generate command lists with command-specific separators, filtering disabled commands
final filteredLinuxCmdTypes = StatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
final linuxCommands = filteredLinuxCmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight();
final filteredBsdCmdTypes = BSDStatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
final bsdCommands = filteredBsdCmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight();
return '''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\t$linuxCommands
else
\t$bsdCommands
fi''';
}
/// Get Unix process command with busybox detection
String _getUnixProcessCommand() {
return '''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\tif [ "\$isBusybox" != "" ]; then
\t\tps w
\telse
\t\tps -aux
\tfi
else
\tps -ax
fi''';
}
/// Get Unix shutdown command with privilege detection
String _getUnixShutdownCommand() {
return '''
if [ "\$userId" = "0" ]; then
\tshutdown -h now
else
\tsudo -S shutdown -h now
fi''';
}
/// Get Unix reboot command with privilege detection
String _getUnixRebootCommand() {
return '''
if [ "\$userId" = "0" ]; then
\treboot
else
\tsudo -S reboot
fi''';
}
/// Get Unix suspend command with privilege detection
String _getUnixSuspendCommand() {
return '''
if [ "\$userId" = "0" ]; then
\tsystemctl suspend
else
\tsudo -S systemctl suspend
fi''';
}
}
/// Factory class to get appropriate script builder for platform
class ScriptBuilderFactory {
const ScriptBuilderFactory._();
/// Get the appropriate script builder based on platform
static ScriptBuilder getBuilder(bool isWindows) {
return isWindows ? const WindowsScriptBuilder() : const UnixScriptBuilder();
}
/// Get all available builders (useful for testing)
static List<ScriptBuilder> getAllBuilders() {
return const [WindowsScriptBuilder(), UnixScriptBuilder()];
}
}

View File

@@ -1,150 +0,0 @@
import 'package:server_box/data/res/build_data.dart';
/// Constants used throughout the script system
class ScriptConstants {
const ScriptConstants._();
// Script file names
static const String scriptFile = 'srvboxm_v${BuildData.script}.sh';
static const String scriptFileWindows = 'srvboxm_v${BuildData.script}.ps1';
// Script directories
static const String scriptDirHome = '~/.config/server_box';
static const String scriptDirTmp = '/tmp/server_box';
static const String scriptDirHomeWindows = '%USERPROFILE%/.config/server_box';
static const String scriptDirTmpWindows = '%TEMP%/server_box';
// Command separators and dividers
static const String separator = 'SrvBoxSep';
/// Custom command separator
static const String customCmdSep = 'SrvBoxCusCmdSep';
/// Generate command-specific separator
static String getCmdSeparator(String cmdName) => '$separator.$cmdName';
/// Generate command-specific divider for custom commands
static String getCustomCmdSeparator(String cmdName) => '$customCmdSep.$cmdName';
/// Generate command-specific divider
static String getCmdDivider(String cmdName) => '\necho ${getCmdSeparator(cmdName)}\n\t';
/// Parse script output into command-specific map
static Map<String, String> parseScriptOutput(String raw) {
final result = <String, String>{};
if (raw.isEmpty) return result;
// Parse line by line to properly handle command-specific separators
final lines = raw.split('\n');
String? currentCmd;
final buffer = StringBuffer();
for (final line in lines) {
if (line.startsWith('$separator.')) {
// Save previous command content
if (currentCmd != null) {
result[currentCmd] = buffer.toString().trim();
buffer.clear();
}
// Start new command
currentCmd = line.substring('$separator.'.length);
} else if (line.startsWith('$customCmdSep.')) {
// Save previous command content
if (currentCmd != null) {
result[currentCmd] = buffer.toString().trim();
buffer.clear();
}
// Start new custom command
currentCmd = line.substring('$customCmdSep.'.length);
} else if (currentCmd != null) {
buffer.writeln(line);
}
}
// Don't forget the last command
if (currentCmd != null) {
result[currentCmd] = buffer.toString().trim();
}
return result;
}
// Path separators
static const String unixPathSeparator = '/';
static const String windowsPathSeparator = '\\';
// Script headers
static const String unixScriptHeader =
'''
#!/bin/sh
# Script for ServerBox app v1.0.${BuildData.build}
# DO NOT delete this file while app is running
export LANG=en_US.UTF-8
# If macSign & bsdSign are both empty, then it's linux
macSign=\$(uname -a 2>&1 | grep "Darwin")
bsdSign=\$(uname -a 2>&1 | grep "BSD")
# Link /bin/sh to busybox?
isBusybox=\$(ls -l /bin/sh | grep "busybox")
userId=\$(id -u)
exec 2>/dev/null
''';
static const String windowsScriptHeader =
'''
# PowerShell script for ServerBox app v1.0.${BuildData.build}
# DO NOT delete this file while app is running
\$ErrorActionPreference = "SilentlyContinue"
''';
}
/// Script path configuration and management
class ScriptPaths {
ScriptPaths._();
static final Map<String, String> _scriptDirMap = <String, String>{};
/// Get the script directory for the given [id].
///
/// Default is [ScriptConstants.scriptDirTmp]/[ScriptConstants.scriptFile],
/// if this path is not accessible, it will be changed to
/// [ScriptConstants.scriptDirHome]/[ScriptConstants.scriptFile].
static String getScriptDir(String id, {bool isWindows = false}) {
final defaultTmpDir = isWindows ? ScriptConstants.scriptDirTmpWindows : ScriptConstants.scriptDirTmp;
_scriptDirMap[id] ??= defaultTmpDir;
return _scriptDirMap[id]!;
}
/// Switch between tmp and home directories for script storage
static String switchScriptDir(String id, {bool isWindows = false}) {
return switch (_scriptDirMap[id]) {
ScriptConstants.scriptDirTmp => _scriptDirMap[id] = ScriptConstants.scriptDirHome,
ScriptConstants.scriptDirTmpWindows => _scriptDirMap[id] = ScriptConstants.scriptDirHomeWindows,
ScriptConstants.scriptDirHome => _scriptDirMap[id] = ScriptConstants.scriptDirTmp,
ScriptConstants.scriptDirHomeWindows => _scriptDirMap[id] = ScriptConstants.scriptDirTmpWindows,
_ =>
_scriptDirMap[id] = isWindows ? ScriptConstants.scriptDirHomeWindows : ScriptConstants.scriptDirHome,
};
}
/// Get the full script path for the given [id]
static String getScriptPath(String id, {bool isWindows = false}) {
final dir = getScriptDir(id, isWindows: isWindows);
final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
return '$dir$separator$fileName';
}
/// Clear cached script directories (useful for testing)
static void clearCache() {
_scriptDirMap.clear();
}
}

View File

@@ -1,102 +0,0 @@
import 'package:server_box/data/model/app/scripts/script_builders.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/provider/server.dart';
/// Shell functions available in the ServerBox application
enum ShellFunc {
status('SbStatus'),
process('SbProcess'),
shutdown('SbShutdown'),
reboot('SbReboot'),
suspend('SbSuspend');
/// The function name used in scripts
final String name;
const ShellFunc(this.name);
/// Get the command line flag for this function
String get flag => switch (this) {
ShellFunc.process => 'p',
ShellFunc.shutdown => 'sd',
ShellFunc.reboot => 'r',
ShellFunc.suspend => 'sp',
ShellFunc.status => 's',
};
/// Execute this shell function on the specified server
String exec(String id, {SystemType? systemType}) {
final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType);
final isWindows = systemType == SystemType.windows;
final builder = ScriptBuilderFactory.getBuilder(isWindows);
return builder.getExecCommand(scriptPath, this);
}
}
/// Manager class for shell function operations
class ShellFuncManager {
const ShellFuncManager._();
/// Normalize a directory path to ensure it doesn't end with trailing separators
static String _normalizeDir(String dir, bool isWindows) {
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
// Remove all trailing separators
final pattern = RegExp('${RegExp.escape(separator)}+\$');
return dir.replaceAll(pattern, '');
}
/// Get the script directory for the given [id].
///
/// Checks for custom script directory first, then falls back to default.
static String getScriptDir(String id, {SystemType? systemType}) {
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
final isWindows = systemType == SystemType.windows;
if (customScriptDir != null) return _normalizeDir(customScriptDir, isWindows);
return ScriptPaths.getScriptDir(id, isWindows: isWindows);
}
/// Switch between tmp and home directories for script storage
static void switchScriptDir(String id, {SystemType? systemType}) {
final isWindows = systemType == SystemType.windows;
ScriptPaths.switchScriptDir(id, isWindows: isWindows);
}
/// Get the full script path for the given [id]
static String getScriptPath(String id, {SystemType? systemType}) {
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) {
final isWindows = systemType == SystemType.windows;
final normalizedDir = _normalizeDir(customScriptDir, isWindows);
final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
return '$normalizedDir$separator$fileName';
}
final isWindows = systemType == SystemType.windows;
return ScriptPaths.getScriptPath(id, isWindows: isWindows);
}
/// Get the installation shell command for the script
static String getInstallShellCmd(String id, {SystemType? systemType}) {
final scriptDir = getScriptDir(id, systemType: systemType);
final isWindows = systemType == SystemType.windows;
final normalizedDir = _normalizeDir(scriptDir, isWindows);
final builder = ScriptBuilderFactory.getBuilder(isWindows);
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
final scriptPath = '$normalizedDir$separator${builder.scriptFileName}';
return builder.getInstallCommand(normalizedDir, scriptPath);
}
/// Generate complete script based on system type
static String allScript(Map<String, String>? customCmds, {SystemType? systemType, List<String>? disabledCmdTypes}) {
final isWindows = systemType == SystemType.windows;
final builder = ScriptBuilderFactory.getBuilder(isWindows);
return builder.buildScript(customCmds, disabledCmdTypes);
}
}

View File

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

View File

@@ -0,0 +1,261 @@
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/model/server/system.dart';
enum ShellFunc {
status,
//docker,
process,
shutdown,
reboot,
suspend,
;
static const seperator = 'SrvBoxSep';
/// The suffix `\t` is for formatting
static const cmdDivider = '\necho $seperator\n\t';
/// srvboxm -> ServerBox Mobile
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
static const scriptDirHome = '~/.config/server_box';
static const scriptDirTmp = '/tmp/server_box';
static final _scriptDirMap = <String, String>{};
/// Get the script directory for the given [id].
///
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// it will be changed to [scriptDirHome]/[scriptFile].
static String getScriptDir(String id) {
final customScriptDir =
ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
return _scriptDirMap.putIfAbsent(id, () {
return scriptDirTmp;
});
}
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
_ => _scriptDirMap[id] = scriptDirHome,
};
static String getScriptPath(String id) {
return '${getScriptDir(id)}/$scriptFile';
}
static String getInstallShellCmd(String id) {
final scriptDir = getScriptDir(id);
final scriptPath = '$scriptDir/$scriptFile';
return '''
mkdir -p $scriptDir
cat > $scriptPath
chmod 755 $scriptPath
''';
}
String get flag => switch (this) {
ShellFunc.process => 'p',
ShellFunc.shutdown => 'sd',
ShellFunc.reboot => 'r',
ShellFunc.suspend => 'sp',
ShellFunc.status => 's',
// ShellFunc.docker=> 'd',
};
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
String get name {
switch (this) {
case ShellFunc.status:
return 'status';
// case ShellFunc.docker:
// // `dockeR` -> avoid conflict with `docker` command
// return 'dockeR';
case ShellFunc.process:
return 'process';
case ShellFunc.shutdown:
return 'ShutDown';
case ShellFunc.reboot:
return 'Reboot';
case ShellFunc.suspend:
return 'Suspend';
}
}
String get _cmd {
switch (this) {
case ShellFunc.status:
return '''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\t${StatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
else
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
fi''';
// case ShellFunc.docker:
// return '''
// result=\$(docker version 2>&1 | grep "permission denied")
// if [ "\$result" != "" ]; then
// \t${_dockerCmds.join(_cmdDivider)}
// else
// \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
// fi''';
case ShellFunc.process:
return '''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\tif [ "\$isBusybox" != "" ]; then
\t\tps w
\telse
\t\tps -aux
\tfi
else
\tps -ax
fi
''';
case ShellFunc.shutdown:
return '''
if [ "\$userId" = "0" ]; then
\tshutdown -h now
else
\tsudo -S shutdown -h now
fi''';
case ShellFunc.reboot:
return '''
if [ "\$userId" = "0" ]; then
\treboot
else
\tsudo -S reboot
fi''';
case ShellFunc.suspend:
return '''
if [ "\$userId" = "0" ]; then
\tsystemctl suspend
else
\tsudo -S systemctl suspend
fi''';
}
}
static String allScript(Map<String, String>? customCmds) {
final sb = StringBuffer();
sb.write('''
#!/bin/sh
# Script for ServerBox app v1.0.${BuildData.build}
# DO NOT delete this file while app is running
export LANG=en_US.UTF-8
# If macSign & bsdSign are both empty, then it's linux
macSign=\$(uname -a 2>&1 | grep "Darwin")
bsdSign=\$(uname -a 2>&1 | grep "BSD")
# Link /bin/sh to busybox?
isBusybox=\$(ls -l /bin/sh | grep "busybox")
userId=\$(id -u)
exec 2>/dev/null
''');
// Write each func
for (final func in values) {
final customCmdsStr = () {
if (func == ShellFunc.status &&
customCmds != null &&
customCmds.isNotEmpty) {
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
}
return '';
}();
sb.write('''
${func.name}() {
${func._cmd.split('\n').map((e) => '\t$e').join('\n')}
$customCmdsStr
}
''');
}
// Write switch case
sb.write('case \$1 in\n');
for (final func in values) {
sb.write('''
'-${func.flag}')
${func.name}
;;
''');
}
sb.write('''
*)
echo "Invalid argument \$1"
;;
esac''');
return sb.toString();
}
}
extension EnumX on Enum {
/// Find out the required segment from [segments]
String find(List<String> segments) {
return segments[index];
}
}
enum StatusCmdType {
echo._('echo ${SystemType.linuxSign}'),
time._('date +%s'),
net._('cat /proc/net/dev'),
sys._('cat /etc/*-release | grep PRETTY_NAME'),
cpu._('cat /proc/stat | grep cpu'),
uptime._('uptime'),
conn._('cat /proc/net/snmp'),
disk._('df'),
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
host._('cat /etc/hostname'),
diskio._('cat /proc/diskstats'),
battery._(
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
nvidia._('nvidia-smi -q -x'),
sensors._('sensors'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"'),
;
final String cmd;
const StatusCmdType._(this.cmd);
}
enum BSDStatusCmdType {
echo._('echo ${SystemType.bsdSign}'),
time._('date +%s'),
net._('netstat -ibn'),
sys._('uname -or'),
cpu._('top -l 1 | grep "CPU usage"'),
uptime._('uptime'),
disk._('df -k'),
mem._('top -l 1 | grep PhysMem'),
//temp,
host._('hostname'),
cpuBrand._('sysctl -n machdep.cpu.brand_string'),
;
final String cmd;
const BSDStatusCmdType._(this.cmd);
}
extension StatusCmdTypeX on StatusCmdType {
String get i18n => switch (this) {
StatusCmdType.sys => l10n.system,
StatusCmdType.host => l10n.host,
StatusCmdType.uptime => l10n.uptime,
StatusCmdType.battery => l10n.battery,
final val => val.name,
};
}

View File

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

View File

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

View File

@@ -24,7 +24,14 @@ final class PodmanImg implements ContainerImg {
final int? size; final int? size;
final int? containers; final int? containers;
PodmanImg({this.repository, this.tag, this.id, this.created, this.size, this.containers}); PodmanImg({
this.repository,
this.tag,
this.id,
this.created,
this.size,
this.containers,
});
@override @override
String? get sizeMB => size?.bytes2Str; String? get sizeMB => size?.bytes2Str;
@@ -32,27 +39,28 @@ final class PodmanImg implements ContainerImg {
@override @override
int? get containersCount => containers; int? get containersCount => containers;
factory PodmanImg.fromRawJson(String str) => PodmanImg.fromJson(json.decode(str)); factory PodmanImg.fromRawJson(String str) =>
PodmanImg.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg( factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
repository: json['repository'], repository: json['repository'],
tag: json['tag'], tag: json['tag'],
id: json['Id'], id: json['Id'],
created: json['Created'], created: json['Created'],
size: json['Size'], size: json['Size'],
containers: json['Containers'], containers: json['Containers'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'repository': repository, 'repository': repository,
'tag': tag, 'tag': tag,
'Id': id, 'Id': id,
'Created': created, 'Created': created,
'Size': size, 'Size': size,
'Containers': containers, 'Containers': containers,
}; };
} }
final class DockerImg implements ContainerImg { final class DockerImg implements ContainerImg {
@@ -79,9 +87,11 @@ final class DockerImg implements ContainerImg {
String? get sizeMB => size; String? get sizeMB => size;
@override @override
int? get containersCount => containers == 'N/A' ? 0 : int.tryParse(containers); int? get containersCount =>
containers == 'N/A' ? 0 : int.tryParse(containers);
factory DockerImg.fromRawJson(String str) => DockerImg.fromJson(json.decode(str)); factory DockerImg.fromRawJson(String str) =>
DockerImg.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
@@ -111,11 +121,11 @@ final class DockerImg implements ContainerImg {
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'Containers': containers, 'Containers': containers,
'CreatedAt': createdAt, 'CreatedAt': createdAt,
'ID': id, 'ID': id,
'Repository': repository, 'Repository': repository,
'Size': size, 'Size': size,
'Tag': tag, 'Tag': tag,
}; };
} }

View File

@@ -42,7 +42,15 @@ final class PodmanPs implements ContainerPs {
@override @override
String? disk; String? disk;
PodmanPs({this.command, this.created, this.exited, this.id, this.image, this.names, this.startedAt}); PodmanPs({
this.command,
this.created,
this.exited,
this.id,
this.image,
this.names,
this.startedAt,
});
@override @override
String? get name => names?.firstOrNull; String? get name => names?.firstOrNull;
@@ -70,29 +78,36 @@ final class PodmanPs implements ContainerPs {
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn'; disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
} }
factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str)); factory PodmanPs.fromRawJson(String str) =>
PodmanPs.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs( factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
command: json['Command'] == null ? [] : List<String>.from(json['Command']!.map((x) => x)), command: json['Command'] == null
created: json['Created'] == null ? null : DateTime.parse(json['Created']), ? []
exited: json['Exited'], : List<String>.from(json['Command']!.map((x) => x)),
id: json['Id'], created:
image: json['Image'], json['Created'] == null ? null : DateTime.parse(json['Created']),
names: json['Names'] == null ? [] : List<String>.from(json['Names']!.map((x) => x)), exited: json['Exited'],
startedAt: json['StartedAt'], id: json['Id'],
); image: json['Image'],
names: json['Names'] == null
? []
: List<String>.from(json['Names']!.map((x) => x)),
startedAt: json['StartedAt'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'Command': command == null ? [] : List<dynamic>.from(command!.map((x) => x)), 'Command':
'Created': created?.toIso8601String(), command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
'Exited': exited, 'Created': created?.toIso8601String(),
'Id': id, 'Exited': exited,
'Image': image, 'Id': id,
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)), 'Image': image,
'StartedAt': startedAt, 'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
}; 'StartedAt': startedAt,
};
} }
final class DockerPs implements ContainerPs { final class DockerPs implements ContainerPs {
@@ -112,7 +127,12 @@ final class DockerPs implements ContainerPs {
@override @override
String? disk; String? disk;
DockerPs({this.id, this.image, this.names, this.state}); DockerPs({
this.id,
this.image,
this.names,
this.state,
});
@override @override
String? get name => names; String? get name => names;
@@ -139,6 +159,11 @@ final class DockerPs implements ContainerPs {
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks /// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
factory DockerPs.parse(String raw) { factory DockerPs.parse(String raw) {
final parts = raw.split(Miscs.multiBlankreg); final parts = raw.split(Miscs.multiBlankreg);
return DockerPs(id: parts[0], state: parts[1], names: parts[2], image: parts[3].trim()); return DockerPs(
id: parts[0],
state: parts[1],
names: parts[2],
image: parts[3].trim(),
);
} }
} }

View File

@@ -3,15 +3,16 @@ import 'package:server_box/data/model/container/ps.dart';
enum ContainerType { enum ContainerType {
docker, docker,
podman; podman,
;
ContainerPs Function(String str) get ps => switch (this) { ContainerPs Function(String str) get ps => switch (this) {
ContainerType.docker => DockerPs.parse, ContainerType.docker => DockerPs.parse,
ContainerType.podman => PodmanPs.fromRawJson, ContainerType.podman => PodmanPs.fromRawJson,
}; };
ContainerImg Function(String str) get img => switch (this) { ContainerImg Function(String str) get img => switch (this) {
ContainerType.docker => DockerImg.fromRawJson, ContainerType.docker => DockerImg.fromRawJson,
ContainerType.podman => PodmanImg.fromRawJson, ContainerType.podman => PodmanImg.fromRawJson,
}; };
} }

View File

@@ -22,6 +22,8 @@ enum PkgManager {
return 'opkg list-upgradable'; return 'opkg list-upgradable';
case PkgManager.apk: case PkgManager.apk:
return 'apk list --upgradable'; return 'apk list --upgradable';
default:
return null;
} }
} }
@@ -54,6 +56,8 @@ enum PkgManager {
return 'opkg upgrade $args'; return 'opkg upgrade $args';
case PkgManager.apk: case PkgManager.apk:
return 'apk upgrade'; return 'apk upgrade';
default:
return null;
} }
} }
@@ -62,7 +66,8 @@ enum PkgManager {
case PkgManager.yum: case PkgManager.yum:
list = list.sublist(2); list = list.sublist(2);
list.removeWhere((element) => element.isEmpty); list.removeWhere((element) => element.isEmpty);
final endLine = list.lastIndexWhere((element) => element.contains('Obsoleting Packages')); final endLine = list.lastIndexWhere(
(element) => element.contains('Obsoleting Packages'));
if (endLine != -1 && list.isNotEmpty) { if (endLine != -1 && list.isNotEmpty) {
list = list.sublist(0, endLine); list = list.sublist(0, endLine);
} }
@@ -70,7 +75,8 @@ enum PkgManager {
case PkgManager.apt: case PkgManager.apt:
// avoid other outputs // avoid other outputs
// such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...] // such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...]
final idx = list.indexWhere((element) => element.contains('[upgradable from:')); final idx =
list.indexWhere((element) => element.contains('[upgradable from:'));
if (idx == -1) { if (idx == -1) {
return []; return [];
} }
@@ -103,7 +109,6 @@ enum PkgManager {
return PkgManager.apt; return PkgManager.apt;
case Dist.opensuse: case Dist.opensuse:
return PkgManager.zypper; return PkgManager.zypper;
case Dist.coreelec:
case Dist.wrt: case Dist.wrt:
return PkgManager.opkg; return PkgManager.opkg;
case Dist.arch: case Dist.arch:

View File

@@ -1,188 +0,0 @@
import 'dart:convert';
/// AMD GPU monitoring data structures
/// Supports both amd-smi and rocm-smi tools
/// Example JSON output:
/// [
/// {
/// "name": "AMD Radeon RX 7900 XTX",
/// "device_id": "0",
/// "temp": 45,
/// "power": "120W / 355W",
/// "memory": {
/// "total": 24576,
/// "used": 1024,
/// "unit": "MB",
/// "processes": [
/// {
/// "pid": 2456,
/// "name": "firefox",
/// "memory": 512
/// }
/// ]
/// },
/// "utilization": 75,
/// "fan_speed": 1200,
/// "clock_speed": 2400
/// }
/// ]
class AmdSmi {
static List<AmdSmiItem> fromJson(String raw) {
try {
final jsonData = json.decode(raw);
if (jsonData is! List) return [];
return jsonData
.map((gpu) => _parseGpuItem(gpu))
.where((item) => item != null)
.cast<AmdSmiItem>()
.toList();
} catch (e) {
return [];
}
}
static AmdSmiItem? _parseGpuItem(Map<String, dynamic> gpu) {
try {
final name = gpu['name'] ?? gpu['card_model'] ?? gpu['device_name'] ?? 'Unknown AMD GPU';
final deviceId = gpu['device_id']?.toString() ?? gpu['gpu_id']?.toString() ?? '0';
// Temperature parsing
final tempRaw = gpu['temperature'] ?? gpu['temp'] ?? gpu['gpu_temp'];
final temp = _parseIntValue(tempRaw);
// Power parsing
final powerDraw = gpu['power_draw'] ?? gpu['current_power'];
final powerCap = gpu['power_cap'] ?? gpu['power_limit'] ?? gpu['max_power'];
final power = _formatPower(powerDraw, powerCap);
// Memory parsing
final memory = _parseMemory(gpu['memory'] ?? gpu['vram'] ?? {});
// Utilization parsing
final utilization = _parseIntValue(gpu['utilization'] ?? gpu['gpu_util'] ?? gpu['activity']);
// Fan speed parsing
final fanSpeed = _parseIntValue(gpu['fan_speed'] ?? gpu['fan_rpm']);
// Clock speed parsing
final clockSpeed = _parseIntValue(gpu['clock_speed'] ?? gpu['gpu_clock'] ?? gpu['sclk']);
return AmdSmiItem(
deviceId: deviceId,
name: name,
temp: temp,
power: power,
memory: memory,
utilization: utilization,
fanSpeed: fanSpeed,
clockSpeed: clockSpeed,
);
} catch (e) {
return null;
}
}
static int _parseIntValue(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is String) {
// Remove units and parse (e.g., "45°C" -> 45, "1200 RPM" -> 1200)
final cleanValue = value.replaceAll(RegExp(r'[^\d]'), '');
return int.tryParse(cleanValue) ?? 0;
}
return 0;
}
static String _formatPower(dynamic draw, dynamic cap) {
final drawValue = _parseIntValue(draw);
final capValue = _parseIntValue(cap);
if (drawValue == 0 && capValue == 0) return 'N/A';
if (capValue == 0) return '${drawValue}W';
return '${drawValue}W / ${capValue}W';
}
static AmdSmiMem _parseMemory(Map<String, dynamic> memData) {
final total = _parseIntValue(memData['total'] ?? memData['total_memory']);
final used = _parseIntValue(memData['used'] ?? memData['used_memory']);
final unit = memData['unit']?.toString() ?? 'MB';
final processes = <AmdSmiMemProcess>[];
final processesData = memData['processes'];
if (processesData is List) {
for (final proc in processesData) {
if (proc is Map<String, dynamic>) {
final process = _parseProcess(proc);
if (process != null) processes.add(process);
}
}
}
return AmdSmiMem(total, used, unit, processes);
}
static AmdSmiMemProcess? _parseProcess(Map<String, dynamic> procData) {
final pid = _parseIntValue(procData['pid']);
final name = procData['name']?.toString() ?? procData['process_name']?.toString() ?? 'Unknown';
final memory = _parseIntValue(procData['memory'] ?? procData['used_memory']);
if (pid == 0) return null;
return AmdSmiMemProcess(pid, name, memory);
}
}
class AmdSmiItem {
final String deviceId;
final String name;
final int temp;
final String power;
final AmdSmiMem memory;
final int utilization;
final int fanSpeed;
final int clockSpeed;
const AmdSmiItem({
required this.deviceId,
required this.name,
required this.temp,
required this.power,
required this.memory,
required this.utilization,
required this.fanSpeed,
required this.clockSpeed,
});
@override
String toString() {
return 'AmdSmiItem{name: $name, temp: $temp, power: $power, utilization: $utilization%, memory: $memory}';
}
}
class AmdSmiMem {
final int total;
final int used;
final String unit;
final List<AmdSmiMemProcess> processes;
const AmdSmiMem(this.total, this.used, this.unit, this.processes);
@override
String toString() {
return 'AmdSmiMem{total: $total, used: $used, unit: $unit, processes: ${processes.length}}';
}
}
class AmdSmiMemProcess {
final int pid;
final String name;
final int memory;
const AmdSmiMemProcess(this.pid, this.name, this.memory);
@override
String toString() {
return 'AmdSmiMemProcess{pid: $pid, name: $name, memory: $memory}';
}
}

View File

@@ -19,7 +19,13 @@ class Battery {
final int? cycle; final int? cycle;
final String? tech; final String? tech;
const Battery({required this.status, this.percent, this.name, this.cycle, this.tech}); const Battery({
required this.status,
this.percent,
this.name,
this.cycle,
this.tech,
});
factory Battery.fromRaw(String raw) { factory Battery.fromRaw(String raw) {
final lines = raw.split('\n'); final lines = raw.split('\n');
@@ -57,7 +63,8 @@ enum BatteryStatus {
charging, charging,
discharging, discharging,
full, full,
unknown; unknown,
;
static BatteryStatus parse(String? status) { static BatteryStatus parse(String? status) {
switch (status) { switch (status) {

View File

@@ -6,11 +6,17 @@ class Conn {
final int passive; final int passive;
final int fail; final int fail;
const Conn({required this.maxConn, required this.active, required this.passive, required this.fail}); const Conn({
required this.maxConn,
required this.active,
required this.passive,
required this.fail,
});
static Conn? parse(String raw) { static Conn? parse(String raw) {
final lines = raw.split('\n'); final lines = raw.split('\n');
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'), orElse: () => ''); final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
orElse: () => '');
if (idx != '') { if (idx != '') {
final vals = idx.split(Miscs.blankReg); final vals = idx.split(Miscs.blankReg);
return Conn( return Conn(

View File

@@ -200,98 +200,22 @@ final class CpuBrand {
} }
final _bsdCpuPercentReg = RegExp(r'(\d+\.\d+)%'); final _bsdCpuPercentReg = RegExp(r'(\d+\.\d+)%');
final _macCpuPercentReg = RegExp(
r'CPU usage: ([\d.]+)% user, ([\d.]+)% sys, ([\d.]+)% idle');
final _freebsdCpuPercentReg = RegExp(
r'CPU: ([\d.]+)% user, ([\d.]+)% nice, ([\d.]+)% system, '
r'([\d.]+)% interrupt, ([\d.]+)% idle');
/// Parse CPU status on BSD system with support for different BSD variants /// TODO: Change this implementation to parse cpu status on BSD system
/// ///
/// Supports multiple formats: /// [raw]:
/// - macOS: "CPU usage: 14.70% user, 12.76% sys, 72.52% idle" /// CPU usage: 14.70% user, 12.76% sys, 72.52% idle
/// - FreeBSD: "CPU: 5.2% user, 0.0% nice, 3.1% system, 0.1% interrupt, 91.6% idle"
/// - Generic BSD: fallback to percentage extraction
Cpus parseBsdCpu(String raw) { Cpus parseBsdCpu(String raw) {
final init = InitStatus.cpus;
// Try macOS format first
final macMatch = _macCpuPercentReg.firstMatch(raw);
if (macMatch != null) {
final userPercent = double.parse(macMatch.group(1)!).toInt();
final sysPercent = double.parse(macMatch.group(2)!).toInt();
final idlePercent = double.parse(macMatch.group(3)!).toInt();
init.add([
SingleCpuCore(
'cpu0',
userPercent,
sysPercent,
0, // nice
idlePercent,
0, // iowait
0, // irq
0, // softirq
),
]);
return init;
}
// Try FreeBSD format
final freebsdMatch = _freebsdCpuPercentReg.firstMatch(raw);
if (freebsdMatch != null) {
final userPercent = double.parse(freebsdMatch.group(1)!).toInt();
final nicePercent = double.parse(freebsdMatch.group(2)!).toInt();
final sysPercent = double.parse(freebsdMatch.group(3)!).toInt();
final irqPercent = double.parse(freebsdMatch.group(4)!).toInt();
final idlePercent = double.parse(freebsdMatch.group(5)!).toInt();
init.add([
SingleCpuCore(
'cpu0',
userPercent,
sysPercent,
nicePercent,
idlePercent,
0, // iowait
irqPercent,
0, // softirq
),
]);
return init;
}
// Fallback to generic percentage extraction
final percents = _bsdCpuPercentReg final percents = _bsdCpuPercentReg
.allMatches(raw) .allMatches(raw)
.map((e) => double.parse(e.group(1) ?? '0')) .map((e) => double.parse(e.group(1) ?? '0') * 100)
.toList(); .toList();
if (percents.length != 3) return InitStatus.cpus;
if (percents.length >= 3) {
// Validate that percentages are reasonable (0-100 range) final init = InitStatus.cpus;
final validPercents = percents.where((p) => p >= 0 && p <= 100).toList(); init.add([
if (validPercents.length != percents.length) { SingleCpuCore('cpu', percents[0].toInt(), 0, 0,
Loggers.app.warning('BSD CPU fallback parsing found invalid percentages in: $raw'); percents[2].toInt() + percents[1].toInt(), 0, 0, 0),
} ]);
init.add([
SingleCpuCore(
'cpu0',
percents[0].toInt(), // user
percents.length > 1 ? percents[1].toInt() : 0, // sys
0, // nice
percents.length > 2 ? percents[2].toInt() : 0, // idle
0, // iowait
0, // irq
0, // softirq
),
]);
return init;
} else if (percents.isNotEmpty) {
Loggers.app.warning('BSD CPU fallback parsing found ${percents.length} percentages (expected at least 3) in: $raw');
} else {
Loggers.app.warning('BSD CPU fallback parsing found no percentages in: $raw');
}
return init; return init;
} }

View File

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

View File

@@ -2,29 +2,85 @@
part of 'custom.dart'; 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 // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
ServerCustom _$ServerCustomFromJson(Map<String, dynamic> json) => ServerCustom( ServerCustom _$ServerCustomFromJson(Map<String, dynamic> json) => ServerCustom(
pveAddr: json['pveAddr'] as String?, pveAddr: json['pveAddr'] as String?,
pveIgnoreCert: json['pveIgnoreCert'] as bool? ?? false, pveIgnoreCert: json['pveIgnoreCert'] as bool? ?? false,
cmds: (json['cmds'] as Map<String, dynamic>?)?.map( cmds: (json['cmds'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String), (k, e) => MapEntry(k, e as String),
), ),
preferTempDev: json['preferTempDev'] as String?, preferTempDev: json['preferTempDev'] as String?,
logoUrl: json['logoUrl'] as String?, logoUrl: json['logoUrl'] as String?,
netDev: json['netDev'] as String?, netDev: json['netDev'] as String?,
scriptDir: json['scriptDir'] as String?, scriptDir: json['scriptDir'] as String?,
); );
Map<String, dynamic> _$ServerCustomToJson(ServerCustom instance) => Map<String, dynamic> _$ServerCustomToJson(ServerCustom instance) =>
<String, dynamic>{ <String, dynamic>{
if (instance.pveAddr case final value?) 'pveAddr': value, 'pveAddr': instance.pveAddr,
'pveIgnoreCert': instance.pveIgnoreCert, 'pveIgnoreCert': instance.pveIgnoreCert,
if (instance.cmds case final value?) 'cmds': value, 'cmds': instance.cmds,
if (instance.preferTempDev case final value?) 'preferTempDev': value, 'preferTempDev': instance.preferTempDev,
if (instance.logoUrl case final value?) 'logoUrl': value, 'logoUrl': instance.logoUrl,
if (instance.netDev case final value?) 'netDev': value, 'netDev': instance.netDev,
if (instance.scriptDir case final value?) 'scriptDir': value, 'scriptDir': instance.scriptDir,
}; };

View File

@@ -1,207 +1,29 @@
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart'; import 'package:server_box/data/model/server/time_seq.dart';
import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
class Disk with EquatableMixin { class Disk {
final String path; final String fs;
final String? fsTyp;
final String mount; final String mount;
final int usedPercent; final int usedPercent;
final BigInt used; final BigInt used;
final BigInt size; final BigInt size;
final BigInt avail; 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({ const Disk({
required this.path, required this.fs,
this.fsTyp,
required this.mount, required this.mount,
required this.usedPercent, required this.usedPercent,
required this.used, required this.used,
required this.size, required this.size,
required this.avail, required this.avail,
this.name,
this.kname,
this.uuid,
this.children = const [],
}); });
static List<Disk> parse(String raw) { 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 list = <Disk>[];
final items = raw.split('\n'); final items = raw.split('\n');
if (items.isNotEmpty) items.removeAt(0); items.removeAt(0);
var pathCache = ''; var pathCache = '';
for (var item in items) { for (var item in items) {
if (item.isEmpty) { if (item.isEmpty) {
@@ -220,16 +42,14 @@ class Disk with EquatableMixin {
final fs = vals[0]; final fs = vals[0];
final mount = vals[5]; final mount = vals[5];
if (!_shouldCalc(fs, mount)) continue; if (!_shouldCalc(fs, mount)) continue;
list.add( list.add(Disk(
Disk( fs: fs,
path: fs, mount: mount,
mount: mount, usedPercent: int.parse(vals[4].replaceFirst('%', '')),
usedPercent: int.parse(vals[4].replaceFirst('%', '')), used: BigInt.parse(vals[2]),
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024), size: BigInt.parse(vals[1]),
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024), avail: BigInt.parse(vals[3]),
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024), ));
),
);
} catch (e) { } catch (e) {
continue; continue;
} }
@@ -238,19 +58,9 @@ class Disk with EquatableMixin {
} }
@override @override
List<Object?> get props => [ String toString() {
path, return 'Disk{dev: $fs, mount: $mount, usedPercent: $usedPercent, used: $used, size: $size, avail: $avail}';
name, }
kname,
fsTyp,
mount,
usedPercent,
used,
size,
avail,
uuid,
children,
];
} }
class DiskIO extends TimeSeq<List<DiskIOPiece>> { class DiskIO extends TimeSeq<List<DiskIOPiece>> {
@@ -262,16 +72,9 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
} }
(double?, double?) _getSpeed(String dev) { (double?, double?) _getSpeed(String dev) {
// Extract the device name from path if needed if (dev.startsWith('/dev/')) dev = dev.substring(5);
String searchDev = dev; final old = pre.firstWhereOrNull((e) => e.dev == dev);
if (dev.startsWith('/dev/')) { final new_ = now.firstWhereOrNull((e) => e.dev == 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); if (old == null || new_ == null) return (null, null);
final sectorsRead = new_.sectorsRead - old.sectorsRead; final sectorsRead = new_.sectorsRead - old.sectorsRead;
final sectorsWrite = new_.sectorsWrite - old.sectorsWrite; final sectorsWrite = new_.sectorsWrite - old.sectorsWrite;
@@ -301,14 +104,11 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
!item.dev.startsWith('vd') && !item.dev.startsWith('vd') &&
!item.dev.startsWith('hd') && !item.dev.startsWith('hd') &&
!item.dev.startsWith('mmcblk') && !item.dev.startsWith('mmcblk') &&
!item.dev.startsWith('sr')) { !item.dev.startsWith('sr')) continue;
continue;
}
final (read_, write_) = _getSpeed(item.dev); final (read_, write_) = _getSpeed(item.dev);
read += read_ ?? 0; read += read_ ?? 0;
write += write_ ?? 0; write += write_ ?? 0;
} }
final readStr = '${read.bytes2Str}/s'; final readStr = '${read.bytes2Str}/s';
final writeStr = '${write.bytes2Str}/s'; final writeStr = '${write.bytes2Str}/s';
return (readStr, writeStr); return (readStr, writeStr);
@@ -326,14 +126,12 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
try { try {
final dev = vals[2]; final dev = vals[2];
if (dev.startsWith('loop')) continue; if (dev.startsWith('loop')) continue;
items.add( items.add(DiskIOPiece(
DiskIOPiece( dev: dev,
dev: dev, sectorsRead: int.parse(vals[5]),
sectorsRead: int.parse(vals[5]), sectorsWrite: int.parse(vals[9]),
sectorsWrite: int.parse(vals[9]), time: time,
time: time, ));
),
);
} catch (e) { } catch (e) {
continue; continue;
} }
@@ -348,7 +146,12 @@ class DiskIOPiece extends TimeSeqIface<DiskIOPiece> {
final int sectorsWrite; final int sectorsWrite;
final int time; final int time;
DiskIOPiece({required this.dev, required this.sectorsRead, required this.sectorsWrite, required this.time}); DiskIOPiece({
required this.dev,
required this.sectorsRead,
required this.sectorsWrite,
required this.time,
});
@override @override
bool same(DiskIOPiece other) => dev == other.dev; bool same(DiskIOPiece other) => dev == other.dev;
@@ -358,13 +161,12 @@ class DiskUsage {
final BigInt used; final BigInt used;
final BigInt size; final BigInt size;
DiskUsage({required this.used, required this.size}); DiskUsage({
required this.used,
required this.size,
});
double get usedPercent { double get usedPercent => used / size * 100;
// Avoid division by zero
if (size == BigInt.zero) return 0;
return used / size * 100;
}
/// Find all devs, add their used and size /// Find all devs, add their used and size
static DiskUsage parse(List<Disk> disks) { static DiskUsage parse(List<Disk> disks) {
@@ -372,12 +174,9 @@ class DiskUsage {
var used = BigInt.zero; var used = BigInt.zero;
var size = BigInt.zero; var size = BigInt.zero;
for (var disk in disks) { for (var disk in disks) {
if (!_shouldCalc(disk.path, disk.mount)) continue; if (!_shouldCalc(disk.fs, disk.mount)) continue;
// Use a combination of path and kernel name to uniquely identify disks if (devs.contains(disk.fs)) continue;
// This helps distinguish between multiple physical disks in BTRFS RAID setups devs.add(disk.fs);
final uniqueId = '${disk.path}:${disk.kname ?? "unknown"}';
if (devs.contains(uniqueId)) continue;
devs.add(uniqueId);
used += disk.used; used += disk.used;
size += disk.size; size += disk.size;
} }
@@ -386,24 +185,12 @@ class DiskUsage {
} }
bool _shouldCalc(String fs, String mount) { bool _shouldCalc(String fs, String mount) {
// Skip swap partitions
// if (mount == '[SWAP]') return false;
// Include standard filesystems
if (fs.startsWith('/dev')) return true; if (fs.startsWith('/dev')) return true;
// Some NAS may have mounted path like this `//192.168.1.2/` // Some NAS may have mounted path like this `//192.168.1.2/`
if (fs.startsWith('//')) return true; if (fs.startsWith('//')) return true;
if (mount.startsWith('/mnt')) return true; if (mount.startsWith('/mnt')) return true;
// if (fs.startsWith('shm') ||
// Include common filesystem types // fs.startsWith('overlay') ||
// final commonFsTypes = ['ext2', 'ext3', 'ext4', 'xfs', 'btrfs', 'zfs', 'ntfs', 'fat', 'vfat']; // fs.startsWith('tmpfs')) return false;
// if (commonFsTypes.any((type) => fs.toLowerCase() == type)) return true; return false;
// 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

@@ -1,293 +0,0 @@
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

@@ -1,489 +0,0 @@
// 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

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

View File

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

View File

@@ -5,7 +5,11 @@ class Memory {
final int free; final int free;
final int avail; final int avail;
const Memory({required this.total, required this.free, required this.avail}); const Memory({
required this.total,
required this.free,
required this.avail,
});
double get availPercent { double get availPercent {
if (avail == 0) { if (avail == 0) {
@@ -19,99 +23,46 @@ class Memory {
static Memory parse(String raw) { static Memory parse(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList(); final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
final total = int.tryParse( final total = int.tryParse(items
items.firstWhereOrNull((e) => e?.group(1) == 'MemTotal:') .firstWhereOrNull((e) => e?.group(1) == 'MemTotal:')
?.group(2) ?? '1') ?? 1; ?.group(2) ??
final free = int.tryParse( '1') ??
items.firstWhereOrNull((e) => e?.group(1) == 'MemFree:') 1;
?.group(2) ?? '0') ?? 0; final free = int.tryParse(items
final available = int.tryParse( .firstWhereOrNull((e) => e?.group(1) == 'MemFree:')
items.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:') ?.group(2) ??
?.group(2) ?? '0') ?? 0; '0') ??
0;
final available = int.tryParse(items
.firstWhereOrNull((e) => e?.group(1) == 'MemAvailable:')
?.group(2) ??
'0') ??
0;
return Memory(total: total, free: free, avail: available); return Memory(
total: total,
free: free,
avail: available,
);
} }
} }
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB'); final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
/// Parse BSD/macOS memory from top output
///
/// Supports formats like:
/// - macOS: "PhysMem: 32G used (1536M wired), 64G unused."
/// - FreeBSD: "Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free"
Memory parseBsdMemory(String raw) {
// Try macOS format first: "PhysMem: 32G used (1536M wired), 64G unused."
final macMemReg = RegExp(
r'PhysMem:\s*([\d.]+)([KMGT])\s*used.*?,\s*([\d.]+)([KMGT])\s*unused');
final macMatch = macMemReg.firstMatch(raw);
if (macMatch != null) {
final usedAmount = double.parse(macMatch.group(1)!);
final usedUnit = macMatch.group(2)!;
final freeAmount = double.parse(macMatch.group(3)!);
final freeUnit = macMatch.group(4)!;
final usedKB = _convertToKB(usedAmount, usedUnit);
final freeKB = _convertToKB(freeAmount, freeUnit);
return Memory(total: usedKB + freeKB, free: freeKB, avail: freeKB);
}
// Try FreeBSD format: "Mem: 456M Active, 2918M Inact, 1127M Wired, 187M Cache, 829M Buf, 3535M Free"
final freeBsdReg = RegExp(
r'(\d+)([KMGT])\s+(Active|Inact|Wired|Cache|Buf|Free)', caseSensitive: false);
final matches = freeBsdReg.allMatches(raw);
if (matches.isNotEmpty) {
double usedKB = 0;
double freeKB = 0;
for (final match in matches) {
final amount = double.parse(match.group(1)!);
final unit = match.group(2)!;
final keyword = match.group(3)!.toLowerCase();
final kb = _convertToKB(amount, unit);
// Only sum known keywords
if (keyword == 'active' || keyword == 'inact' || keyword == 'wired' || keyword == 'cache' || keyword == 'buf') {
usedKB += kb;
} else if (keyword == 'free') {
freeKB += kb;
}
}
return Memory(total: (usedKB + freeKB).round(), free: freeKB.round(), avail: freeKB.round());
}
// If neither format matches, throw an error to avoid misinterpretation
throw FormatException('Unrecognized BSD/macOS memory format: $raw');
}
/// Convert memory size to KB based on unit
int _convertToKB(double amount, String unit) {
switch (unit.toUpperCase()) {
case 'T':
return (amount * 1024 * 1024 * 1024).round();
case 'G':
return (amount * 1024 * 1024).round();
case 'M':
return (amount * 1024).round();
case 'K':
case '':
return amount.round();
default:
return amount.round();
}
}
class Swap { class Swap {
final int total; final int total;
final int free; final int free;
final int cached; final int cached;
const Swap({required this.total, required this.free, required this.cached}); const Swap({
required this.total,
required this.free,
required this.cached,
});
double get usedPercent => total == 0 ? 0.0 : 1 - free / total; double get usedPercent => 1 - free / total;
double get freePercent => total == 0 ? 0.0 : free / total; double get freePercent => free / total;
@override @override
String toString() { String toString() {
@@ -121,16 +72,26 @@ class Swap {
static Swap parse(String raw) { static Swap parse(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList(); final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
final total = int.tryParse( final total = int.tryParse(items
items.firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:') .firstWhereOrNull((e) => e?.group(1) == 'SwapTotal:')
?.group(2) ?? '1') ?? 0; ?.group(2) ??
final free = int.tryParse( '1') ??
items.firstWhereOrNull((e) => e?.group(1) == 'SwapFree:') 0;
?.group(2) ?? '1') ?? 0; final free = int.tryParse(items
final cached = int.tryParse( .firstWhereOrNull((e) => e?.group(1) == 'SwapFree:')
items.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:') ?.group(2) ??
?.group(2) ?? '0') ?? 0; '1') ??
0;
final cached = int.tryParse(items
.firstWhereOrNull((e) => e?.group(1) == 'SwapCached:')
?.group(2) ??
'0') ??
0;
return Swap(total: total, free: free, cached: cached); return Swap(
total: total,
free: free,
cached: cached,
);
} }
} }

View File

@@ -1,5 +1,3 @@
// ignore_for_file: unintended_html_in_doc_comment
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart'; import 'package:server_box/data/model/server/time_seq.dart';
@@ -16,7 +14,12 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
bool same(NetSpeedPart other) => device == other.device; bool same(NetSpeedPart other) => device == other.device;
} }
typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut}); typedef CachedNetVals = ({
String sizeIn,
String sizeOut,
String speedIn,
String speedOut,
});
class NetSpeed extends TimeSeq<List<NetSpeedPart>> { class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
NetSpeed(super.init1, super.init2); NetSpeed(super.init1, super.init2);
@@ -27,14 +30,20 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
devices.addAll(now.map((e) => e.device).toList()); devices.addAll(now.map((e) => e.device).toList());
realIfaces.clear(); realIfaces.clear();
realIfaces.addAll(devices.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix)))); realIfaces.addAll(devices
.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
final sizeIn = this.sizeIn(); final sizeIn = this.sizeIn();
final sizeOut = this.sizeOut(); final sizeOut = this.sizeOut();
final speedIn = this.speedIn(); final speedIn = this.speedIn();
final speedOut = this.speedOut(); final speedOut = this.speedOut();
cachedVals = (sizeIn: sizeIn, sizeOut: sizeOut, speedIn: speedIn, speedOut: speedOut); cachedVals = (
sizeIn: sizeIn,
sizeOut: sizeOut,
speedIn: speedIn,
speedOut: speedOut,
);
} }
/// Cached network device list /// Cached network device list
@@ -47,13 +56,15 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
/// Cached non-virtual network device prefix /// Cached non-virtual network device prefix
final realIfaces = <String>[]; final realIfaces = <String>[];
CachedNetVals cachedVals = (sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s'); CachedNetVals cachedVals =
(sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
/// Time diff between [pre] and [now] /// Time diff between [pre] and [now]
BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time); BigInt get _timeDiff => BigInt.from(now[0].time - pre[0].time);
double speedInBytes(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff; double speedInBytes(int i) => (now[i].bytesIn - pre[i].bytesIn) / _timeDiff;
double speedOutBytes(int i) => (now[i].bytesOut - pre[i].bytesOut) / _timeDiff; double speedOutBytes(int i) =>
(now[i].bytesOut - pre[i].bytesOut) / _timeDiff;
BigInt sizeInBytes(int i) => now[i].bytesIn; BigInt sizeInBytes(int i) => now[i].bytesIn;
BigInt sizeOutBytes(int i) => now[i].bytesOut; BigInt sizeOutBytes(int i) => now[i].bytesOut;

View File

@@ -35,17 +35,25 @@ class NvidiaSmi {
.firstOrNull .firstOrNull
?.innerText; ?.innerText;
final power = gpu.findElements('gpu_power_readings').firstOrNull; final power = gpu.findElements('gpu_power_readings').firstOrNull;
final powerDraw = power?.findElements('power_draw').firstOrNull?.innerText; final powerDraw =
final powerLimit = power?.findElements('current_power_limit').firstOrNull?.innerText; power?.findElements('power_draw').firstOrNull?.innerText;
final powerLimit =
power?.findElements('current_power_limit').firstOrNull?.innerText;
final memory = gpu.findElements('fb_memory_usage').firstOrNull; final memory = gpu.findElements('fb_memory_usage').firstOrNull;
final memoryUsed = memory?.findElements('used').firstOrNull?.innerText; final memoryUsed = memory?.findElements('used').firstOrNull?.innerText;
final memoryTotal = memory?.findElements('total').firstOrNull?.innerText; final memoryTotal = memory?.findElements('total').firstOrNull?.innerText;
final processes = gpu.findElements('processes').firstOrNull?.findElements('process_info'); final processes = gpu
final memoryProcesses = List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) { .findElements('processes')
.firstOrNull
?.findElements('process_info');
final memoryProcesses =
List<NvidiaSmiMemProcess?>.generate(processes?.length ?? 0, (index) {
final process = processes?.elementAt(index); final process = processes?.elementAt(index);
final pid = process?.findElements('pid').firstOrNull?.innerText; final pid = process?.findElements('pid').firstOrNull?.innerText;
final name = process?.findElements('process_name').firstOrNull?.innerText; final name =
final memory = process?.findElements('used_memory').firstOrNull?.innerText; process?.findElements('process_name').firstOrNull?.innerText;
final memory =
process?.findElements('used_memory').firstOrNull?.innerText;
if (pid != null && name != null && memory != null) { if (pid != null && name != null && memory != null) {
return NvidiaSmiMemProcess( return NvidiaSmiMemProcess(
int.tryParse(pid) ?? 0, int.tryParse(pid) ?? 0,

View File

@@ -1,6 +1,7 @@
final parseFailed = Exception('Parse failed'); final parseFailed = Exception('Parse failed');
final seqReg = RegExp(r'seq=(.+) ttl=(.+) time=(.+) ms'); final seqReg = RegExp(r'seq=(.+) ttl=(.+) time=(.+) ms');
final packetReg = RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss'); final packetReg =
RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
final timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms'); final timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms');
final timeAlpineReg = RegExp(r'round-trip min/avg/max = (.+)/(.+)/(.+) ms'); final timeAlpineReg = RegExp(r'round-trip min/avg/max = (.+)/(.+)/(.+) ms');
final ipReg = RegExp(r' \((\S+)\)'); final ipReg = RegExp(r' \((\S+)\)');
@@ -14,13 +15,17 @@ class PingResult {
PingResult.parse(this.serverName, String raw) { PingResult.parse(this.serverName, String raw) {
final lines = raw.split('\n'); final lines = raw.split('\n');
lines.removeWhere((element) => element.isEmpty); lines.removeWhere((element) => element.isEmpty);
final statisticIndex = lines.indexWhere((element) => element.startsWith('---')); final statisticIndex =
lines.indexWhere((element) => element.startsWith('---'));
if (statisticIndex == -1) { if (statisticIndex == -1) {
throw parseFailed; throw parseFailed;
} }
final statisticRaw = lines.sublist(statisticIndex + 1); final statisticRaw = lines.sublist(statisticIndex + 1);
statistic = PingStatistics.parse(statisticRaw); statistic = PingStatistics.parse(statisticRaw);
results = lines.sublist(1, statisticIndex).map((e) => PingSeqResult.parse(e)).toList(); results = lines
.sublist(1, statisticIndex)
.map((e) => PingSeqResult.parse(e))
.toList();
ip = ipReg.firstMatch(lines[0])?.group(1); ip = ipReg.firstMatch(lines[0])?.group(1);
} }
} }

View File

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

View File

@@ -2,6 +2,47 @@
part of 'private_key_info.dart'; 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 // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
@@ -13,4 +54,7 @@ PrivateKeyInfo _$PrivateKeyInfoFromJson(Map<String, dynamic> json) =>
); );
Map<String, dynamic> _$PrivateKeyInfoToJson(PrivateKeyInfo instance) => Map<String, dynamic> _$PrivateKeyInfoToJson(PrivateKeyInfo instance) =>
<String, dynamic>{'id': instance.id, 'private_key': instance.key}; <String, dynamic>{
'id': instance.id,
'private_key': instance.key,
};

View File

@@ -107,7 +107,10 @@ class PsResult {
final List<Proc> procs; final List<Proc> procs;
final String? error; final String? error;
const PsResult({required this.procs, this.error}); const PsResult({
required this.procs,
this.error,
});
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) { factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
final lines = raw.split('\n').map((e) => e.trim()).toList(); final lines = raw.split('\n').map((e) => e.trim()).toList();
@@ -164,7 +167,14 @@ class PsResult {
} }
} }
enum ProcSortMode { cpu, mem, pid, user, name } enum ProcSortMode {
cpu,
mem,
pid,
user,
name,
;
}
extension _StrIndex on List<String> { extension _StrIndex on List<String> {
int? indexOfOrNull(String val) { int? indexOfOrNull(String val) {

View File

@@ -6,24 +6,25 @@ enum PveResType {
qemu, qemu,
node, node,
storage, storage,
sdn; sdn,
;
static PveResType? fromString(String type) => switch (type.toLowerCase()) { static PveResType? fromString(String type) => switch (type.toLowerCase()) {
'lxc' => PveResType.lxc, 'lxc' => PveResType.lxc,
'qemu' => PveResType.qemu, 'qemu' => PveResType.qemu,
'node' => PveResType.node, 'node' => PveResType.node,
'storage' => PveResType.storage, 'storage' => PveResType.storage,
'sdn' => PveResType.sdn, 'sdn' => PveResType.sdn,
_ => null, _ => null,
}; };
String get toStr => switch (this) { String get toStr => switch (this) {
PveResType.node => l10n.node, PveResType.node => l10n.node,
PveResType.qemu => 'QEMU', PveResType.qemu => 'QEMU',
PveResType.lxc => 'LXC', PveResType.lxc => 'LXC',
PveResType.storage => l10n.storage, PveResType.storage => l10n.storage,
PveResType.sdn => 'SDN', PveResType.sdn => 'SDN',
}; };
} }
sealed class PveResIface { sealed class PveResIface {
@@ -333,7 +334,13 @@ final class PveSdn extends PveResIface implements PveCtrlIface {
@override @override
final String status; final String status;
PveSdn({required this.id, required this.type, required this.sdn, required this.node, required this.status}); PveSdn({
required this.id,
required this.type,
required this.sdn,
required this.node,
required this.status,
});
static PveSdn fromJson(Map<String, dynamic> json) { static PveSdn fromJson(Map<String, dynamic> json) {
return PveSdn( return PveSdn(
@@ -372,7 +379,8 @@ final class PveRes {
bool get onlyOneNode => nodes.length == 1; bool get onlyOneNode => nodes.length == 1;
int get length => qemus.length + lxcs.length + nodes.length + storages.length + sdns.length; int get length =>
qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
PveResIface operator [](int index) { PveResIface operator [](int index) {
if (index < nodes.length) { if (index < nodes.length) {
@@ -424,13 +432,29 @@ final class PveRes {
} }
if (old != null) { if (old != null) {
qemus.reorder(order: old.qemus.map((e) => e.id).toList(), finder: (e, s) => e.id == s); qemus.reorder(
lxcs.reorder(order: old.lxcs.map((e) => e.id).toList(), finder: (e, s) => e.id == s); order: old.qemus.map((e) => e.id).toList(),
nodes.reorder(order: old.nodes.map((e) => e.id).toList(), finder: (e, s) => e.id == s); finder: (e, s) => e.id == s);
storages.reorder(order: old.storages.map((e) => e.id).toList(), finder: (e, s) => e.id == s); lxcs.reorder(
sdns.reorder(order: old.sdns.map((e) => e.id).toList(), finder: (e, s) => e.id == s); order: old.lxcs.map((e) => e.id).toList(),
finder: (e, s) => e.id == s);
nodes.reorder(
order: old.nodes.map((e) => e.id).toList(),
finder: (e, s) => e.id == s);
storages.reorder(
order: old.storages.map((e) => e.id).toList(),
finder: (e, s) => e.id == s);
sdns.reorder(
order: old.sdns.map((e) => e.id).toList(),
finder: (e, s) => e.id == s);
} }
return PveRes(qemus: qemus, lxcs: lxcs, nodes: nodes, storages: storages, sdns: sdns); return PveRes(
qemus: qemus,
lxcs: lxcs,
nodes: nodes,
storages: storages,
sdns: sdns,
);
} }
} }

View File

@@ -15,12 +15,12 @@ final class SensorAdaptor {
static const isa = SensorAdaptor(isaRaw); static const isa = SensorAdaptor(isaRaw);
static SensorAdaptor parse(String raw) => switch (raw) { static SensorAdaptor parse(String raw) => switch (raw) {
acpiRaw => acpi, acpiRaw => acpi,
pciRaw => pci, pciRaw => pci,
virtualRaw => virtual, virtualRaw => virtual,
isaRaw => isa, isaRaw => isa,
_ => SensorAdaptor(raw), _ => SensorAdaptor(raw),
}; };
} }
final class SensorItem { final class SensorItem {
@@ -28,7 +28,11 @@ final class SensorItem {
final SensorAdaptor adapter; final SensorAdaptor adapter;
final Map<String, String> details; final Map<String, String> details;
const SensorItem({required this.device, required this.adapter, required this.details}); const SensorItem({
required this.device,
required this.adapter,
required this.details,
});
String get toMarkdown { String get toMarkdown {
final sb = StringBuffer(); final sb = StringBuffer();
@@ -68,7 +72,8 @@ final class SensorItem {
final len = sensorLines.length; final len = sensorLines.length;
if (len < 3) continue; if (len < 3) continue;
final device = sensorLines.first; final device = sensorLines.first;
final adapter = SensorAdaptor.parse(sensorLines[1].split(':').last.trim()); final adapter =
SensorAdaptor.parse(sensorLines[1].split(':').last.trim());
final details = <String, String>{}; final details = <String, String>{};
for (var idx = 2; idx < len; idx++) { for (var idx = 2; idx < len; idx++) {
@@ -79,7 +84,11 @@ final class SensorItem {
final value = detailParts[1].trim(); final value = detailParts[1].trim();
details[key] = value; details[key] = value;
} }
sensors.add(SensorItem(device: device, adapter: adapter, details: details)); sensors.add(SensorItem(
device: device,
adapter: adapter,
details: details,
));
} }
return sensors; return sensors;

View File

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

View File

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

View File

@@ -1,221 +0,0 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'server_private_info.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Spi {
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity);
/// Serializes this Spi to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
}
/// @nodoc
abstract mixin class $SpiCopyWith<$Res> {
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
@useResult
$Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
});
}
/// @nodoc
class _$SpiCopyWithImpl<$Res>
implements $SpiCopyWith<$Res> {
_$SpiCopyWithImpl(this._self, this._then);
final Spi _self;
final $Res Function(Spi) _then;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
as int,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
as String,pwd: freezed == pwd ? _self.pwd : pwd // ignore: cast_nullable_to_non_nullable
as String?,keyId: freezed == keyId ? _self.keyId : keyId // ignore: cast_nullable_to_non_nullable
as String?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self.disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
}
/// @nodoc
@JsonSerializable(includeIfNull: false)
class _Spi extends Spi {
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
@override final String name;
@override final String ip;
@override final int port;
@override final String user;
@override final String? pwd;
/// [id] of private key
@override@JsonKey(name: 'pubKeyId') final String? keyId;
final List<String>? _tags;
@override List<String>? get tags {
final value = _tags;
if (value == null) return null;
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override final String? alterUrl;
@override@JsonKey() final bool autoConnect;
/// [id] of the jump server
@override final String? jumpId;
@override final ServerCustom? custom;
@override final WakeOnLanCfg? wolCfg;
/// It only applies to SSH terminal.
final Map<String, String>? _envs;
/// It only applies to SSH terminal.
@override Map<String, String>? get envs {
final value = _envs;
if (value == null) return null;
if (_envs is EqualUnmodifiableMapView) return _envs;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override@JsonKey(fromJson: Spi.parseId) final String id;
/// Custom system type (unix or windows). If set, skip auto-detection.
@override@JsonKey(includeIfNull: false) final SystemType? customSystemType;
/// Disabled command types for this server
final List<String>? _disabledCmdTypes;
/// Disabled command types for this server
@override@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes {
final value = _disabledCmdTypes;
if (value == null) return null;
if (_disabledCmdTypes is EqualUnmodifiableListView) return _disabledCmdTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SpiCopyWith<_Spi> get copyWith => __$SpiCopyWithImpl<_Spi>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SpiToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
}
/// @nodoc
abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
@override @useResult
$Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
});
}
/// @nodoc
class __$SpiCopyWithImpl<$Res>
implements _$SpiCopyWith<$Res> {
__$SpiCopyWithImpl(this._self, this._then);
final _Spi _self;
final $Res Function(_Spi) _then;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
return _then(_Spi(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
as int,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
as String,pwd: freezed == pwd ? _self.pwd : pwd // ignore: cast_nullable_to_non_nullable
as String?,keyId: freezed == keyId ? _self.keyId : keyId // ignore: cast_nullable_to_non_nullable
as String?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self._disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
}
// dart format on

View File

@@ -2,62 +2,118 @@
part of 'server_private_info.dart'; 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 // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi( Spi _$SpiFromJson(Map<String, dynamic> json) => Spi(
name: json['name'] as String, name: json['name'] as String,
ip: json['ip'] as String, ip: json['ip'] as String,
port: (json['port'] as num).toInt(), port: (json['port'] as num).toInt(),
user: json['user'] as String, user: json['user'] as String,
pwd: json['pwd'] as String?, pwd: json['pwd'] as String?,
keyId: json['pubKeyId'] as String?, keyId: json['pubKeyId'] as String?,
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(), tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
alterUrl: json['alterUrl'] as String?, alterUrl: json['alterUrl'] as String?,
autoConnect: json['autoConnect'] as bool? ?? true, autoConnect: json['autoConnect'] as bool? ?? true,
jumpId: json['jumpId'] as String?, jumpId: json['jumpId'] as String?,
custom: json['custom'] == null custom: json['custom'] == null
? null ? null
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>), : ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
wolCfg: json['wolCfg'] == null wolCfg: json['wolCfg'] == null
? null ? null
: WakeOnLanCfg.fromJson(json['wolCfg'] as Map<String, dynamic>), : WakeOnLanCfg.fromJson(json['wolCfg'] as Map<String, dynamic>),
envs: (json['envs'] as Map<String, dynamic>?)?.map( envs: (json['envs'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String), (k, e) => MapEntry(k, e as String),
), ),
id: json['id'] == null ? '' : Spi.parseId(json['id']), );
customSystemType: $enumDecodeNullable(
_$SystemTypeEnumMap,
json['customSystemType'],
),
disabledCmdTypes: (json['disabledCmdTypes'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{ Map<String, dynamic> _$SpiToJson(Spi instance) => <String, dynamic>{
'name': instance.name, 'name': instance.name,
'ip': instance.ip, 'ip': instance.ip,
'port': instance.port, 'port': instance.port,
'user': instance.user, 'user': instance.user,
if (instance.pwd case final value?) 'pwd': value, 'pwd': instance.pwd,
if (instance.keyId case final value?) 'pubKeyId': value, 'pubKeyId': instance.keyId,
if (instance.tags case final value?) 'tags': value, 'tags': instance.tags,
if (instance.alterUrl case final value?) 'alterUrl': value, 'alterUrl': instance.alterUrl,
'autoConnect': instance.autoConnect, 'autoConnect': instance.autoConnect,
if (instance.jumpId case final value?) 'jumpId': value, 'jumpId': instance.jumpId,
if (instance.custom case final value?) 'custom': value, 'custom': instance.custom,
if (instance.wolCfg case final value?) 'wolCfg': value, 'wolCfg': instance.wolCfg,
if (instance.envs case final value?) 'envs': value, 'envs': instance.envs,
'id': instance.id, };
if (_$SystemTypeEnumMap[instance.customSystemType] case final value?)
'customSystemType': value,
if (instance.disabledCmdTypes case final value?) 'disabledCmdTypes': value,
};
const _$SystemTypeEnumMap = {
SystemType.linux: 'linux',
SystemType.bsd: 'bsd',
SystemType.windows: 'windows',
};

View File

@@ -1,33 +1,27 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/server/amd.dart';
import 'package:server_box/data/model/server/battery.dart'; import 'package:server_box/data/model/server/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/nvdia.dart';
import 'package:server_box/data/model/server/sensors.dart'; import 'package:server_box/data/model/server/sensors.dart';
import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/temp.dart';
import 'package:server_box/data/model/server/windows_parser.dart'; 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 { class ServerStatusUpdateReq {
final ServerStatus ss; final ServerStatus ss;
final Map<String, String> parsedOutput; final List<String> segments;
final SystemType system; final SystemType system;
final Map<String, String> customCmds; final Map<String, String> customCmds;
const ServerStatusUpdateReq({ const ServerStatusUpdateReq({
required this.system, required this.system,
required this.ss, required this.ss,
required this.parsedOutput, required this.segments,
required this.customCmds, required this.customCmds,
}); });
} }
@@ -36,27 +30,28 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
return switch (req.system) { return switch (req.system) {
SystemType.linux => _getLinuxStatus(req), SystemType.linux => _getLinuxStatus(req),
SystemType.bsd => _getBsdStatus(req), SystemType.bsd => _getBsdStatus(req),
SystemType.windows => _getWindowsStatus(req),
}; };
} }
// Wrap each operation with a try-catch, so that if one operation fails, // Wrap each operation with a try-catch, so that if one operation fails,
// the following operations can still be executed. // the following operations can still be executed.
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async { Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
final parsedOutput = req.parsedOutput; final segments = req.segments;
final time = int.tryParse(StatusCmdType.time.findInMap(parsedOutput)) ?? final time = int.tryParse(StatusCmdType.time.find(segments)) ??
DateTime.now().millisecondsSinceEpoch ~/ 1000; DateTime.now().millisecondsSinceEpoch ~/ 1000;
try { try {
final net = NetSpeed.parse(StatusCmdType.net.findInMap(parsedOutput), time); final net = NetSpeed.parse(StatusCmdType.net.find(segments), time);
req.ss.netSpeed.update(net); req.ss.netSpeed.update(net);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final sys = _parseSysVer(StatusCmdType.sys.findInMap(parsedOutput)); final sys = _parseSysVer(
StatusCmdType.sys.find(segments),
);
if (sys != null) { if (sys != null) {
req.ss.more[StatusCmdType.sys] = sys; req.ss.more[StatusCmdType.sys] = sys;
} }
@@ -65,7 +60,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
final host = _parseHostName(StatusCmdType.host.findInMap(parsedOutput)); final host = _parseHostName(StatusCmdType.host.find(segments));
if (host != null) { if (host != null) {
req.ss.more[StatusCmdType.host] = host; req.ss.more[StatusCmdType.host] = host;
} }
@@ -74,9 +69,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
final cpus = SingleCpuCore.parse(StatusCmdType.cpu.findInMap(parsedOutput)); final cpus = SingleCpuCore.parse(StatusCmdType.cpu.find(segments));
req.ss.cpu.update(cpus); req.ss.cpu.update(cpus);
final brand = CpuBrand.parse(StatusCmdType.cpuBrand.findInMap(parsedOutput)); final brand = CpuBrand.parse(StatusCmdType.cpuBrand.find(segments));
req.ss.cpu.brand.clear(); req.ss.cpu.brand.clear();
req.ss.cpu.brand.addAll(brand); req.ss.cpu.brand.addAll(brand);
} catch (e, s) { } catch (e, s) {
@@ -85,15 +80,15 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
try { try {
req.ss.temps.parse( req.ss.temps.parse(
StatusCmdType.tempType.findInMap(parsedOutput), StatusCmdType.tempType.find(segments),
StatusCmdType.tempVal.findInMap(parsedOutput), StatusCmdType.tempVal.find(segments),
); );
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final tcp = Conn.parse(StatusCmdType.conn.findInMap(parsedOutput)); final tcp = Conn.parse(StatusCmdType.conn.find(segments));
if (tcp != null) { if (tcp != null) {
req.ss.tcp = tcp; req.ss.tcp = tcp;
} }
@@ -102,20 +97,20 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
req.ss.disk = Disk.parse(StatusCmdType.disk.findInMap(parsedOutput)); req.ss.disk = Disk.parse(StatusCmdType.disk.find(segments));
req.ss.diskUsage = DiskUsage.parse(req.ss.disk); req.ss.diskUsage = DiskUsage.parse(req.ss.disk);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.mem = Memory.parse(StatusCmdType.mem.findInMap(parsedOutput)); req.ss.mem = Memory.parse(StatusCmdType.mem.find(segments));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final uptime = _parseUpTime(StatusCmdType.uptime.findInMap(parsedOutput)); final uptime = _parseUpTime(StatusCmdType.uptime.find(segments));
if (uptime != null) { if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime; req.ss.more[StatusCmdType.uptime] = uptime;
} }
@@ -124,39 +119,26 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
req.ss.swap = Swap.parse(StatusCmdType.mem.findInMap(parsedOutput)); req.ss.swap = Swap.parse(StatusCmdType.mem.find(segments));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final diskio = DiskIO.parse(StatusCmdType.diskio.findInMap(parsedOutput), time); final diskio = DiskIO.parse(StatusCmdType.diskio.find(segments), time);
req.ss.diskIO.update(diskio); req.ss.diskIO.update(diskio);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.findInMap(parsedOutput)); req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments));
req.ss.diskSmart = smarts;
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.findInMap(parsedOutput)); final battery = StatusCmdType.battery.find(segments);
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final battery = StatusCmdType.battery.findInMap(parsedOutput);
/// Only collect li-poly batteries /// Only collect li-poly batteries
final batteries = Batteries.parse(battery, true); final batteries = Batteries.parse(battery, true);
@@ -169,7 +151,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
final sensors = SensorItem.parse(StatusCmdType.sensors.findInMap(parsedOutput)); final sensors = SensorItem.parse(StatusCmdType.sensors.find(segments));
if (sensors.isNotEmpty) { if (sensors.isNotEmpty) {
req.ss.sensors.clear(); req.ss.sensors.clear();
req.ss.sensors.addAll(sensors); req.ss.sensors.addAll(sensors);
@@ -179,9 +161,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
for (final entry in req.customCmds.entries) { for (int idx = 0; idx < req.customCmds.length; idx++) {
final key = entry.key; final key = req.customCmds.keys.elementAt(idx);
final value = req.parsedOutput[key] ?? ''; final value = req.segments[idx + req.system.segmentsLen];
req.ss.customCmds[key] = value; req.ss.customCmds[key] = value;
} }
} catch (e, s) { } catch (e, s) {
@@ -193,36 +175,36 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
// Same as above, wrap with try-catch // Same as above, wrap with try-catch
Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async { Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
final parsedOutput = req.parsedOutput; final segments = req.segments;
try { try {
final time = int.parse(BSDStatusCmdType.time.findInMap(parsedOutput)); final time = int.parse(BSDStatusCmdType.time.find(segments));
final net = NetSpeed.parseBsd(BSDStatusCmdType.net.findInMap(parsedOutput), time); final net = NetSpeed.parseBsd(BSDStatusCmdType.net.find(segments), time);
req.ss.netSpeed.update(net); req.ss.netSpeed.update(net);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.findInMap(parsedOutput); req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.find(segments);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.findInMap(parsedOutput)); req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.find(segments));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { // try {
req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.findInMap(parsedOutput)); // req.ss.mem = parseBsdMem(BSDStatusCmdType.mem.find(segments));
} catch (e, s) { // } catch (e, s) {
Loggers.app.warning(e, s); // Loggers.app.warning(e, s);
} // }
try { try {
final uptime = _parseUpTime(BSDStatusCmdType.uptime.findInMap(parsedOutput)); final uptime = _parseUpTime(BSDStatusCmdType.uptime.find(segments));
if (uptime != null) { if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime; req.ss.more[StatusCmdType.uptime] = uptime;
} }
@@ -231,7 +213,7 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
} }
try { try {
req.ss.disk = Disk.parse(BSDStatusCmdType.disk.findInMap(parsedOutput)); req.ss.disk = Disk.parse(BSDStatusCmdType.disk.find(segments));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
@@ -240,48 +222,13 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
// raw: // raw:
// 19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00 // 19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00
// 19:39:15 up 1 day, 2:34, 1 user, load average: 0.00, 0.00, 0.00
// 19:39:15 up 2:34, 1 user, load average: 0.00, 0.00, 0.00
// 19:39:15 up 34 min, 1 user, load average: 0.00, 0.00, 0.00
String? _parseUpTime(String raw) { String? _parseUpTime(String raw) {
final splitedUp = raw.split('up '); final splitedUp = raw.split('up ');
if (splitedUp.length == 2) { if (splitedUp.length == 2) {
final uptimePart = splitedUp[1]; final splitedComma = splitedUp[1].split(', ');
final splitedComma = uptimePart.split(', '); if (splitedComma.length >= 2) {
return splitedComma[0];
if (splitedComma.isEmpty) return null;
// Handle different uptime formats
final firstPart = splitedComma[0].trim();
// Case 1: "61 days" or "1 day" - need to get the time part from next segment
if (firstPart.contains('day')) {
if (splitedComma.length >= 2) {
final timePart = splitedComma[1].trim();
// Check if it's in HH:MM format
if (timePart.contains(':') &&
!timePart.contains('user') &&
!timePart.contains('load')) {
return '$firstPart, $timePart';
}
}
return firstPart;
} }
// Case 2: "2:34" (hours:minutes) - already in good format
if (firstPart.contains(':') &&
!firstPart.contains('user') &&
!firstPart.contains('load')) {
return firstPart;
}
// Case 3: "34 min" - already in good format
if (firstPart.contains('min')) {
return firstPart;
}
// Fallback: return first part
return firstPart;
} }
return null; return null;
} }
@@ -296,409 +243,6 @@ String? _parseSysVer(String raw) {
String? _parseHostName(String raw) { String? _parseHostName(String raw) {
if (raw.isEmpty) return null; if (raw.isEmpty) return null;
if (raw.contains(ScriptConstants.scriptFile)) return null; if (raw.contains(ShellFunc.scriptFile)) return null;
return raw; return raw;
} }
// Windows status parsing implementation
Future<ServerStatus> _getWindowsStatus(ServerStatusUpdateReq req) async {
final parsedOutput = req.parsedOutput;
final time = int.tryParse(WindowsStatusCmdType.time.findInMap(parsedOutput)) ??
DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Parse all different resource types using helper methods
_parseWindowsNetworkData(req, parsedOutput, time);
_parseWindowsSystemData(req, parsedOutput);
_parseWindowsHostData(req, parsedOutput);
_parseWindowsCpuData(req, parsedOutput);
_parseWindowsMemoryData(req, parsedOutput);
_parseWindowsDiskData(req, parsedOutput);
_parseWindowsUptimeData(req, parsedOutput);
_parseWindowsDiskIOData(req, parsedOutput, time);
_parseWindowsConnectionData(req, parsedOutput);
_parseWindowsBatteryData(req, parsedOutput);
_parseWindowsTemperatureData(req, parsedOutput);
_parseWindowsGpuData(req, parsedOutput);
WindowsParser.parseCustomCommands(req.ss, req.parsedOutput, req.customCmds);
return req.ss;
}
/// Parse Windows network data
void _parseWindowsNetworkData(ServerStatusUpdateReq req, Map<String, String> parsedOutput, int time) {
try {
final netRaw = WindowsStatusCmdType.net.findInMap(parsedOutput);
if (netRaw.isNotEmpty &&
netRaw != 'null' &&
!netRaw.contains('network_error') &&
!netRaw.contains('error') &&
!netRaw.contains('Exception')) {
final netParts = _parseWindowsNetwork(netRaw, time);
if (netParts.isNotEmpty) {
req.ss.netSpeed.update(netParts);
}
}
} catch (e, s) {
Loggers.app.warning('Windows network parsing failed: $e', s);
}
}
/// Parse Windows system information
void _parseWindowsSystemData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final sys = WindowsStatusCmdType.sys.findInMap(parsedOutput);
if (sys.isNotEmpty) {
req.ss.more[StatusCmdType.sys] = sys;
}
} catch (e, s) {
Loggers.app.warning('Windows system parsing failed: $e', s);
}
}
/// Parse Windows host information
void _parseWindowsHostData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final host = _parseHostName(WindowsStatusCmdType.host.findInMap(parsedOutput));
if (host != null) {
req.ss.more[StatusCmdType.host] = host;
}
} catch (e, s) {
Loggers.app.warning('Windows host parsing failed: $e', s);
}
}
/// Parse Windows CPU data and brand information
void _parseWindowsCpuData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
// Windows CPU parsing - JSON format from PowerShell
final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
if (cpuRaw.isNotEmpty &&
cpuRaw != 'null' &&
!cpuRaw.contains('error') &&
!cpuRaw.contains('Exception')) {
final cpus = WindowsParser.parseCpu(cpuRaw, req.ss);
if (cpus.isNotEmpty) {
req.ss.cpu.update(cpus);
}
}
// Windows CPU brand parsing
final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
if (brandRaw.isNotEmpty && brandRaw != 'null') {
req.ss.cpu.brand.clear();
req.ss.cpu.brand[brandRaw.trim()] = 1;
}
} catch (e, s) {
Loggers.app.warning('Windows CPU parsing failed: $e', s);
}
}
/// Parse Windows memory data
void _parseWindowsMemoryData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final memRaw = WindowsStatusCmdType.mem.findInMap(parsedOutput);
if (memRaw.isNotEmpty &&
memRaw != 'null' &&
!memRaw.contains('error') &&
!memRaw.contains('Exception')) {
final memory = WindowsParser.parseMemory(memRaw);
if (memory != null) {
req.ss.mem = memory;
}
}
} catch (e, s) {
Loggers.app.warning('Windows memory parsing failed: $e', s);
}
}
/// Parse Windows disk data
void _parseWindowsDiskData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final diskRaw = WindowsStatusCmdType.disk.findInMap(parsedOutput);
if (diskRaw.isNotEmpty && diskRaw != 'null') {
final disks = WindowsParser.parseDisks(diskRaw);
req.ss.disk = disks;
req.ss.diskUsage = DiskUsage.parse(disks);
}
} catch (e, s) {
Loggers.app.warning('Windows disk parsing failed: $e', s);
}
}
/// Parse Windows uptime data
void _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime;
}
} catch (e, s) {
Loggers.app.warning('Windows uptime parsing failed: $e', s);
}
}
/// Parse Windows disk I/O data
void _parseWindowsDiskIOData(ServerStatusUpdateReq req, Map<String, String> parsedOutput, int time) {
try {
final diskIOraw = WindowsStatusCmdType.diskio.findInMap(parsedOutput);
if (diskIOraw.isNotEmpty && diskIOraw != 'null') {
final diskio = _parseWindowsDiskIO(diskIOraw, time);
req.ss.diskIO.update(diskio);
}
} catch (e, s) {
Loggers.app.warning('Windows disk I/O parsing failed: $e', s);
}
}
/// Parse Windows connection data
void _parseWindowsConnectionData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final connStr = WindowsStatusCmdType.conn.findInMap(parsedOutput);
final connCount = int.tryParse(connStr.trim());
if (connCount != null) {
req.ss.tcp = Conn(maxConn: 0, active: connCount, passive: 0, fail: 0);
}
} catch (e, s) {
Loggers.app.warning('Windows connection parsing failed: $e', s);
}
}
/// Parse Windows battery data
void _parseWindowsBatteryData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final batteryRaw = WindowsStatusCmdType.battery.findInMap(parsedOutput);
if (batteryRaw.isNotEmpty && batteryRaw != 'null') {
final batteries = _parseWindowsBatteries(batteryRaw);
req.ss.batteries.clear();
if (batteries.isNotEmpty) {
req.ss.batteries.addAll(batteries);
}
}
} catch (e, s) {
Loggers.app.warning('Windows battery parsing failed: $e', s);
}
}
/// Parse Windows temperature data
void _parseWindowsTemperatureData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final tempRaw = WindowsStatusCmdType.temp.findInMap(parsedOutput);
if (tempRaw.isNotEmpty && tempRaw != 'null') {
_parseWindowsTemperatures(req.ss.temps, tempRaw);
}
} catch (e, s) {
Loggers.app.warning('Windows temperature parsing failed: $e', s);
}
}
/// Parse Windows GPU data (NVIDIA/AMD)
void _parseWindowsGpuData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning('Windows NVIDIA GPU parsing failed: $e', s);
}
try {
req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning('Windows AMD GPU parsing failed: $e', s);
}
}
List<Battery> _parseWindowsBatteries(String raw) {
try {
final dynamic jsonData = json.decode(raw);
final List<Battery> batteries = [];
final batteryList = jsonData is List ? jsonData : [jsonData];
for (final batteryData in batteryList) {
final chargeRemaining =
batteryData['EstimatedChargeRemaining'] as int? ?? 0;
final batteryStatus = batteryData['BatteryStatus'] as int? ?? 0;
// Windows battery status: 1=Other, 2=Unknown, 3=Full, 4=Low,
// 5=Critical, 6=Charging, 7=ChargingAndLow, 8=ChargingAndCritical,
// 9=Undefined, 10=PartiallyCharged
final isCharging = batteryStatus == 6 ||
batteryStatus == 7 ||
batteryStatus == 8;
batteries.add(
Battery(
name: 'Battery',
percent: chargeRemaining,
status: isCharging
? BatteryStatus.charging
: BatteryStatus.discharging,
),
);
}
return batteries;
} catch (e) {
return [];
}
}
List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
try {
final dynamic jsonData = json.decode(raw);
final List<NetSpeedPart> netParts = [];
// PowerShell Get-Counter returns a structure with CounterSamples
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
final samples = jsonData['CounterSamples'] as List?;
if (samples != null && samples.length >= 2) {
// We need 2 samples to calculate speed (interval between them)
final Map<String, double> interfaceRx = {};
final Map<String, double> interfaceTx = {};
for (final sample in samples) {
final path = sample['Path']?.toString() ?? '';
final cookedValue = sample['CookedValue'] as num? ?? 0;
if (path.contains('Bytes Received/sec')) {
final interfaceName = _extractInterfaceName(path);
if (interfaceName.isNotEmpty) {
interfaceRx[interfaceName] = cookedValue.toDouble();
}
} else if (path.contains('Bytes Sent/sec')) {
final interfaceName = _extractInterfaceName(path);
if (interfaceName.isNotEmpty) {
interfaceTx[interfaceName] = cookedValue.toDouble();
}
}
}
// Create NetSpeedPart for each interface
for (final interfaceName in interfaceRx.keys) {
final rx = interfaceRx[interfaceName] ?? 0;
final tx = interfaceTx[interfaceName] ?? 0;
netParts.add(
NetSpeedPart(
interfaceName,
BigInt.from(rx.toInt()),
BigInt.from(tx.toInt()),
currentTime,
),
);
}
}
}
return netParts;
} catch (e) {
return [];
}
}
String _extractInterfaceName(String path) {
// Extract interface name from path like
// "\\Computer\\NetworkInterface(Interface Name)\\..."
final match = RegExp(r'\\NetworkInterface\(([^)]+)\)\\').firstMatch(path);
return match?.group(1) ?? '';
}
List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
try {
final dynamic jsonData = json.decode(raw);
final List<DiskIOPiece> diskParts = [];
// PowerShell Get-Counter returns a structure with CounterSamples
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
final samples = jsonData['CounterSamples'] as List?;
if (samples != null) {
final Map<String, double> diskReads = {};
final Map<String, double> diskWrites = {};
for (final sample in samples) {
final path = sample['Path']?.toString() ?? '';
final cookedValue = sample['CookedValue'] as num? ?? 0;
if (path.contains('Disk Read Bytes/sec')) {
final diskName = _extractDiskName(path);
if (diskName.isNotEmpty) {
diskReads[diskName] = cookedValue.toDouble();
}
} else if (path.contains('Disk Write Bytes/sec')) {
final diskName = _extractDiskName(path);
if (diskName.isNotEmpty) {
diskWrites[diskName] = cookedValue.toDouble();
}
}
}
// Create DiskIOPiece for each disk - convert bytes to sectors
// (assuming 512 bytes per sector)
for (final diskName in diskReads.keys) {
final readBytes = diskReads[diskName] ?? 0;
final writeBytes = diskWrites[diskName] ?? 0;
final sectorsRead = (readBytes / 512).round();
final sectorsWrite = (writeBytes / 512).round();
diskParts.add(
DiskIOPiece(
dev: diskName,
sectorsRead: sectorsRead,
sectorsWrite: sectorsWrite,
time: currentTime,
),
);
}
}
}
return diskParts;
} catch (e) {
return [];
}
}
String _extractDiskName(String path) {
// Extract disk name from path like
// "\\Computer\\PhysicalDisk(Disk Name)\\..."
final match = RegExp(r'\\PhysicalDisk\(([^)]+)\)\\').firstMatch(path);
return match?.group(1) ?? '';
}
void _parseWindowsTemperatures(Temperatures temps, String raw) {
try {
// Handle error output
if (raw.contains('Error') ||
raw.contains('Exception') ||
raw.contains('The term')) {
return;
}
final dynamic jsonData = json.decode(raw);
final tempList = jsonData is List ? jsonData : [jsonData];
// Create fake type and value strings that the existing parse method can handle
final typeLines = <String>[];
final valueLines = <String>[];
for (int i = 0; i < tempList.length; i++) {
final item = tempList[i];
final typeName = item['InstanceName']?.toString() ?? 'Unknown';
final temperature = item['Temperature'] as num?;
if (temperature != null) {
// Convert to the format expected by the existing parse method
typeLines.add('/sys/class/thermal/thermal_zone$i/$typeName');
// Convert to millicelsius (multiply by 1000)
// as expected by Linux parsing
valueLines.add((temperature * 1000).round().toString());
}
}
if (typeLines.isNotEmpty && valueLines.isNotEmpty) {
temps.parse(typeLines.join('\n'), valueLines.join('\n'));
}
} catch (e) {
// If JSON parsing fails, ignore temperature data
}
}

View File

@@ -1,50 +1,62 @@
import 'dart:async'; import 'dart:async';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:xterm/core.dart'; import 'package:xterm/core.dart';
part 'snippet.g.dart'; part 'snippet.g.dart';
part 'snippet.freezed.dart';
@freezed @JsonSerializable()
abstract class Snippet with _$Snippet { @HiveType(typeId: 2)
const factory Snippet({ class Snippet {
required String name, @HiveField(0)
required String script, final String name;
List<String>? tags, @HiveField(1)
String? note, final String script;
@HiveField(2)
final List<String>? tags;
@HiveField(3)
final String? note;
/// List of server id that this snippet should be auto run on /// List of server id that this snippet should be auto run on
List<String>? autoRunOn, @HiveField(4)
}) = _Snippet; final List<String>? autoRunOn;
factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json); const Snippet({
required this.name,
required this.script,
this.tags,
this.note,
this.autoRunOn,
});
static const example = Snippet( factory Snippet.fromJson(Map<String, dynamic> json) =>
name: 'example', _$SnippetFromJson(json);
script: 'echo hello',
tags: ['tag'], Map<String, dynamic> toJson() => _$SnippetToJson(this);
note: 'note',
autoRunOn: ['server_id'],
);
}
extension SnippetX on Snippet {
static final fmtFinder = RegExp(r'\$\{[^{}]+\}'); static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
String fmtWithSpi(Spi spi) { String fmtWithSpi(Spi spi) {
return script.replaceAllMapped(fmtFinder, (match) { return script.replaceAllMapped(
final key = match.group(0); fmtFinder,
final func = fmtArgs[key]; (match) {
if (func != null) return func(spi); final key = match.group(0);
// If not found, return the original content for further processing final func = fmtArgs[key];
return key ?? ''; if (func != null) return func(spi);
}); // If not found, return the original content for further processing
return key ?? '';
},
);
} }
Future<void> runInTerm(Terminal terminal, Spi spi, {bool autoEnter = false}) async { Future<void> runInTerm(
Terminal terminal,
Spi spi, {
bool autoEnter = false,
}) async {
final argsFmted = fmtWithSpi(spi); final argsFmted = fmtWithSpi(spi);
final matches = fmtFinder.allMatches(argsFmted); final matches = fmtFinder.allMatches(argsFmted);
@@ -112,7 +124,11 @@ extension SnippetX on Snippet {
if (autoEnter) terminal.keyInput(TerminalKey.enter); if (autoEnter) terminal.keyInput(TerminalKey.enter);
} }
Future<void> _doTermKeys(Terminal terminal, MapEntry<String, TerminalKey> termKey, String key) async { Future<void> _doTermKeys(
Terminal terminal,
MapEntry<String, TerminalKey> termKey,
String key,
) async {
// if (termKey.value == TerminalKey.enter) { // if (termKey.value == TerminalKey.enter) {
// terminal.keyInput(TerminalKey.enter); // terminal.keyInput(TerminalKey.enter);
// return; // return;
@@ -129,7 +145,11 @@ extension SnippetX on Snippet {
// `${ctrl+ad}` -> `ctrla + d` // `${ctrl+ad}` -> `ctrla + d`
final chars = key.substring(termKey.key.length + 1, key.length - 1); final chars = key.substring(termKey.key.length + 1, key.length - 1);
if (chars.isEmpty) return; if (chars.isEmpty) return;
final ok = terminal.charInput(chars.codeUnitAt(0), ctrl: ctrlAlt.ctrl, alt: ctrlAlt.alt); final ok = terminal.charInput(
chars.codeUnitAt(0),
ctrl: ctrlAlt.ctrl,
alt: ctrlAlt.alt,
);
if (!ok) { if (!ok) {
Loggers.app.warning('Failed to input: $key'); Loggers.app.warning('Failed to input: $key');
} }
@@ -151,7 +171,18 @@ extension SnippetX on Snippet {
}; };
/// r'${ctrl+ad}' -> TerminalKey.control, a, d /// r'${ctrl+ad}' -> TerminalKey.control, a, d
static final fmtTermKeys = {r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt}; static final fmtTermKeys = {
r'${ctrl': TerminalKey.control,
r'${alt': TerminalKey.alt,
};
static const example = Snippet(
name: 'example',
script: 'echo hello',
tags: ['tag'],
note: 'note',
autoRunOn: ['server_id'],
);
} }
class SnippetResult { class SnippetResult {
@@ -159,7 +190,11 @@ class SnippetResult {
final String result; final String result;
final Duration time; final Duration time;
SnippetResult({required this.dest, required this.result, required this.time}); SnippetResult({
required this.dest,
required this.result,
required this.time,
});
} }
typedef SnippetFuncCtx = ({Terminal term, String raw}); typedef SnippetFuncCtx = ({Terminal term, String raw});
@@ -171,7 +206,10 @@ abstract final class SnippetFuncs {
r'${enter': SnippetFuncs.enter, r'${enter': SnippetFuncs.enter,
}; };
static const help = {'sleep': 'Sleep for a few seconds', 'enter': 'Enter a few times'}; static const help = {
'sleep': 'Sleep for a few seconds',
'enter': 'Enter a few times',
};
static FutureOr<void> sleep(SnippetFuncCtx ctx) async { static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
final seconds = int.tryParse(ctx.raw); final seconds = int.tryParse(ctx.raw);

View File

@@ -1,179 +0,0 @@
// 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,24 +2,74 @@
part of 'snippet.dart'; 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 // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_Snippet _$SnippetFromJson(Map<String, dynamic> json) => _Snippet( Snippet _$SnippetFromJson(Map<String, dynamic> json) => Snippet(
name: json['name'] as String, name: json['name'] as String,
script: json['script'] as String, script: json['script'] as String,
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(), tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
note: json['note'] as String?, note: json['note'] as String?,
autoRunOn: (json['autoRunOn'] as List<dynamic>?) autoRunOn: (json['autoRunOn'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList(), .toList(),
); );
Map<String, dynamic> _$SnippetToJson(_Snippet instance) => <String, dynamic>{ Map<String, dynamic> _$SnippetToJson(Snippet instance) => <String, dynamic>{
'name': instance.name, 'name': instance.name,
'script': instance.script, 'script': instance.script,
'tags': instance.tags, 'tags': instance.tags,
'note': instance.note, 'note': instance.note,
'autoRunOn': instance.autoRunOn, 'autoRunOn': instance.autoRunOn,
}; };

View File

@@ -1,55 +1,32 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:server_box/data/model/app/shell_func.dart';
enum SystemType { enum SystemType {
linux(linuxSign), linux._(linuxSign),
bsd(bsdSign), bsd._(bsdSign),
windows(windowsSign); ;
final String? value; final String value;
const SystemType([this.value]); const SystemType._(this.value);
static const linuxSign = '__linux'; static const linuxSign = '__linux';
static const bsdSign = '__bsd'; static const bsdSign = '__bsd';
static const windowsSign = '__windows';
/// Used for parsing system types from shell output.
///
/// This method looks for specific system signatures in the shell output
/// and returns the corresponding SystemType. If no signature is found,
/// it defaults to Linux but logs the detection failure for debugging.
static SystemType parse(String value) { static SystemType parse(String value) {
// Log the raw value for debugging purposes (truncated to avoid spam)
final truncatedValue = value.length > 100
? '${value.substring(0, 100)}...'
: value;
if (value.contains(windowsSign)) {
Loggers.app.info('System detected as Windows from signature in: $truncatedValue');
return SystemType.windows;
}
if (value.contains(bsdSign)) { if (value.contains(bsdSign)) {
Loggers.app.info('System detected as BSD from signature in: $truncatedValue');
return SystemType.bsd; return SystemType.bsd;
} }
// Log when falling back to Linux detection
if (value.trim().isEmpty) {
Loggers.app.warning(
'System detection received empty input, defaulting to Linux. '
'This may indicate a script execution issue.'
);
} else if (!value.contains(linuxSign)) {
Loggers.app.warning(
'System detection could not find any known signatures (Windows: $windowsSign, '
'BSD: $bsdSign, Linux: $linuxSign) in output: "$truncatedValue". '
'Defaulting to Linux, but this may cause incorrect parsing.'
);
} else {
Loggers.app.info('System detected as Linux from signature in: $truncatedValue');
}
return SystemType.linux; return SystemType.linux;
} }
bool isSegmentsLenMatch(int len) => len == segmentsLen;
int get segmentsLen {
switch (this) {
case SystemType.linux:
return StatusCmdType.values.length;
case SystemType.bsd:
return BSDStatusCmdType.values.length;
}
}
} }

View File

@@ -8,24 +8,26 @@ enum SystemdUnitFunc {
reload, reload,
enable, enable,
disable, disable,
status; status,
;
IconData get icon => switch (this) { IconData get icon => switch (this) {
start => Icons.play_arrow, start => Icons.play_arrow,
stop => Icons.stop, stop => Icons.stop,
restart => Icons.refresh, restart => Icons.refresh,
reload => Icons.refresh, reload => Icons.refresh,
enable => Icons.check, enable => Icons.check,
disable => Icons.close, disable => Icons.close,
status => Icons.info, status => Icons.info,
}; };
} }
enum SystemdUnitType { enum SystemdUnitType {
service, service,
socket, socket,
mount, mount,
timer; timer,
;
static SystemdUnitType? fromString(String? value) { static SystemdUnitType? fromString(String? value) {
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase()); return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
@@ -34,12 +36,13 @@ enum SystemdUnitType {
enum SystemdUnitScope { enum SystemdUnitScope {
system, system,
user; user,
;
Color? get color => switch (this) { Color? get color => switch (this) {
system => Colors.red, system => Colors.red,
_ => null, _ => null,
}; };
String getCmdPrefix(bool isRoot) { String getCmdPrefix(bool isRoot) {
if (this == system) { if (this == system) {
@@ -54,16 +57,17 @@ enum SystemdUnitState {
inactive, inactive,
failed, failed,
activating, activating,
deactivating; deactivating,
;
static SystemdUnitState? fromString(String? value) { static SystemdUnitState? fromString(String? value) {
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase()); return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
} }
Color? get color => switch (this) { Color? get color => switch (this) {
failed => Colors.red, failed => Colors.red,
_ => null, _ => null,
}; };
} }
final class SystemdUnit { final class SystemdUnit {
@@ -81,7 +85,10 @@ final class SystemdUnit {
required this.state, required this.state,
}); });
String getCmd({required SystemdUnitFunc func, required bool isRoot}) { String getCmd({
required SystemdUnitFunc func,
required bool isRoot,
}) {
final prefix = scope.getCmdPrefix(isRoot); final prefix = scope.getCmdPrefix(isRoot);
return '$prefix ${func.name} $name'; return '$prefix ${func.name} $name';
} }

View File

@@ -40,7 +40,11 @@ class Fifo<T> extends ListBase<T> {
abstract class TimeSeq<T extends List<TimeSeqIface>> extends Fifo<T> { abstract class TimeSeq<T extends List<TimeSeqIface>> extends Fifo<T> {
/// Due to the design, at least two elements are required, otherwise [pre] / /// Due to the design, at least two elements are required, otherwise [pre] /
/// [now] will throw. /// [now] will throw.
TimeSeq(T init1, T init2, {super.capacity}) : super(list: [init1, init2]); TimeSeq(
T init1,
T init2, {
super.capacity,
}) : super(list: [init1, init2]);
T get pre { T get pre {
return _list[length - 2]; return _list[length - 2];

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