Compare commits

..

19 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
966a60a82d migrate: appleos 2024-08-17 22:59:32 +08:00
lollipopkit🏳️‍⚧️
76e98c6468 feat: custom shell script install path (#545) 2024-08-17 22:44:35 +08:00
lollipopkit🏳️‍⚧️
d7ae8b75b8 feat: custom net dev (#543) 2024-08-17 21:57:39 +08:00
lollipopkit🏳️‍⚧️
b5329e2692 fix: tags dialog 2024-08-16 21:02:05 +08:00
lollipopkit🏳️‍⚧️
ef297673f3 fix: privatekey update actually creates a new key (#541)
Fixes #540
2024-08-16 21:00:34 +08:00
lollipopkit🏳️‍⚧️
7558b4806d opt.: TagSwitcher related 2024-08-16 19:09:54 +08:00
Noo6
f7ef8a3915 opt: hidden at launch on linux and macos 2024-08-16 16:19:58 +08:00
lollipopkit🏳️‍⚧️
38366a2ef3 refactors (#539) 2024-08-16 01:24:43 +08:00
lollipopkit🏳️‍⚧️
7e5bb54c98 opt.: hide logo if distribution == null (#536) 2024-08-15 18:02:31 +08:00
lollipopkit🏳️‍⚧️
7ce3854392 opt.: use json_serializable 2024-08-15 16:44:13 +08:00
lollipopkit🏳️‍⚧️
195ddd2bcc refactor: SSHClientX.exec 2024-08-15 11:27:22 +08:00
lollipopkit🏳️‍⚧️
267b0b0a69 opt.: sftp home & back (#533) 2024-08-14 19:01:44 +08:00
lollipopkit🏳️‍⚧️
41e3fcb23a feat: systemd management (#532) 2024-08-14 14:29:03 +08:00
lollipopkit🏳️‍⚧️
46d5840276 feat: share server via qrcode (#530) 2024-08-13 20:04:01 +08:00
lollipopkit🏳️‍⚧️
fe566e97ca chore: README 2024-08-11 23:29:11 +08:00
lollipopkit🏳️‍⚧️
ddd1524d63 opt.: macos icon (#527) 2024-08-11 22:40:11 +08:00
lollipopkit🏳️‍⚧️
4d8268c614 fix: ssh tab focus mgmt (#525)
Fixes #522
2024-08-11 22:36:52 +08:00
lollipopkit🏳️‍⚧️
568b97606a opt.: split single list into multiples on desktop (#524) 2024-08-11 20:53:25 +08:00
lollipopkit🏳️‍⚧️
42cc2416a1 chore: README 2024-08-09 11:59:45 +08:00
127 changed files with 2799 additions and 2296 deletions

15
.github/FUNDING.yml vendored
View File

@@ -1,14 +1 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: lollipopkit
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
custom: ['https://afdian.com/a/lollipopkit'] # Replace with up to 4 custom sponsorship URLs
custom: ['https://cdn.lpkt.cn/donate']

View File

@@ -19,7 +19,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.22.3'
flutter-version: '3.24.0'
- uses: actions/setup-java@v4
with:
distribution: 'zulu'

View File

@@ -2,10 +2,11 @@ English | [简体中文](README_zh.md)
<h2 align="center">Flutter Server Box</h2>
<p align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink">
</p>
<div align="center">
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
<img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow">
</div>
<p align="center">
A Flutter project which provide charts to display <a href="../../issues/43">Linux</a> server status and tools to manage server.
@@ -14,6 +15,17 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
</p>
## 🏙️ Screenshots
<table>
<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/2.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
</tr>
</table>
## 📥 Install
Platform | From
@@ -22,34 +34,22 @@ iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
**Please only download pkgs from the source that you trust!**
- `AppStore` & `CDN` packages are built by myself
- Github releases are built by Github Actions
- Other sources are built by themselves
Please only download pkgs from the source that **you trust**!
## 🔖 Feature
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process`...
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
- 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); Español, Русский язык, Português, 日本語 (Generated by GPT)
## 🏙️ ScreenShots
<table>
<tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/1.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/2.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/3.png"></td>
</tr>
<tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/4.png"> </td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/5.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/6.png"></td>
</tr>
</table>
## 🆘 Help
<div align="center">
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div>
- In order to push server status to your portable device without opening ServerBox app (Such as **message push** and **home widget**), you need to install [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor) on your servers, and config it correctly. See [wiki](https://github.com/lollipopkit/server_box_monitor/wiki) for more details.
- **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki).

View File

@@ -2,10 +2,11 @@
<h2 align="center">Flutter Server Box</h2>
<p align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink">
</p>
<div align="center">
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
<img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow">
</div>
<p align="center">
使用 Flutter 开发的 <a href="../../issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
@@ -13,6 +14,18 @@
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>
</p>
## 🏙️ 截屏
<table>
<tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
</tr>
</table>
## 📥 安装
平台 | 下载
@@ -21,49 +34,34 @@ iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
**请不要从不受信任的来源下载!**
- `AppStore` & `CDN` 的包由我构建
- Github 的包由 Github Actions 构建
- 其他来源由其所有者构建
请从 **信任** 的来源下载!
## 🔖 特点
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程` 管理...
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
- 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`...
- 本地化
- English, 简体中文
- Español, Русский язык, Português, 日本語 (Generated by GPT)
- 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);
## 🏙️ 截屏
<table>
<tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/1.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/2.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/3.png"></td>
</tr>
<tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/4.png"> </td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/5.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/6.png"></td>
</tr>
</table>
- 感谢贡献者们!
## 🆘 帮助
- 吹水、参与开发、了解如何使用QQ群 **762870488**
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),并且正确配置,详情可见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。
- **常见问题**可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
<div align="center">
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div>
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),详情见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
反馈前须知:
1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。
2. 反馈问题前请检查是否是 serverbox 的问题。
3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受
确认了解上述内容后,请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
## 🧱 贡献
任何正面的贡献都欢迎。

View File

@@ -30,14 +30,19 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
library_private_types_in_public_api: false
library_private_types_in_public_api: true
use_build_context_synchronously: false
depend_on_referenced_packages: false
prefer_final_locals: true
unnecessary_parenthesis: true
implicit_call_tearoffs: true
always_declare_return_types: true
always_use_package_imports: true
annotate_overrides: true
avoid_empty_else: true
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
avoid_return_types_on_setters: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -1,6 +1,4 @@
PODS:
- device_info_plus (0.0.1):
- Flutter
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0)
@@ -8,19 +6,71 @@ PODS:
- Flutter
- flutter_native_splash (0.0.1):
- Flutter
- GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleMLKit/BarcodeScanning (6.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 5.0.0)
- GoogleMLKit/MLKitCore (6.0.0):
- MLKitCommon (~> 11.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (7.13.3)
- GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilitiesComponents (1.1.0):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (3.5.0)
- icloud_storage (0.0.1):
- Flutter
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- MLImage (1.0.0-beta5)
- MLKitBarcodeScanning (5.0.0):
- MLKitCommon (~> 11.0)
- MLKitVision (~> 7.0)
- MLKitCommon (11.0.0):
- GoogleDataTransport (< 10.0, >= 9.4.1)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0)
- GoogleUtilitiesComponents (~> 1.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (7.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta5)
- MLKitCommon (~> 11.0)
- mobile_scanner (5.1.1):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 6.0.0)
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- plain_notification_token (0.0.1):
- Flutter
- PromisesObjC (2.4.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -32,28 +82,43 @@ PODS:
- Flutter
- watch_connectivity (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- icloud_storage (from `.symlinks/plugins/icloud_storage/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- plain_notification_token (from `.symlinks/plugins/plain_notification_token/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS:
trunk:
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GoogleUtilitiesComponents
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
@@ -66,12 +131,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/icloud_storage/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
plain_notification_token:
:path: ".symlinks/plugins/plain_notification_token/ios"
share_plus:
@@ -84,24 +149,38 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
watch_connectivity:
:path: ".symlinks/plugins/watch_connectivity/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
local_auth_darwin: 4d56c90c2683319835a61274b57620df9c4520ab
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
mobile_scanner: 8564358885a9253c43f822435b70f9345c87224f
nanopb: 438bc412db1928dac798aa6fd75726007be04262
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
watch_connectivity: 715eb484685e05846eab74795348a44bb2809b82
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8

View File

@@ -690,7 +690,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -700,7 +700,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -826,7 +826,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -836,7 +836,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -854,7 +854,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -864,7 +864,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -885,7 +885,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -898,7 +898,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -924,7 +924,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -937,7 +937,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -960,7 +960,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -973,7 +973,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -996,7 +996,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1008,7 +1008,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
@@ -1037,7 +1037,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1049,7 +1049,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;
@@ -1075,7 +1075,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051;
CURRENT_PROJECT_VERSION = 1070;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1087,7 +1087,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1051;
MARKETING_VERSION = 1.0.1070;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;

View File

@@ -2,7 +2,7 @@ import UIKit
import WidgetKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@@ -1,76 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>zh</string>
</array>
<key>CFBundleName</key>
<string>ServerBox</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSBonjourServices</key>
<array>
<string>_dartobservatory._tcp</string>
</array>
<key>NSFaceIDUsageDescription</key>
<string>Required for auth</string>
<key>NSLocalNetworkUsageDescription</key>
<string>ServerBox needs to access your local network to discover and connect to your server.</string>
<key>NSUserActivityTypes</key>
<array>
<string>ConfigurationIntent</string>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>zh</string>
</array>
<key>CFBundleName</key>
<string>ServerBox</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false />
<key>LSRequiresIPhoneOS</key>
<true />
<key>LSSupportsOpeningDocumentsInPlace</key>
<true />
<key>NSBonjourServices</key>
<array>
<string>_dartobservatory._tcp</string>
</array>
<key>NSUserActivityTypes</key>
<array>
<string>ConfigurationIntent</string>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false />
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
<key>NSLocalNetworkUsageDescription</key>
<string>Access your local network to discover and connect to your server.</string>
<key>NSFaceIDUsageDescription</key>
<string>Required for auth</string>
<key>NSCameraUsageDescription</key>
<string>Scan QR codes and etc.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Get QR code and etc.</string>
</dict>
</plist>

View File

@@ -64,9 +64,14 @@
<array>
<string>_dartobservatory._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>ServerBox needs to access your local network to discover and connect to your server.</string>
<string>Access your local network to discover and connect to your server.</string>
<key>NSFaceIDUsageDescription</key>
<string>Required for auth</string>
<key>NSCameraUsageDescription</key>
<string>Scan QR codes and etc.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Get QR code and etc.</string>
</dict>
</plist>

View File

@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@@ -28,13 +28,13 @@
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<false />
<key>LSRequiresIPhoneOS</key>
<true/>
<true />
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<true />
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<true />
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
@@ -44,7 +44,7 @@
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<false />
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@@ -59,8 +59,13 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<false />
<key>NSFaceIDUsageDescription</key>
<string>Required for auth</string>
<key>NSCameraUsageDescription</key>
<string>Scan QR codes and etc.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Get QR code and etc.</string>
</dict>
</plist>
</plist>

View File

@@ -5,72 +5,74 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/widgets.dart';
import '../../data/res/misc.dart';
import 'package:server_box/data/res/misc.dart';
typedef _OnStdout = void Function(String data, SSHSession session);
typedef _OnStdin = void Function(SSHSession session);
typedef OnStdout = void Function(String data, SSHSession session);
typedef OnStdin = void Function(SSHSession session);
typedef PwdRequestFunc = Future<String?> Function(String? user);
extension SSHClientX on SSHClient {
Future<SSHSession> exec(
String cmd, {
_OnStdout? onStderr,
_OnStdout? onStdout,
_OnStdin? stdin,
bool redirectToBash = false, // not working yet. do not use
Future<(SSHSession, String)> exec(
OnStdin onStdin, {
String? entry,
SSHPtyConfig? pty,
OnStdout? onStdout,
OnStdout? onStderr,
bool stdout = true,
bool stderr = true,
Map<String, String>? env,
}) async {
final session = await execute(redirectToBash ? "head -1 | bash" : cmd);
if (redirectToBash) {
session.stdin.add("$cmd\n".uint8List);
}
final session = await execute(
entry ?? 'cat | sh',
pty: pty,
environment: env,
);
final result = BytesBuilder(copy: false);
final stdoutDone = Completer<void>();
final stderrDone = Completer<void>();
if (onStdout != null) {
session.stdout.listen(
(e) => onStdout(e.string, session),
onDone: stdoutDone.complete,
);
} else {
stdoutDone.complete();
}
session.stdout.listen(
(e) {
onStdout?.call(e.string, session);
if (stdout) result.add(e);
},
onDone: stdoutDone.complete,
onError: stderrDone.completeError,
);
if (onStderr != null) {
session.stderr.listen(
(e) => onStderr(e.string, session),
onDone: stderrDone.complete,
);
} else {
stderrDone.complete();
}
session.stderr.listen(
(e) {
onStderr?.call(e.string, session);
if (stderr) result.add(e);
},
onDone: stderrDone.complete,
onError: stderrDone.completeError,
);
if (stdin != null) {
stdin(session);
}
onStdin(session);
await stdoutDone.future;
await stderrDone.future;
session.close();
return session;
return (session, result.takeBytes().string);
}
Future<int?> execWithPwd(
String cmd, {
String script, {
String? entry,
BuildContext? context,
_OnStdout? onStdout,
_OnStdout? onStderr,
_OnStdin? stdin,
bool redirectToBash = false, // not working yet. do not use
OnStdout? onStdout,
OnStdout? onStderr,
required String id,
}) async {
var isRequestingPwd = false;
final session = await exec(
cmd,
redirectToBash: redirectToBash,
final (session, _) = await exec(
(sess) {
sess.stdin.add('$script\n'.uint8List);
sess.stdin.close();
},
onStderr: (data, session) async {
onStderr?.call(data, session);
if (isRequestingPwd) return;
@@ -83,56 +85,38 @@ extension SSHClientX on SSHClient {
? await context.showPwdDialog(title: user, id: id)
: null;
if (pwd == null || pwd.isEmpty) {
session.kill(SSHSignal.TERM);
session.stdin.close();
} else {
session.stdin.add('$pwd\n'.uint8List);
}
isRequestingPwd = false;
}
},
onStdout: (data, sink) async {
onStdout?.call(data, sink);
},
stdin: stdin,
onStdout: onStdout,
entry: entry,
);
return session.exitCode;
}
Future<Uint8List> runForOutput(
String command, {
bool runInPty = false,
Future<String> execForOutput(
String script, {
SSHPtyConfig? pty,
bool stdout = true,
bool stderr = true,
Map<String, String>? environment,
Future<void> Function(SSHSession)? action,
String? entry,
Map<String, String>? env,
}) async {
final session = await execute(
command,
pty: runInPty ? const SSHPtyConfig() : null,
environment: environment,
final ret = await exec(
(session) {
session.stdin.add('$script\n'.uint8List);
session.stdin.close();
},
pty: pty,
env: env,
stdout: stdout,
stderr: stderr,
entry: entry,
);
final result = BytesBuilder(copy: false);
final stdoutDone = Completer<void>();
final stderrDone = Completer<void>();
session.stdout.listen(
stdout ? result.add : (_) {},
onDone: stdoutDone.complete,
onError: stderrDone.completeError,
);
session.stderr.listen(
stderr ? result.add : (_) {},
onDone: stderrDone.complete,
onError: stderrDone.completeError,
);
if (action != null) await action(session);
await stdoutDone.future;
await stderrDone.future;
return result.takeBytes();
return ret.$2;
}
}

View File

@@ -1,9 +1,7 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/backup.dart';
import 'package:server_box/view/page/container.dart';
@@ -22,18 +20,18 @@ import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/setting/seq/virt_key.dart';
import 'package:server_box/view/page/storage/local.dart';
import '../data/model/server/snippet.dart';
import '../view/page/editor.dart';
import '../view/page/process.dart';
import '../view/page/server/edit.dart';
import '../view/page/server/tab.dart';
import '../view/page/setting/entry.dart';
import '../view/page/setting/seq/srv_detail_seq.dart';
import '../view/page/setting/seq/srv_seq.dart';
import '../view/page/snippet/edit.dart';
import '../view/page/snippet/list.dart';
import '../view/page/storage/sftp.dart';
import '../view/page/storage/sftp_mission.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/view/page/editor.dart';
import 'package:server_box/view/page/process.dart';
import 'package:server_box/view/page/server/edit.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/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/snippet/list.dart';
import 'package:server_box/view/page/storage/sftp.dart';
import 'package:server_box/view/page/storage/sftp_mission.dart';
class AppRoutes {
final Widget page;
@@ -60,7 +58,7 @@ class AppRoutes {
return Future.value(null);
}
static AppRoutes serverDetail({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes serverDetail({Key? key, required Spi spi}) {
return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail');
}
@@ -68,7 +66,7 @@ class AppRoutes {
return AppRoutes(ServerPage(key: key), 'server_tab');
}
static AppRoutes serverEdit({Key? key, ServerPrivateInfo? spi}) {
static AppRoutes serverEdit({Key? key, Spi? spi}) {
return AppRoutes(
ServerEditPage(spi: spi),
'server_${spi == null ? 'add' : 'edit'}',
@@ -99,7 +97,7 @@ class AppRoutes {
static AppRoutes ssh({
Key? key,
required ServerPrivateInfo spi,
required Spi spi,
String? initCmd,
Snippet? initSnippet,
}) {
@@ -134,10 +132,7 @@ class AppRoutes {
}
static AppRoutes sftp(
{Key? key,
required ServerPrivateInfo spi,
String? initPath,
bool isSelect = false}) {
{Key? key, required Spi spi, String? initPath, bool isSelect = false}) {
return AppRoutes(
SftpPage(
key: key,
@@ -152,17 +147,7 @@ class AppRoutes {
return AppRoutes(BackupPage(key: key), 'backup');
}
static AppRoutes debug({Key? key}) {
return AppRoutes(
DebugPage(
key: key,
args: const DebugPageArgs(title: 'Logs(${BuildData.build})'),
),
'debug',
);
}
static AppRoutes docker({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes docker({Key? key, required Spi spi}) {
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
}
@@ -198,7 +183,7 @@ class AppRoutes {
return AppRoutes(PingPage(key: key), 'ping');
}
static AppRoutes process({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes process({Key? key, required Spi spi}) {
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
}
@@ -232,7 +217,7 @@ class AppRoutes {
'snippet_result');
}
static AppRoutes iperf({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes iperf({Key? key, required Spi spi}) {
return AppRoutes(IPerfPage(key: key, spi: spi), 'iperf');
}
@@ -240,7 +225,7 @@ class AppRoutes {
return AppRoutes(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
}
static AppRoutes pve({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes pve({Key? key, required Spi spi}) {
return AppRoutes(PvePage(key: key, spi: spi), 'pve');
}
}

View File

@@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/res/store.dart';
import '../../data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
/// Must put this func out of any Class.
///
@@ -42,7 +42,7 @@ String getPrivateKey(String id) {
}
Future<SSHClient> genClient(
ServerPrivateInfo spi, {
Spi spi, {
void Function(GenSSHClientStatus)? onStatus,
/// Only pass this param if using multi-threading and key login
@@ -52,10 +52,10 @@ Future<SSHClient> genClient(
String? jumpPrivateKey,
Duration timeout = const Duration(seconds: 5),
/// [ServerPrivateInfo] of the jump server
/// [Spi] of the jump server
///
/// Must pass this param if using multi-threading and key login
ServerPrivateInfo? jumpSpi,
Spi? jumpSpi,
/// Handle keyboard-interactive authentication
FutureOr<List<String>?> Function(SSHUserInfoRequest)? onKeyboardInteractive,
@@ -95,8 +95,8 @@ Future<SSHClient> genClient(
try {
final ipPort = spi.fromStringUrl();
return await SSHSocket.connect(
ipPort.ip,
ipPort.port,
ipPort.$1,
ipPort.$2,
timeout: timeout,
);
} catch (e) {

View File

@@ -4,16 +4,16 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/provider/app.dart';
abstract final class KeybordInteractive {
static FutureOr<List<String>?> defaultHandle(
ServerPrivateInfo spi, {
Spi spi, {
BuildContext? ctx,
}) async {
try {
final res = await (ctx ?? Pros.app.ctx)?.showPwdDialog(
title: '2FA ${l10n.pwd}',
final res = await (ctx ?? AppProvider.ctx)?.showPwdDialog(
title: l10n.pwd,
id: spi.id,
label: spi.id,
);

View File

@@ -9,7 +9,7 @@ import 'package:server_box/data/model/app/backup.dart';
import 'package:server_box/data/model/app/sync.dart';
import 'package:server_box/data/res/misc.dart';
import '../../../data/model/app/error.dart';
import 'package:server_box/data/model/app/error.dart';
abstract final class ICloud {
static const _containerId = 'iCloud.tech.lolli.serverbox';

View File

@@ -2,24 +2,27 @@ import 'dart:convert';
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:logging/logging.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart';
part 'backup.g.dart';
const backupFormatVersion = 1;
final _logger = Logger('Backup');
@JsonSerializable()
class Backup {
// backup format version
final int version;
final String date;
final List<ServerPrivateInfo> spis;
final List<Spi> spis;
final List<Snippet> snippets;
final List<PrivateKeyInfo> keys;
final Map<String, dynamic> container;
@@ -37,31 +40,9 @@ class Backup {
this.lastModTime,
});
Backup.fromJson(Map<String, dynamic> json)
: version = json['version'] as int,
date = json['date'],
spis = (json['spis'] as List)
.map((e) => ServerPrivateInfo.fromJson(e))
.toList(),
snippets =
(json['snippets'] as List).map((e) => Snippet.fromJson(e)).toList(),
keys = (json['keys'] as List)
.map((e) => PrivateKeyInfo.fromJson(e))
.toList(),
container = json['container'] ?? {},
lastModTime = json['lastModTime'],
history = json['history'] ?? {};
factory Backup.fromJson(Map<String, dynamic> json) => _$BackupFromJson(json);
Map<String, dynamic> toJson() => {
'version': version,
'date': date,
'spis': spis,
'snippets': snippets,
'keys': keys,
'container': container,
'lastModTime': lastModTime,
'history': history,
};
Map<String, dynamic> toJson() => _$BackupToJson(this);
Backup.loadFromStore()
: version = backupFormatVersion,
@@ -195,14 +176,14 @@ class Backup {
}
}
Pros.reload();
Provider.reload();
RNodes.app.notify();
_logger.info('Restore success');
}
Backup.fromJsonString(String raw)
: this.fromJson(json.decode(_diyDecrypt(raw)));
factory Backup.fromJsonString(String raw) =>
Backup.fromJson(json.decode(_diyDecrypt(raw)));
}
String _diyEncrypt(String raw) => json.encode(

View File

@@ -0,0 +1,35 @@
// 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>,
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,
};

View File

@@ -2,29 +2,47 @@ import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
part 'server_func.g.dart';
@HiveType(typeId: 6)
enum ServerFuncBtn {
@HiveField(0)
terminal,
terminal._(),
@HiveField(1)
sftp,
sftp._(),
@HiveField(2)
container,
container._(),
@HiveField(3)
process,
process._(),
//@HiveField(4)
//pkg,
@HiveField(5)
snippet,
snippet._(),
@HiveField(6)
iperf,
iperf._(),
// @HiveField(7)
// pve,
@HiveField(8)
systemd._(1058),
;
final int? addedVersion;
const ServerFuncBtn._([this.addedVersion]);
static void autoAddNewFuncs(int cur) {
if (cur >= systemd.addedVersion!) {
final prop = Stores.setting.serverFuncBtns;
final list = prop.fetch();
if (!list.contains(systemd.index)) {
list.add(systemd.index);
prop.put(list);
}
}
}
static final defaultIdxs = [
terminal,
sftp,
@@ -32,6 +50,7 @@ enum ServerFuncBtn {
process,
//pkg,
snippet,
systemd,
].map((e) => e.index).toList();
IconData get icon => switch (this) {
@@ -42,6 +61,7 @@ enum ServerFuncBtn {
process => Icons.list_alt_outlined,
terminal => Icons.terminal,
iperf => Icons.speed,
systemd => MingCute.plugin_2_fill,
};
String get toStr => switch (this) {
@@ -52,5 +72,6 @@ enum ServerFuncBtn {
process => l10n.process,
terminal => l10n.terminal,
iperf => 'iperf',
systemd => 'Systemd',
};
}

View File

@@ -25,6 +25,8 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
return ServerFuncBtn.snippet;
case 6:
return ServerFuncBtn.iperf;
case 8:
return ServerFuncBtn.systemd;
default:
return ServerFuncBtn.terminal;
}
@@ -51,6 +53,9 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
case ServerFuncBtn.iperf:
writer.writeByte(6);
break;
case ServerFuncBtn.systemd:
writer.writeByte(8);
break;
}
}

View File

@@ -2,7 +2,6 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/res/store.dart';
part 'net_view.g.dart';
@@ -27,36 +26,43 @@ enum NetViewType {
NetViewType.speed => l10n.speed,
};
(String, String) build(ServerStatus ss) {
final ignoreLocal = Stores.setting.ignoreLocalNet.fetch();
switch (this) {
case NetViewType.conn:
return (
'${l10n.conn}:\n${ss.tcp.maxConn}',
'${libL10n.fail}:\n${ss.tcp.fail}',
);
case NetViewType.speed:
if (ignoreLocal) {
/// If no device is specified, return the cached value (only real devices,
/// such as ethX, wlanX...).
(String, String) build(ServerStatus ss, {String? dev}) {
final notSepcifyDev = dev == null || dev.isEmpty;
try {
switch (this) {
case NetViewType.conn:
return (
'↓:\n${ss.netSpeed.cachedRealVals.speedIn}',
'↑:\n${ss.netSpeed.cachedRealVals.speedOut}',
'${l10n.conn}:\n${ss.tcp.maxConn}',
'${libL10n.fail}:\n${ss.tcp.fail}',
);
}
return (
'↓:\n${ss.netSpeed.speedIn()}',
':\n${ss.netSpeed.speedOut()}',
);
case NetViewType.traffic:
if (ignoreLocal) {
case NetViewType.speed:
if (notSepcifyDev) {
return (
':\n${ss.netSpeed.cachedVals.speedIn}',
'↑:\n${ss.netSpeed.cachedVals.speedOut}',
);
}
return (
'↓:\n${ss.netSpeed.cachedRealVals.sizeIn}',
'↑:\n${ss.netSpeed.cachedRealVals.sizeOut}',
'↓:\n${ss.netSpeed.speedIn(device: dev)}',
'↑:\n${ss.netSpeed.speedOut(device: dev)}',
);
}
return (
'↓:\n${ss.netSpeed.sizeIn()}',
':\n${ss.netSpeed.sizeOut()}',
);
case NetViewType.traffic:
if (notSepcifyDev) {
return (
':\n${ss.netSpeed.cachedVals.sizeIn}',
'↑:\n${ss.netSpeed.cachedVals.sizeOut}',
);
}
return (
'↓:\n${ss.netSpeed.sizeIn(device: dev)}',
'↑:\n${ss.netSpeed.sizeOut(device: dev)}',
);
}
} catch (e, s) {
Loggers.app.warning('NetViewType.build', e, s);
return ('N/A', 'N/A');
}
}

View File

@@ -1,7 +1,8 @@
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/provider/server.dart';
import '../../res/build_data.dart';
import '../server/system.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/model/server/system.dart';
enum ShellFunc {
status,
@@ -29,6 +30,9 @@ enum ShellFunc {
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// it will be changed to [scriptDirHome]/[scriptFile].
static String getScriptDir(String id) {
final customScriptDir =
ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
return _scriptDirMap.putIfAbsent(id, () {
return scriptDirTmp;
});
@@ -47,11 +51,11 @@ enum ShellFunc {
static String getInstallShellCmd(String id) {
final scriptDir = getScriptDir(id);
final scriptPath = '$scriptDir/$scriptFile';
return """
return '''
mkdir -p $scriptDir
cat > $scriptPath
chmod 744 $scriptPath
""";
''';
}
String get flag => switch (this) {

View File

@@ -45,21 +45,21 @@ final class PodmanImg implements ContainerImg {
String toRawJson() => json.encode(toJson());
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
repository: json["repository"],
tag: json["tag"],
id: json["Id"],
created: json["Created"],
size: json["Size"],
containers: json["Containers"],
repository: json['repository'],
tag: json['tag'],
id: json['Id'],
created: json['Created'],
size: json['Size'],
containers: json['Containers'],
);
Map<String, dynamic> toJson() => {
"repository": repository,
"tag": tag,
"Id": id,
"Created": created,
"Size": size,
"Containers": containers,
'repository': repository,
'tag': tag,
'Id': id,
'Created': created,
'Size': size,
'Containers': containers,
};
}
@@ -96,36 +96,36 @@ final class DockerImg implements ContainerImg {
String toRawJson() => json.encode(toJson());
factory DockerImg.fromJson(Map<String, dynamic> json) {
final containers = switch (json["Containers"]) {
final containers = switch (json['Containers']) {
final String a => a,
final Object? a => a.toString(),
};
final repo = switch (json["Repository"] ?? json["Names"]) {
final repo = switch (json['Repository'] ?? json['Names']) {
final String a => a,
final List a => a.firstOrNull.toString(),
final Object? a => a.toString(),
};
final size = switch (json["Size"]) {
final size = switch (json['Size']) {
final String a => a,
final int a => a.bytes2Str,
final Object? a => a.toString(),
};
return DockerImg(
containers: containers,
createdAt: json["CreatedAt"],
id: json["ID"] ?? json["Id"] ?? '',
createdAt: json['CreatedAt'],
id: json['ID'] ?? json['Id'] ?? '',
repository: repo,
size: size,
tag: json["Tag"],
tag: json['Tag'],
);
}
Map<String, dynamic> toJson() => {
"Containers": containers,
"CreatedAt": createdAt,
"ID": id,
"Repository": repository,
"Size": size,
"Tag": tag,
'Containers': containers,
'CreatedAt': createdAt,
'ID': id,
'Repository': repository,
'Size': size,
'Tag': tag,
};
}

View File

@@ -84,29 +84,29 @@ final class PodmanPs implements ContainerPs {
String toRawJson() => json.encode(toJson());
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
command: json["Command"] == null
command: json['Command'] == null
? []
: List<String>.from(json["Command"]!.map((x) => x)),
: List<String>.from(json['Command']!.map((x) => x)),
created:
json["Created"] == null ? null : DateTime.parse(json["Created"]),
exited: json["Exited"],
id: json["Id"],
image: json["Image"],
names: json["Names"] == null
json['Created'] == null ? null : DateTime.parse(json['Created']),
exited: json['Exited'],
id: json['Id'],
image: json['Image'],
names: json['Names'] == null
? []
: List<String>.from(json["Names"]!.map((x) => x)),
startedAt: json["StartedAt"],
: List<String>.from(json['Names']!.map((x) => x)),
startedAt: json['StartedAt'],
);
Map<String, dynamic> toJson() => {
"Command":
'Command':
command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
"Created": created?.toIso8601String(),
"Exited": exited,
"Id": id,
"Image": image,
"Names": names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
"StartedAt": startedAt,
'Created': created?.toIso8601String(),
'Exited': exited,
'Id': id,
'Image': image,
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
'StartedAt': startedAt,
};
}

View File

@@ -34,9 +34,9 @@ class UpgradePkgInfo {
}
void _parseApt(String raw) {
final split1 = raw.split("/");
final split1 = raw.split('/');
package = split1[0];
final split2 = split1[1].split(" ");
final split2 = split1[1].split(' ');
newVersion = split2[1];
arch = split2[2];
nowVersion = split2[5].replaceFirst(']', '');
@@ -53,7 +53,7 @@ class UpgradePkgInfo {
}
void _parseZypper(String raw) {
final cols = raw.split("|");
final cols = raw.split('|');
package = cols[2].trim();
nowVersion = cols[3].trim();
newVersion = cols[4].trim();

View File

@@ -1,4 +1,4 @@
import '../../res/misc.dart';
import 'package:server_box/data/res/misc.dart';
class Conn {
final int maxConn;

View File

@@ -1,7 +1,9 @@
import 'package:hive_flutter/adapters.dart';
import 'package:json_annotation/json_annotation.dart';
part 'custom.g.dart';
@JsonSerializable()
@HiveType(typeId: 7)
final class ServerCustom {
// @HiveField(0)
@@ -19,6 +21,14 @@ final class ServerCustom {
@HiveField(5)
final String? logoUrl;
/// The device name of the network interface displayed in the home server card.
@HiveField(6)
final String? netDev;
/// The directory where the script is stored.
@HiveField(7)
final String? scriptDir;
const ServerCustom({
//this.temperature,
this.pveAddr,
@@ -26,51 +36,14 @@ final class ServerCustom {
this.cmds,
this.preferTempDev,
this.logoUrl,
this.netDev,
this.scriptDir,
});
static ServerCustom fromJson(Map<String, dynamic> json) {
//final temperature = json["temperature"] as String?;
final pveAddr = json["pveAddr"] as String?;
final pveIgnoreCert = json["pveIgnoreCert"] as bool;
final cmds = json["cmds"] as Map<String, dynamic>?;
final preferTempDev = json["preferTempDev"] as String?;
final logoUrl = json["logoUrl"] as String?;
return ServerCustom(
//temperature: temperature,
pveAddr: pveAddr,
pveIgnoreCert: pveIgnoreCert,
cmds: cmds?.cast<String, String>(),
preferTempDev: preferTempDev,
logoUrl: logoUrl,
);
}
factory ServerCustom.fromJson(Map<String, dynamic> json) =>
_$ServerCustomFromJson(json);
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
// if (temperature != null) {
// json["temperature"] = temperature;
// }
if (pveAddr != null) {
json["pveAddr"] = pveAddr;
}
json["pveIgnoreCert"] = pveIgnoreCert;
if (cmds != null) {
json["cmds"] = cmds;
}
if (preferTempDev != null) {
json["preferTempDev"] = preferTempDev;
}
if (logoUrl != null) {
json["logoUrl"] = logoUrl;
}
return json;
}
@override
String toString() {
return toJson().toString();
}
Map<String, dynamic> toJson() => _$ServerCustomToJson(this);
@override
bool operator ==(Object other) {
@@ -80,7 +53,9 @@ final class ServerCustom {
other.pveIgnoreCert == pveIgnoreCert &&
other.cmds == cmds &&
other.preferTempDev == preferTempDev &&
other.logoUrl == logoUrl;
other.logoUrl == logoUrl &&
other.netDev == netDev &&
other.scriptDir == scriptDir;
}
@override
@@ -90,5 +65,7 @@ final class ServerCustom {
pveIgnoreCert.hashCode ^
cmds.hashCode ^
preferTempDev.hashCode ^
logoUrl.hashCode;
logoUrl.hashCode ^
netDev.hashCode ^
scriptDir.hashCode;
}

View File

@@ -22,13 +22,15 @@ class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
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(5)
..writeByte(7)
..writeByte(1)
..write(obj.pveAddr)
..writeByte(2)
@@ -38,7 +40,11 @@ class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
..writeByte(4)
..write(obj.preferTempDev)
..writeByte(5)
..write(obj.logoUrl);
..write(obj.logoUrl)
..writeByte(6)
..write(obj.netDev)
..writeByte(7)
..write(obj.scriptDir);
}
@override
@@ -51,3 +57,30 @@ class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ServerCustom _$ServerCustomFromJson(Map<String, dynamic> json) => ServerCustom(
pveAddr: json['pveAddr'] as String?,
pveIgnoreCert: json['pveIgnoreCert'] as bool? ?? false,
cmds: (json['cmds'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
preferTempDev: json['preferTempDev'] as String?,
logoUrl: json['logoUrl'] as String?,
netDev: json['netDev'] as String?,
scriptDir: json['scriptDir'] as String?,
);
Map<String, dynamic> _$ServerCustomToJson(ServerCustom instance) =>
<String, dynamic>{
'pveAddr': instance.pveAddr,
'pveIgnoreCert': instance.pveIgnoreCert,
'cmds': instance.cmds,
'preferTempDev': instance.preferTempDev,
'logoUrl': instance.logoUrl,
'netDev': instance.netDev,
'scriptDir': instance.scriptDir,
};

View File

@@ -1,7 +1,7 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart';
import '../../res/misc.dart';
import 'package:server_box/data/res/misc.dart';
class Disk {
final String fs;

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'time_seq.dart';
import 'package:server_box/data/model/server/time_seq.dart';
class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
final String device;
@@ -14,6 +14,13 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
bool same(NetSpeedPart other) => device == other.device;
}
typedef CachedNetVals = ({
String sizeIn,
String sizeOut,
String speedIn,
String speedOut,
});
class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
NetSpeed(super.init1, super.init2);
@@ -24,14 +31,14 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
realIfaces.clear();
realIfaces.addAll(devices
.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix)))
.toList());
.where((e) => realIfacePrefixs.any((prefix) => e.startsWith(prefix))));
final sizeIn = this.sizeIn();
final sizeOut = this.sizeOut();
final speedIn = this.speedIn();
final speedOut = this.speedOut();
cachedRealVals = (
cachedVals = (
sizeIn: sizeIn,
sizeOut: sizeOut,
speedIn: speedIn,
@@ -49,12 +56,7 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
/// Cached non-virtual network device prefix
final realIfaces = <String>[];
({
String sizeIn,
String sizeOut,
String speedIn,
String speedOut,
}) cachedRealVals =
CachedNetVals cachedVals =
(sizeIn: '0kb', sizeOut: '0kb', speedIn: '0kb/s', speedOut: '0kb/s');
/// Time diff between [pre] and [now]
@@ -67,7 +69,8 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
BigInt sizeOutBytes(int i) => now[i].bytesOut;
String speedIn({String? device}) {
if (pre[0].device == '' || now[0].device == '') return '0kb/s';
if (pre.isEmpty || now.isEmpty) return 'N/A';
if (pre.length != now.length) return 'N/A';
if (device == null) {
var speed = 0.0;
for (final device in devices) {
@@ -84,7 +87,8 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
}
String sizeIn({String? device}) {
if (pre[0].device == '' || now[0].device == '') return '0kb';
if (pre.isEmpty || now.isEmpty) return 'N/A';
if (pre.length != now.length) return 'N/A';
if (device == null) {
var size = BigInt.from(0);
for (final device in devices) {
@@ -101,7 +105,8 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
}
String speedOut({String? device}) {
if (pre[0].device == '' || now[0].device == '') return '0kb/s';
if (pre.isEmpty || now.isEmpty) return 'N/A';
if (pre.length != now.length) return 'N/A';
if (device == null) {
var speed = 0.0;
for (final device in devices) {
@@ -118,7 +123,8 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
}
String sizeOut({String? device}) {
if (pre[0].device == '' || now[0].device == '') return '0kb';
if (pre.isEmpty || now.isEmpty) return 'N/A';
if (pre.length != now.length) return 'N/A';
if (device == null) {
var size = BigInt.from(0);
for (final device in devices) {

View File

@@ -1,11 +1,14 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
part 'private_key_info.g.dart';
@JsonSerializable()
@HiveType(typeId: 1)
class PrivateKeyInfo {
@HiveField(0)
final String id;
@JsonKey(name: 'private_key')
@HiveField(1)
final String key;
@@ -14,6 +17,11 @@ class PrivateKeyInfo {
required this.key,
});
factory PrivateKeyInfo.fromJson(Map<String, dynamic> json) =>
_$PrivateKeyInfoFromJson(json);
Map<String, dynamic> toJson() => _$PrivateKeyInfoToJson(this);
String? get type {
final lines = key.split('\n');
if (lines.length < 2) {
@@ -26,15 +34,4 @@ class PrivateKeyInfo {
}
return splited[1];
}
PrivateKeyInfo.fromJson(Map<String, dynamic> json)
: id = json["id"].toString(),
key = json["private_key"].toString();
Map<String, dynamic> toJson() {
final data = <String, String>{};
data["id"] = id;
data["private_key"] = key;
return data;
}
}

View File

@@ -42,3 +42,19 @@ class PrivateKeyInfoAdapter extends TypeAdapter<PrivateKeyInfo> {
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PrivateKeyInfo _$PrivateKeyInfoFromJson(Map<String, dynamic> json) =>
PrivateKeyInfo(
id: json['id'] as String,
key: json['private_key'] as String,
);
Map<String, dynamic> _$PrivateKeyInfoToJson(PrivateKeyInfo instance) =>
<String, dynamic>{
'id': instance.id,
'private_key': instance.key,
};

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import '../../../data/res/misc.dart';
import 'package:server_box/data/res/misc.dart';
class _ProcValIdxMap {
final int pid;
@@ -58,7 +58,7 @@ class Proc {
required this.command,
});
factory Proc.parse(String raw, _ProcValIdxMap map) {
factory Proc._parse(String raw, _ProcValIdxMap map) {
final parts = raw.split(RegExp(r'\s+'));
return Proc(
user: map.user == null ? null : parts[map.user!],
@@ -139,7 +139,7 @@ class PsResult {
final line = lines[i];
if (line.isEmpty) continue;
try {
procs.add(Proc.parse(line, map));
procs.add(Proc._parse(line, map));
} catch (e, trace) {
errs.add('$line: $e');
Loggers.app.warning('Process failed', e, trace);

View File

@@ -15,7 +15,7 @@ import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/temp.dart';
class Server implements TagPickable {
ServerPrivateInfo spi;
Spi spi;
ServerStatus status;
SSHClient? client;
ServerConn conn;
@@ -90,5 +90,5 @@ enum ServerConn {
/// Status parsing finished
finished;
operator <(ServerConn other) => index < other.index;
bool operator <(ServerConn other) => index < other.index;
}

View File

@@ -1,16 +1,26 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/provider/server.dart';
import '../app/error.dart';
import 'package:server_box/data/model/app/error.dart';
part 'server_private_info.g.dart';
/// In former version, it's called `ServerPrivateInfo`.
/// In the first version, it's called `ServerPrivateInfo` which was designed to
/// store the private information of a server.
///
/// Some params named as `spi` in the codebase which is the abbreviation of `ServerPrivateInfo`.
///
/// Nowaday, more fields are added to this class, and it's renamed to `Spi`.
@JsonSerializable()
@HiveType(typeId: 3)
class ServerPrivateInfo {
class Spi {
@HiveField(0)
final String name;
@HiveField(1)
@@ -23,14 +33,15 @@ class ServerPrivateInfo {
final String? pwd;
/// [id] of private key
@JsonKey(name: 'pubKeyId')
@HiveField(5)
final String? keyId;
@HiveField(6)
final List<String>? tags;
@HiveField(7)
final String? alterUrl;
@HiveField(8)
final bool? autoConnect;
@HiveField(8, defaultValue: true)
final bool autoConnect;
/// [id] of the jump server
@HiveField(9)
@@ -48,7 +59,7 @@ class ServerPrivateInfo {
final String id;
const ServerPrivateInfo({
const Spi({
required this.name,
required this.ip,
required this.port,
@@ -57,97 +68,28 @@ class ServerPrivateInfo {
this.keyId,
this.tags,
this.alterUrl,
this.autoConnect,
this.autoConnect = true,
this.jumpId,
this.custom,
this.wolCfg,
this.envs,
}) : id = '$user@$ip:$port';
static ServerPrivateInfo fromJson(Map<String, dynamic> json) {
final ip = json["ip"] as String? ?? '';
final port = json["port"] as int? ?? 22;
final user = json["user"] as String? ?? 'root';
final name = json["name"] as String? ?? '';
final pwd = json["pwd"] as String? ?? json["authorization"] as String?;
final keyId = json["pubKeyId"] as String?;
final tags = (json["tags"] as List?)?.cast<String>();
final alterUrl = json["alterUrl"] as String?;
final autoConnect = json["autoConnect"] as bool?;
final jumpId = json["jumpId"] as String?;
final custom = json["customCmd"] == null
? null
: ServerCustom.fromJson(json["custom"].cast<String, dynamic>());
final wolCfg = json["wolCfg"] == null
? null
: WakeOnLanCfg.fromJson(json["wolCfg"].cast<String, dynamic>());
final envs_ = json["envs"] as Map<String, dynamic>?;
final envs = <String, String>{};
if (envs_ != null) {
envs_.forEach((key, value) {
if (value is String) {
envs[key] = value;
}
});
}
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
return ServerPrivateInfo(
name: name,
ip: ip,
port: port,
user: user,
pwd: pwd,
keyId: keyId,
tags: tags,
alterUrl: alterUrl,
autoConnect: autoConnect,
jumpId: jumpId,
custom: custom,
wolCfg: wolCfg,
envs: envs.isEmpty ? null : envs,
);
}
Map<String, dynamic> toJson() => _$SpiToJson(this);
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["name"] = name;
data["ip"] = ip;
data["port"] = port;
data["user"] = user;
if (pwd != null) {
data["pwd"] = pwd;
}
if (keyId != null) {
data["pubKeyId"] = keyId;
}
if (tags != null) {
data["tags"] = tags;
}
if (alterUrl != null) {
data["alterUrl"] = alterUrl;
}
if (autoConnect != null) {
data["autoConnect"] = autoConnect;
}
if (jumpId != null) {
data["jumpId"] = jumpId;
}
if (custom != null) {
data["custom"] = custom?.toJson();
}
if (wolCfg != null) {
data["wolCfg"] = wolCfg?.toJson();
}
if (envs != null) {
data["envs"] = envs;
}
return data;
}
@override
String toString() => id;
}
Server? get server => Pros.server.pick(spi: this);
Server? get jumpServer => Pros.server.pick(id: jumpId);
extension Spix on Spi {
String toJsonString() => json.encode(toJson());
bool shouldReconnect(ServerPrivateInfo old) {
VNode<Server>? get server => ServerProvider.pick(spi: this);
VNode<Server>? get jumpServer => ServerProvider.pick(id: jumpId);
bool shouldReconnect(Spi old) {
return id != old.id ||
pwd != old.pwd ||
keyId != old.keyId ||
@@ -156,7 +98,7 @@ class ServerPrivateInfo {
custom?.cmds != old.custom?.cmds;
}
_IpPort fromStringUrl() {
(String, int) fromStringUrl() {
if (alterUrl == null) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
}
@@ -173,15 +115,10 @@ class ServerPrivateInfo {
if (port <= 0 || port > 65535) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
}
return _IpPort(ip_, port_);
return (ip_, port_);
}
@override
String toString() {
return id;
}
static const example = ServerPrivateInfo(
static const example = Spi(
name: 'name',
ip: 'ip',
port: 22,
@@ -202,11 +139,6 @@ class ServerPrivateInfo {
logoUrl: 'https://example.com/logo.png',
),
);
}
class _IpPort {
final String ip;
final int port;
_IpPort(this.ip, this.port);
bool get isRoot => user == 'root';
}

View File

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

View File

@@ -5,12 +5,12 @@ import 'package:server_box/data/model/server/sensors.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/system.dart';
import '../app/shell_func.dart';
import 'cpu.dart';
import 'disk.dart';
import 'memory.dart';
import 'net_speed.dart';
import 'conn.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/cpu.dart';
import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/net_speed.dart';
import 'package:server_box/data/model/server/conn.dart';
class ServerStatusUpdateReq {
final ServerStatus ss;

View File

@@ -2,13 +2,15 @@ import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:xterm/core.dart';
import '../app/tag_pickable.dart';
import 'package:server_box/data/model/app/tag_pickable.dart';
part 'snippet.g.dart';
@JsonSerializable()
@HiveType(typeId: 2)
class Snippet implements TagPickable {
@HiveField(0)
@@ -32,22 +34,10 @@ class Snippet implements TagPickable {
this.autoRunOn,
});
Snippet.fromJson(Map<String, dynamic> json)
: name = json['name'].toString(),
script = json['script'].toString(),
tags = json['tags']?.cast<String>(),
note = json['note']?.toString(),
autoRunOn = json['autoRunOn']?.cast<String>();
factory Snippet.fromJson(Map<String, dynamic> json) =>
_$SnippetFromJson(json);
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['name'] = name;
data['script'] = script;
data['tags'] = tags;
data['note'] = note;
data['autoRunOn'] = autoRunOn;
return data;
}
Map<String, dynamic> toJson() => _$SnippetToJson(this);
@override
bool containsTag(String tag) {
@@ -59,7 +49,7 @@ class Snippet implements TagPickable {
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
String fmtWithSpi(ServerPrivateInfo spi) {
String fmtWithSpi(Spi spi) {
return script.replaceAllMapped(
fmtFinder,
(match) {
@@ -74,7 +64,7 @@ class Snippet implements TagPickable {
Future<void> runInTerm(
Terminal terminal,
ServerPrivateInfo spi, {
Spi spi, {
bool autoEnter = false,
}) async {
final argsFmted = fmtWithSpi(spi);
@@ -170,12 +160,12 @@ class Snippet implements TagPickable {
}
static final fmtArgs = {
r'${host}': (ServerPrivateInfo spi) => spi.ip,
r'${port}': (ServerPrivateInfo spi) => spi.port.toString(),
r'${user}': (ServerPrivateInfo spi) => spi.user,
r'${pwd}': (ServerPrivateInfo spi) => spi.pwd ?? '',
r'${id}': (ServerPrivateInfo spi) => spi.id,
r'${name}': (ServerPrivateInfo spi) => spi.name,
r'${host}': (Spi spi) => spi.ip,
r'${port}': (Spi spi) => spi.port.toString(),
r'${user}': (Spi spi) => spi.user,
r'${pwd}': (Spi spi) => spi.pwd ?? '',
r'${id}': (Spi spi) => spi.id,
r'${name}': (Spi spi) => spi.name,
};
/// r'${ctrl+ad}' -> TerminalKey.control, a, d

View File

@@ -51,3 +51,25 @@ class SnippetAdapter extends TypeAdapter<Snippet> {
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Snippet _$SnippetFromJson(Map<String, dynamic> json) => Snippet(
name: json['name'] as String,
script: json['script'] as String,
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
note: json['note'] as String?,
autoRunOn: (json['autoRunOn'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$SnippetToJson(Snippet instance) => <String, dynamic>{
'name': instance.name,
'script': instance.script,
'tags': instance.tags,
'note': instance.note,
'autoRunOn': instance.autoRunOn,
};

View File

@@ -0,0 +1,118 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
enum SystemdUnitFunc {
start,
stop,
restart,
reload,
enable,
disable,
status,
;
IconData get icon => switch (this) {
start => Icons.play_arrow,
stop => Icons.stop,
restart => Icons.refresh,
reload => Icons.refresh,
enable => Icons.check,
disable => Icons.close,
status => Icons.info,
};
}
enum SystemdUnitType {
service,
socket,
mount,
timer,
;
static SystemdUnitType? fromString(String? value) {
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
}
}
enum SystemdUnitScope {
system,
user,
;
Color? get color => switch (this) {
system => Colors.red,
_ => null,
};
String getCmdPrefix(bool isRoot) {
if (this == system) {
return isRoot ? 'systemctl' : 'sudo systemctl';
}
return 'systemctl --user';
}
}
enum SystemdUnitState {
active,
inactive,
failed,
activating,
deactivating,
;
static SystemdUnitState? fromString(String? value) {
return values.firstWhereOrNull((e) => e.name == value?.toLowerCase());
}
Color? get color => switch (this) {
failed => Colors.red,
_ => null,
};
}
final class SystemdUnit {
final String name;
final String? description;
final SystemdUnitType type;
final SystemdUnitScope scope;
final SystemdUnitState state;
SystemdUnit({
required this.name,
this.description,
required this.type,
required this.scope,
required this.state,
});
String getCmd({
required SystemdUnitFunc func,
required bool isRoot,
}) {
final prefix = scope.getCmdPrefix(isRoot);
return '$prefix ${func.name} $name';
}
List<SystemdUnitFunc> get availableFuncs {
final funcs = <SystemdUnitFunc>{};
switch (state) {
case SystemdUnitState.active:
funcs.addAll([SystemdUnitFunc.stop, SystemdUnitFunc.restart]);
break;
case SystemdUnitState.inactive:
funcs.addAll([SystemdUnitFunc.start]);
break;
case SystemdUnitState.failed:
funcs.addAll([SystemdUnitFunc.restart]);
break;
case SystemdUnitState.activating:
funcs.addAll([SystemdUnitFunc.stop]);
break;
case SystemdUnitState.deactivating:
funcs.addAll([SystemdUnitFunc.start]);
break;
}
funcs.addAll([SystemdUnitFunc.status]);
return funcs.toList();
}
}

View File

@@ -1,10 +1,12 @@
import 'dart:io';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:wake_on_lan/wake_on_lan.dart';
part 'wol_cfg.g.dart';
@JsonSerializable()
@HiveType(typeId: 8)
final class WakeOnLanCfg {
@HiveField(0)
@@ -54,22 +56,8 @@ final class WakeOnLanCfg {
);
}
static WakeOnLanCfg fromJson(Map<String, dynamic> json) {
return WakeOnLanCfg(
mac: json['mac'] as String,
ip: json['ip'] as String,
pwd: json['pwd'] as String?,
);
}
factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) =>
_$WakeOnLanCfgFromJson(json);
Map<String, dynamic> toJson() {
final map = <String, dynamic>{
'mac': mac,
'ip': ip,
};
if (pwd != null) {
map['pwd'] = pwd;
}
return map;
}
Map<String, dynamic> toJson() => _$WakeOnLanCfgToJson(this);
}

View File

@@ -45,3 +45,20 @@ class WakeOnLanCfgAdapter extends TypeAdapter<WakeOnLanCfg> {
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
WakeOnLanCfg _$WakeOnLanCfgFromJson(Map<String, dynamic> json) => WakeOnLanCfg(
mac: json['mac'] as String,
ip: json['ip'] as String,
pwd: json['pwd'] as String?,
);
Map<String, dynamic> _$WakeOnLanCfgToJson(WakeOnLanCfg instance) =>
<String, dynamic>{
'mac': instance.mac,
'ip': instance.ip,
'pwd': instance.pwd,
};

View File

@@ -2,12 +2,14 @@ import 'package:fl_lib/fl_lib.dart';
class AbsolutePath {
String _path;
final _prePath = <String>[];
AbsolutePath(this._path);
String get path => _path;
final List<String> _prePath;
AbsolutePath(this._path) : _prePath = ['/'];
void update(String newPath) {
/// Update path, not set path
set path(String newPath) {
_prePath.add(_path);
if (newPath == '..') {
_path = _path.substring(0, _path.lastIndexOf('/'));
@@ -16,10 +18,6 @@ class AbsolutePath {
}
return;
}
if (newPath == '/') {
_path = '/';
return;
}
if (newPath.startsWith('/')) {
_path = newPath;
return;

View File

@@ -2,9 +2,11 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:server_box/data/model/sftp/absolute_path.dart';
class SftpBrowserStatus {
List<SftpName>? files;
AbsolutePath? path;
final List<SftpName> files = [];
final AbsolutePath path = AbsolutePath('/');
SftpClient? client;
SftpBrowserStatus();
SftpBrowserStatus(SSHClient client) {
client.sftp().then((value) => this.client = value);
}
}

View File

@@ -1,12 +1,12 @@
part of 'worker.dart';
class SftpReq {
final ServerPrivateInfo spi;
final Spi spi;
final String remotePath;
final String localPath;
final SftpReqType type;
String? privateKey;
ServerPrivateInfo? jumpSpi;
Spi? jumpSpi;
String? jumpPrivateKey;
SftpReq(

View File

@@ -1,30 +1,7 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
class AppProvider extends ChangeNotifier {
BuildContext? ctx;
final class AppProvider {
const AppProvider._();
bool isWearOS = false;
Future<void> init() async {
await _initIsWearOS();
}
Future<void> _initIsWearOS() async {
if (!isAndroid) {
isWearOS = false;
return;
}
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
const feat = 'android.hardware.type.watch';
final hasFeat = androidInfo.systemFeatures.contains(feat);
if (hasFeat) {
isWearOS = true;
return;
}
}
static BuildContext? ctx;
}

View File

@@ -70,7 +70,7 @@ class ContainerProvider extends ChangeNotifier {
}
final res = await client?.run(_wrap(ContainerCmdType.images.exec(type)));
if (res?.string.toLowerCase().contains("permission denied") ?? false) {
if (res?.string.toLowerCase().contains('permission denied') ?? false) {
return sudoCompleter.complete(true);
}
return sudoCompleter.complete(false);

View File

@@ -1,35 +1,41 @@
import 'package:flutter/material.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/res/store.dart';
class PrivateKeyProvider extends ChangeNotifier {
List<PrivateKeyInfo> get pkis => _pkis;
late List<PrivateKeyInfo> _pkis;
class PrivateKeyProvider extends Provider {
const PrivateKeyProvider._();
static const instance = PrivateKeyProvider._();
static final pkis = <PrivateKeyInfo>[].vn;
@override
void load() {
_pkis = Stores.key.fetch();
super.load();
pkis.value = Stores.key.fetch();
}
void add(PrivateKeyInfo info) {
_pkis.add(info);
static void add(PrivateKeyInfo info) {
pkis.value.add(info);
pkis.notify();
Stores.key.put(info);
notifyListeners();
}
void delete(PrivateKeyInfo info) {
_pkis.removeWhere((e) => e.id == info.id);
static void delete(PrivateKeyInfo info) {
pkis.value.removeWhere((e) => e.id == info.id);
pkis.notify();
Stores.key.delete(info);
notifyListeners();
}
void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final idx = _pkis.indexWhere((e) => e.id == old.id);
static void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final idx = pkis.value.indexWhere((e) => e.id == old.id);
if (idx == -1) {
_pkis.add(newInfo);
pkis.value.add(newInfo);
Stores.key.put(newInfo);
Stores.key.delete(old);
} else {
_pkis[idx] = newInfo;
pkis.value[idx] = newInfo;
Stores.key.put(newInfo);
}
Stores.key.put(newInfo);
notifyListeners();
pkis.notify();
}
}

View File

@@ -15,7 +15,7 @@ import 'package:dartssh2/dartssh2.dart';
typedef PveCtrlFunc = Future<bool> Function(String node, String id);
final class PveProvider extends ChangeNotifier {
final ServerPrivateInfo spi;
final Spi spi;
late String addr;
late final SSHClient _client;
late final ServerSocket _serverSocket;
@@ -23,7 +23,7 @@ final class PveProvider extends ChangeNotifier {
int _localPort = 0;
PveProvider({required this.spi}) {
final client = spi.server?.client;
final client = spi.server?.value.client;
if (client == null) {
throw Exception('Server client is null');
}
@@ -104,7 +104,7 @@ final class PveProvider extends ChangeNotifier {
socket.cast<List<int>>().pipe(forward.sink);
});*/
if (url.isScheme("https")) {
if (url.isScheme('https')) {
return SecureSocket.startConnect('localhost', _localPort,
onBadCertificate: (_) => true);
} else {

View File

@@ -4,41 +4,41 @@ import 'dart:async';
import 'package:computer/computer.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/system.dart';
// import 'package:server_box/data/model/sftp/req.dart';
// import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart';
import '../../core/utils/server.dart';
import '../model/server/server.dart';
import '../model/server/server_private_info.dart';
import '../model/server/server_status_update_req.dart';
import '../model/server/try_limiter.dart';
import '../res/status.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_status_update_req.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/res/status.dart';
class ServerProvider extends ChangeNotifier {
final Map<String, Server> _servers = {};
Iterable<Server> get servers => _servers.values;
final List<String> _serverOrder = [];
List<String> get serverOrder => _serverOrder;
final _tags = ValueNotifier(<String>{});
ValueNotifier<Set<String>> get tags => _tags;
class ServerProvider extends Provider {
const ServerProvider._();
static const instance = ServerProvider._();
Timer? _timer;
static final Map<String, VNode<Server>> servers = {};
static final serverOrder = <String>[].vn;
static final _tags = <String>{}.vn;
static VNode<Set<String>> get tags => _tags;
final _manualDisconnectedIds = <String>{};
static Timer? _timer;
static final _manualDisconnectedIds = <String>{};
@override
Future<void> load() async {
// Issue #147
super.load();
// #147
// Clear all servers because of restarting app will cause duplicate servers
final oldServers = Map<String, Server>.from(_servers);
_servers.clear();
_serverOrder.clear();
final oldServers = Map<String, VNode<Server>>.from(servers);
servers.clear();
serverOrder.value.clear();
final spis = Stores.server.fetch();
for (int idx = 0; idx < spis.length; idx++) {
@@ -46,12 +46,13 @@ class ServerProvider extends ChangeNotifier {
final originServer = oldServers[spi.id];
final newServer = genServer(spi);
/// Issues #258
/// #258
/// If not [shouldReconnect], then keep the old state.
if (originServer != null && !originServer.spi.shouldReconnect(spi)) {
newServer.conn = originServer.conn;
if (originServer != null &&
!originServer.value.spi.shouldReconnect(spi)) {
newServer.conn = originServer.value.conn;
}
_servers[spi.id] = newServer;
servers[spi.id] = newServer.vn;
}
final serverOrder_ = Stores.setting.serverOrder.fetch();
if (serverOrder_.isNotEmpty) {
@@ -59,35 +60,37 @@ class ServerProvider extends ChangeNotifier {
order: serverOrder_,
finder: (n, id) => n.id == id,
);
_serverOrder.addAll(spis.map((e) => e.id));
serverOrder.value.addAll(spis.map((e) => e.id));
} else {
_serverOrder.addAll(_servers.keys);
serverOrder.value.addAll(servers.keys);
}
// Must use [equals] to compare [Order] here.
if (!_serverOrder.equals(serverOrder_)) {
Stores.setting.serverOrder.put(_serverOrder);
if (!serverOrder.value.equals(serverOrder_)) {
Stores.setting.serverOrder.put(serverOrder.value);
}
_updateTags();
notifyListeners();
// Must notify here, or the UI will not be updated.
serverOrder.notify();
}
/// Get a [Server] by [spi] or [id].
///
/// Priority: [spi] > [id]
Server? pick({ServerPrivateInfo? spi, String? id}) {
static VNode<Server>? pick({Spi? spi, String? id}) {
if (spi != null) {
return _servers[spi.id];
return servers[spi.id];
}
if (id != null) {
return _servers[id];
return servers[id];
}
return null;
}
void _updateTags() {
for (final s in _servers.values) {
if (s.spi.tags == null) continue;
for (final t in s.spi.tags!) {
static void _updateTags() {
for (final s in servers.values) {
final tags = s.value.spi.tags;
if (tags == null) continue;
for (final t in tags) {
if (!_tags.value.contains(t)) {
_tags.value.add(t);
}
@@ -96,14 +99,14 @@ class ServerProvider extends ChangeNotifier {
_tags.value = (_tags.value.toList()..sort()).toSet();
}
Server genServer(ServerPrivateInfo spi) {
static Server genServer(Spi spi) {
return Server(spi, InitStatus.status, ServerConn.disconnected);
}
/// if [spi] is specificed then only refresh this server
/// [onlyFailed] only refresh failed servers
Future<void> refresh({
ServerPrivateInfo? spi,
static Future<void> refresh({
Spi? spi,
bool onlyFailed = false,
}) async {
if (spi != null) {
@@ -112,23 +115,24 @@ class ServerProvider extends ChangeNotifier {
return;
}
await Future.wait(_servers.values.map((s) => _connectFn(s, onlyFailed)));
await Future.wait(servers.values.map((val) async {
final s = val.value;
if (onlyFailed) {
if (s.conn != ServerConn.failed) return;
TryLimiter.reset(s.spi.id);
}
if (_manualDisconnectedIds.contains(s.spi.id)) return;
if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) {
return;
}
return await _getData(s.spi);
}));
}
Future<void> _connectFn(Server s, bool onlyFailed) async {
if (onlyFailed) {
if (s.conn != ServerConn.failed) return;
TryLimiter.reset(s.spi.id);
}
if (!(s.spi.autoConnect ?? true) && s.conn == ServerConn.disconnected ||
_manualDisconnectedIds.contains(s.spi.id)) {
return;
}
return await _getData(s.spi);
}
Future<void> startAutoRefresh() async {
static Future<void> startAutoRefresh() async {
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
stopAutoRefresh();
if (duration == 0) return;
@@ -141,84 +145,85 @@ class ServerProvider extends ChangeNotifier {
});
}
void stopAutoRefresh() {
static void stopAutoRefresh() {
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
}
bool get isAutoRefreshOn => _timer != null;
static bool get isAutoRefreshOn => _timer != null;
void setDisconnected() {
for (final s in _servers.values) {
s.conn = ServerConn.disconnected;
static void setDisconnected() {
for (final s in servers.values) {
s.value.conn = ServerConn.disconnected;
s.notify();
}
//TryLimiter.clear();
notifyListeners();
}
void closeServer({String? id}) {
static void closeServer({String? id}) {
if (id == null) {
for (final s in _servers.values) {
_closeOneServer(s.spi.id);
for (final s in servers.values) {
_closeOneServer(s.value.spi.id);
}
return;
}
_closeOneServer(id);
}
void _closeOneServer(String id) {
final item = _servers[id];
static void _closeOneServer(String id) {
final s = servers[id];
final item = s?.value;
item?.client?.close();
item?.client = null;
item?.conn = ServerConn.disconnected;
_manualDisconnectedIds.add(id);
notifyListeners();
s?.notify();
}
void addServer(ServerPrivateInfo spi) {
_servers[spi.id] = genServer(spi);
notifyListeners();
static void addServer(Spi spi) {
servers[spi.id] = genServer(spi).vn;
Stores.server.put(spi);
_serverOrder.add(spi.id);
Stores.setting.serverOrder.put(_serverOrder);
serverOrder.value.add(spi.id);
serverOrder.notify();
Stores.setting.serverOrder.put(serverOrder.value);
_updateTags();
refresh(spi: spi);
}
void delServer(String id) {
_servers.remove(id);
_serverOrder.remove(id);
Stores.setting.serverOrder.put(_serverOrder);
_updateTags();
notifyListeners();
static void delServer(String id) {
servers.remove(id);
serverOrder.value.remove(id);
serverOrder.notify();
Stores.setting.serverOrder.put(serverOrder.value);
Stores.server.delete(id);
}
void deleteAll() {
_servers.clear();
_serverOrder.clear();
Stores.setting.serverOrder.put(_serverOrder);
_updateTags();
notifyListeners();
Stores.server.deleteAll();
}
Future<void> updateServer(
ServerPrivateInfo old,
ServerPrivateInfo newSpi,
static void deleteAll() {
servers.clear();
serverOrder.value.clear();
serverOrder.notify();
Stores.setting.serverOrder.put(serverOrder.value);
Stores.server.deleteAll();
_updateTags();
}
static Future<void> updateServer(
Spi old,
Spi newSpi,
) async {
if (old != newSpi) {
Stores.server.update(old, newSpi);
_servers[old.id]?.spi = newSpi;
servers[old.id]?.value.spi = newSpi;
if (newSpi.id != old.id) {
_servers[newSpi.id] = _servers[old.id]!;
_servers[newSpi.id]?.spi = newSpi;
_servers.remove(old.id);
_serverOrder.update(old.id, newSpi.id);
Stores.setting.serverOrder.put(_serverOrder);
servers[newSpi.id] = servers[old.id]!;
servers.remove(old.id);
serverOrder.value.update(old.id, newSpi.id);
Stores.setting.serverOrder.put(serverOrder.value);
serverOrder.notify();
}
// Only reconnect if neccessary
@@ -227,33 +232,32 @@ class ServerProvider extends ChangeNotifier {
TryLimiter.reset(newSpi.id);
refresh(spi: newSpi);
}
// Only update if [spi.tags] changed
_updateTags();
}
_updateTags();
}
void _setServerState(Server s, ServerConn ss) {
s.conn = ss;
notifyListeners();
static void _setServerState(VNode<Server> s, ServerConn ss) {
s.value.conn = ss;
s.notify();
}
Future<void> _getData(ServerPrivateInfo spi) async {
static Future<void> _getData(Spi spi) async {
final sid = spi.id;
final s = _servers[sid];
final s = servers[sid];
if (s == null) return;
final sv = s.value;
if (!TryLimiter.canTry(sid)) {
if (s.conn != ServerConn.failed) {
if (sv.conn != ServerConn.failed) {
_setServerState(s, ServerConn.failed);
}
return;
}
s.status.err = null;
sv.status.err = null;
if (s.needGenClient || (s.client?.isClosed ?? true)) {
if (sv.needGenClient || (sv.client?.isClosed ?? true)) {
_setServerState(s, ServerConn.connecting);
final wol = spi.wolCfg;
@@ -274,7 +278,7 @@ class ServerProvider extends ChangeNotifier {
try {
final time1 = DateTime.now();
s.client = await genClient(
sv.client = await genClient(
spi,
timeout: Duration(seconds: Stores.setting.timeout.fetch()),
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),
@@ -288,7 +292,7 @@ class ServerProvider extends ChangeNotifier {
}
} catch (e) {
TryLimiter.inc(sid);
s.status.err = SSHErr(type: SSHErrType.connect, message: e.toString());
sv.status.err = SSHErr(type: SSHErrType.connect, message: e.toString());
_setServerState(s, ServerConn.failed);
/// In order to keep privacy, print [spi.name] instead of [spi.id]
@@ -301,28 +305,28 @@ class ServerProvider extends ChangeNotifier {
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
try {
final writeScriptResult = await s.client!.runForOutput(
ShellFunc.getInstallShellCmd(spi.id),
action: (session) async {
final (_, writeScriptResult) = await sv.client!.exec(
(session) async {
session.stdin.add(scriptRaw);
session.stdin.close();
},
entry: ShellFunc.getInstallShellCmd(spi.id),
);
if (writeScriptResult.isNotEmpty) {
ShellFunc.switchScriptDir(spi.id);
throw String.fromCharCodes(writeScriptResult);
throw writeScriptResult;
}
} on SSHAuthAbortError catch (e) {
TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
s.status.err = err;
sv.status.err = err;
Loggers.app.warning(err);
_setServerState(s, ServerConn.failed);
return;
} on SSHAuthFailError catch (e) {
TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
s.status.err = err;
sv.status.err = err;
Loggers.app.warning(err);
_setServerState(s, ServerConn.failed);
return;
@@ -330,18 +334,18 @@ class ServerProvider extends ChangeNotifier {
// If max try times < 2 and can't write script, this will stop the status getting and etc.
// TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.writeScript, message: e.toString());
s.status.err = err;
sv.status.err = err;
Loggers.app.warning(err);
_setServerState(s, ServerConn.failed);
}
}
if (s.conn == ServerConn.connecting) return;
if (sv.conn == ServerConn.connecting) return;
/// Keep [finished] state, or the UI will be refreshed to [loading] state
/// instead of the '$Temp | $Uptime'.
/// eg: '32C | 7 days'
if (s.conn != ServerConn.finished) {
if (sv.conn != ServerConn.finished) {
_setServerState(s, ServerConn.loading);
}
@@ -349,17 +353,17 @@ class ServerProvider extends ChangeNotifier {
String? raw;
try {
raw = await s.client?.run(ShellFunc.status.exec(spi.id)).string;
raw = await sv.client?.run(ShellFunc.status.exec(spi.id)).string;
segments = raw?.split(ShellFunc.seperator).map((e) => e.trim()).toList();
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
if (Stores.setting.keepStatusWhenErr.fetch()) {
// Keep previous server status when err occurs
if (s.conn != ServerConn.failed && s.status.more.isNotEmpty) {
if (sv.conn != ServerConn.failed && sv.status.more.isNotEmpty) {
return;
}
}
TryLimiter.inc(sid);
s.status.err = SSHErr(
sv.status.err = SSHErr(
type: SSHErrType.segements,
message: 'Seperate segments failed, raw:\n$raw',
);
@@ -368,7 +372,7 @@ class ServerProvider extends ChangeNotifier {
}
} catch (e) {
TryLimiter.inc(sid);
s.status.err = SSHErr(type: SSHErrType.getStatus, message: e.toString());
sv.status.err = SSHErr(type: SSHErrType.getStatus, message: e.toString());
_setServerState(s, ServerConn.failed);
Loggers.app.warning('Get status from ${spi.name} failed', e);
return;
@@ -379,36 +383,36 @@ class ServerProvider extends ChangeNotifier {
if (!systemType.isSegmentsLenMatch(segments.length - customCmdLen)) {
TryLimiter.inc(sid);
if (raw.contains('Could not chdir to home directory /var/services/')) {
s.status.err = SSHErr(type: SSHErrType.chdir, message: raw);
sv.status.err = SSHErr(type: SSHErrType.chdir, message: raw);
_setServerState(s, ServerConn.failed);
return;
}
final expected = systemType.segmentsLen;
final actual = segments.length;
s.status.err = SSHErr(
sv.status.err = SSHErr(
type: SSHErrType.segements,
message: 'Segments: expect $expected, got $actual, raw:\n\n$raw',
);
_setServerState(s, ServerConn.failed);
return;
}
s.status.system = systemType;
sv.status.system = systemType;
try {
final req = ServerStatusUpdateReq(
ss: s.status,
ss: sv.status,
segments: segments,
system: systemType,
customCmds: spi.custom?.cmds ?? {},
);
s.status = await Computer.shared.start(
sv.status = await Computer.shared.start(
getStatus,
req,
taskName: 'StatusUpdateReq<${s.id}>',
taskName: 'StatusUpdateReq<${sv.id}>',
);
} catch (e, trace) {
TryLimiter.inc(sid);
s.status.err = SSHErr(
sv.status.err = SSHErr(
type: SSHErrType.getStatus,
message: 'Parse failed: $e\n\n$raw',
);

View File

@@ -1,38 +1,41 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/sftp/worker.dart';
class SftpProvider extends ChangeNotifier {
final List<SftpReqStatus> _status = [];
List<SftpReqStatus> get status => _status;
class SftpProvider extends Provider {
const SftpProvider._();
static const instance = SftpProvider._();
SftpReqStatus? get(int id) {
return _status.singleWhere((element) => element.id == id);
static final status = <SftpReqStatus>[].vn;
static SftpReqStatus? get(int id) {
return status.value.singleWhere((element) => element.id == id);
}
int add(SftpReq req, {Completer? completer}) {
final status = SftpReqStatus(
notifyListeners: notifyListeners,
static int add(SftpReq req, {Completer? completer}) {
final reqStat = SftpReqStatus(
notifyListeners: status.notify,
completer: completer,
req: req,
);
_status.add(status);
return status.id;
status.value.add(reqStat);
status.notify();
return reqStat.id;
}
@override
void dispose() {
for (final item in _status) {
static void dispose() {
for (final item in status.value) {
item.dispose();
}
super.dispose();
status.value.clear();
status.notify();
}
void cancel(int id) {
final idx = _status.indexWhere((element) => element.id == id);
_status[idx].dispose();
_status.removeAt(idx);
notifyListeners();
static void cancel(int id) {
final idx = status.value.indexWhere((e) => e.id == id);
status.value[idx].dispose();
status.value.removeAt(idx);
status.notify();
}
}

View File

@@ -1,21 +1,21 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/res/store.dart';
class SnippetProvider extends ChangeNotifier {
late List<Snippet> _snippets;
List<Snippet> get snippets => _snippets;
class SnippetProvider extends Provider {
const SnippetProvider._();
static const instance = SnippetProvider._();
final tags = ValueNotifier(<String>{});
static final snippets = <Snippet>[].vn;
static final tags = <String>{}.vn;
@override
void load() {
_snippets = Stores.snippet.fetch();
super.load();
final snippets_ = Stores.snippet.fetch();
final order = Stores.setting.snippetOrder.fetch();
if (order.isNotEmpty) {
final surplus = _snippets.reorder(
final surplus = snippets_.reorder(
order: order,
finder: (n, name) => n.name == name,
);
@@ -24,12 +24,13 @@ class SnippetProvider extends ChangeNotifier {
Stores.setting.snippetOrder.put(order);
}
}
snippets.value = snippets_;
_updateTags();
}
void _updateTags() {
static void _updateTags() {
final tags_ = <String>{};
for (final s in _snippets) {
for (final s in snippets.value) {
final t = s.tags;
if (t != null) {
tags_.addAll(t);
@@ -38,31 +39,31 @@ class SnippetProvider extends ChangeNotifier {
tags.value = tags_;
}
void add(Snippet snippet) {
_snippets.add(snippet);
static void add(Snippet snippet) {
snippets.value.add(snippet);
snippets.notify();
Stores.snippet.put(snippet);
_updateTags();
notifyListeners();
}
void del(Snippet snippet) {
_snippets.remove(snippet);
static void del(Snippet snippet) {
snippets.value.remove(snippet);
snippets.notify();
Stores.snippet.delete(snippet);
_updateTags();
notifyListeners();
}
void update(Snippet old, Snippet newOne) {
static void update(Snippet old, Snippet newOne) {
snippets.value.remove(old);
snippets.value.add(newOne);
snippets.notify();
Stores.snippet.delete(old);
Stores.snippet.put(newOne);
_snippets.remove(old);
_snippets.add(newOne);
_updateTags();
notifyListeners();
}
void renameTag(String old, String newOne) {
for (final s in _snippets) {
static void renameTag(String old, String newOne) {
for (final s in snippets.value) {
if (s.tags?.contains(old) ?? false) {
s.tags?.remove(old);
s.tags?.add(newOne);
@@ -70,8 +71,5 @@ class SnippetProvider extends ChangeNotifier {
}
}
_updateTags();
notifyListeners();
}
String get export => json.encode(snippets);
}

View File

@@ -0,0 +1,162 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/systemd.dart';
import 'package:server_box/data/provider/server.dart';
final class SystemdProvider {
late final VNode<Server> _si;
late final bool _isRoot;
SystemdProvider.init(Spi spi) {
_isRoot = spi.isRoot;
_si = ServerProvider.pick(spi: spi)!;
getUnits();
}
final isBusy = false.vn;
final units = <SystemdUnit>[].vn;
Future<void> getUnits() async {
isBusy.value = true;
try {
final client = _si.value.client;
final result = await client!.execForOutput(_getUnitsCmd);
final units = result.split('\n');
final userUnits = <String>[];
final systemUnits = <String>[];
for (final unit in units) {
if (unit.startsWith('/etc/systemd/system')) {
systemUnits.add(unit);
} else if (unit.startsWith('~/.config/systemd/user')) {
userUnits.add(unit);
} else if (unit.trim().isNotEmpty) {
Loggers.app.warning('Unknown unit: $unit');
}
}
final parsedUserUnits =
await _parseUnitObj(userUnits, SystemdUnitScope.user);
final parsedSystemUnits =
await _parseUnitObj(systemUnits, SystemdUnitScope.system);
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
} catch (e, s) {
Loggers.app.warning('Parse systemd', e, s);
}
isBusy.value = false;
}
Future<List<SystemdUnit>> _parseUnitObj(
List<String> unitNames,
SystemdUnitScope scope,
) async {
final unitNames_ = unitNames
.map((e) => e.trim().split('/').last.split('.').first)
.toList();
final script = '''
for unit in ${unitNames_.join(' ')}; do
state=\$(systemctl show --no-pager \$unit)
echo -n "${ShellFunc.seperator}\n\$state"
done
''';
final client = _si.value.client!;
final result = await client.execForOutput(script);
final units = result.split(ShellFunc.seperator);
final parsedUnits = <SystemdUnit>[];
for (final unit in units) {
final parts = unit.split('\n');
var name = '';
var type = '';
var state = '';
String? description;
for (final part in parts) {
if (part.startsWith('Id=')) {
final val = _getIniVal(part).split('.');
name = val.first;
type = val.last;
continue;
}
if (part.startsWith('ActiveState=')) {
state = _getIniVal(part);
continue;
}
if (part.startsWith('Description=')) {
description = _getIniVal(part);
continue;
}
}
final unitType = SystemdUnitType.fromString(type);
if (unitType == null) {
Loggers.app.warning('Unit type: $type');
continue;
}
final unitState = SystemdUnitState.fromString(state);
if (unitState == null) {
Loggers.app.warning('Unit state: $state');
continue;
}
parsedUnits.add(SystemdUnit(
name: name,
type: unitType,
scope: scope,
state: unitState,
description: description,
));
}
parsedUnits.sort((a, b) {
// user units first
if (a.scope != b.scope) {
return a.scope == SystemdUnitScope.user ? -1 : 1;
}
// active units first
if (a.state != b.state) {
return a.state == SystemdUnitState.active ? -1 : 1;
}
return a.name.compareTo(b.name);
});
return parsedUnits;
}
late final _getUnitsCmd = '''
get_files() {
unit_type=\$1
base_dir=\$2
# If base_dir is not a directory, return
if [ ! -d "\$base_dir" ]; then
return
fi
find "\$base_dir" -type f -name "*.\$unit_type" -print | sort
}
get_type_files() {
unit_type=\$1
base_dir=""
${_isRoot ? """
get_files \$unit_type /etc/systemd/system
get_files \$unit_type ~/.config/systemd/user""" : """
get_files \$unit_type ~/.config/systemd/user"""}
}
types="service socket mount timer"
for type in \$types; do
get_type_files \$type
done
''';
}
String _getIniVal(String line) {
return line.split('=').last;
}

View File

@@ -1,7 +1,8 @@
// This file is generated by fl_build. Do not edit.
// ignore_for_file: prefer_single_quotes
class BuildData {
static const String name = "ServerBox";
static const int build = 1051;
static const int script = 56;
static const int build = 1070;
static const int script = 58;
}

View File

@@ -97,6 +97,7 @@ abstract final class GithubIds {
'h-lyf',
'88484396',
'honggeigei',
'likecreep',
};
}

View File

@@ -1,19 +0,0 @@
import 'package:server_box/data/provider/app.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/provider/sftp.dart';
import 'package:server_box/data/provider/snippet.dart';
abstract final class Pros {
static final app = AppProvider();
static final key = PrivateKeyProvider();
static final server = ServerProvider();
static final sftp = SftpProvider();
static final snippet = SnippetProvider();
static void reload() {
key.load();
server.load();
snippet.load();
}
}

View File

@@ -1,12 +1,12 @@
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/temp.dart';
import '../model/server/cpu.dart';
import '../model/server/disk.dart';
import '../model/server/memory.dart';
import '../model/server/net_speed.dart';
import '../model/server/conn.dart';
import '../model/server/system.dart';
import 'package:server_box/data/model/server/cpu.dart';
import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/net_speed.dart';
import 'package:server_box/data/model/server/conn.dart';
import 'package:server_box/data/model/server/system.dart';
abstract final class InitStatus {
static SingleCpuCore get _initOneTimeCpuStatus => SingleCpuCore(

View File

@@ -23,6 +23,10 @@ abstract final class Stores {
snippet,
];
static Future<void> init() async {
await Future.wait(all.map((store) => store.init()));
}
static int? get lastModTime {
int? lastModTime = 0;
for (final store in all) {

View File

@@ -1,9 +1,8 @@
abstract final class Urls {
static const cdnBase = 'https://cdn.lolli.tech/serverbox';
static const cdnBase = 'https://cdn.lpkt.cn/serverbox';
static const updateCfg = '$cdnBase/update2.json';
static const myGithub = 'https://github.com/lollipopkit';
static const thisRepo = '$myGithub/flutter_server_box';
static const appHelp = '$thisRepo#-help';
static const appWiki = '$thisRepo/wiki';
static const analysis = 'https://countly.lolli.tech';
}

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import '../model/server/private_key_info.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
class PrivateKeyStore extends PersistentStore {
PrivateKeyStore() : super('key');

View File

@@ -1,21 +1,21 @@
import 'package:fl_lib/fl_lib.dart';
import '../model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
class ServerStore extends PersistentStore {
ServerStore() : super('server');
void put(ServerPrivateInfo info) {
void put(Spi info) {
box.put(info.id, info);
box.updateLastModified();
}
List<ServerPrivateInfo> fetch() {
List<Spi> fetch() {
final ids = box.keys;
final List<ServerPrivateInfo> ss = [];
final List<Spi> ss = [];
for (final id in ids) {
final s = box.get(id);
if (s != null && s is ServerPrivateInfo) {
if (s != null && s is Spi) {
ss.add(s);
}
}
@@ -32,7 +32,7 @@ class ServerStore extends PersistentStore {
box.updateLastModified();
}
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
void update(Spi old, Spi newInfo) {
if (!have(old)) {
throw Exception('Old spi: $old not found');
}
@@ -40,5 +40,5 @@ class ServerStore extends PersistentStore {
put(newInfo);
}
bool have(ServerPrivateInfo s) => box.get(s.id) != null;
bool have(Spi s) => box.get(s.id) != null;
}

View File

@@ -3,8 +3,8 @@ import 'package:server_box/data/model/app/menu/server_func.dart';
import 'package:server_box/data/model/app/server_detail_card.dart';
import 'package:server_box/data/model/ssh/virtual_key.dart';
import '../model/app/net_view.dart';
import '../res/default.dart';
import 'package:server_box/data/model/app/net_view.dart';
import 'package:server_box/data/res/default.dart';
class SettingStore extends PersistentStore {
SettingStore() : super('setting');
@@ -180,7 +180,7 @@ class SettingStore extends PersistentStore {
/// Ignore local network device (eg: br-xxx, ovs-system...)
/// when building traffic view on server tab
late final ignoreLocalNet = property('ignoreLocalNet', true);
//late final ignoreLocalNet = property('ignoreLocalNet', true);
/// Remerber pwd in memory
/// Used for [DialogX.showPwdDialog]

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import '../model/server/snippet.dart';
import 'package:server_box/data/model/server/snippet.dart';
class SnippetStore extends PersistentStore {
SnippetStore() : super('snippet');

View File

@@ -44,7 +44,7 @@ final class _IntroPage extends StatelessWidget {
final selected = await ctx.showPickSingleDialog(
title: libL10n.language,
items: AppLocalizations.supportedLocales,
name: (p0) => p0.nativeName,
display: (p0) => p0.nativeName,
initial: _setting.locale.fetch().toLocale,
);
if (selected != null) {

View File

@@ -65,7 +65,6 @@
"goBackQ": "Zurückkommen?",
"goto": "Pfad öffnen",
"hideTitleBar": "Titelleiste ausblenden",
"hideTitleBarTip": "Nach dem Einschalten halten Sie bitte die drei Tasten in der oberen rechten Ecke gedrückt, um sie zu ziehen.",
"highlight": "Code highlight",
"homeWidgetUrlConfig": "Home-Widget-Link konfigurieren",
"host": "Host",
@@ -162,6 +161,8 @@
"size": "Größe",
"snippet": "Snippet",
"softWrap": "Weicher Umbruch",
"specifyDev": "Gerät angeben",
"specifyDevTip": "Zum Beispiel bezieht sich die Standard-Netzwerkverkehrsstatistik auf alle Geräte. Hier können Sie ein bestimmtes Gerät angeben.",
"speed": "Tempo",
"spentTime": "Benötigte Zeit: {time}",
"sshTermHelp": "Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Go back?",
"goto": "Go to",
"hideTitleBar": "Hide title bar",
"hideTitleBarTip": "After turning it on, please hold down the three buttons in the top right corner to drag.",
"highlight": "Code highlighting",
"homeWidgetUrlConfig": "Config home widget url",
"host": "Host",
@@ -162,6 +161,8 @@
"size": "Size",
"snippet": "Snippet",
"softWrap": "Soft wrap",
"specifyDev": "Specify device",
"specifyDevTip": "For example, network traffic statistics are by default for all devices. You can specify a particular device here.",
"speed": "Speed",
"spentTime": "Spent time: {time}",
"sshTermHelp": "When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "¿Regresar?",
"goto": "Ir a",
"hideTitleBar": "Ocultar barra de título",
"hideTitleBarTip": "Después de encenderlo, mantenga presionados los tres botones en la esquina superior derecha para arrastrar.",
"highlight": "Resaltar código",
"homeWidgetUrlConfig": "Configuración de URL del widget de inicio",
"host": "Anfitrión",
@@ -162,6 +161,8 @@
"size": "Tamaño",
"snippet": "Fragmento de código",
"softWrap": "Salto de línea suave",
"specifyDev": "Especificar dispositivo",
"specifyDevTip": "Por ejemplo, las estadísticas de tráfico de red son por defecto para todos los dispositivos. Aquí puede especificar un dispositivo en particular.",
"speed": "Velocidad",
"spentTime": "Tiempo gastado: {time}",
"sshTermHelp": "Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Revenir en arrière ?",
"goto": "Aller à",
"hideTitleBar": "Masquer la barre de titre",
"hideTitleBarTip": "Après l'avoir allumé, veuillez maintenir les trois boutons dans le coin supérieur droit pour les faire glisser.",
"highlight": "Mise en surbrillance du code",
"homeWidgetUrlConfig": "Configurer l'URL du widget d'accueil",
"host": "Hôte",
@@ -162,6 +161,8 @@
"size": "Taille",
"snippet": "Extrait",
"softWrap": "Retour à la ligne souple",
"specifyDev": "Spécifier l'appareil",
"specifyDevTip": "Par exemple, les statistiques de trafic réseau concernent par défaut tous les appareils. Vous pouvez spécifier ici un appareil particulier.",
"speed": "Vitesse",
"spentTime": "Temps écoulé : {time}",
"sshTermHelp": "Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu'aucun texte n'est sélectionné et qu'il y a du contenu dans le presse-papiers. L'icône de code colle des extraits de code dans le terminal et les exécute.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Datang kembali?",
"goto": "Pergi ke",
"hideTitleBar": "Sembunyikan bilah judul",
"hideTitleBarTip": "Setelah dinyalakan, tekan dan tahan tiga tombol di sudut kanan atas untuk menyeret.",
"highlight": "Sorotan kode",
"homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah",
"host": "Host",
@@ -162,6 +161,8 @@
"size": "Ukuran",
"snippet": "Snippet",
"softWrap": "Pembungkus lembut",
"specifyDev": "Tentukan perangkat",
"specifyDevTip": "Misalnya, statistik lalu lintas jaringan secara default adalah untuk semua perangkat. Anda dapat menentukan perangkat tertentu di sini.",
"speed": "Kecepatan",
"spentTime": "Menghabiskan waktu: {time}",
"sshTermHelp": "Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "戻りますか?",
"goto": "移動",
"hideTitleBar": "タイトルバーを非表示にする",
"hideTitleBarTip": "電源を入れた後、右上隅の3つのボタンを押し続けてドラッグしてください。",
"highlight": "コードハイライト",
"homeWidgetUrlConfig": "ホームウィジェットURL設定",
"host": "ホスト",
@@ -162,6 +161,8 @@
"size": "サイズ",
"snippet": "スニペット",
"softWrap": "ソフトラップ",
"specifyDev": "デバイスを指定",
"specifyDevTip": "例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。",
"speed": "速度",
"spentTime": "時間を費やしました: {time}",
"sshTermHelp": "ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Terug gaan?",
"goto": "Ga naar",
"hideTitleBar": "Titelbalk verbergen",
"hideTitleBarTip": "Houd na het inschakelen de drie knoppen in de rechterbovenhoek ingedrukt om te slepen.",
"highlight": "Code-highlight",
"homeWidgetUrlConfig": "Home-widget-url configureren",
"host": "Host",
@@ -162,6 +161,8 @@
"size": "Grootte",
"snippet": "Fragment",
"softWrap": "Zachte wrap",
"specifyDev": "Apparaat specificeren",
"specifyDevTip": "Bijvoorbeeld, netwerkverkeersstatistieken zijn standaard voor alle apparaten. Hier kunt u een specifiek apparaat opgeven.",
"speed": "Snelheid",
"spentTime": "Gebruikte tijd: {time}",
"sshTermHelp": "Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Voltar?",
"goto": "Ir para",
"hideTitleBar": "Ocultar barra de título",
"hideTitleBarTip": "Após ligar, segure os três botões no canto superior direito para arrastar.",
"highlight": "Destaque de código",
"homeWidgetUrlConfig": "Configuração de URL do widget da tela inicial",
"host": "Host",
@@ -162,6 +161,8 @@
"size": "Tamanho",
"snippet": "Snippet",
"softWrap": "Quebra de linha suave",
"specifyDev": "Especificar dispositivo",
"specifyDevTip": "Por exemplo, as estatísticas de tráfego de rede são por padrão para todos os dispositivos. Você pode especificar um dispositivo específico aqui.",
"speed": "Velocidade",
"spentTime": "Tempo gasto: {time}",
"sshTermHelp": "Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Вернуться?",
"goto": "перейти к",
"hideTitleBar": "Скрыть заголовок",
"hideTitleBarTip": "После включения удерживайте три кнопки в правом верхнем углу, чтобы перетаскивать.",
"highlight": "подсветка кода",
"homeWidgetUrlConfig": "конфигурация URL виджета домашнего экрана",
"host": "хост",
@@ -162,6 +161,8 @@
"size": "размер",
"snippet": "фрагмент",
"softWrap": "Мягкий перенос",
"specifyDev": "Указать устройство",
"specifyDevTip": "Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.",
"speed": "скорость",
"spentTime": "Затрачено времени: {time}",
"sshTermHelp": "Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Geri dön?",
"goto": "Git",
"hideTitleBar": "Başlık çubuğunu gizle",
"hideTitleBarTip": "Açtıktan sonra, sağ üst köşedeki üç düğmeyi basılı tutarak sürükleyin.",
"highlight": "Kod vurgulama",
"homeWidgetUrlConfig": "Ana sayfa widget URL'sini yapılandır",
"host": "Sunucu",
@@ -162,6 +161,8 @@
"size": "Boyut",
"snippet": "Parça",
"softWrap": "Yumuşak kaydırma",
"specifyDev": "Cihazı belirle",
"specifyDevTip": "Örneğin, ağ trafiği istatistikleri varsayılan olarak tüm cihazlar içindir. Burada belirli bir cihazı belirtebilirsiniz.",
"speed": "Hız",
"spentTime": "Harcanan zaman: {time}",
"sshTermHelp": "Terminal kaydırılabilir olduğunda, yatay sürükleme metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapatır. Dosya simgesi mevcut yolu SFTP'de açar. Pano düğmesi metin seçildiğinde içeriği kopyalar ve metin seçilmediğinde ve panoda içerik olduğunda panodaki içeriği terminale yapıştırır. Kod simgesi kod parçacıklarını terminale yapıştırır ve çalıştırır.",

View File

@@ -65,7 +65,6 @@
"goBackQ": "返回?",
"goto": "前往",
"hideTitleBar": "隐藏标题栏",
"hideTitleBarTip": "开启后请按住右上角三个按钮来拖动",
"highlight": "代码高亮",
"homeWidgetUrlConfig": "桌面部件链接配置",
"host": "主机",
@@ -162,6 +161,8 @@
"size": "大小",
"snippet": "代码片段",
"softWrap": "自动换行",
"specifyDev": "指定设备",
"specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备",
"speed": "速度",
"spentTime": "耗时: {time}",
"sshTermHelp": "在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。",

View File

@@ -65,7 +65,6 @@
"goBackQ": "返回?",
"goto": "前往",
"hideTitleBar": "隱藏標題欄",
"hideTitleBarTip": "開啟後請按住右上角三個按鈕來拖動",
"highlight": "代碼高亮",
"homeWidgetUrlConfig": "桌面部件鏈接配置",
"host": "主機",
@@ -162,6 +161,8 @@
"size": "大小",
"snippet": "程式片段",
"softWrap": "軟換行",
"specifyDev": "指定裝置",
"specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。",
"speed": "速度",
"spentTime": "耗時: {time}",
"sshTermHelp": "在終端可滾動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。文件圖標會打開當前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端。代碼圖標會貼上代碼片段到終端並執行。",

View File

@@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:server_box/app.dart';
import 'package:server_box/core/utils/sync/icloud.dart';
import 'package:server_box/core/utils/sync/webdav.dart';
@@ -21,26 +20,18 @@ 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/wol_cfg.dart';
import 'package:server_box/data/model/ssh/virtual_key.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/provider/sftp.dart';
import 'package:server_box/data/provider/snippet.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart';
Future<void> main() async {
_runInZone(() async {
await _initApp();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Pros.app),
ChangeNotifierProvider(create: (_) => Pros.server),
ChangeNotifierProvider(create: (_) => Pros.snippet),
ChangeNotifierProvider(create: (_) => Pros.key),
ChangeNotifierProvider(create: (_) => Pros.sftp),
],
child: const MyApp(),
),
);
runApp(const MyApp());
});
}
@@ -85,7 +76,7 @@ Future<void> _initData() async {
// Ordered by typeId
Hive.registerAdapter(PrivateKeyInfoAdapter()); // 1
Hive.registerAdapter(SnippetAdapter()); // 2
Hive.registerAdapter(ServerPrivateInfoAdapter()); // 3
Hive.registerAdapter(SpiAdapter()); // 3
Hive.registerAdapter(VirtKeyAdapter()); // 4
Hive.registerAdapter(NetViewTypeAdapter()); // 5
Hive.registerAdapter(ServerFuncBtnAdapter()); // 6
@@ -93,17 +84,13 @@ Future<void> _initData() async {
Hive.registerAdapter(WakeOnLanCfgAdapter()); // 8
await PrefStore.init(); // Call this before accessing any store
await Stores.init();
await Stores.setting.init();
await Stores.server.init();
await Stores.key.init();
await Stores.snippet.init();
await Stores.container.init();
await Stores.history.init();
Pros.snippet.load();
Pros.key.load();
await Pros.app.init();
// DO NOT change the order of these providers.
PrivateKeyProvider.instance.load();
SnippetProvider.instance.load();
ServerProvider.instance.load();
SftpProvider.instance.load();
if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta;
}
@@ -142,6 +129,7 @@ Future<void> _doVersionRelated() async {
// How to upgrade the data is inside each own func.
if (curVer < newVer) {
ServerDetailCards.autoAddNewCards(newVer);
ServerFuncBtn.autoAddNewFuncs(newVer);
Stores.setting.lastVer.put(newVer);
}
}

View File

@@ -10,8 +10,8 @@ import 'package:server_box/core/utils/sync/webdav.dart';
import 'package:server_box/data/model/app/backup.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/provider/snippet.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart';
import 'package:icons_plus/icons_plus.dart';
@@ -30,18 +30,22 @@ class BackupPage extends StatelessWidget {
}
Widget _buildBody(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(13),
return MultiList(
widthDivider: 2,
children: [
_buildTip(),
CenterGreyTitle(libL10n.sync),
if (isMacOS || isIOS) _buildIcloud(context),
_buildWebdav(context),
_buildFile(context),
_buildClipboard(context),
CenterGreyTitle(libL10n.import),
_buildBulkImportServers(context),
_buildImportSnippet(context),
[
CenterGreyTitle(libL10n.sync),
_buildTip(),
if (isMacOS || isIOS) _buildIcloud(context),
_buildWebdav(context),
_buildFile(context),
_buildClipboard(context),
],
[
CenterGreyTitle(libL10n.import),
_buildBulkImportServers(context),
_buildImportSnippet(context),
],
],
);
}
@@ -152,9 +156,8 @@ class BackupPage extends StatelessWidget {
trailing: ListenableBuilder(
listenable: webdavLoading,
builder: (_, __) {
if (webdavLoading.value) {
return UIs.centerSizedLoadingSmall;
}
if (webdavLoading.value) return SizedLoading.centerSmall;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -263,7 +266,7 @@ class BackupPage extends StatelessWidget {
actions: Btn.ok(
onTap: () {
for (final snippet in snippets) {
Pros.snippet.add(snippet);
SnippetProvider.add(snippet);
}
context.pop();
context.pop();
@@ -302,7 +305,7 @@ class BackupPage extends StatelessWidget {
);
} catch (e, s) {
Loggers.app.warning('Import backup failed', e, s);
context.showErrDialog(e: e, s: s, operation: libL10n.restore);
context.showErrDialog(e, s, libL10n.restore);
}
}
@@ -326,7 +329,7 @@ class BackupPage extends StatelessWidget {
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
await dlBak.restore(force: true);
} catch (e, s) {
context.showErrDialog(e: e, s: s, operation: libL10n.restore);
context.showErrDialog(e, s, libL10n.restore);
Loggers.app.warning('Download webdav backup failed', e, s);
} finally {
webdavLoading.value = false;
@@ -335,7 +338,7 @@ class BackupPage extends StatelessWidget {
Future<void> _onTapWebdavUp(BuildContext context) async {
webdavLoading.value = true;
final date = DateTime.now().ymdhms(ymdSep: "-", hmsSep: "-", sep: "-");
final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');
final bakName = '$date-${Miscs.bakFileName}';
try {
await Backup.backup(bakName);
@@ -345,7 +348,7 @@ class BackupPage extends StatelessWidget {
}
Loggers.app.info('Upload webdav backup success');
} catch (e, s) {
context.showErrDialog(e: e, s: s, operation: l10n.upload);
context.showErrDialog(e, s, l10n.upload);
Loggers.app.warning('Upload webdav backup failed', e, s);
} finally {
webdavLoading.value = false;
@@ -431,14 +434,14 @@ class BackupPage extends StatelessWidget {
);
} catch (e, s) {
Loggers.app.warning('Import backup failed', e, s);
context.showErrDialog(e: e, s: s, operation: libL10n.restore);
context.showErrDialog(e, s, libL10n.restore);
}
}
void _onBulkImportServers(BuildContext context) async {
final data = await context.showImportDialog(
title: l10n.server,
modelDef: ServerPrivateInfo.example.toJson(),
modelDef: Spix.example.toJson(),
);
if (data == null) return;
final text = String.fromCharCodes(data);
@@ -447,7 +450,7 @@ class BackupPage extends StatelessWidget {
final (spis, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start((val) {
final list = json.decode(val) as List;
return list.map((e) => ServerPrivateInfo.fromJson(e)).toList();
return list.map((e) => Spi.fromJson(e)).toList();
}, text.trim()),
);
if (err != null || spis == null) return;
@@ -469,7 +472,7 @@ class BackupPage extends StatelessWidget {
context.showSnackBar(libL10n.success);
}
} catch (e, s) {
context.showErrDialog(e: e, s: s, operation: libL10n.import);
context.showErrDialog(e, s, libL10n.import);
Loggers.app.warning('Import servers failed', e, s);
}
}

View File

@@ -11,13 +11,13 @@ import 'package:server_box/data/model/container/image.dart';
import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/store.dart';
import '../../data/model/container/ps.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/container.dart';
import '../widget/two_line_text.dart';
import 'package:server_box/data/model/container/ps.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/container.dart';
import 'package:server_box/view/widget/two_line_text.dart';
class ContainerPage extends StatefulWidget {
final ServerPrivateInfo spi;
final Spi spi;
const ContainerPage({required this.spi, super.key});
@override
@@ -27,7 +27,7 @@ class ContainerPage extends StatefulWidget {
class _ContainerPageState extends State<ContainerPage> {
final _textController = TextEditingController();
late final _container = ContainerProvider(
client: widget.spi.server?.client,
client: widget.spi.server?.value.client,
userName: widget.spi.user,
hostId: widget.spi.id,
context: context,

View File

@@ -12,7 +12,7 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/highlight.dart';
import 'package:server_box/data/res/store.dart';
import '../widget/two_line_text.dart';
import 'package:server_box/view/widget/two_line_text.dart';
class EditorPage extends StatefulWidget {
/// If path is not null, then it's a file editor

View File

@@ -14,22 +14,22 @@ final class _AppBar extends CustomAppBar {
@override
Widget build(BuildContext context) {
if (isDesktop) return super.build(context);
final placeholder = SizedBox(
height: CustomAppBar.barHeight ?? 0 + MediaQuery.of(context).padding.top,
);
return ValBuilder(
listenable: landscape,
builder: (ls) {
if (ls) return placeholder;
return selectIndex.listenVal(
(idx) {
if (idx == AppTab.ssh.index) {
return placeholder;
}
if (isDesktop) return super.build(context);
return ValBuilder(
listenable: selectIndex,
builder: (idx) {
if (idx == AppTab.ssh.index) {
return placeholder;
}
listenable: landscape,
builder: (ls) {
if (ls) return placeholder;
return super.build(context);
},
);

View File

@@ -8,13 +8,13 @@ import 'package:server_box/core/extension/build.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/tab.dart';
import 'package:server_box/data/provider/app.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/github_id.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/res/url.dart';
import 'package:server_box/view/page/ssh/page.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
part 'appbar.dart';
@@ -65,7 +65,7 @@ class _HomePageState extends State<HomePage>
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
Pros.server.closeServer();
ServerProvider.closeServer();
_pageController.dispose();
WakelockPlus.disable();
}
@@ -78,8 +78,8 @@ class _HomePageState extends State<HomePage>
switch (state) {
case AppLifecycleState.resumed:
if (_shouldAuth) _goAuth();
if (!Pros.server.isAutoRefreshOn) {
Pros.server.startAutoRefresh();
if (!ServerProvider.isAutoRefreshOn) {
ServerProvider.startAutoRefresh();
}
HomeWidgetMC.update();
break;
@@ -93,7 +93,7 @@ class _HomePageState extends State<HomePage>
// }
} else {
//Pros.server.setDisconnected();
Pros.server.stopAutoRefresh();
ServerProvider.stopAutoRefresh();
}
break;
default:
@@ -104,7 +104,7 @@ class _HomePageState extends State<HomePage>
@override
Widget build(BuildContext context) {
super.build(context);
Pros.app.ctx = context;
AppProvider.ctx = context;
final appBar = _AppBar(
selectIndex: _selectIndex,
@@ -120,7 +120,7 @@ class _HomePageState extends State<HomePage>
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
onPressed: () async {
await Pros.server.refresh();
await ServerProvider.refresh();
},
);
},
@@ -128,7 +128,10 @@ class _HomePageState extends State<HomePage>
IconButton(
icon: const Icon(Icons.developer_mode, size: 21),
tooltip: 'Debug',
onPressed: () => AppRoutes.debug().go(context),
onPressed: () => DebugPage.route.go(
context,
args: const DebugPageArgs(title: 'Debug(${BuildData.build})'),
),
),
],
);
@@ -141,7 +144,7 @@ class _HomePageState extends State<HomePage>
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, index) => AppTab.values[index].page,
onPageChanged: (value) {
SSHPage.focusNode.unfocus();
FocusScope.of(context).unfocus();
if (!_switchingPage) {
_selectIndex.value = value;
}
@@ -330,8 +333,7 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
);
}
HomeWidgetMC.update();
await Pros.server.load();
await Pros.server.refresh();
await ServerProvider.refresh();
}
// Future<void> _reqNotiPerm() async {

View File

@@ -1,102 +0,0 @@
import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:server_box/data/model/server/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/provider.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/res/url.dart';
final class WearHome extends StatefulWidget {
const WearHome({super.key});
@override
State<WearHome> createState() => _WearHomeState();
}
final class _WearHomeState extends State<WearHome> with AfterLayoutMixin {
late final _pageCtrl =
PageController(initialPage: Pros.server.servers.isNotEmpty ? 1 : 0);
@override
Widget build(BuildContext context) {
return _buildBody();
}
Widget _buildBody() {
return Consumer<ServerProvider>(builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return const Center(child: Text('No server'));
}
return PageView.builder(
controller: _pageCtrl,
itemCount: pro.servers.length + 1,
itemBuilder: (_, index) {
if (index == 0) return _buildInit();
final id = pro.serverOrder[index];
final server = Pros.server.pick(id: id);
if (server == null) return UIs.placeholder;
return _buildEachSever(server);
},
);
});
}
Widget _buildInit() {
return Center(
child: Column(
children: [
IconButton(onPressed: () {}, icon: const Icon(Icons.add)),
UIs.height7,
Text(libL10n.restore)
],
),
);
}
Widget _buildEachSever(Server srv) {
final mem = () {
final total = srv.status.mem.total;
final used = srv.status.mem.total - srv.status.mem.avail;
return '${used.bytes2Str} / ${total.bytes2Str}';
}();
final disk = () {
final total = srv.status.diskUsage?.size.kb2Str;
final used = srv.status.diskUsage?.used.kb2Str;
return '$used / $total';
}();
final net = '${srv.status.netSpeed.cachedRealVals.speedIn}'
'${srv.status.netSpeed.cachedRealVals.speedOut}';
return Padding(
padding: const EdgeInsets.all(7),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(srv.spi.name, style: UIs.text15Bold),
UIs.height7,
KvRow(k: 'CPU', v: '${srv.status.cpu.usedPercent()}%'),
KvRow(k: 'Mem', v: mem),
KvRow(k: 'Disk', v: disk),
KvRow(k: 'Net', v: net)
],
),
);
}
@override
FutureOr<void> afterFirstLayout(BuildContext context) async {
if (Stores.setting.autoCheckAppUpdate.fetch()) {
AppUpdateIface.doUpdate(
build: BuildData.build,
url: Urls.updateCfg,
context: context,
);
}
await Pros.server.load();
await Pros.server.refresh();
}
}

View File

@@ -5,7 +5,7 @@ import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
class IPerfPage extends StatefulWidget {
final ServerPrivateInfo spi;
final Spi spi;
const IPerfPage({super.key, required this.spi});
@override

View File

@@ -3,9 +3,9 @@ import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/provider/server.dart';
import '../../data/model/server/ping_result.dart';
import 'package:server_box/data/model/server/ping_result.dart';
/// Only permit ipv4 / ipv6 / domain chars
final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+');
@@ -150,7 +150,7 @@ class _PingPageState extends State<PingPage>
return;
}
if (Pros.server.serverOrder.isEmpty) {
if (ServerProvider.serverOrder.value.isEmpty) {
context.showSnackBar(l10n.pingNoServer);
return;
}
@@ -161,7 +161,8 @@ class _PingPageState extends State<PingPage>
return;
}
await Future.wait(Pros.server.servers.map((e) async {
await Future.wait(ServerProvider.servers.values.map((v) async {
final e = v.value;
if (e.client == null) {
return;
}

View File

@@ -5,11 +5,11 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart';
import '../../../core/utils/server.dart';
import '../../../data/model/server/private_key_info.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
const _format = 'text/plain';
@@ -89,7 +89,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
)),
actions: Btn.ok(
onTap: () {
Pros.key.delete(widget.pki!);
PrivateKeyProvider.delete(widget.pki!);
context.pop();
context.pop();
},
@@ -107,7 +107,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
}
String _standardizeLineSeparators(String value) {
return value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
return value.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
}
Widget _buildFAB() {
@@ -199,14 +199,15 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
return;
}
FocusScope.of(context).unfocus();
_loading.value = UIs.centerSizedLoading;
_loading.value = SizedLoading.centerMedium;
try {
final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);
final pki = PrivateKeyInfo(id: name, key: decrypted);
if (widget.pki != null) {
Pros.key.update(widget.pki!, pki);
final originPki = widget.pki;
if (originPki != null) {
PrivateKeyProvider.update(originPki, pki);
} else {
Pros.key.add(pki);
PrivateKeyProvider.add(pki);
}
} catch (e) {
context.showSnackBar(e.toString());

View File

@@ -3,13 +3,12 @@ import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
import '../../../core/route.dart';
import '../../../data/model/server/private_key_info.dart';
import '../../../data/provider/private_key.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/provider/private_key.dart';
class PrivateKeysListPage extends StatefulWidget {
const PrivateKeysListPage({super.key});
@@ -35,16 +34,16 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
}
Widget _buildBody() {
return Consumer<PrivateKeyProvider>(
builder: (_, key, __) {
if (key.pkis.isEmpty) {
return PrivateKeyProvider.pkis.listenVal(
(pkis) {
if (pkis.isEmpty) {
return Center(child: Text(libL10n.empty));
}
return ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.pkis.length,
itemCount: pkis.length,
itemBuilder: (context, idx) {
final item = key.pkis[idx];
final item = pkis[idx];
return CardX(
child: ListTile(
leading: Text(

View File

@@ -6,13 +6,13 @@ import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
import '../../data/model/app/shell_func.dart';
import '../../data/model/server/proc.dart';
import '../../data/model/server/server_private_info.dart';
import '../widget/two_line_text.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/proc.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/view/widget/two_line_text.dart';
class ProcessPage extends StatefulWidget {
final ServerPrivateInfo spi;
final Spi spi;
const ProcessPage({super.key, required this.spi});
@override
@@ -37,7 +37,7 @@ class _ProcessPageState extends State<ProcessPage> {
@override
void initState() {
super.initState();
_client = widget.spi.server?.client;
_client = widget.spi.server?.value.client;
final duration =
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
_timer = Timer.periodic(duration, (_) => _refresh());

View File

@@ -11,7 +11,7 @@ import 'package:server_box/view/widget/percent_circle.dart';
import 'package:server_box/view/widget/two_line_text.dart';
final class PvePage extends StatefulWidget {
final ServerPrivateInfo spi;
final Spi spi;
const PvePage({
super.key,

View File

@@ -4,7 +4,6 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:provider/provider.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/server_detail_card.dart';
import 'package:server_box/data/model/app/shell_func.dart';
@@ -20,16 +19,15 @@ import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/widget/server_func_btns.dart';
import '../../../../core/route.dart';
import '../../../../data/model/server/server.dart';
import '../../../../data/provider/server.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server.dart';
part 'misc.dart';
class ServerDetailPage extends StatefulWidget {
const ServerDetailPage({super.key, required this.spi});
final ServerPrivateInfo spi;
final Spi spi;
@override
State<ServerDetailPage> createState() => _ServerDetailPageState();
@@ -58,9 +56,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
late MediaQueryData _media;
final List<String> _cardsOrder = [];
final _settings = Stores.setting;
final _netSortType = ValueNotifier(_NetSortType.device);
late final _collapse = Stores.setting.collapseUIDefault.fetch();
late final _textFactor = TextScaler.linear(Stores.setting.textFactor.fetch());
late final _collapse = _settings.collapseUIDefault.fetch();
late final _textFactor = TextScaler.linear(_settings.textFactor.fetch());
@override
void didChangeDependencies() {
@@ -71,39 +70,34 @@ class _ServerDetailPageState extends State<ServerDetailPage>
@override
void initState() {
super.initState();
final order = Stores.setting.detailCardOrder.fetch();
final order = _settings.detailCardOrder.fetch();
order.removeWhere((e) => !ServerDetailCards.names.contains(e));
_cardsOrder.addAll(order);
}
@override
Widget build(BuildContext context) {
return Consumer<ServerProvider>(builder: (_, provider, __) {
final s = widget.spi.server;
if (s == null) {
return Scaffold(
appBar: const CustomAppBar(),
body: Center(child: Text(libL10n.empty)),
);
}
return _buildMainPage(s);
});
final s = widget.spi.server;
if (s == null) {
return Scaffold(
appBar: const CustomAppBar(),
body: Center(child: Text(libL10n.empty)),
);
}
return s.listenVal(_buildMainPage);
}
Widget _buildMainPage(Server si) {
final buildFuncs = !Stores.setting.moveServerFuncs.fetch();
final logoUrl = si.spi.custom?.logoUrl ??
Stores.setting.serverLogoUrl.fetch().selfIfNotNullEmpty;
final buildLogo = logoUrl != null;
final logo = _buildLogo(si);
final children = [
if (buildLogo)
_buildLogo(logoUrl, si.status.more[StatusCmdType.sys]?.dist),
if (buildFuncs) ServerFuncBtns(spi: widget.spi),
logo,
if (buildFuncs) ServerFuncBtns(spi: si.spi),
];
for (final card in _cardsOrder) {
final buildFunc = _cardBuildMap[card];
if (buildFunc != null) {
children.add(buildFunc(si.status));
children.add(buildFunc(si));
}
}
return Scaffold(
@@ -123,6 +117,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
return CustomAppBar(
title: Text(si.spi.name),
actions: [
ShareBtn(
data: si.spi.toJsonString(),
tip: si.spi.name,
tip2: '${libL10n.share} ${l10n.server} ~ ServerBox',
),
IconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
@@ -136,12 +135,17 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildLogo(String logoUrl, Dist? dist) {
if (dist != null) {
logoUrl = logoUrl
.replaceFirst('{DIST}', dist.name)
.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
}
Widget _buildLogo(Server si) {
var logoUrl = si.spi.custom?.logoUrl ??
_settings.serverLogoUrl.fetch().selfIfNotNullEmpty;
if (logoUrl == null) return UIs.placeholder;
final dist = si.status.more[StatusCmdType.sys]?.dist;
if (dist == null) return UIs.placeholder;
logoUrl = logoUrl
.replaceFirst('{DIST}', dist.name)
.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
return Padding(
padding: const EdgeInsets.symmetric(vertical: 13),
child: ExtendedImage.network(
@@ -152,7 +156,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildAbout(ServerStatus ss) {
Widget _buildAbout(Server si) {
final ss = si.status;
return CardX(
child: ExpandTile(
leading: const Icon(MingCute.information_fill, size: 20),
@@ -180,7 +185,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildCPUView(ServerStatus ss) {
Widget _buildCPUView(Server si) {
final ss = si.status;
final percent = ss.cpu.usedPercent(coreIdx: 0).toInt();
final details = [
_buildDetailPercent(ss.cpu.user, 'user'),
@@ -344,7 +350,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildMemView(ServerStatus ss) {
Widget _buildMemView(Server si) {
final ss = si.status;
final free = ss.mem.free / ss.mem.total * 100;
final avail = ss.mem.availPercent * 100;
final used = ss.mem.usedPercent * 100;
@@ -391,7 +398,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildSwapView(ServerStatus ss) {
Widget _buildSwapView(Server si) {
final ss = si.status;
if (ss.swap.total == 0) return UIs.placeholder;
final used = ss.swap.usedPercent * 100;
final cached = ss.swap.cached / ss.swap.total * 100;
@@ -426,7 +434,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildGpuView(ServerStatus ss) {
Widget _buildGpuView(Server si) {
final ss = si.status;
if (ss.nvidia == null || ss.nvidia?.isEmpty == true) return UIs.placeholder;
final children = ss.nvidia?.map((e) => _buildGpuItem(e)).toList() ?? [];
return CardX(
@@ -536,7 +545,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildDiskView(ServerStatus ss) {
Widget _buildDiskView(Server si) {
final ss = si.status;
final children = List.generate(
ss.disk.length, (idx) => _buildDiskItem(ss.disk[idx], ss));
return CardX(
@@ -600,7 +610,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildNetView(ServerStatus ss) {
Widget _buildNetView(Server si) {
final ss = si.status;
final ns = ss.netSpeed;
final children = <Widget>[];
final devices = ns.devices;
@@ -683,14 +694,15 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildTemperature(ServerStatus ss) {
Widget _buildTemperature(Server si) {
final ss = si.status;
if (ss.temps.isEmpty) {
return UIs.placeholder;
}
return CardX(
child: ExpandTile(
title: Text(l10n.temperature),
leading: const Icon(Icons.ac_unit, size: 17),
leading: const Icon(Icons.ac_unit, size: 20),
initiallyExpanded: _getInitExpand(ss.temps.devices.length),
childrenPadding: const EdgeInsets.only(bottom: 7),
children: ss.temps.devices
@@ -706,14 +718,20 @@ class _ServerDetailPageState extends State<ServerDetailPage>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(key, style: UIs.text15),
Text(key, style: UIs.text15).paddingSymmetric(horizontal: 5).tap(
onTap: () {
Pfs.copy(key);
context.showSnackBar('${libL10n.copy} ${libL10n.success}');
},
),
Text('${val?.toStringAsFixed(1)}°C', style: UIs.text13Grey),
],
),
);
}
Widget _buildBatteries(ServerStatus ss) {
Widget _buildBatteries(Server si) {
final ss = si.status;
if (ss.batteries.isEmpty) {
return UIs.placeholder;
}
@@ -754,7 +772,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildSensors(ServerStatus ss) {
Widget _buildSensors(Server si) {
final ss = si.status;
if (ss.sensors.isEmpty) return UIs.placeholder;
return CardX(
child: ExpandTile(
@@ -817,20 +836,21 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildPve(_) {
final addr = widget.spi.custom?.pveAddr;
if (addr == null) return UIs.placeholder;
Widget _buildPve(Server si) {
final addr = si.spi.custom?.pveAddr;
if (addr == null || addr.isEmpty) return UIs.placeholder;
return CardX(
child: ListTile(
title: const Text('PVE'),
leading: const Icon(FontAwesome.server_solid, size: 17),
trailing: const Icon(Icons.chevron_right),
onTap: () => AppRoutes.pve(spi: widget.spi).go(context),
onTap: () => AppRoutes.pve(spi: si.spi).go(context),
),
);
}
Widget _buildCustom(ServerStatus ss) {
Widget _buildCustom(Server si) {
final ss = si.status;
if (ss.customCmds.isEmpty) return UIs.placeholder;
return CardX(
child: ExpandTile(

View File

@@ -1,20 +1,23 @@
import 'dart:convert';
import 'package:choice/choice.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:provider/provider.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/provider/server.dart';
import '../../../core/route.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/provider/private_key.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/private_key.dart';
class ServerEditPage extends StatefulWidget {
const ServerEditPage({super.key, this.spi});
final ServerPrivateInfo? spi;
final Spi? spi;
@override
State<ServerEditPage> createState() => _ServerEditPageState();
@@ -33,6 +36,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final _wolMacCtrl = TextEditingController();
final _wolIpCtrl = TextEditingController();
final _wolPwdCtrl = TextEditingController();
final _netDevCtrl = TextEditingController();
final _scriptDirCtrl = TextEditingController();
final _nameFocus = FocusNode();
final _ipFocus = FocusNode();
@@ -44,7 +49,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final _keyIdx = ValueNotifier<int?>(null);
final _autoConnect = ValueNotifier(true);
final _jumpServer = ValueNotifier<String?>(null);
final _jumpServer = nvn<String?>();
final _pveIgnoreCert = ValueNotifier(false);
final _env = <String, String>{}.vn;
final _customCmds = <String, String>{}.vn;
@@ -70,6 +75,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_wolMacCtrl.dispose();
_wolIpCtrl.dispose();
_wolPwdCtrl.dispose();
_netDevCtrl.dispose();
_scriptDirCtrl.dispose();
}
@override
@@ -80,50 +87,29 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
@override
Widget build(BuildContext context) {
final actions = <Widget>[];
if (widget.spi != null) actions.add(_buildDelBtn());
return GestureDetector(
onTap: () => _focusScope.unfocus(),
child: Scaffold(
appBar: _buildAppBar(),
appBar: CustomAppBar(title: Text(libL10n.edit), actions: actions),
body: _buildForm(),
floatingActionButton: _buildFAB(),
),
);
}
PreferredSizeWidget _buildAppBar() {
return CustomAppBar(
title: Text(libL10n.edit),
actions: widget.spi != null ? [_buildDelBtn()] : null,
);
}
Widget _buildDelBtn() {
return IconButton(
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: StatefulBuilder(builder: (ctx, setState) {
return Text(libL10n.askContinue(
'${libL10n.delete} ${l10n.server}(${widget.spi!.name})',
));
}),
actions: Btn.ok(
onTap: () async {
context.pop();
Pros.server.delServer(widget.spi!.id);
context.pop(true);
},
red: true,
).toList,
);
},
icon: const Icon(Icons.delete),
);
}
Widget _buildForm() {
final children = [
final topItems = [
_buildWriteScriptTip(),
if (isMobile) _buildQrScan(),
];
final children = [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: topItems.joinWith(UIs.width13).toList(),
),
Input(
autoFocus: true,
controller: _nameController,
@@ -167,7 +153,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
hint: 'root',
suggestion: false,
),
TagTile(tags: _tags, allTags: Pros.server.tags.value).cardx,
TagTile(tags: _tags, allTags: ServerProvider.tags.value).cardx,
ListTile(
title: Text(l10n.autoConnect),
trailing: ListenableBuilder(
@@ -237,16 +223,16 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
}
Widget _buildKeyAuth() {
const padding = EdgeInsets.only(left: 23, right: 13);
return Consumer<PrivateKeyProvider>(
builder: (_, key, __) {
final tiles = List<Widget>.generate(key.pkis.length, (index) {
final e = key.pkis[index];
return PrivateKeyProvider.pkis.listenVal(
(pkis) {
final tiles = List<Widget>.generate(pkis.length, (index) {
final e = pkis[index];
return ListTile(
contentPadding: padding,
leading: Text(
'#${index + 1}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
contentPadding: const EdgeInsets.only(left: 10, right: 15),
leading: Radio<int>(
value: index,
groupValue: _keyIdx.value,
onChanged: (value) => _keyIdx.value = value,
),
title: Text(e.id, textAlign: TextAlign.start),
subtitle: Text(
@@ -254,10 +240,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
textAlign: TextAlign.start,
style: UIs.textGrey,
),
trailing: Radio<int>(
value: index,
groupValue: _keyIdx.value,
onChanged: (value) => _keyIdx.value = value,
trailing: Btn.icon(
icon: const Icon(Icons.edit),
onTap: () => AppRoutes.keyEdit(pki: e).go(context),
),
onTap: () => _keyIdx.value = index,
);
@@ -265,11 +250,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
tiles.add(
ListTile(
title: Text(libL10n.add),
contentPadding: padding,
trailing: const Padding(
padding: EdgeInsets.only(right: 13),
child: Icon(Icons.add),
),
contentPadding: const EdgeInsets.only(left: 23, right: 23),
trailing: const Icon(Icons.add),
onTap: () => AppRoutes.keyEdit().go(context),
),
);
@@ -308,7 +290,6 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
return ExpandTile(
title: Text(l10n.more),
children: [
UIs.height7,
Input(
controller: _logoUrlCtrl,
type: TextInputType.url,
@@ -318,24 +299,52 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
suggestion: false,
),
_buildAltUrl(),
_buildScriptDir(),
_buildEnvs(),
UIs.height7,
..._buildPVEs(),
UIs.height7,
..._buildCustomCmds(),
UIs.height7,
Text(l10n.temperature, style: UIs.text13Grey),
UIs.height7,
_buildPVEs(),
_buildCustomCmds(),
_buildCustomDev(),
_buildWOLs(),
],
);
}
Widget _buildScriptDir() {
return Input(
controller: _scriptDirCtrl,
type: TextInputType.text,
label: '${l10n.remotePath} (Shell ${l10n.install})',
icon: Icons.folder,
hint: '~/.config/server_box',
suggestion: false,
);
}
Widget _buildCustomDev() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CenterGreyTitle(l10n.specifyDev),
ListTile(
leading: const Icon(MingCute.question_line),
title: TipText(libL10n.note, l10n.specifyDevTip),
).cardx,
Input(
controller: _preferTempDevCtrl,
type: TextInputType.text,
label: libL10n.device,
label: l10n.temperature,
icon: MingCute.low_temperature_line,
hint: 'nvme-pci-0400',
suggestion: false,
),
UIs.height7,
..._buildWOLs(),
Input(
controller: _netDevCtrl,
type: TextInputType.text,
label: l10n.net,
icon: ZondIcons.network,
hint: 'eth0',
suggestion: false,
),
],
);
}
@@ -352,115 +361,108 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
);
}
List<Widget> _buildPVEs() {
Widget _buildPVEs() {
const addr = 'https://127.0.0.1:8006';
return [
const Text('PVE', style: UIs.text13Grey),
UIs.height7,
Autocomplete<String>(
optionsBuilder: (val) {
final v = val.text;
if (v.startsWith(addr.substring(0, v.length))) {
return [addr];
}
return [];
},
onSelected: (val) => _pveAddrCtrl.text = val,
fieldViewBuilder: (_, ctrl, node, __) => Input(
controller: ctrl,
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CenterGreyTitle('PVE'),
Input(
controller: _pveAddrCtrl,
type: TextInputType.url,
icon: MingCute.web_line,
node: node,
label: 'URL',
hint: addr,
suggestion: false,
),
),
ListTile(
leading: const Icon(MingCute.certificate_line),
title: Text('PVE ${l10n.ignoreCert}'),
subtitle: Text(l10n.pveIgnoreCertTip, style: UIs.text12Grey),
trailing: ListenableBuilder(
listenable: _pveIgnoreCert,
builder: (_, __) => Switch(
value: _pveIgnoreCert.value,
onChanged: (val) {
_pveIgnoreCert.value = val;
},
ListTile(
leading: const Icon(MingCute.certificate_line),
title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip),
trailing: ListenableBuilder(
listenable: _pveIgnoreCert,
builder: (_, __) => Switch(
value: _pveIgnoreCert.value,
onChanged: (val) {
_pveIgnoreCert.value = val;
},
),
),
).cardx,
],
);
}
Widget _buildCustomCmds() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CenterGreyTitle(l10n.customCmd),
_customCmds.listenVal(
(vals) {
return ListTile(
leading: const Icon(BoxIcons.bxs_file_json),
title: const Text('JSON'),
subtitle: vals.isEmpty
? null
: Text(vals.keys.join(','), style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () async {
final res = await KvEditor.route.go(
context,
args: KvEditorArgs(data: _customCmds.value),
);
if (res == null) return;
_customCmds.value = res;
},
);
},
).cardx,
ListTile(
leading: const Icon(MingCute.doc_line),
title: Text(libL10n.doc),
trailing: const Icon(Icons.open_in_new, size: 17),
onTap: () => l10n.customCmdDocUrl.launch(),
).cardx,
],
);
}
Widget _buildWOLs() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CenterGreyTitle('Wake On LAN (beta)'),
ListTile(
leading: const Icon(BoxIcons.bxs_help_circle),
title: TipText(libL10n.about, l10n.wolTip),
).cardx,
Input(
controller: _wolMacCtrl,
type: TextInputType.text,
label: 'MAC ${l10n.addr}',
icon: Icons.computer,
hint: '00:11:22:33:44:55',
suggestion: false,
),
).cardx,
];
}
List<Widget> _buildCustomCmds() {
return [
Text(l10n.customCmd, style: UIs.text13Grey),
UIs.height7,
_customCmds.listenVal(
(vals) {
return ListTile(
leading: const Icon(BoxIcons.bxs_file_json),
title: const Text('JSON'),
subtitle: vals.isEmpty
? null
: Text(vals.keys.join(','), style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () async {
final res = await KvEditor.route.go(
context,
args: KvEditorArgs(data: _customCmds.value),
);
if (res == null) return;
_customCmds.value = res;
},
);
},
).cardx,
ListTile(
leading: const Icon(MingCute.doc_line),
title: Text(libL10n.doc),
trailing: const Icon(Icons.open_in_new, size: 17),
onTap: () => l10n.customCmdDocUrl.launch(),
).cardx,
];
}
List<Widget> _buildWOLs() {
return [
const Text('Wake On LAN (beta)', style: UIs.text13Grey),
UIs.height7,
ListTile(
leading: const Icon(BoxIcons.bxs_help_circle),
title: Text(libL10n.about),
subtitle: Text(l10n.wolTip, style: UIs.text12Grey),
).cardx,
Input(
controller: _wolMacCtrl,
type: TextInputType.text,
label: 'MAC ${l10n.addr}',
icon: Icons.computer,
hint: '00:11:22:33:44:55',
suggestion: false,
),
Input(
controller: _wolIpCtrl,
type: TextInputType.text,
label: 'IP ${l10n.addr}',
icon: ZondIcons.network,
hint: '192.168.1.x',
suggestion: false,
),
Input(
controller: _wolPwdCtrl,
type: TextInputType.text,
obscureText: true,
label: l10n.pwd,
icon: Icons.password,
hint: l10n.pwd,
suggestion: false,
),
];
Input(
controller: _wolIpCtrl,
type: TextInputType.text,
label: 'IP ${l10n.addr}',
icon: ZondIcons.network,
hint: '192.168.1.x',
suggestion: false,
),
Input(
controller: _wolPwdCtrl,
type: TextInputType.text,
obscureText: true,
label: l10n.pwd,
icon: Icons.password,
hint: l10n.pwd,
suggestion: false,
),
],
);
}
Widget _buildFAB() {
@@ -471,42 +473,49 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
}
Widget _buildJumpServer() {
return ListenableBuilder(
listenable: _jumpServer,
builder: (_, __) {
final children = Pros.server.servers
.where((element) => element.spi.jumpId == null)
.where((element) => element.spi.id != widget.spi?.id)
.map(
(e) => ListTile(
title: Text(e.spi.name),
subtitle: Text(e.spi.id, style: UIs.textGrey),
trailing: Radio<String>(
groupValue: _jumpServer.value,
value: e.spi.id,
onChanged: (val) => _jumpServer.value = val,
),
onTap: () => _jumpServer.value = e.spi.id,
contentPadding: const EdgeInsets.symmetric(horizontal: 17),
),
)
.toList();
children.add(ListTile(
title: Text(libL10n.clear),
trailing: const Icon(Icons.clear),
onTap: () => _jumpServer.value = null,
contentPadding: const EdgeInsets.symmetric(horizontal: 17),
));
return CardX(
child: ExpandTile(
leading: const Icon(Icons.map),
initiallyExpanded: _jumpServer.value != null,
title: Text(l10n.jumpServer),
children: children,
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
final srvs = ServerProvider.servers.values
.map((e) => e.value)
.where((e) => e.spi.jumpId == null)
.where((e) => e.spi.id != widget.spi?.id)
.toList();
final choice = _jumpServer.listenVal(
(val) {
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
return Choice<Server>(
multiple: false,
clearable: true,
value: srv != null ? [srv] : [],
builder: (state, _) => Wrap(
children: List<Widget>.generate(
srvs.length,
(index) {
final item = srvs[index];
return ChoiceChipX<Server>(
label: item.spi.name,
state: state,
value: item,
onSelected: (srv, on) {
if (on) {
_jumpServer.value = srv.spi.id;
} else {
_jumpServer.value = null;
}
},
);
},
),
),
);
},
);
return ExpandTile(
leading: const Icon(Icons.map),
initiallyExpanded: _jumpServer.value != null,
childrenPadding: padding,
title: Text(l10n.jumpServer),
children: [choice],
).cardx;
}
void _onSave() async {
@@ -551,6 +560,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
cmds: customCmds.isEmpty ? null : customCmds,
preferTempDev: _preferTempDevCtrl.text.selfIfNotNullEmpty,
logoUrl: _logoUrlCtrl.text.selfIfNotNullEmpty,
netDev: _netDevCtrl.text.selfIfNotNullEmpty,
scriptDir: _scriptDirCtrl.text.selfIfNotNullEmpty,
);
final wolEmpty = _wolMacCtrl.text.isEmpty &&
@@ -571,7 +582,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
}
}
final spi = ServerPrivateInfo(
final spi = Spi(
name: _nameController.text.isEmpty
? _ipController.text
: _nameController.text,
@@ -580,7 +591,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
user: _usernameController.text,
pwd: _passwordController.text.selfIfNotNullEmpty,
keyId: _keyIdx.value != null
? Pros.key.pkis.elementAt(_keyIdx.value!).id
? PrivateKeyProvider.pkis.value.elementAt(_keyIdx.value!).id
: null,
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
alterUrl: _altUrlController.text.selfIfNotNullEmpty,
@@ -592,77 +603,124 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
);
if (widget.spi == null) {
Pros.server.addServer(spi);
ServerProvider.addServer(spi);
} else {
Pros.server.updateServer(widget.spi!, spi);
ServerProvider.updateServer(widget.spi!, spi);
}
context.pop();
}
Widget _buildWriteScriptTip() {
return Center(
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () {
context.showRoundDialog(
title: libL10n.attention,
child: SimpleMarkdown(data: l10n.writeScriptTip),
actions: [Btn.ok()],
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.tips_and_updates, size: 15, color: Colors.grey),
UIs.width13,
Text(libL10n.attention, style: UIs.textGrey)
],
).paddingSymmetric(horizontal: 13, vertical: 3),
),
).paddingOnly(bottom: 13);
}
@override
void afterFirstLayout(BuildContext context) {
final spi = widget.spi;
if (spi != null) {
_nameController.text = spi.name;
_ipController.text = spi.ip;
_portController.text = spi.port.toString();
_usernameController.text = spi.user;
if (spi.keyId == null) {
_passwordController.text = spi.pwd ?? '';
} else {
_keyIdx.value = Pros.key.pkis.indexWhere(
(e) => e.id == widget.spi!.keyId,
);
}
/// List in dart is passed by pointer, so you need to copy it here
_tags.value = spi.tags?.toSet() ?? {};
_altUrlController.text = spi.alterUrl ?? '';
_autoConnect.value = spi.autoConnect ?? true;
_jumpServer.value = spi.jumpId;
final custom = spi.custom;
if (custom != null) {
_pveAddrCtrl.text = custom.pveAddr ?? '';
_pveIgnoreCert.value = custom.pveIgnoreCert;
_customCmds.value = custom.cmds ?? {};
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
_logoUrlCtrl.text = custom.logoUrl ?? '';
}
final wol = spi.wolCfg;
if (wol != null) {
_wolMacCtrl.text = wol.mac;
_wolIpCtrl.text = wol.ip;
_wolPwdCtrl.text = wol.pwd ?? '';
}
_env.value = spi.envs ?? {};
_initWithSpi(spi);
}
}
void _initWithSpi(Spi spi) {
_nameController.text = spi.name;
_ipController.text = spi.ip;
_portController.text = spi.port.toString();
_usernameController.text = spi.user;
if (spi.keyId == null) {
_passwordController.text = spi.pwd ?? '';
} else {
_keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere(
(e) => e.id == widget.spi!.keyId,
);
}
/// List in dart is passed by pointer, so you need to copy it here
_tags.value = spi.tags?.toSet() ?? {};
_altUrlController.text = spi.alterUrl ?? '';
_autoConnect.value = spi.autoConnect;
_jumpServer.value = spi.jumpId;
final custom = spi.custom;
if (custom != null) {
_pveAddrCtrl.text = custom.pveAddr ?? '';
_pveIgnoreCert.value = custom.pveIgnoreCert;
_customCmds.value = custom.cmds ?? {};
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
_logoUrlCtrl.text = custom.logoUrl ?? '';
}
final wol = spi.wolCfg;
if (wol != null) {
_wolMacCtrl.text = wol.mac;
_wolIpCtrl.text = wol.ip;
_wolPwdCtrl.text = wol.pwd ?? '';
}
_env.value = spi.envs ?? {};
_netDevCtrl.text = spi.custom?.netDev ?? '';
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
}
Widget _buildWriteScriptTip() {
return Btn.tile(
text: libL10n.attention,
icon: const Icon(Icons.tips_and_updates, color: Colors.grey),
onTap: () {
context.showRoundDialog(
title: libL10n.attention,
child: SimpleMarkdown(data: l10n.writeScriptTip),
actions: Btnx.oks,
);
},
textStyle: UIs.textGrey,
mainAxisSize: MainAxisSize.min,
);
}
Widget _buildQrScan() {
return Btn.tile(
text: libL10n.import,
icon: const Icon(Icons.qr_code, color: Colors.grey),
onTap: () async {
final codes = await BarcodeScannerPage.route.go(
context,
args: const BarcodeScannerPageArgs(),
);
final code = codes?.firstOrNull?.rawValue;
if (code == null) return;
try {
final spi = Spi.fromJson(json.decode(code));
_initWithSpi(spi);
} catch (e, s) {
context.showErrDialog(e, s);
}
},
textStyle: UIs.textGrey,
mainAxisSize: MainAxisSize.min,
);
}
Widget _buildDelBtn() {
return IconButton(
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: StatefulBuilder(builder: (ctx, setState) {
return Text(libL10n.askContinue(
'${libL10n.delete} ${l10n.server}(${widget.spi!.name})',
));
}),
actions: Btn.ok(
onTap: () async {
context.pop();
ServerProvider.delServer(widget.spi!.id);
context.pop(true);
},
red: true,
).toList,
);
},
icon: const Icon(Icons.delete),
);
}
}

View File

@@ -4,21 +4,19 @@ import 'dart:math' as math;
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:provider/provider.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/widget/percent_circle.dart';
import '../../../core/route.dart';
import '../../../data/model/app/net_view.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/provider/server.dart';
import '../../widget/server_func_btns.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/net_view.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/provider/server.dart';
import 'package:server_box/view/widget/server_func_btns.dart';
class ServerPage extends StatefulWidget {
const ServerPage({super.key});
@@ -42,7 +40,7 @@ class _ServerPageState extends State<ServerPage>
Timer? _timer;
String? _tag;
final _tag = ''.vn;
bool _useDoubleColumn = false;
final _scrollController = ScrollController();
@@ -86,7 +84,11 @@ class _ServerPageState extends State<ServerPage>
Widget _buildPortrait() {
return Scaffold(
appBar: _buildTagsSwitcher(Pros.server),
appBar: TagSwitcher(
tags: ServerProvider.tags,
onTagChanged: (p0) => _tag.value = p0,
initTag: _tag.value,
),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _autoHideKey.currentState?.show(),
@@ -138,84 +140,73 @@ class _ServerPageState extends State<ServerPage>
}
Widget _buildLandscapeBody() {
return Consumer<ServerProvider>(builder: (_, pro, __) {
if (pro.serverOrder.isEmpty) {
return ServerProvider.serverOrder.listenVal((order) {
if (order.isEmpty) {
return Center(
child: Text(libL10n.empty, textAlign: TextAlign.center),
);
}
return PageView.builder(
itemCount: pro.serverOrder.length,
itemCount: order.length,
itemBuilder: (_, idx) {
final id = pro.serverOrder[idx];
final srv = pro.pick(id: id);
final id = order[idx];
final srv = ServerProvider.pick(id: id);
if (srv == null) return UIs.placeholder;
final title = _buildServerCardTitle(srv);
final List<Widget> children = [
title,
..._buildNormalCard(srv.status, srv.spi).joinWith(SizedBox(
height: _media.size.height / 10,
))
];
return srv.listenVal((srv) {
final title = _buildServerCardTitle(srv);
final List<Widget> children = [
title,
..._buildNormalCard(srv.status, srv.spi).joinWith(SizedBox(
height: _media.size.height / 10,
))
];
return Padding(
padding: _media.padding,
child: ListenableBuilder(
listenable: _getCardNoti(id),
builder: (_, __) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
},
),
);
return Padding(
padding: _media.padding,
child: ListenableBuilder(
listenable: _getCardNoti(id),
builder: (_, __) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
},
),
);
});
},
);
});
}
Widget _buildBody() {
final child = Consumer<ServerProvider>(
builder: (_, pro, __) {
if (!pro.tags.value.contains(_tag)) {
_tag = null;
}
if (pro.serverOrder.isEmpty) {
return ServerProvider.serverOrder.listenVal(
(order) {
if (order.isEmpty) {
return Center(
child: Text(libL10n.empty, textAlign: TextAlign.center),
);
}
final filtered = _filterServers(pro);
if (_useDoubleColumn &&
Stores.setting.doubleColumnServersPage.fetch()) {
return _buildBodyMedium(pro: pro, filtered: filtered);
}
return _buildBodySmall(pro: pro, filtered: filtered);
return _tag.listenVal(
(val) {
final filtered = _filterServers(order);
if (_useDoubleColumn &&
Stores.setting.doubleColumnServersPage.fetch()) {
return _buildBodyMedium(filtered);
}
return _buildBodySmall(filtered: filtered);
},
);
},
);
return child;
}
TagSwitcher _buildTagsSwitcher(ServerProvider provider) {
return TagSwitcher(
tags: provider.tags,
width: _media.size.width,
onTagChanged: (p0) => setState(() {
_tag = p0;
}),
initTag: _tag,
);
}
Widget _buildBodySmall({
required ServerProvider pro,
required List<String> filtered,
EdgeInsets? padding = const EdgeInsets.fromLTRB(7, 0, 7, 7),
}) {
@@ -227,15 +218,14 @@ class _ServerPageState extends State<ServerPage>
itemBuilder: (_, index) {
// Issue #130
if (index == count - 1) return UIs.height77;
return _buildEachServerCard(pro.pick(id: filtered[index]));
final vnode = ServerProvider.pick(id: filtered[index]);
if (vnode == null) return UIs.placeholder;
return vnode.listenVal(_buildEachServerCard);
},
);
}
Widget _buildBodyMedium({
required ServerProvider pro,
required List<String> filtered,
}) {
Widget _buildBodyMedium(List<String> filtered) {
final mid = (filtered.length / 2).ceil();
final filteredLeft = filtered.sublist(0, mid);
final filteredRight = filtered.sublist(mid);
@@ -243,14 +233,12 @@ class _ServerPageState extends State<ServerPage>
children: [
Expanded(
child: _buildBodySmall(
pro: pro,
filtered: filteredLeft,
padding: const EdgeInsets.only(left: 7),
),
),
Expanded(
child: _buildBodySmall(
pro: pro,
filtered: filteredRight,
padding: const EdgeInsets.only(right: 7),
),
@@ -265,7 +253,7 @@ class _ServerPageState extends State<ServerPage>
}
return CardX(
key: Key(srv.spi.id + (_tag ?? '')),
key: Key(srv.spi.id + _tag.value),
child: InkWell(
onTap: () {
if (srv.canViewDetails) {
@@ -416,7 +404,7 @@ class _ServerPageState extends State<ServerPage>
];
}
List<Widget> _buildNormalCard(ServerStatus ss, ServerPrivateInfo spi) {
List<Widget> _buildNormalCard(ServerStatus ss, Spi spi) {
return [
UIs.height13,
Row(
@@ -491,7 +479,7 @@ class _ServerPageState extends State<ServerPage>
),
() {
TryLimiter.reset(s.spi.id);
Pros.server.refresh(spi: s.spi);
ServerProvider.refresh(spi: s.spi);
},
),
ServerConn.disconnected => (
@@ -500,7 +488,7 @@ class _ServerPageState extends State<ServerPage>
size: 19,
color: Colors.grey,
),
() => Pros.server.refresh(spi: s.spi)
() => ServerProvider.refresh(spi: s.spi)
),
ServerConn.finished => (
const Icon(
@@ -508,7 +496,7 @@ class _ServerPageState extends State<ServerPage>
size: 17,
color: Colors.grey,
),
() => Pros.server.closeServer(id: s.spi.id),
() => ServerProvider.closeServer(id: s.spi.id),
),
_ when Stores.setting.serverTabUseOldUI.fetch() => (
ServerFuncBtnsTopRight(spi: s.spi),
@@ -599,18 +587,15 @@ ${ss.err?.message ?? 'null'}
Widget _buildNet(ServerStatus ss, String id) {
final cardNoti = _getCardNoti(id);
final type = cardNoti.value.net ?? Stores.setting.netViewType.fetch();
final (a, b) = type.build(ss);
final device = ServerProvider.pick(id: id)?.value.spi.custom?.netDev;
final (a, b) = type.build(ss, dev: device);
return AnimatedSwitcher(
duration: const Duration(milliseconds: 377),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(opacity: animation, child: child);
},
transitionBuilder: (c, anim) => FadeTransition(opacity: anim, child: c),
child: _buildIOData(
a,
b,
onTap: () {
cardNoti.value = cardNoti.value.copyWith(net: type.next);
},
onTap: () => cardNoti.value = cardNoti.value.copyWith(net: type.next),
key: ValueKey(type),
),
);
@@ -653,15 +638,19 @@ ${ss.err?.message ?? 'null'}
@override
Future<void> afterFirstLayout(BuildContext context) async {
await Pros.server.load();
Pros.server.startAutoRefresh();
ServerProvider.refresh();
ServerProvider.startAutoRefresh();
}
List<String> _filterServers(ServerProvider pro) => pro.serverOrder
.where((e) => pro.serverOrder.contains(e))
.where((e) =>
_tag == null || (pro.pick(id: e)?.spi.tags?.contains(_tag) ?? false))
.toList();
List<String> _filterServers(List<String> order) {
final tag = _tag.value;
if (tag == kDefaultTag) return order;
return order.where((e) {
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
if (tags == null) return false;
return tags.contains(tag);
}).toList();
}
static const _kCardHeightMin = 23.0;
static const _kCardHeightFlip = 99.0;
@@ -747,7 +736,7 @@ class _CardStatus {
}
extension _ServerX on Server {
String? getTopRightStr(ServerPrivateInfo spi) {
String? getTopRightStr(Spi spi) {
switch (conn) {
case ServerConn.disconnected:
return null;

View File

@@ -9,11 +9,10 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/res/url.dart';
import 'package:server_box/view/page/setting/platform/platform_pub.dart';
import '../../../core/route.dart';
import '../../../data/model/app/net_view.dart';
import '../../../data/res/build_data.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/net_view.dart';
import 'package:server_box/data/res/build_data.dart';
const _kIconSize = 23.0;
@@ -54,27 +53,28 @@ class _SettingPageState extends State<SettingPage> {
),
],
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 17),
body: MultiList(
widthDivider: 2.3,
thumbVisibility: true,
children: [
const CenterGreyTitle('App'),
_buildApp(),
CenterGreyTitle(l10n.server),
_buildServer(),
CenterGreyTitle(l10n.container),
_buildContainer(),
const CenterGreyTitle('SSH'),
_buildSSH(),
const CenterGreyTitle('SFTP'),
_buildSFTP(),
CenterGreyTitle(l10n.editor),
_buildEditor(),
[const CenterGreyTitle('App'), _buildApp()],
[CenterGreyTitle(l10n.server), _buildServer()],
[
const CenterGreyTitle('SSH'),
_buildSSH(),
const CenterGreyTitle('SFTP'),
_buildSFTP()
],
[
CenterGreyTitle(l10n.container),
_buildContainer(),
CenterGreyTitle(l10n.editor),
_buildEditor(),
],
/// Fullscreen Mode is designed for old mobile phone which can be
/// used as a status screen.
if (isMobile) CenterGreyTitle(l10n.fullScreen),
if (isMobile) _buildFullScreen(),
const SizedBox(height: 37),
if (isMobile) [CenterGreyTitle(l10n.fullScreen), _buildFullScreen()],
],
),
);
@@ -202,7 +202,7 @@ class _SettingPageState extends State<SettingPage> {
title: libL10n.setting,
items: List.generate(10, (idx) => idx == 1 ? null : idx),
initial: _setting.serverStatusUpdateInterval.fetch(),
name: (p0) => p0 == 0 ? l10n.manual : '$p0 ${l10n.second}',
display: (p0) => p0 == 0 ? l10n.manual : '$p0 ${l10n.second}',
);
if (val != null) {
_setting.serverStatusUpdateInterval.put(val);
@@ -331,7 +331,7 @@ class _SettingPageState extends State<SettingPage> {
final selected = await context.showPickSingleDialog(
title: l10n.maxRetryCount,
items: List.generate(10, (index) => index),
name: (p0) => '$p0 ${l10n.times}',
display: (p0) => '$p0 ${l10n.times}',
initial: val,
);
if (selected != null) {
@@ -356,7 +356,7 @@ class _SettingPageState extends State<SettingPage> {
final selected = await context.showPickSingleDialog(
title: libL10n.themeMode,
items: List.generate(len + 2, (index) => index),
name: (p0) => _buildThemeModeStr(p0),
display: (p0) => _buildThemeModeStr(p0),
initial: _setting.themeMode.fetch(),
);
if (selected != null) {
@@ -503,7 +503,7 @@ class _SettingPageState extends State<SettingPage> {
final selected = await context.showPickSingleDialog(
title: libL10n.language,
items: AppLocalizations.supportedLocales,
name: (p0) => p0.nativeName,
display: (p0) => p0.nativeName,
initial: _setting.locale.fetch().toLocale,
);
if (selected != null) {
@@ -543,7 +543,7 @@ class _SettingPageState extends State<SettingPage> {
final selected = await context.showPickSingleDialog(
title: l10n.theme,
items: themeMap.keys.toList(),
name: (p0) => p0,
display: (p0) => p0,
initial: _setting.editorTheme.fetch(),
);
if (selected != null) {
@@ -565,7 +565,7 @@ class _SettingPageState extends State<SettingPage> {
final selected = await context.showPickSingleDialog(
title: l10n.theme,
items: themeMap.keys.toList(),
name: (p0) => p0,
display: (p0) => p0,
initial: _setting.editorDarkTheme.fetch(),
);
if (selected != null) {
@@ -688,7 +688,7 @@ class _SettingPageState extends State<SettingPage> {
final selected = await context.showPickSingleDialog(
title: l10n.netViewType,
items: NetViewType.values,
name: (p0) => p0.toStr,
display: (p0) => p0.toStr,
initial: _setting.netViewType.fetch(),
);
if (selected != null) {
@@ -948,6 +948,7 @@ class _SettingPageState extends State<SettingPage> {
return ExpandTile(
leading: const Icon(MingCute.more_3_fill),
title: Text(l10n.more),
initiallyExpanded: isDesktop,
children: [
_buildRememberPwdInMem(),
_buildTextScaler(),
@@ -996,7 +997,7 @@ class _SettingPageState extends State<SettingPage> {
final selected = await context.showPickSingleDialog(
title: l10n.theme,
items: List.generate(3, (index) => index),
name: (p0) => index2Str(p0),
display: (p0) => index2Str(p0),
initial: _setting.termTheme.fetch(),
);
if (selected != null) {
@@ -1010,13 +1011,13 @@ class _SettingPageState extends State<SettingPage> {
return ExpandTile(
leading: const Icon(MingCute.more_3_fill),
title: Text(l10n.more),
initiallyExpanded: isDesktop,
children: [
_buildBeta(),
if (isMobile) _buildWakeLock(),
_buildCollapseUI(),
_buildCupertinoRoute(),
if (isDesktop) _buildHideTitleBar(),
if (isDesktop) PlatformPublicSettings.buildSaveWindowSize(),
],
);
}
@@ -1031,7 +1032,6 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildHideTitleBar() {
return ListTile(
title: Text(l10n.hideTitleBar),
subtitle: Text(l10n.hideTitleBarTip, style: UIs.textGrey),
trailing: StoreSwitch(prop: _setting.hideTitleBar),
);
}

View File

@@ -1,8 +1,6 @@
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/res/store.dart';
import 'package:window_manager/window_manager.dart';
abstract final class PlatformPublicSettings {
static Widget buildBioAuth() {
@@ -46,49 +44,4 @@ abstract final class PlatformPublicSettings {
},
);
}
static Widget buildSaveWindowSize() {
final isBusy = false.vn;
// Only show [FadeIn] when previous state is busy.
var lastIsBusy = false;
final prop = Stores.setting.windowSize;
return ListTile(
title: Text(l10n.rememberWindowSize),
/// Copied from `fl_build/view/store_switch`
trailing: ValBuilder(
listenable: isBusy,
builder: (busy) {
return ValBuilder(
listenable: prop.listenable(),
builder: (value) {
if (busy) {
lastIsBusy = true;
return UIs.centerSizedLoadingSmall.paddingOnly(right: 17);
}
final switcher = Switch(
value: value.isNotEmpty,
onChanged: (value) async {
isBusy.value = true;
final size = await windowManager.getSize();
isBusy.value = false;
prop.put(size.toIntStr());
},
);
if (lastIsBusy) {
final ret = FadeIn(child: switcher);
lastIsBusy = false;
return ret;
}
return switcher;
},
);
},
),
);
}
}

View File

@@ -2,7 +2,7 @@ import 'dart:ui';
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/res/provider.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/store.dart';
class ServerOrderPage extends StatefulWidget {
@@ -45,24 +45,28 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
}
Widget _buildBody() {
if (Pros.server.serverOrder.isEmpty) {
return Center(child: Text(libL10n.empty));
}
return ReorderableListView.builder(
footer: const SizedBox(height: 77),
onReorder: (oldIndex, newIndex) => setState(() {
Pros.server.serverOrder.move(
oldIndex,
newIndex,
property: Stores.setting.serverOrder,
);
}),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
buildDefaultDragHandles: false,
itemBuilder: (_, idx) => _buildItem(idx),
itemCount: Pros.server.serverOrder.length,
proxyDecorator: _proxyDecorator,
);
final orderNode = ServerProvider.serverOrder;
return orderNode.listenVal((order) {
if (order.isEmpty) {
return Center(child: Text(libL10n.empty));
}
return ReorderableListView.builder(
footer: const SizedBox(height: 77),
onReorder: (oldIndex, newIndex) => setState(() {
orderNode.value.move(
oldIndex,
newIndex,
property: Stores.setting.serverOrder,
);
orderNode.notify();
}),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
buildDefaultDragHandles: false,
itemBuilder: (_, idx) => _buildItem(idx),
itemCount: order.length,
proxyDecorator: _proxyDecorator,
);
});
}
Widget _buildItem(int index) {
@@ -74,8 +78,8 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
}
Widget _buildCardTile(int index) {
final id = Pros.server.serverOrder[index];
final spi = Pros.server.pick(id: id)?.spi;
final id = ServerProvider.serverOrder.value[index];
final spi = ServerProvider.pick(id: id)?.value.spi;
if (spi == null) {
return const SizedBox();
}

View File

@@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/provider/snippet.dart';
class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({super.key, this.snippet});
@@ -55,7 +56,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
)),
actions: Btn.ok(
onTap: () {
Pros.snippet.del(widget.snippet!);
SnippetProvider.del(widget.snippet!);
context.pop();
context.pop();
},
@@ -89,9 +90,9 @@ class _SnippetEditPageState extends State<SnippetEditPage>
autoRunOn: _autoRunOn.value.isEmpty ? null : _autoRunOn.value,
);
if (widget.snippet != null) {
Pros.snippet.update(widget.snippet!, snippet);
SnippetProvider.update(widget.snippet!, snippet);
} else {
Pros.snippet.add(snippet);
SnippetProvider.add(snippet);
}
context.pop();
},
@@ -120,7 +121,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
icon: Icons.note,
suggestion: true,
),
TagTile(tags: _tags, allTags: Pros.snippet.tags.value).cardx,
TagTile(tags: _tags, allTags: SnippetProvider.tags.value).cardx,
Input(
controller: _scriptController,
node: _scriptNode,
@@ -145,7 +146,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
final subtitle = vals.isEmpty
? null
: vals
.map((e) => Pros.server.pick(id: e)?.spi.name ?? e)
.map((e) => ServerProvider.pick(id: e)?.value.spi.name ?? e)
.join(', ');
return ListTile(
leading: const Padding(
@@ -163,11 +164,12 @@ class _SnippetEditPageState extends State<SnippetEditPage>
overflow: TextOverflow.ellipsis,
),
onTap: () async {
vals.removeWhere((e) => !Pros.server.serverOrder.contains(e));
vals.removeWhere(
(e) => !ServerProvider.serverOrder.value.contains(e));
final serverIds = await context.showPickDialog(
title: l10n.autoRun,
items: Pros.server.serverOrder,
name: (e) => Pros.server.pick(id: e)?.spi.name ?? e,
items: ServerProvider.serverOrder.value,
display: (e) => ServerProvider.pick(id: e)?.value.spi.name ?? e,
initial: vals,
clearable: true,
);

View File

@@ -1,11 +1,10 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:server_box/data/res/store.dart';
import '../../../data/model/server/snippet.dart';
import '/core/route.dart';
import '/data/provider/snippet.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/provider/snippet.dart';
class SnippetListPage extends StatefulWidget {
const SnippetListPage({super.key});
@@ -15,19 +14,16 @@ class SnippetListPage extends StatefulWidget {
}
class _SnippetListPageState extends State<SnippetListPage> {
late MediaQueryData _media;
String? _tag;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
}
final _tag = ''.vn;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TagSwitcher(
tags: SnippetProvider.tags,
onTagChanged: (tag) => _tag.value = tag,
initTag: _tag.value,
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
heroTag: 'snippetAdd',
@@ -38,46 +34,42 @@ class _SnippetListPageState extends State<SnippetListPage> {
}
Widget _buildBody() {
return Consumer<SnippetProvider>(
builder: (_, provider, __) {
if (provider.snippets.isEmpty) {
return Center(child: Text(libL10n.empty));
}
return SnippetProvider.snippets.listenVal(
(snippets) {
if (snippets.isEmpty) return Center(child: Text(libL10n.empty));
return _tag.listenVal((tag) => _buildSnippetList(snippets, tag));
},
);
}
final filtered = provider.snippets
.where((e) => _tag == null || (e.tags?.contains(_tag) ?? false))
.toList();
Widget _buildSnippetList(List<Snippet> snippets, String tag) {
final filtered = tag == kDefaultTag
? snippets
: snippets.where((e) => e.tags?.contains(tag) ?? false).toList();
return ReorderableListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 11),
itemCount: filtered.length,
onReorder: (oldIdx, newIdx) => setState(() {
provider.snippets.moveByItem(
oldIdx,
newIdx,
filtered: filtered,
onMove: (p0) {
Stores.setting.snippetOrder.put(p0.map((e) => e.name).toList());
},
);
}),
header: TagSwitcher(
tags: provider.tags,
onTagChanged: (tag) => setState(() => _tag = tag),
initTag: _tag,
width: _media.size.width,
),
footer: UIs.height77,
buildDefaultDragHandles: false,
itemBuilder: (context, idx) {
final snippet = filtered.elementAt(idx);
return ReorderableDelayedDragStartListener(
key: ValueKey(idx),
index: idx,
child: _buildSnippetItem(snippet),
);
return ReorderableListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 9),
itemCount: filtered.length,
onReorder: (oldIdx, newIdx) {
snippets.moveByItem(
oldIdx,
newIdx,
filtered: filtered,
onMove: (p0) {
Stores.setting.snippetOrder.put(p0.map((e) => e.name).toList());
},
);
SnippetProvider.snippets.notify();
},
footer: UIs.height77,
buildDefaultDragHandles: false,
itemBuilder: (context, idx) {
final snippet = filtered.elementAt(idx);
return ReorderableDelayedDragStartListener(
key: ValueKey(idx),
index: idx,
child: _buildSnippetItem(snippet),
);
},
);
}

View File

@@ -11,27 +11,28 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/provider/snippet.dart';
import 'package:server_box/data/provider/virtual_keyboard.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:xterm/core.dart';
import 'package:xterm/ui.dart' hide TerminalThemes;
import '../../../core/route.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/model/ssh/virtual_key.dart';
import '../../../data/res/terminal.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/ssh/virtual_key.dart';
import 'package:server_box/data/res/terminal.dart';
const _echoPWD = 'echo \$PWD';
class SSHPage extends StatefulWidget {
final ServerPrivateInfo spi;
final Spi spi;
final String? initCmd;
final Snippet? initSnippet;
final bool notFromTab;
final Function()? onSessionEnd;
final GlobalKey<TerminalViewState>? terminalKey;
final FocusNode? focusNode;
const SSHPage({
super.key,
@@ -41,10 +42,9 @@ class SSHPage extends StatefulWidget {
this.notFromTab = true,
this.onSessionEnd,
this.terminalKey,
this.focusNode,
});
static final focusNode = FocusNode();
@override
State<SSHPage> createState() => SSHPageState();
}
@@ -68,7 +68,7 @@ class SSHPageState extends State<SSHPage>
bool _isDark = false;
Timer? _virtKeyLongPressTimer;
late SSHClient? _client = widget.spi.server?.client;
late SSHClient? _client = widget.spi.server?.value.client;
Timer? _discontinuityTimer;
@override
@@ -158,7 +158,7 @@ class SSHPageState extends State<SSHPage>
CustomAppBar.barHeight ?? _media.padding.top,
),
hideScrollBar: false,
focusNode: SSHPage.focusNode,
focusNode: widget.focusNode,
),
),
);
@@ -298,14 +298,14 @@ class SSHPageState extends State<SSHPage>
case VirtualKeyFunc.snippet:
final snippets = await context.showPickWithTagDialog<Snippet>(
title: l10n.snippet,
tags: Pros.snippet.tags,
tags: SnippetProvider.tags,
itemsBuilder: (e) {
if (e == null) return Pros.snippet.snippets;
return Pros.snippet.snippets
if (e == kDefaultTag) return SnippetProvider.snippets.value;
return SnippetProvider.snippets.value
.where((element) => element.tags?.contains(e) ?? false)
.toList();
},
name: (e) => e.name,
display: (e) => e.name,
);
if (snippets == null || snippets.isEmpty) return;
@@ -417,7 +417,7 @@ class SSHPageState extends State<SSHPage>
_initService();
for (final snippet in Pros.snippet.snippets) {
for (final snippet in SnippetProvider.snippets.value) {
if (snippet.autoRunOn?.contains(widget.spi.id) == true) {
snippet.runInTerm(_terminal, widget.spi);
}
@@ -432,7 +432,7 @@ class SSHPageState extends State<SSHPage>
widget.initSnippet!.runInTerm(_terminal, widget.spi);
}
SSHPage.focusNode.requestFocus();
widget.focusNode?.requestFocus();
await session.done;
if (mounted && widget.notFromTab) {

View File

@@ -2,12 +2,10 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:provider/provider.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/view/page/ssh/page.dart';
class SSHTabPage extends StatefulWidget {
@@ -17,12 +15,12 @@ class SSHTabPage extends StatefulWidget {
State<SSHTabPage> createState() => _SSHTabPageState();
}
typedef _TabMap = Map<String, ({Widget page, GlobalKey<SSHPageState>? key})>;
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
class _SSHTabPageState extends State<SSHTabPage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
late final _TabMap _tabMap = {
libL10n.add: (page: _buildAddPage(), key: null),
libL10n.add: (page: _buildAddPage(), focus: null),
};
final _pageCtrl = PageController();
final _fabVN = 0.vn;
@@ -61,12 +59,9 @@ class _SSHTabPageState extends State<SSHTabPage>
void _onTapTab(int idx) async {
await _toPage(idx);
SSHPage.focusNode.unfocus();
}
void _onTapClose(String name) async {
SSHPage.focusNode.unfocus();
final confirm = await showDialog<bool>(
context: context,
builder: (context) {
@@ -89,16 +84,19 @@ class _SSHTabPageState extends State<SSHTabPage>
Widget _buildAddPage() {
return Center(
key: const Key('sshTabAddServer'),
child: Consumer<ServerProvider>(builder: (_, pro, __) {
if (pro.serverOrder.isEmpty) {
child: ServerProvider.serverOrder.listenVal((order) {
if (order.isEmpty) {
return Center(
child: Text(libL10n.empty, textAlign: TextAlign.center),
);
}
final ratio = context.media.size.aspectRatio;
return GridView.builder(
padding: const EdgeInsets.all(7),
cacheExtent: 50,
itemBuilder: (context, idx) {
final spi = Pros.server.pick(id: pro.serverOrder[idx])?.spi;
final spi = ServerProvider.pick(id: order[idx])?.value.spi;
if (spi == null) return UIs.placeholder;
return CardX(
child: InkWell(
@@ -109,10 +107,7 @@ class _SSHTabPageState extends State<SSHTabPage>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
spi.name,
style: Theme.of(context).textTheme.bodyLarge,
),
Text(spi.name, style: UIs.text18),
const Icon(Icons.chevron_right)
],
),
@@ -120,10 +115,10 @@ class _SSHTabPageState extends State<SSHTabPage>
),
);
},
itemCount: pro.servers.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
itemCount: ServerProvider.servers.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3,
childAspectRatio: 3 * (ratio / (9 / 16)),
crossAxisSpacing: 3,
mainAxisSpacing: 3,
),
@@ -150,7 +145,7 @@ class _SSHTabPageState extends State<SSHTabPage>
);
}
void _onTapInitCard(ServerPrivateInfo spi) async {
void _onTapInitCard(Spi spi) async {
final name = () {
final reg = RegExp('${spi.name}\\((\\d+)\\)');
final idxs = _tabMap.keys
@@ -178,7 +173,7 @@ class _SSHTabPageState extends State<SSHTabPage>
_tabMap.remove(name);
},
),
key: key,
focus: FocusNode(),
);
_tabRN.notify();
// Wait for the page to be built
@@ -187,8 +182,14 @@ class _SSHTabPageState extends State<SSHTabPage>
await _toPage(idx);
}
Future<void> _toPage(int idx) => _pageCtrl.animateToPage(idx,
duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut);
Future<void> _toPage(int idx) async {
await _pageCtrl.animateToPage(idx,
duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut);
final focus = _tabMap.values.elementAt(idx).focus;
if (focus != null) {
FocusScope.of(context).requestFocus(focus);
}
}
@override
bool get wantKeepAlive => true;
@@ -221,7 +222,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 5),
itemCount: names.length,
itemBuilder: (_, idx) => _buillItem(idx),
itemBuilder: (_, idx) => _buildItem(idx),
separatorBuilder: (_, __) => Padding(
padding: const EdgeInsets.symmetric(vertical: 17),
child: Container(
@@ -234,7 +235,10 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
);
}
Widget _buillItem(int idx) {
static const kWideWidth = 90.0;
static const kNarrowWidth = 60.0;
Widget _buildItem(int idx) {
final name = names[idx];
final selected = idxVN.value == idx;
final color = selected ? null : Colors.grey;
@@ -255,31 +259,28 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
textAlign: TextAlign.center,
textWidthBasis: TextWidthBasis.parent,
);
final Widget btn;
if (selected) {
btn = Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Btn.icon(
icon: Icon(MingCute.close_circle_fill, color: color, size: 17),
onTap: () => onClose(name),
padding: null,
),
SizedBox(width: kNarrowWidth - 10, child: text),
],
);
} else {
btn = Center(child: text);
}
child = AnimatedContainer(
width: selected ? 90 : 57,
width: selected ? kWideWidth : kNarrowWidth,
duration: Durations.medium3,
padding: selected ? const EdgeInsets.symmetric(horizontal: 7) : null,
curve: Curves.fastEaseInToSlowEaseOut,
child: switch (selected) {
true => Row(
children: [
if (selected)
FadeIn(
child: Btn.icon(
icon: Icon(
MingCute.close_circle_fill,
color: color,
size: 17,
),
onTap: () => onClose(name),
),
),
const Spacer(),
SizedBox(width: 50, child: text),
],
),
false => Center(child: text),
},
child: btn,
);
}

View File

@@ -5,12 +5,13 @@ 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/sftp/worker.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/provider/sftp.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/view/widget/omit_start_text.dart';
import '../../../core/route.dart';
import '../../../data/model/app/path_with_prefix.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/path_with_prefix.dart';
class LocalStoragePage extends StatefulWidget {
final bool isPickFile;
@@ -282,12 +283,13 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
onTap: () async {
context.pop();
final spi = await context.showPickSingleDialog<ServerPrivateInfo>(
final spi = await context.showPickSingleDialog<Spi>(
title: libL10n.select,
items: Pros.server.serverOrder
.map((e) => Pros.server.pick(id: e)?.spi)
items: ServerProvider.serverOrder.value
.map((e) => ServerProvider.pick(id: e)?.value.spi)
.whereType<Spi>()
.toList(),
name: (e) => e.name,
display: (e) => e.name,
);
if (spi == null) return;
@@ -299,7 +301,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
return;
}
Pros.sftp.add(SftpReq(
SftpProvider.add(SftpReq(
spi,
'$remotePath/$fileName',
file.absolute.path,

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