Compare commits

..

1 Commits

Author SHA1 Message Date
Noo6
b56e033773 fix: sftp open file on windows 2024-11-14 14:18:32 +08:00
215 changed files with 8427 additions and 25028 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 provide charts to display <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> server status and tools to manage server. A Flutter project which 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>
## 📥 Install ## 📥 Install
|Platform| From| Platform | From
|--|--| --- | ---
| 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)
Please only download pkgs from the source that **you trust**! Please only download pkgs from the source that **you trust**!
## 🔖 Feature
- `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,33 +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).
## 🧱 Contribution
## 🧱 Contribution
Any positive contribution is welcome. Any positive contribution is welcome.
### 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 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。 使用 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,30 +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更好看的反馈不一定会接受
## 🧱 贡献
## 🧱 贡献
任何正面的贡献都欢迎。 任何正面的贡献都欢迎。
### 开发 ### 开发
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"

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 = 1201; 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.1201; 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 = 1201; 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.1201; 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 = 1201; 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.1201; 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 = 1201; 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.1201; 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 = 1201; 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.1201; 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 = 1201; 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.1201; 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 = 1201; 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.1201; 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 = 1201; 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.1201; 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 = 1201; 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.1201; 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,18 +14,11 @@ 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() {
@@ -37,84 +30,23 @@ class PhoneConnMgr: NSObject, WCSessionDelegate, ObservableObject {
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,67 +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) {
@@ -94,14 +75,6 @@ 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: 450, name: MOBILE),
Breakpoint(start: 451, end: 800, name: TABLET),
Breakpoint(start: 801, end: 1920, name: DESKTOP),
],
),
locale: locale, locale: locale,
localizationsDelegates: const [ localizationsDelegates: const [
LibLocalizations.delegate, LibLocalizations.delegate,
@@ -114,25 +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,17 +12,17 @@ extension SftpFileX on SftpFileMode {
UnixPerm toUnixPerm() { UnixPerm toUnixPerm() {
return UnixPerm( return UnixPerm(
user: UnixPermOp( user: RWX(
r: userRead, r: userRead,
w: userWrite, w: userWrite,
x: userExecute, x: userExecute,
), ),
group: UnixPermOp( group: RWX(
r: groupRead, r: groupRead,
w: groupWrite, w: groupWrite,
x: groupExecute, x: groupExecute,
), ),
other: UnixPermOp( other: RWX(
r: otherRead, r: otherRead,
w: otherWrite, w: otherWrite,
x: otherExecute, x: otherExecute,

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

@@ -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].
@@ -31,7 +32,7 @@ enum GenSSHClientStatus {
} }
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( throw SSHErr(
type: SSHErrType.noPrivateKey, type: SSHErrType.noPrivateKey,
@@ -58,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);

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

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

View File

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

View File

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

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

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

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

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

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

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

@@ -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,208 +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) {
@@ -222,12 +43,12 @@ class Disk with EquatableMixin {
final mount = vals[5]; final mount = vals[5];
if (!_shouldCalc(fs, mount)) continue; if (!_shouldCalc(fs, mount)) continue;
list.add(Disk( list.add(Disk(
path: fs, fs: fs,
mount: mount, mount: mount,
usedPercent: int.parse(vals[4].replaceFirst('%', '')), usedPercent: int.parse(vals[4].replaceFirst('%', '')),
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024), used: BigInt.parse(vals[2]),
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024), size: BigInt.parse(vals[1]),
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024), avail: BigInt.parse(vals[3]),
)); ));
} catch (e) { } catch (e) {
continue; continue;
@@ -237,8 +58,9 @@ class Disk with EquatableMixin {
} }
@override @override
List<Object?> get props => String toString() {
[path, name, kname, fsTyp, mount, usedPercent, used, size, avail, uuid, children]; return 'Disk{dev: $fs, mount: $mount, usedPercent: $usedPercent, used: $used, size: $size, avail: $avail}';
}
} }
class DiskIO extends TimeSeq<List<DiskIOPiece>> { class DiskIO extends TimeSeq<List<DiskIOPiece>> {
@@ -250,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;
@@ -289,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);
@@ -354,11 +166,7 @@ class DiskUsage {
required this.size, 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) {
@@ -366,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;
} }
@@ -380,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,6 @@ enum Dist {
alpine, alpine,
rocky, rocky,
deepin, deepin,
coreelec,
; ;
} }

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

View File

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

@@ -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/shell_func.dart'; import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/amd.dart';
import 'package:server_box/data/model/server/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,15 +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/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
@@ -18,75 +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,
}) = _Spi; /// It only applies to SSH terminal.
@HiveField(12)
final Map<String, String>? envs;
final String id;
const Spi({
required this.name,
required this.ip,
required this.port,
required this.user,
required this.pwd,
this.keyId,
this.tags,
this.alterUrl,
this.autoConnect = true,
this.jumpId,
this.custom,
this.wolCfg,
this.envs,
}) : id = '$user@$ip:$port';
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 ||
@@ -118,27 +122,27 @@ 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,
user: 'root', user: 'root',
pwd: 'pwd', pwd: 'pwd',
keyId: 'private_key_id', keyId: 'private_key_id',
tags: ['tag1', 'tag2'], tags: ['tag1', 'tag2'],
alterUrl: 'user@ip:port', alterUrl: 'user@ip:port',
autoConnect: true, autoConnect: true,
jumpId: 'jump_server_id', jumpId: 'jump_server_id',
custom: ServerCustom( custom: ServerCustom(
pveAddr: 'http://localhost:8006', pveAddr: 'http://localhost:8006',
pveIgnoreCert: false, pveIgnoreCert: false,
cmds: { cmds: {
'echo': 'echo hello', 'echo': 'echo hello',
}, },
preferTempDev: 'nvme-pci-0400', preferTempDev: 'nvme-pci-0400',
logoUrl: 'https://example.com/logo.png', logoUrl: 'https://example.com/logo.png',
), ),
id: 'id'); );
bool get isRoot => user == 'root'; bool get isRoot => user == 'root';
} }

View File

@@ -1,202 +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;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity);
/// Serializes this Spi to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id);
}
/// @nodoc
abstract mixin class $SpiCopyWith<$Res> {
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
@useResult
$Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id
});
}
/// @nodoc
class _$SpiCopyWithImpl<$Res>
implements $SpiCopyWith<$Res> {
_$SpiCopyWithImpl(this._self, this._then);
final Spi _self;
final $Res Function(Spi) _then;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
as int,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
as String,pwd: freezed == pwd ? _self.pwd : pwd // ignore: cast_nullable_to_non_nullable
as String?,keyId: freezed == keyId ? _self.keyId : keyId // ignore: cast_nullable_to_non_nullable
as String?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable(includeIfNull: false)
class _Spi extends Spi {
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = ''}): _tags = tags,_envs = envs,super._();
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
@override final String name;
@override final String ip;
@override final int port;
@override final String user;
@override final String? pwd;
/// [id] of private key
@override@JsonKey(name: 'pubKeyId') final String? keyId;
final List<String>? _tags;
@override List<String>? get tags {
final value = _tags;
if (value == null) return null;
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override final String? alterUrl;
@override@JsonKey() final bool autoConnect;
/// [id] of the jump server
@override final String? jumpId;
@override final ServerCustom? custom;
@override final WakeOnLanCfg? wolCfg;
/// It only applies to SSH terminal.
final Map<String, String>? _envs;
/// It only applies to SSH terminal.
@override Map<String, String>? get envs {
final value = _envs;
if (value == null) return null;
if (_envs is EqualUnmodifiableMapView) return _envs;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override@JsonKey(fromJson: Spi.parseId) final String id;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SpiCopyWith<_Spi> get copyWith => __$SpiCopyWithImpl<_Spi>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SpiToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id);
}
/// @nodoc
abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
@override @useResult
$Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id
});
}
/// @nodoc
class __$SpiCopyWithImpl<$Res>
implements _$SpiCopyWith<$Res> {
__$SpiCopyWithImpl(this._self, this._then);
final _Spi _self;
final $Res Function(_Spi) _then;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,}) {
return _then(_Spi(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
as int,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
as String,pwd: freezed == pwd ? _self.pwd : pwd // ignore: cast_nullable_to_non_nullable
as String?,keyId: freezed == keyId ? _self.keyId : keyId // ignore: cast_nullable_to_non_nullable
as String?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,49 +2,35 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/private_key_info.dart';
class PrivateKeyStore extends HiveStore { class PrivateKeyStore extends PersistentStore {
PrivateKeyStore._() : super('key'); PrivateKeyStore._() : super('key');
static final instance = PrivateKeyStore._(); static final instance = PrivateKeyStore._();
void put(PrivateKeyInfo info) { void put(PrivateKeyInfo info) {
set(info.id, info); box.put(info.id, info);
box.updateLastModified();
} }
List<PrivateKeyInfo> fetch() { List<PrivateKeyInfo> fetch() {
final keys = box.keys;
final ps = <PrivateKeyInfo>[]; final ps = <PrivateKeyInfo>[];
for (final key in keys()) { for (final key in keys) {
final s = get<PrivateKeyInfo>( final s = box.get(key);
key, if (s != null && s is PrivateKeyInfo) {
fromObj: (val) {
if (val is PrivateKeyInfo) return val;
if (val is Map<dynamic, dynamic>) {
final map = val.toStrDynMap;
if (map == null) return null;
try {
final pki = PrivateKeyInfo.fromJson(map as Map<String, dynamic>);
put(pki);
return pki;
} catch (e) {
dprint('Parsing PrivateKeyInfo from JSON', e);
}
}
return null;
},
);
if (s != null) {
ps.add(s); ps.add(s);
} }
} }
return ps; return ps;
} }
PrivateKeyInfo? fetchOne(String? id) { PrivateKeyInfo? get(String? id) {
if (id == null) return null; if (id == null) return null;
return box.get(id); return box.get(id);
} }
void delete(PrivateKeyInfo s) { void delete(PrivateKeyInfo s) {
remove(s.id); box.delete(s.id);
box.updateLastModified();
} }
} }

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