Compare commits

...

78 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
749fd4d800 fix: ci 2025-01-29 23:56:27 +08:00
lollipopkit🏳️‍⚧️
bec4a3b314 chore: bump version 2025-01-29 23:44:44 +08:00
lollipopkit🏳️‍⚧️
9e5babec76 opt.: close after saving (#684) 2025-01-29 15:10:50 +08:00
lollipopkit🏳️‍⚧️
dbbb10364b fix: webdav settings (#683) 2025-01-29 13:13:12 +08:00
lollipopkit🏳️‍⚧️
16948c3e0f new: provide .deb & .rpm 2025-01-29 12:56:03 +08:00
lollipopkit🏳️‍⚧️
e39fb23b66 bug: unix perm switcher (#674) 2025-01-14 11:19:47 +08:00
lollipopkit🏳️‍⚧️
4777166dd9 migrate: fl_lib v235 2025-01-13 21:57:12 +08:00
Noo6
0ae0241800 opt: window title bar (#672)
* opt: window title bar

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

* opt: `local.dart` fmt

---------

Co-authored-by: Noo6 <72285529+No06@users.noreply.github.com>
2024-11-14 14:42:15 +08:00
Noo6
d706886343 fix: sftp open file on windows (#633) 2024-11-14 14:24:57 +08:00
Noo6
7dda63af8a fix: add file on windows 2024-11-14 11:20:45 +08:00
lollipopkit🏳️‍⚧️
00d303ac36 fix: editing pref store (#618) 2024-10-29 19:37:19 +08:00
lollipopkit🏳️‍⚧️
229983d82e chore: bump version 2024-10-17 14:06:45 +08:00
Mased
4928ca600d [Translation] some Russian fixes (#604) 2024-10-07 21:04:36 +08:00
lollipopkit🏳️‍⚧️
89ec2d94d6 opt.: virt keys loading 2024-10-05 10:18:00 +08:00
lollipopkit🏳️‍⚧️
393c3e6388 Merge branch 'main' of github.com:lollipopkit/flutter_server_box 2024-10-05 10:00:41 +08:00
Gitro
dee458e926 new: material you icon (#599) 2024-10-04 12:19:10 +08:00
lollipopkit🏳️‍⚧️
f89228db40 opt.: ssh page 2024-09-28 17:09:35 +08:00
lollipopkit🏳️‍⚧️
3b6fb6194b opt.: more virtual keys (#596) 2024-09-28 14:40:22 +08:00
lollipopkit🏳️‍⚧️
02444fc2f0 new: ios18 dark app icon (#594)
Fixes #593
2024-09-25 19:16:14 +08:00
lollipopkit🏳️‍⚧️
aef317a140 opt.: dismiss notification if no ssh conn (#592) 2024-09-24 22:01:35 +08:00
lollipopkit🏳️‍⚧️
47aedb2f2e fix: rename file 2024-09-22 16:06:09 +08:00
lollipopkit🏳️‍⚧️
eab06abcaf opt. 2024-09-21 23:12:15 +08:00
lollipopkit🏳️‍⚧️
c062c12a0e opt.: redesigned settings page (#587) 2024-09-21 22:37:42 +08:00
lollipopkit🏳️‍⚧️
d7669c94b8 opt.: spi with same id (#585) 2024-09-21 11:54:56 +08:00
lollipopkit🏳️‍⚧️
50af289574 migrate: fl_lib 2024-09-21 11:01:41 +08:00
lollipopkit🏳️‍⚧️
90b88ed795 opt.: sync immediately after changes (#577) 2024-09-14 17:08:51 +08:00
CakesTwix
d611fdcd50 l10n: Added Ukrainian lang (#574) 2024-09-14 14:48:23 +08:00
lollipopkit🏳️‍⚧️
db9b2dd818 fix: snippet fmt (#570) 2024-09-01 21:07:32 +08:00
lollipopkit🏳️‍⚧️
edb49ead67 opt.: split webdav & other settings (#569) 2024-08-31 21:45:09 +08:00
lollipopkit🏳️‍⚧️
7f0dc656b8 Merge pull request #568 from lollipopkit/lollipopkit/issue564 2024-08-31 19:36:50 +08:00
lollipopkit🏳️‍⚧️
b33d0bbc3e opt.: back btn on scan page
Fixes #564
2024-08-31 19:33:57 +08:00
lollipopkit🏳️‍⚧️
7d0ea8a58b Merge branch 'android_service' 2024-08-31 19:28:04 +08:00
Noo6
c18732d8f3 fix: backup on windows (#563) 2024-08-30 22:44:08 +08:00
Shin
157af0a354 l10n: fixed Japanese translations. (#558) 2024-08-30 11:35:13 +08:00
lollipopkit🏳️‍⚧️
2d9dc044f9 new: custom foreground service on Android (#556) 2024-08-28 16:42:09 +08:00
lollipopkit🏳️‍⚧️
479250c207 init 2024-08-28 13:44:54 +08:00
Noo6
aef7ec911f opt: ssh tab page 2024-08-28 13:18:21 +08:00
lollipopkit🏳️‍⚧️
4f9ee7781f fix: ssh tab page UI (#555) 2024-08-27 17:20:47 +08:00
lollipopkit🏳️‍⚧️
eb83d05c81 fix: ssh alter url (#554) 2024-08-27 15:22:26 +08:00
lollipopkit🏳️‍⚧️
329fd33b69 fix: lang switch (#553) 2024-08-27 14:36:00 +08:00
lollipopkit🏳️‍⚧️
931c5f0bf6 opt.: alterUrl (#550) 2024-08-25 22:52:47 +08:00
lollipopkit🏳️‍⚧️
bcbf1fbc17 fix: fdroid build (#548) 2024-08-25 22:06:55 +08:00
Noo6
3e7315dac6 fix: ssh page displays the CustomAppBar on desktop 2024-08-18 20:04:39 +08:00
Noo6
4cecfdf7a8 opt: ssh card text overflow 2024-08-18 13:57:20 +08:00
Noo6
0346821cf5 opt: windows and linux drag area 2024-08-18 13:27:16 +08:00
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
208 changed files with 16959 additions and 4682 deletions

15
.github/FUNDING.yml vendored
View File

@@ -1,14 +1 @@
# These are supported funding model platforms custom: ['https://cdn.lpkt.cn/donate']
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

View File

@@ -19,7 +19,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.22.3' flutter-version: '3.27.3'
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: 'zulu'
@@ -53,14 +53,31 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Flutter - name: Install Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
- name: Install dependencies
run: |
sudo apt update
# Basic
sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev libvulkan-dev desktop-file-utils
# App Specific
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev
# Packaging
sudo apt install -y rpm patchelf
- name: Build - name: Build
run: | run: |
dart run fl_build -p linux dart run fl_build
dart run flutter_distributor:main release --name linux --skip-clean
- name: Rename artifacts
run: |
deb_name=$(ls dist/*/*.deb)
mv $deb_name ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.deb
rpm_name=$(ls dist/*/*.rpm)
mv $rpm_name ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.rpm
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.deb
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.rpm
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -46,6 +46,7 @@ app.*.map.json
/android/app/release /android/app/release
/android/app/fjy.androidstudio.key /android/app/fjy.androidstudio.key
/android/app/app.key
/release /release
test.dart test.dart

View File

@@ -2,54 +2,54 @@ English | [简体中文](README_zh.md)
<h2 align="center">Flutter Server Box</h2> <h2 align="center">Flutter Server Box</h2>
<p align="center"> <div align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink"> <a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink"> <img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
</p> <img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow">
</div>
<p align="center"> <p align="center">
A Flutter project which provide charts to display <a href="../../issues/43">Linux</a> server status and tools to manage server. A Flutter project which provide charts to display <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> server status and tools to manage server.
<br> <br>
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>. Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
</p> </p>
## 📥 Install ## 🏙️ Screenshots
Platform | From
--- | ---
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
**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
## 🔖 Feature
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process`...
- 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> <table>
<tr> <tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/1.png"></td> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/2.png"></td> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/3.png"></td> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
</tr> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
<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> </tr>
</table> </table>
## 📥 Install
| Platform | From |
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) |
| Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) |
| Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) |
Please only download pkgs from the source that **you trust**!
## 🔖 Feature
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
- Platform specific: `Bio auth``Msg push``Home widget``watchOS App`...
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)
## 🆘 Help ## 🆘 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. - 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). - **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki).

View File

@@ -2,68 +2,66 @@
<h2 align="center">Flutter Server Box</h2> <h2 align="center">Flutter Server Box</h2>
<p align="center"> <div align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink"> <a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink"> <img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
</p> <img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow">
</div>
<p align="center"> <p align="center">
使用 Flutter 开发的 <a href="../../issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。 使用 Flutter 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
<br> <br>
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a> 特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>
</p> </p>
## 📥 安装
平台 | 下载
--- | ---
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 & 进程` 管理...
- 特殊支持:`生物认证``推送``桌面小部件``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> <table>
<tr> <tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/1.png"></td> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/2.png"></td> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/3.png"></td> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
</tr> <td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
<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> </tr>
</table> </table>
## 📥 安装
平台 | 下载
----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)
请从 **信任** 的来源下载!
## 🔖 特点
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
- 特殊支持:`生物认证``推送``桌面小部件``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), Українська мова [@CakesTwix](https://github.com/CakesTwix);
- 感谢贡献者们!
## 🆘 帮助 ## 🆘 帮助
- 吹水、参与开发、了解如何使用QQ群 **762870488** <div align="center">
- 为了可以在不使用 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)。 <a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
- **常见问题**可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。 <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 模版提交。 1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。
2. 反馈问题前请检查是否是 serverbox 的问题。 2. 反馈问题前请检查是否是 serverbox 的问题。
3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受 3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受
确认了解上述内容后,请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
## 🧱 贡献 ## 🧱 贡献
任何正面的贡献都欢迎。 任何正面的贡献都欢迎。
@@ -74,7 +72,7 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
3. 运行 `dart run fl_build -p PLATFORM` 构建应用 3. 运行 `dart run fl_build -p PLATFORM` 构建应用
### 翻译 ### 翻译
[指南](https://blog.lolli.tech/faq/) 可在我的博客中找到。 [指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。
## 💡 我的其它 Apps ## 💡 我的其它 Apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。 - [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。

View File

@@ -30,14 +30,19 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file # `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint. # producing the lint.
rules: rules:
library_private_types_in_public_api: false library_private_types_in_public_api: true
use_build_context_synchronously: false use_build_context_synchronously: false
depend_on_referenced_packages: false depend_on_referenced_packages: false
prefer_final_locals: true prefer_final_locals: true
unnecessary_parenthesis: true unnecessary_parenthesis: true
implicit_call_tearoffs: 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 # 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 # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@@ -53,7 +53,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "tech.lolli.toolbox" applicationId "tech.lolli.toolbox"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
@@ -87,10 +86,12 @@ android {
debug { debug {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
resValue "string", "app_name", "SrvBxD"
} }
profile { profile {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
resValue "string", "app_name", "SrvBxP"
} }
} }
} }

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -10,12 +11,13 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:label="ServerBox" android:label="@string/app_name"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:allowBackup="true" android:allowBackup="true"
android:hasFragileUserData="true" android:hasFragileUserData="true"
android:restoreAnyVersion="true"> android:restoreAnyVersion="true"
tools:targetApi="q">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -29,12 +31,12 @@
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
@@ -43,11 +45,6 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="dataSync"
/>
<receiver <receiver
android:name=".widget.HomeWidget" android:name=".widget.HomeWidget"
android:exported="false" android:exported="false"
@@ -67,7 +64,12 @@
android:resource="@xml/home_widget" /> android:resource="@xml/home_widget" />
</receiver> </receiver>
<service android:name=".KeepAliveService"/> <service
android:name=".ForegroundService"
android:enabled="true"
android:foregroundServiceType="dataSync"
android:exported="false" />
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and https://developer.android.com/training/package-visibility?hl=en and
@@ -76,8 +78,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain" />
</intent> </intent>
</queries> </queries>
</manifest> </manifest>

View File

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

View File

@@ -1,18 +0,0 @@
package tech.lolli.toolbox
import android.app.Service
import android.content.Intent
import android.os.IBinder
import org.jetbrains.annotations.Nullable
class KeepAliveService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
@Nullable
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

View File

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

View File

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

View File

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

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

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

View File

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

View File

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

View File

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

13
distribute_options.yaml Normal file
View File

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

View File

@@ -1,24 +1,31 @@
PODS: PODS:
- device_info_plus (0.0.1): - app_links (0.0.2):
- Flutter
- camera_avfoundation (0.0.1):
- Flutter - Flutter
- file_picker (0.0.1): - file_picker (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_background_service_ios (0.0.3): - flutter_inappwebview_ios (0.0.1):
- Flutter - Flutter
- flutter_native_splash (0.0.1): - flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- flutter_native_splash (2.4.3):
- Flutter - Flutter
- icloud_storage (0.0.1): - icloud_storage (0.0.1):
- Flutter - Flutter
- local_auth_darwin (0.0.1): - local_auth_darwin (0.0.1):
- Flutter - Flutter
- FlutterMacOS
- OrderedSet (6.0.3)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- plain_notification_token (0.0.1): - plain_notification_token (0.0.1):
- Flutter - Flutter
- share_plus (0.0.1): - share_plus (0.0.1):
@@ -34,16 +41,16 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - app_links (from `.symlinks/plugins/app_links/ios`)
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- icloud_storage (from `.symlinks/plugins/icloud_storage/ios`) - icloud_storage (from `.symlinks/plugins/icloud_storage/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - 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`) - plain_notification_token (from `.symlinks/plugins/plain_notification_token/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -51,15 +58,21 @@ DEPENDENCIES:
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`) - watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
SPEC REPOS:
trunk:
- OrderedSet
EXTERNAL SOURCES: EXTERNAL SOURCES:
device_info_plus: app_links:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/app_links/ios"
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_background_service_ios: flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_background_service_ios/ios" :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
icloud_storage: icloud_storage:
@@ -70,8 +83,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
plain_notification_token: plain_notification_token:
:path: ".symlinks/plugins/plain_notification_token/ios" :path: ".symlinks/plugins/plain_notification_token/ios"
share_plus: share_plus:
@@ -86,18 +97,19 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/watch_connectivity/ios" :path: ".symlinks/plugins/watch_connectivity/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287 file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
local_auth_darwin: 4d56c90c2683319835a61274b57620df9c4520ab local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1 plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
@@ -105,4 +117,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8 PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8
COCOAPODS: 1.15.2 COCOAPODS: 1.16.2

View File

@@ -302,7 +302,6 @@
E33A3E4A2A626DD0009744AB /* Embed Foundation Extensions */, E33A3E4A2A626DD0009744AB /* Embed Foundation Extensions */,
E39515D52AB5AD64003602C1 /* Embed Watch Content */, E39515D52AB5AD64003602C1 /* Embed Watch Content */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
955896919A10AA2BEC131F36 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -452,23 +451,6 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
955896919A10AA2BEC131F36 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@@ -690,7 +672,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -700,7 +682,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -826,7 +808,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -836,7 +818,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -854,7 +836,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -864,7 +846,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -885,7 +867,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -898,7 +880,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -924,7 +906,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -937,7 +919,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -960,7 +942,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -973,7 +955,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -996,7 +978,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1008,7 +990,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
@@ -1037,7 +1019,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1049,7 +1031,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;
@@ -1075,7 +1057,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1051; CURRENT_PROJECT_VERSION = 1128;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1087,7 +1069,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1051; MARKETING_VERSION = 1.0.1128;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1 +1,37 @@
{"images":[{"scale":"3x","idiom":"universal","filename":"AppIcon-29.0x29.0@3x.png","size":"29x29","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-29.0x29.0@2x.png","size":"29x29","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-64.0x64.0@3x.png","size":"64x64","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-20.0x20.0@2x.png","size":"20x20","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-60.0x60.0@2x.png","size":"60x60","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-40.0x40.0@3x.png","size":"40x40","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-76.0x76.0@2x.png","size":"76x76","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-38.0x38.0@3x.png","size":"38x38","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-68.0x68.0@2x.png","size":"68x68","platform":"ios"},{"scale":"1x","idiom":"universal","filename":"AppIcon-1024.0x1024.0@1x.png","size":"1024x1024","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-64.0x64.0@2x.png","size":"64x64","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-40.0x40.0@2x.png","size":"40x40","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-83.5x83.5@2x.png","size":"83.5x83.5","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-20.0x20.0@3x.png","size":"20x20","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-38.0x38.0@2x.png","size":"38x38","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-60.0x60.0@3x.png","size":"60x60","platform":"ios"}],"info":{"version":1,"author":"appicon"}} {
"images" : [
{
"filename" : "Icon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "icon-1024 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

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

View File

@@ -3,7 +3,7 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true />
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
@@ -28,13 +28,13 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false />
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true />
<key>LSSupportsOpeningDocumentsInPlace</key> <key>LSSupportsOpeningDocumentsInPlace</key>
<true/> <true />
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true />
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
@@ -44,7 +44,7 @@
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIStatusBarHidden</key> <key>UIStatusBarHidden</key>
<false/> <false />
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@@ -59,8 +59,13 @@
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false />
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Required for auth</string> <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> </dict>
</plist> </plist>

View File

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

View File

@@ -1,12 +1,13 @@
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:fl_lib/l10n/gen_l10n/lib_l10n.dart'; import 'package:fl_lib/generated/l10n/lib_l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/build_data.dart'; import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/rebuild.dart'; import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/generated/l10n/l10n.dart';
import 'package:server_box/view/page/home/home.dart'; import 'package:server_box/view/page/home/home.dart';
import 'package:icons_plus/icons_plus.dart'; import 'package:icons_plus/icons_plus.dart';
@@ -74,6 +75,7 @@ class MyApp extends StatelessWidget {
final locale = Stores.setting.locale.fetch().toLocale; final locale = Stores.setting.locale.fetch().toLocale;
return MaterialApp( return MaterialApp(
key: ValueKey(locale),
locale: locale, locale: locale,
localizationsDelegates: const [ localizationsDelegates: const [
LibLocalizations.delegate, LibLocalizations.delegate,
@@ -86,19 +88,21 @@ class MyApp extends StatelessWidget {
themeMode: themeMode, themeMode: themeMode,
theme: light.fixWindowsFont, theme: light.fixWindowsFont,
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont, darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
home: Builder( home: VirtualWindowFrame(
builder: (context) { child: Builder(
context.setLibL10n(); builder: (context) {
final appL10n = AppLocalizations.of(context); context.setLibL10n();
if (appL10n != null) l10n = appL10n; final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
final intros = _IntroPage.builders; final intros = _IntroPage.builders;
if (intros.isNotEmpty) { if (intros.isNotEmpty) {
return _IntroPage(intros); return _IntroPage(intros);
} }
return const HomePage(); return const HomePage();
}, },
),
), ),
); );
} }

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

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
import 'package:server_box/data/res/build_data.dart';
extension BuildDataX on BuildData {
static const versionStr = 'v1.0.${BuildData.build}';
}

View File

@@ -1,4 +1,4 @@
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:server_box/generated/l10n/l10n.dart';
import 'package:flutter_gen/gen_l10n/l10n_en.dart'; import 'package:server_box/generated/l10n/l10n_en.dart';
AppLocalizations l10n = AppLocalizationsEn(); AppLocalizations l10n = AppLocalizationsEn();

View File

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

View File

@@ -1,17 +1,13 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/store.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'; import 'package:server_box/view/page/container.dart';
import 'package:server_box/view/page/home/home.dart'; import 'package:server_box/view/page/home/home.dart';
import 'package:server_box/view/page/iperf.dart'; import 'package:server_box/view/page/iperf.dart';
import 'package:server_box/view/page/ping.dart'; import 'package:server_box/view/page/ping.dart';
import 'package:server_box/view/page/private_key/edit.dart'; import 'package:server_box/view/page/private_key/edit.dart';
import 'package:server_box/view/page/private_key/list.dart';
import 'package:server_box/view/page/pve.dart'; import 'package:server_box/view/page/pve.dart';
import 'package:server_box/view/page/server/detail/view.dart'; import 'package:server_box/view/page/server/detail/view.dart';
import 'package:server_box/view/page/setting/platform/android.dart'; import 'package:server_box/view/page/setting/platform/android.dart';
@@ -20,20 +16,14 @@ import 'package:server_box/view/page/setting/seq/srv_func_seq.dart';
import 'package:server_box/view/page/snippet/result.dart'; import 'package:server_box/view/page/snippet/result.dart';
import 'package:server_box/view/page/ssh/page.dart'; import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/setting/seq/virt_key.dart'; import 'package:server_box/view/page/setting/seq/virt_key.dart';
import 'package:server_box/view/page/storage/local.dart'; import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/view/page/process.dart';
import '../data/model/server/snippet.dart'; import 'package:server_box/view/page/server/tab.dart';
import '../view/page/editor.dart'; import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
import '../view/page/process.dart'; import 'package:server_box/view/page/setting/seq/srv_seq.dart';
import '../view/page/server/edit.dart'; import 'package:server_box/view/page/snippet/edit.dart';
import '../view/page/server/tab.dart'; import 'package:server_box/view/page/storage/sftp.dart';
import '../view/page/setting/entry.dart'; import 'package:server_box/view/page/storage/sftp_mission.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';
class AppRoutes { class AppRoutes {
final Widget page; final Widget page;
@@ -60,7 +50,7 @@ class AppRoutes {
return Future.value(null); 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'); return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail');
} }
@@ -68,13 +58,6 @@ class AppRoutes {
return AppRoutes(ServerPage(key: key), 'server_tab'); return AppRoutes(ServerPage(key: key), 'server_tab');
} }
static AppRoutes serverEdit({Key? key, ServerPrivateInfo? spi}) {
return AppRoutes(
ServerEditPage(spi: spi),
'server_${spi == null ? 'add' : 'edit'}',
);
}
static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) { static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) {
return AppRoutes( return AppRoutes(
PrivateKeyEditPage(pki: pki), PrivateKeyEditPage(pki: pki),
@@ -82,10 +65,6 @@ class AppRoutes {
); );
} }
static AppRoutes keyList({Key? key}) {
return AppRoutes(PrivateKeysListPage(key: key), 'key_detail');
}
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) { static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
return AppRoutes( return AppRoutes(
SnippetEditPage(snippet: snippet), SnippetEditPage(snippet: snippet),
@@ -93,13 +72,9 @@ class AppRoutes {
); );
} }
static AppRoutes snippetList({Key? key}) {
return AppRoutes(SnippetListPage(key: key), 'snippet_detail');
}
static AppRoutes ssh({ static AppRoutes ssh({
Key? key, Key? key,
required ServerPrivateInfo spi, required Spi spi,
String? initCmd, String? initCmd,
Snippet? initSnippet, Snippet? initSnippet,
}) { }) {
@@ -118,26 +93,12 @@ class AppRoutes {
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting'); return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
} }
static AppRoutes localStorage(
{Key? key, bool isPickFile = false, String? initDir}) {
return AppRoutes(
LocalStoragePage(
key: key,
isPickFile: isPickFile,
initDir: initDir,
),
'local_storage');
}
static AppRoutes sftpMission({Key? key}) { static AppRoutes sftpMission({Key? key}) {
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission'); return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
} }
static AppRoutes sftp( static AppRoutes sftp(
{Key? key, {Key? key, required Spi spi, String? initPath, bool isSelect = false}) {
required ServerPrivateInfo spi,
String? initPath,
bool isSelect = false}) {
return AppRoutes( return AppRoutes(
SftpPage( SftpPage(
key: key, key: key,
@@ -148,44 +109,10 @@ class AppRoutes {
'sftp'); 'sftp');
} }
static AppRoutes backup({Key? key}) { static AppRoutes docker({Key? key, required Spi spi}) {
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}) {
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker'); return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
} }
/// - Pop true if the text is changed & [path] is not null
/// - Pop text if [path] is null
static AppRoutes editor({
Key? key,
String? path,
String? text,
String? langCode,
String? title,
}) {
return AppRoutes(
EditorPage(
key: key,
path: path,
text: text,
langCode: langCode,
title: title,
),
'editor');
}
// static AppRoutes fullscreen({Key? key}) { // static AppRoutes fullscreen({Key? key}) {
// return AppRoutes(FullScreenPage(key: key), 'fullscreen'); // return AppRoutes(FullScreenPage(key: key), 'fullscreen');
// } // }
@@ -198,14 +125,10 @@ class AppRoutes {
return AppRoutes(PingPage(key: key), 'ping'); 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'); return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
} }
static AppRoutes settings({Key? key}) {
return AppRoutes(SettingPage(key: key), 'setting');
}
static AppRoutes serverOrder({Key? key}) { static AppRoutes serverOrder({Key? key}) {
return AppRoutes(ServerOrderPage(key: key), 'server_order'); return AppRoutes(ServerOrderPage(key: key), 'server_order');
} }
@@ -232,7 +155,7 @@ class AppRoutes {
'snippet_result'); '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'); return AppRoutes(IPerfPage(key: key, spi: spi), 'iperf');
} }
@@ -240,7 +163,7 @@ class AppRoutes {
return AppRoutes(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq'); 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'); return AppRoutes(PvePage(key: key, spi: spi), 'pve');
} }
} }

37
lib/core/sync.dart Normal file
View File

@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/backup.dart';
const bakSync = BakSyncer._();
final icloud = ICloud(containerId: 'iCloud.tech.lolli.serverbox');
final class BakSyncer extends SyncIface<Backup> {
const BakSyncer._() : super();
@override
void init() {
Webdav.shared.prefix = 'serverbox/';
}
@override
Future<void> saveToFile() => Backup.backup();
@override
Future<Backup> fromFile(String path) async {
final content = await File(path).readAsString();
return Backup.fromJsonString(content);
}
@override
RemoteStorage? get remoteStorage {
final icloudEnabled = PrefProps.icloudSync.get();
if (icloudEnabled) return icloud;
final webdavEnabled = PrefProps.webdavSync.get();
if (webdavEnabled) return Webdav.shared;
return null;
}
}

View File

@@ -1,11 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/res/store.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. /// Must put this func out of any Class.
/// ///
@@ -31,7 +32,7 @@ enum GenSSHClientStatus {
} }
String getPrivateKey(String id) { String getPrivateKey(String id) {
final pki = Stores.key.get(id); final pki = Stores.key.fetchOne(id);
if (pki == null) { if (pki == null) {
throw SSHErr( throw SSHErr(
type: SSHErrType.noPrivateKey, type: SSHErrType.noPrivateKey,
@@ -42,7 +43,7 @@ String getPrivateKey(String id) {
} }
Future<SSHClient> genClient( Future<SSHClient> genClient(
ServerPrivateInfo spi, { Spi spi, {
void Function(GenSSHClientStatus)? onStatus, void Function(GenSSHClientStatus)? onStatus,
/// Only pass this param if using multi-threading and key login /// Only pass this param if using multi-threading and key login
@@ -52,16 +53,18 @@ Future<SSHClient> genClient(
String? jumpPrivateKey, String? jumpPrivateKey,
Duration timeout = const Duration(seconds: 5), 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 /// Must pass this param if using multi-threading and key login
ServerPrivateInfo? jumpSpi, Spi? jumpSpi,
/// Handle keyboard-interactive authentication /// Handle keyboard-interactive authentication
FutureOr<List<String>?> Function(SSHUserInfoRequest)? onKeyboardInteractive, FutureOr<List<String>?> Function(SSHUserInfoRequest)? onKeyboardInteractive,
}) async { }) async {
onStatus?.call(GenSSHClientStatus.socket); onStatus?.call(GenSSHClientStatus.socket);
String? alterUser;
final socket = await () async { final socket = await () async {
// Proxy // Proxy
final jumpSpi_ = () { final jumpSpi_ = () {
@@ -91,15 +94,18 @@ Future<SSHClient> genClient(
timeout: timeout, timeout: timeout,
); );
} catch (e) { } catch (e) {
Loggers.app.warning('genClient', e);
if (spi.alterUrl == null) rethrow; if (spi.alterUrl == null) rethrow;
try { try {
final ipPort = spi.fromStringUrl(); final res = spi.fromStringUrl();
alterUser = res.$2;
return await SSHSocket.connect( return await SSHSocket.connect(
ipPort.ip, res.$1,
ipPort.port, res.$3,
timeout: timeout, timeout: timeout,
); );
} catch (e) { } catch (e) {
Loggers.app.warning('genClient alterUrl', e);
rethrow; rethrow;
} }
} }
@@ -110,7 +116,7 @@ Future<SSHClient> genClient(
onStatus?.call(GenSSHClientStatus.pwd); onStatus?.call(GenSSHClientStatus.pwd);
return SSHClient( return SSHClient(
socket, socket,
username: spi.user, username: alterUser ?? spi.user,
onPasswordRequest: () => spi.pwd, onPasswordRequest: () => spi.pwd,
onUserInfoRequest: onKeyboardInteractive, onUserInfoRequest: onKeyboardInteractive,
// printDebug: debugPrint, // printDebug: debugPrint,

View File

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

View File

@@ -1,224 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:computer/computer.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:icloud_storage/icloud_storage.dart';
import 'package:logging/logging.dart';
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';
abstract final class ICloud {
static const _containerId = 'iCloud.tech.lolli.serverbox';
static final _logger = Logger('iCloud');
/// Upload file to iCloud
///
/// - [relativePath] is the path relative to [Paths.doc],
/// must not starts with `/`
/// - [localPath] has higher priority than [relativePath], but only apply
/// to the local path instead of iCloud path
///
/// Return [null] if upload success, [ICloudErr] otherwise
static Future<ICloudErr?> upload({
required String relativePath,
String? localPath,
}) async {
final completer = Completer<ICloudErr?>();
try {
await ICloudStorage.upload(
containerId: _containerId,
filePath: localPath ?? '${Paths.doc}/$relativePath',
destinationRelativePath: relativePath,
onProgress: (stream) {
stream.listen(
null,
onDone: () => completer.complete(null),
onError: (e) => completer.complete(
ICloudErr(type: ICloudErrType.generic, message: '$e'),
),
);
},
);
} catch (e, s) {
_logger.warning('Upload $relativePath failed', e, s);
completer.complete(ICloudErr(type: ICloudErrType.generic, message: '$e'));
}
return completer.future;
}
static Future<List<ICloudFile>> getAll() async {
return await ICloudStorage.gather(
containerId: _containerId,
);
}
static Future<void> delete(String relativePath) async {
try {
await ICloudStorage.delete(
containerId: _containerId,
relativePath: relativePath,
);
} catch (e, s) {
_logger.warning('Delete $relativePath failed', e, s);
}
}
/// Download file from iCloud
///
/// - [relativePath] is the path relative to [Paths.doc],
/// must not starts with `/`
/// - [localPath] has higher priority than [relativePath], but only apply
/// to the local path instead of iCloud path
///
/// Return `null` if upload success, [ICloudErr] otherwise
static Future<ICloudErr?> download({
required String relativePath,
String? localPath,
}) async {
final completer = Completer<ICloudErr?>();
try {
await ICloudStorage.download(
containerId: _containerId,
relativePath: relativePath,
destinationFilePath: localPath ?? '${Paths.doc}/$relativePath',
onProgress: (stream) {
stream.listen(
null,
onDone: () => completer.complete(null),
onError: (e) => completer.complete(
ICloudErr(type: ICloudErrType.generic, message: '$e'),
),
);
},
);
} catch (e, s) {
_logger.warning('Download $relativePath failed', e, s);
completer.complete(ICloudErr(type: ICloudErrType.generic, message: '$e'));
}
return completer.future;
}
/// Sync file between iCloud and local
///
/// - [relativePaths] is the path relative to [Paths.doc],
/// must not starts with `/`
/// - [bakPrefix] is the suffix of backup file, default to [null].
/// All files downloaded from cloud will be suffixed with [bakPrefix].
///
/// Return `null` if upload success, [ICloudErr] otherwise
static Future<SyncResult<String, ICloudErr>> syncFiles({
required Iterable<String> relativePaths,
String? bakPrefix,
}) async {
final uploadFiles = <String>[];
final downloadFiles = <String>[];
try {
final errs = <String, ICloudErr>{};
final allFiles = await getAll();
/// remove files not in relativePaths
allFiles.removeWhere((e) => !relativePaths.contains(e.relativePath));
final missions = <Future<void>>[];
/// upload files not in iCloud
final missed = relativePaths.where((e) {
return !allFiles.any((f) => f.relativePath == e);
});
missions.addAll(missed.map((e) async {
final err = await upload(relativePath: e);
if (err != null) {
errs[e] = err;
}
}));
final docPath = Paths.doc;
/// compare files in iCloud and local
missions.addAll(allFiles.map((file) async {
final relativePath = file.relativePath;
/// Check date
final localFile = File('$docPath/$relativePath');
if (!localFile.existsSync()) {
/// Local file not found, download remote file
final err = await download(relativePath: relativePath);
if (err != null) {
errs[relativePath] = err;
}
return;
}
final localDate = await localFile.lastModified();
final remoteDate = file.contentChangeDate;
/// Same date, skip
if (remoteDate.difference(localDate) == Duration.zero) return;
/// Local is newer than remote, so upload local file
if (remoteDate.isBefore(localDate)) {
await delete(relativePath);
final err = await upload(relativePath: relativePath);
if (err != null) {
errs[relativePath] = err;
}
uploadFiles.add(relativePath);
return;
}
/// Remote is newer than local, so download remote
final localPath = '$docPath/${bakPrefix ?? ''}$relativePath';
final err = await download(
relativePath: relativePath,
localPath: localPath,
);
if (err != null) {
errs[relativePath] = err;
}
downloadFiles.add(relativePath);
}));
await Future.wait(missions);
return SyncResult(up: uploadFiles, down: downloadFiles, err: errs);
} catch (e, s) {
_logger.warning('Sync: $relativePaths failed', e, s);
return SyncResult(up: uploadFiles, down: downloadFiles, err: {
'Generic': ICloudErr(type: ICloudErrType.generic, message: '$e')
});
} finally {
_logger.info('Sync, up: $uploadFiles, down: $downloadFiles');
}
}
static Future<void> sync() async {
final result = await download(relativePath: Miscs.bakFileName);
if (result != null) {
await backup();
return;
}
final dlFile = await File(Paths.bak).readAsString();
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
await dlBak.restore();
await backup();
}
static Future<void> backup() async {
await Backup.backup();
final uploadResult = await upload(relativePath: Miscs.bakFileName);
if (uploadResult != null) {
_logger.warning('Upload backup failed: $uploadResult');
} else {
_logger.info('Upload backup success');
}
}
}

View File

@@ -1,127 +0,0 @@
import 'dart:io';
import 'package:computer/computer.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:logging/logging.dart';
import 'package:server_box/data/model/app/backup.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
import 'package:webdav_client/webdav_client.dart';
abstract final class Webdav {
/// Some WebDAV provider only support non-root path
static const _prefix = 'srvbox/';
static var _client = WebdavClient(
url: Stores.setting.webdavUrl.fetch(),
user: Stores.setting.webdavUser.fetch(),
pwd: Stores.setting.webdavPwd.fetch(),
);
static final _logger = Logger('Webdav');
static Future<String?> test(String url, String user, String pwd) async {
final client = WebdavClient(url: url, user: user, pwd: pwd);
try {
await client.ping();
return null;
} catch (e, s) {
_logger.warning('Test failed', e, s);
return e.toString();
}
}
static Future<WebdavErr?> upload({
required String relativePath,
String? localPath,
}) async {
try {
await _client.writeFile(
localPath ?? '${Paths.doc}/$relativePath',
_prefix + relativePath,
);
} catch (e, s) {
_logger.warning('Upload $relativePath failed', e, s);
return WebdavErr(type: WebdavErrType.generic, message: '$e');
}
return null;
}
static Future<WebdavErr?> delete(String relativePath) async {
try {
await _client.remove(_prefix + relativePath);
} catch (e, s) {
_logger.warning('Delete $relativePath failed', e, s);
return WebdavErr(type: WebdavErrType.generic, message: '$e');
}
return null;
}
static Future<WebdavErr?> download({
required String relativePath,
String? localPath,
}) async {
try {
await _client.readFile(
_prefix + relativePath,
localPath ?? '${Paths.doc}/$relativePath',
);
} catch (e) {
_logger.warning('Download $relativePath failed');
return WebdavErr(type: WebdavErrType.generic, message: '$e');
}
return null;
}
static Future<List<String>> list() async {
try {
final list = await _client.readDir(_prefix);
final names = <String>[];
for (final item in list) {
if ((item.isDir ?? true) || item.name == null) continue;
names.add(item.name!);
}
return names;
} catch (e, s) {
_logger.warning('List failed', e, s);
return [];
}
}
static void changeClient(String url, String user, String pwd) {
_client = WebdavClient(url: url, user: user, pwd: pwd);
Stores.setting.webdavUrl.put(url);
Stores.setting.webdavUser.put(user);
Stores.setting.webdavPwd.put(pwd);
}
static Future<void> sync() async {
final result = await download(relativePath: Miscs.bakFileName);
if (result != null) {
await backup();
return;
}
try {
final dlFile = await File(Paths.bak).readAsString();
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
await dlBak.restore();
} catch (e) {
_logger.warning('Restore failed: $e');
}
await backup();
}
/// Create a local backup and upload it to WebDAV
static Future<void> backup() async {
await Backup.backup();
final uploadResult = await upload(relativePath: Miscs.bakFileName);
if (uploadResult != null) {
_logger.warning('Upload failed: $uploadResult');
} else {
_logger.info('Upload success');
}
}
}

View File

@@ -2,29 +2,33 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/rebuild.dart'; import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
part 'backup.g.dart';
const backupFormatVersion = 1; const backupFormatVersion = 1;
final _logger = Logger('Backup'); final _logger = Logger('Backup');
class Backup { @JsonSerializable()
class Backup implements Mergeable {
// backup format version // backup format version
final int version; final int version;
final String date; final String date;
final List<ServerPrivateInfo> spis; final List<Spi> spis;
final List<Snippet> snippets; final List<Snippet> snippets;
final List<PrivateKeyInfo> keys; final List<PrivateKeyInfo> keys;
final Map<String, dynamic> container; final Map<String, dynamic> container;
final Map<String, dynamic> history; final Map<String, dynamic> history;
final int? lastModTime; final int? lastModTime;
final Map<String, dynamic>? settings;
const Backup({ const Backup({
required this.version, required this.version,
@@ -34,54 +38,40 @@ class Backup {
required this.keys, required this.keys,
required this.container, required this.container,
required this.history, required this.history,
required this.settings,
this.lastModTime, this.lastModTime,
}); });
Backup.fromJson(Map<String, dynamic> json) factory Backup.fromJson(Map<String, dynamic> json) => _$BackupFromJson(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'] ?? {};
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$BackupToJson(this);
'version': version,
'date': date,
'spis': spis,
'snippets': snippets,
'keys': keys,
'container': container,
'lastModTime': lastModTime,
'history': history,
};
Backup.loadFromStore() static Future<Backup> loadFromStore() async {
: version = backupFormatVersion, final lastModTime = Stores.lastModTime?.millisecondsSinceEpoch;
date = DateTime.now().toString().split('.').firstOrNull ?? '', return Backup(
spis = Stores.server.fetch(), version: backupFormatVersion,
snippets = Stores.snippet.fetch(), date: DateTime.now().toString().split('.').firstOrNull ?? '',
keys = Stores.key.fetch(), spis: Stores.server.fetch(),
container = Stores.container.box.toJson(), snippets: Stores.snippet.fetch(),
lastModTime = Stores.lastModTime, keys: Stores.key.fetch(),
history = Stores.history.box.toJson(); container: await Stores.container.getAllMap(),
lastModTime: lastModTime,
history: await Stores.history.getAllMap(),
settings: await Stores.setting.getAllMap(),
);
}
static Future<String> backup([String? name]) async { static Future<String> backup([String? name]) async {
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson())); final bak = await Backup.loadFromStore();
final path = '${Paths.doc}/${name ?? Miscs.bakFileName}'; final result = _diyEncrypt(json.encode(bak.toJson()));
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result); await File(path).writeAsString(result);
return path; return path;
} }
Future<void> restore({bool force = false}) async { @override
final curTime = Stores.lastModTime ?? 0; Future<void> merge({bool force = false}) async {
final curTime = Stores.lastModTime?.millisecondsSinceEpoch ?? 0;
final bakTime = lastModTime ?? 0; final bakTime = lastModTime ?? 0;
final shouldRestore = force || curTime < bakTime; final shouldRestore = force || curTime < bakTime;
if (!shouldRestore) { if (!shouldRestore) {
@@ -195,14 +185,37 @@ class Backup {
} }
} }
Pros.reload(); // Settings
final settings_ = settings;
if (settings_ != null) {
if (force) {
Stores.setting.box.putAll(settings_);
} else {
final nowSettings = Stores.setting.box.keys.toSet();
final bakSettings = settings_.keys.toSet();
final newSettings = bakSettings.difference(nowSettings);
final delSettings = nowSettings.difference(bakSettings);
final updateSettings = nowSettings.intersection(bakSettings);
for (final s in newSettings) {
Stores.setting.box.put(s, settings_[s]);
}
for (final s in delSettings) {
Stores.setting.box.delete(s);
}
for (final s in updateSettings) {
Stores.setting.box.put(s, settings_[s]);
}
}
}
Provider.reload();
RNodes.app.notify(); RNodes.app.notify();
_logger.info('Restore success'); _logger.info('Restore success');
} }
Backup.fromJsonString(String raw) factory Backup.fromJsonString(String raw) =>
: this.fromJson(json.decode(_diyDecrypt(raw))); Backup.fromJson(json.decode(_diyDecrypt(raw)));
} }
String _diyEncrypt(String raw) => json.encode( String _diyEncrypt(String raw) => json.encode(

View File

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

View File

@@ -2,29 +2,47 @@ import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:icons_plus/icons_plus.dart'; import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
part 'server_func.g.dart'; part 'server_func.g.dart';
@HiveType(typeId: 6) @HiveType(typeId: 6)
enum ServerFuncBtn { enum ServerFuncBtn {
@HiveField(0) @HiveField(0)
terminal, terminal._(),
@HiveField(1) @HiveField(1)
sftp, sftp._(),
@HiveField(2) @HiveField(2)
container, container._(),
@HiveField(3) @HiveField(3)
process, process._(),
//@HiveField(4) //@HiveField(4)
//pkg, //pkg,
@HiveField(5) @HiveField(5)
snippet, snippet._(),
@HiveField(6) @HiveField(6)
iperf, iperf._(),
// @HiveField(7) // @HiveField(7)
// pve, // 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 = [ static final defaultIdxs = [
terminal, terminal,
sftp, sftp,
@@ -32,6 +50,7 @@ enum ServerFuncBtn {
process, process,
//pkg, //pkg,
snippet, snippet,
systemd,
].map((e) => e.index).toList(); ].map((e) => e.index).toList();
IconData get icon => switch (this) { IconData get icon => switch (this) {
@@ -42,6 +61,7 @@ enum ServerFuncBtn {
process => Icons.list_alt_outlined, process => Icons.list_alt_outlined,
terminal => Icons.terminal, terminal => Icons.terminal,
iperf => Icons.speed, iperf => Icons.speed,
systemd => MingCute.plugin_2_fill,
}; };
String get toStr => switch (this) { String get toStr => switch (this) {
@@ -52,5 +72,6 @@ enum ServerFuncBtn {
process => l10n.process, process => l10n.process,
terminal => l10n.terminal, terminal => l10n.terminal,
iperf => 'iperf', iperf => 'iperf',
systemd => 'Systemd',
}; };
} }

View File

@@ -25,6 +25,8 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
return ServerFuncBtn.snippet; return ServerFuncBtn.snippet;
case 6: case 6:
return ServerFuncBtn.iperf; return ServerFuncBtn.iperf;
case 8:
return ServerFuncBtn.systemd;
default: default:
return ServerFuncBtn.terminal; return ServerFuncBtn.terminal;
} }
@@ -51,6 +53,9 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
case ServerFuncBtn.iperf: case ServerFuncBtn.iperf:
writer.writeByte(6); writer.writeByte(6);
break; 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:hive_flutter/hive_flutter.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/res/store.dart';
part 'net_view.g.dart'; part 'net_view.g.dart';
@@ -27,36 +26,43 @@ enum NetViewType {
NetViewType.speed => l10n.speed, NetViewType.speed => l10n.speed,
}; };
(String, String) build(ServerStatus ss) { /// If no device is specified, return the cached value (only real devices,
final ignoreLocal = Stores.setting.ignoreLocalNet.fetch(); /// such as ethX, wlanX...).
switch (this) { (String, String) build(ServerStatus ss, {String? dev}) {
case NetViewType.conn: final notSepcifyDev = dev == null || dev.isEmpty;
return ( try {
'${l10n.conn}:\n${ss.tcp.maxConn}', switch (this) {
'${libL10n.fail}:\n${ss.tcp.fail}', case NetViewType.conn:
);
case NetViewType.speed:
if (ignoreLocal) {
return ( return (
'↓:\n${ss.netSpeed.cachedRealVals.speedIn}', '${l10n.conn}:\n${ss.tcp.maxConn}',
'↑:\n${ss.netSpeed.cachedRealVals.speedOut}', '${libL10n.fail}:\n${ss.tcp.fail}',
); );
} case NetViewType.speed:
return ( if (notSepcifyDev) {
'↓:\n${ss.netSpeed.speedIn()}', return (
':\n${ss.netSpeed.speedOut()}', ':\n${ss.netSpeed.cachedVals.speedIn}',
); '↑:\n${ss.netSpeed.cachedVals.speedOut}',
case NetViewType.traffic: );
if (ignoreLocal) { }
return ( return (
'↓:\n${ss.netSpeed.cachedRealVals.sizeIn}', '↓:\n${ss.netSpeed.speedIn(device: dev)}',
'↑:\n${ss.netSpeed.cachedRealVals.sizeOut}', '↑:\n${ss.netSpeed.speedOut(device: dev)}',
); );
} case NetViewType.traffic:
return ( if (notSepcifyDev) {
'↓:\n${ss.netSpeed.sizeIn()}', return (
':\n${ss.netSpeed.sizeOut()}', ':\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,10 +1,12 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
final _seperator = Pfs.seperator;
/// It's used on platform's file system. /// It's used on platform's file system.
/// So use [Platform.pathSeparator] to join path. /// So use [Platform.pathSeparator] to join path.
class LocalPath { class LocalPath {
final String _prefixPath; final String _prefixPath;
String _path = '/'; String _path = _seperator;
String? _prePath; String? _prePath;
String get path => _prefixPath + _path; String get path => _prefixPath + _path;
@@ -13,20 +15,20 @@ class LocalPath {
void update(String newPath) { void update(String newPath) {
_prePath = _path; _prePath = _path;
if (newPath == '..') { if (newPath == '..') {
_path = _path.substring(0, _path.lastIndexOf('/')); _path = _path.substring(0, _path.lastIndexOf(_seperator));
if (_path == '') { if (_path == '') {
_path = '/'; _path = _seperator;
} }
return; return;
} }
if (newPath == '/') { if (newPath == _seperator) {
_path = '/'; _path = _seperator;
return; return;
} }
_path = _path.joinPath(newPath); _path = _path.joinPath(newPath);
} }
bool get canBack => path != '$_prefixPath/'; bool get canBack => path != '$_prefixPath$_seperator';
bool undo() { bool undo() {
if (_prePath == null || _path == _prePath) { if (_prePath == null || _path == _prePath) {
@@ -38,7 +40,7 @@ class LocalPath {
} }
String _trimSuffix(String prefixPath) { String _trimSuffix(String prefixPath) {
if (prefixPath.endsWith('/')) { if (prefixPath.endsWith(_seperator)) {
return prefixPath.substring(0, prefixPath.length - 1); return prefixPath.substring(0, prefixPath.length - 1);
} }
return prefixPath; return prefixPath;

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
abstract class TagPickable {
bool containsTag(String tag);
String get tagName;
}

View File

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

View File

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

View File

@@ -22,8 +22,6 @@ enum PkgManager {
return 'opkg list-upgradable'; return 'opkg list-upgradable';
case PkgManager.apk: case PkgManager.apk:
return 'apk list --upgradable'; return 'apk list --upgradable';
default:
return null;
} }
} }
@@ -56,8 +54,6 @@ enum PkgManager {
return 'opkg upgrade $args'; return 'opkg upgrade $args';
case PkgManager.apk: case PkgManager.apk:
return 'apk upgrade'; return 'apk upgrade';
default:
return null;
} }
} }

View File

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

View File

@@ -1,7 +1,9 @@
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:json_annotation/json_annotation.dart';
part 'custom.g.dart'; part 'custom.g.dart';
@JsonSerializable()
@HiveType(typeId: 7) @HiveType(typeId: 7)
final class ServerCustom { final class ServerCustom {
// @HiveField(0) // @HiveField(0)
@@ -19,6 +21,14 @@ final class ServerCustom {
@HiveField(5) @HiveField(5)
final String? logoUrl; 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({ const ServerCustom({
//this.temperature, //this.temperature,
this.pveAddr, this.pveAddr,
@@ -26,51 +36,14 @@ final class ServerCustom {
this.cmds, this.cmds,
this.preferTempDev, this.preferTempDev,
this.logoUrl, this.logoUrl,
this.netDev,
this.scriptDir,
}); });
static ServerCustom fromJson(Map<String, dynamic> json) { factory ServerCustom.fromJson(Map<String, dynamic> json) =>
//final temperature = json["temperature"] as String?; _$ServerCustomFromJson(json);
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,
);
}
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() => _$ServerCustomToJson(this);
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();
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -80,7 +53,9 @@ final class ServerCustom {
other.pveIgnoreCert == pveIgnoreCert && other.pveIgnoreCert == pveIgnoreCert &&
other.cmds == cmds && other.cmds == cmds &&
other.preferTempDev == preferTempDev && other.preferTempDev == preferTempDev &&
other.logoUrl == logoUrl; other.logoUrl == logoUrl &&
other.netDev == netDev &&
other.scriptDir == scriptDir;
} }
@override @override
@@ -90,5 +65,7 @@ final class ServerCustom {
pveIgnoreCert.hashCode ^ pveIgnoreCert.hashCode ^
cmds.hashCode ^ cmds.hashCode ^
preferTempDev.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>(), cmds: (fields[3] as Map?)?.cast<String, String>(),
preferTempDev: fields[4] as String?, preferTempDev: fields[4] as String?,
logoUrl: fields[5] as String?, logoUrl: fields[5] as String?,
netDev: fields[6] as String?,
scriptDir: fields[7] as String?,
); );
} }
@override @override
void write(BinaryWriter writer, ServerCustom obj) { void write(BinaryWriter writer, ServerCustom obj) {
writer writer
..writeByte(5) ..writeByte(7)
..writeByte(1) ..writeByte(1)
..write(obj.pveAddr) ..write(obj.pveAddr)
..writeByte(2) ..writeByte(2)
@@ -38,7 +40,11 @@ class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
..writeByte(4) ..writeByte(4)
..write(obj.preferTempDev) ..write(obj.preferTempDev)
..writeByte(5) ..writeByte(5)
..write(obj.logoUrl); ..write(obj.logoUrl)
..writeByte(6)
..write(obj.netDev)
..writeByte(7)
..write(obj.scriptDir);
} }
@override @override
@@ -51,3 +57,30 @@ class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
typeId == other.typeId; 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:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart'; import 'package:server_box/data/model/server/time_seq.dart';
import '../../res/misc.dart'; import 'package:server_box/data/res/misc.dart';
class Disk { class Disk {
final String fs; final String fs;
@@ -104,7 +104,9 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
!item.dev.startsWith('vd') && !item.dev.startsWith('vd') &&
!item.dev.startsWith('hd') && !item.dev.startsWith('hd') &&
!item.dev.startsWith('mmcblk') && !item.dev.startsWith('mmcblk') &&
!item.dev.startsWith('sr')) continue; !item.dev.startsWith('sr')) {
continue;
}
final (read_, write_) = _getSpeed(item.dev); final (read_, write_) = _getSpeed(item.dev);
read += read_ ?? 0; read += read_ ?? 0;
write += write_ ?? 0; write += write_ ?? 0;

View File

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

View File

@@ -1,11 +1,14 @@
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
part 'private_key_info.g.dart'; part 'private_key_info.g.dart';
@JsonSerializable()
@HiveType(typeId: 1) @HiveType(typeId: 1)
class PrivateKeyInfo { class PrivateKeyInfo {
@HiveField(0) @HiveField(0)
final String id; final String id;
@JsonKey(name: 'private_key')
@HiveField(1) @HiveField(1)
final String key; final String key;
@@ -14,6 +17,11 @@ class PrivateKeyInfo {
required this.key, required this.key,
}); });
factory PrivateKeyInfo.fromJson(Map<String, dynamic> json) =>
_$PrivateKeyInfoFromJson(json);
Map<String, dynamic> toJson() => _$PrivateKeyInfoToJson(this);
String? get type { String? get type {
final lines = key.split('\n'); final lines = key.split('\n');
if (lines.length < 2) { if (lines.length < 2) {
@@ -26,15 +34,4 @@ class PrivateKeyInfo {
} }
return splited[1]; 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 && runtimeType == other.runtimeType &&
typeId == other.typeId; 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 'package:fl_lib/fl_lib.dart';
import '../../../data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
class _ProcValIdxMap { class _ProcValIdxMap {
final int pid; final int pid;
@@ -58,7 +58,7 @@ class Proc {
required this.command, required this.command,
}); });
factory Proc.parse(String raw, _ProcValIdxMap map) { factory Proc._parse(String raw, _ProcValIdxMap map) {
final parts = raw.split(RegExp(r'\s+')); final parts = raw.split(RegExp(r'\s+'));
return Proc( return Proc(
user: map.user == null ? null : parts[map.user!], user: map.user == null ? null : parts[map.user!],
@@ -139,7 +139,7 @@ class PsResult {
final line = lines[i]; final line = lines[i];
if (line.isEmpty) continue; if (line.isEmpty) continue;
try { try {
procs.add(Proc.parse(line, map)); procs.add(Proc._parse(line, map));
} catch (e, trace) { } catch (e, trace) {
errs.add('$line: $e'); errs.add('$line: $e');
Loggers.app.warning('Process failed', e, trace); Loggers.app.warning('Process failed', e, trace);

View File

@@ -1,7 +1,6 @@
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart'; import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/app/tag_pickable.dart';
import 'package:server_box/data/model/server/battery.dart'; import 'package:server_box/data/model/server/battery.dart';
import 'package:server_box/data/model/server/conn.dart'; import 'package:server_box/data/model/server/conn.dart';
import 'package:server_box/data/model/server/cpu.dart'; import 'package:server_box/data/model/server/cpu.dart';
@@ -14,8 +13,8 @@ import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/temp.dart'; import 'package:server_box/data/model/server/temp.dart';
class Server implements TagPickable { class Server {
ServerPrivateInfo spi; Spi spi;
ServerStatus status; ServerStatus status;
SSHClient? client; SSHClient? client;
ServerConn conn; ServerConn conn;
@@ -27,14 +26,6 @@ class Server implements TagPickable {
this.client, this.client,
}); });
@override
bool containsTag(String tag) {
return spi.tags?.contains(tag) ?? false;
}
@override
String get tagName => spi.id;
bool get needGenClient => conn < ServerConn.connecting; bool get needGenClient => conn < ServerConn.connecting;
bool get canViewDetails => conn == ServerConn.finished; bool get canViewDetails => conn == ServerConn.finished;
@@ -90,5 +81,5 @@ enum ServerConn {
/// Status parsing finished /// Status parsing finished
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: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/custom.dart';
import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/wol_cfg.dart'; import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/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'; 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) @HiveType(typeId: 3)
class ServerPrivateInfo { class Spi {
@HiveField(0) @HiveField(0)
final String name; final String name;
@HiveField(1) @HiveField(1)
@@ -23,14 +33,15 @@ class ServerPrivateInfo {
final String? pwd; final String? pwd;
/// [id] of private key /// [id] of private key
@JsonKey(name: 'pubKeyId')
@HiveField(5) @HiveField(5)
final String? keyId; final String? keyId;
@HiveField(6) @HiveField(6)
final List<String>? tags; final List<String>? tags;
@HiveField(7) @HiveField(7)
final String? alterUrl; final String? alterUrl;
@HiveField(8) @HiveField(8, defaultValue: true)
final bool? autoConnect; final bool autoConnect;
/// [id] of the jump server /// [id] of the jump server
@HiveField(9) @HiveField(9)
@@ -48,7 +59,7 @@ class ServerPrivateInfo {
final String id; final String id;
const ServerPrivateInfo({ const Spi({
required this.name, required this.name,
required this.ip, required this.ip,
required this.port, required this.port,
@@ -57,97 +68,28 @@ class ServerPrivateInfo {
this.keyId, this.keyId,
this.tags, this.tags,
this.alterUrl, this.alterUrl,
this.autoConnect, this.autoConnect = true,
this.jumpId, this.jumpId,
this.custom, this.custom,
this.wolCfg, this.wolCfg,
this.envs, this.envs,
}) : id = '$user@$ip:$port'; }) : id = '$user@$ip:$port';
static ServerPrivateInfo fromJson(Map<String, dynamic> json) { factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(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;
}
});
}
return ServerPrivateInfo( Map<String, dynamic> toJson() => _$SpiToJson(this);
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() { @override
final Map<String, dynamic> data = <String, dynamic>{}; String toString() => id;
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;
}
Server? get server => Pros.server.pick(spi: this); extension Spix on Spi {
Server? get jumpServer => Pros.server.pick(id: jumpId); 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 || return id != old.id ||
pwd != old.pwd || pwd != old.pwd ||
keyId != old.keyId || keyId != old.keyId ||
@@ -156,7 +98,7 @@ class ServerPrivateInfo {
custom?.cmds != old.custom?.cmds; custom?.cmds != old.custom?.cmds;
} }
_IpPort fromStringUrl() { (String ip, String usr, int port) fromStringUrl() {
if (alterUrl == null) { if (alterUrl == null) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
} }
@@ -164,24 +106,23 @@ class ServerPrivateInfo {
if (splited.length != 2) { if (splited.length != 2) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @');
} }
final splited2 = splited[1].split(':'); final usr = splited[0];
if (splited2.length != 2) { final idx = splited[1].lastIndexOf(':');
if (idx == -1) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :');
} }
final ip_ = splited2[0]; final ip_ = splited[1].substring(0, idx);
final port_ = int.tryParse(splited2[1]) ?? 22; final port_ = int.tryParse(splited[1].substring(idx + 1));
if (port <= 0 || port > 65535) { if (port_ == null || port_ <= 0 || port_ > 65535) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
} }
return _IpPort(ip_, port_); return (ip_, usr, port_);
} }
@override /// Just for showing the struct of the class.
String toString() { ///
return id; /// **NOT** the default value.
} static const example = Spi(
static const example = ServerPrivateInfo(
name: 'name', name: 'name',
ip: 'ip', ip: 'ip',
port: 22, port: 22,
@@ -202,11 +143,6 @@ class ServerPrivateInfo {
logoUrl: 'https://example.com/logo.png', logoUrl: 'https://example.com/logo.png',
), ),
); );
}
class _IpPort { bool get isRoot => user == 'root';
final String ip;
final int port;
_IpPort(this.ip, this.port);
} }

View File

@@ -6,17 +6,17 @@ part of 'server_private_info.dart';
// TypeAdapterGenerator // TypeAdapterGenerator
// ************************************************************************** // **************************************************************************
class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> { class SpiAdapter extends TypeAdapter<Spi> {
@override @override
final int typeId = 3; final int typeId = 3;
@override @override
ServerPrivateInfo read(BinaryReader reader) { Spi read(BinaryReader reader) {
final numOfFields = reader.readByte(); final numOfFields = reader.readByte();
final fields = <int, dynamic>{ final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
}; };
return ServerPrivateInfo( return Spi(
name: fields[0] as String, name: fields[0] as String,
ip: fields[1] as String, ip: fields[1] as String,
port: fields[2] as int, port: fields[2] as int,
@@ -25,7 +25,7 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
keyId: fields[5] as String?, keyId: fields[5] as String?,
tags: (fields[6] as List?)?.cast<String>(), tags: (fields[6] as List?)?.cast<String>(),
alterUrl: fields[7] as 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?, jumpId: fields[9] as String?,
custom: fields[10] as ServerCustom?, custom: fields[10] as ServerCustom?,
wolCfg: fields[11] as WakeOnLanCfg?, wolCfg: fields[11] as WakeOnLanCfg?,
@@ -34,7 +34,7 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
} }
@override @override
void write(BinaryWriter writer, ServerPrivateInfo obj) { void write(BinaryWriter writer, Spi obj) {
writer writer
..writeByte(13) ..writeByte(13)
..writeByte(0) ..writeByte(0)
@@ -71,7 +71,49 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is ServerPrivateInfoAdapter && other is SpiAdapter &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
typeId == other.typeId; 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/server.dart';
import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/system.dart';
import '../app/shell_func.dart'; import 'package:server_box/data/model/app/shell_func.dart';
import 'cpu.dart'; import 'package:server_box/data/model/server/cpu.dart';
import 'disk.dart'; import 'package:server_box/data/model/server/disk.dart';
import 'memory.dart'; import 'package:server_box/data/model/server/memory.dart';
import 'net_speed.dart'; import 'package:server_box/data/model/server/net_speed.dart';
import 'conn.dart'; import 'package:server_box/data/model/server/conn.dart';
class ServerStatusUpdateReq { class ServerStatusUpdateReq {
final ServerStatus ss; final ServerStatus ss;

View File

@@ -2,15 +2,15 @@ import 'dart:async';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:xterm/core.dart'; import 'package:xterm/core.dart';
import '../app/tag_pickable.dart';
part 'snippet.g.dart'; part 'snippet.g.dart';
@JsonSerializable()
@HiveType(typeId: 2) @HiveType(typeId: 2)
class Snippet implements TagPickable { class Snippet {
@HiveField(0) @HiveField(0)
final String name; final String name;
@HiveField(1) @HiveField(1)
@@ -32,34 +32,14 @@ class Snippet implements TagPickable {
this.autoRunOn, this.autoRunOn,
}); });
Snippet.fromJson(Map<String, dynamic> json) factory Snippet.fromJson(Map<String, dynamic> json) =>
: name = json['name'].toString(), _$SnippetFromJson(json);
script = json['script'].toString(),
tags = json['tags']?.cast<String>(),
note = json['note']?.toString(),
autoRunOn = json['autoRunOn']?.cast<String>();
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() => _$SnippetToJson(this);
final data = <String, dynamic>{};
data['name'] = name;
data['script'] = script;
data['tags'] = tags;
data['note'] = note;
data['autoRunOn'] = autoRunOn;
return data;
}
@override
bool containsTag(String tag) {
return tags?.contains(tag) ?? false;
}
@override
String get tagName => name;
static final fmtFinder = RegExp(r'\$\{[^{}]+\}'); static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
String fmtWithSpi(ServerPrivateInfo spi) { String fmtWithSpi(Spi spi) {
return script.replaceAllMapped( return script.replaceAllMapped(
fmtFinder, fmtFinder,
(match) { (match) {
@@ -74,7 +54,7 @@ class Snippet implements TagPickable {
Future<void> runInTerm( Future<void> runInTerm(
Terminal terminal, Terminal terminal,
ServerPrivateInfo spi, { Spi spi, {
bool autoEnter = false, bool autoEnter = false,
}) async { }) async {
final argsFmted = fmtWithSpi(spi); final argsFmted = fmtWithSpi(spi);
@@ -119,11 +99,21 @@ class Snippet implements TagPickable {
if (special != null) { if (special != null) {
final raw = key.substring(special.key.length + 1, key.length - 1); final raw = key.substring(special.key.length + 1, key.length - 1);
await special.value((term: terminal, raw: raw)); await special.value((term: terminal, raw: raw));
} else {
// Term keys
final termKey = _find(fmtTermKeys, key);
if (termKey != null) {
await _doTermKeys(terminal, termKey, key);
} else {
// Normal input
terminal.textInput(key);
}
} }
// Term keys // Text between this and next match
final termKey = _find(fmtTermKeys, key); if (idx < starts.length - 1) {
if (termKey != null) await _doTermKeys(terminal, termKey, key); terminal.textInput(argsFmted.substring(end, starts[idx + 1]));
}
} }
// End term input // End term input
@@ -139,10 +129,10 @@ class Snippet implements TagPickable {
MapEntry<String, TerminalKey> termKey, MapEntry<String, TerminalKey> termKey,
String key, String key,
) async { ) async {
if (termKey.value == TerminalKey.enter) { // if (termKey.value == TerminalKey.enter) {
terminal.keyInput(TerminalKey.enter); // terminal.keyInput(TerminalKey.enter);
return; // return;
} // }
final ctrlAlt = switch (termKey.value) { final ctrlAlt = switch (termKey.value) {
TerminalKey.control => (ctrl: true, alt: false), TerminalKey.control => (ctrl: true, alt: false),
@@ -150,6 +140,8 @@ class Snippet implements TagPickable {
_ => (ctrl: false, alt: false), _ => (ctrl: false, alt: false),
}; };
if (!key.contains('+')) return;
// `${ctrl+ad}` -> `ctrla + d` // `${ctrl+ad}` -> `ctrla + d`
final chars = key.substring(termKey.key.length + 1, key.length - 1); final chars = key.substring(termKey.key.length + 1, key.length - 1);
if (chars.isEmpty) return; if (chars.isEmpty) return;
@@ -170,12 +162,12 @@ class Snippet implements TagPickable {
} }
static final fmtArgs = { static final fmtArgs = {
r'${host}': (ServerPrivateInfo spi) => spi.ip, r'${host}': (Spi spi) => spi.ip,
r'${port}': (ServerPrivateInfo spi) => spi.port.toString(), r'${port}': (Spi spi) => spi.port.toString(),
r'${user}': (ServerPrivateInfo spi) => spi.user, r'${user}': (Spi spi) => spi.user,
r'${pwd}': (ServerPrivateInfo spi) => spi.pwd ?? '', r'${pwd}': (Spi spi) => spi.pwd ?? '',
r'${id}': (ServerPrivateInfo spi) => spi.id, r'${id}': (Spi spi) => spi.id,
r'${name}': (ServerPrivateInfo spi) => spi.name, r'${name}': (Spi spi) => spi.name,
}; };
/// r'${ctrl+ad}' -> TerminalKey.control, a, d /// r'${ctrl+ad}' -> TerminalKey.control, a, d

View File

@@ -51,3 +51,25 @@ class SnippetAdapter extends TypeAdapter<Snippet> {
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
typeId == other.typeId; 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 'dart:io';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:wake_on_lan/wake_on_lan.dart'; import 'package:wake_on_lan/wake_on_lan.dart';
part 'wol_cfg.g.dart'; part 'wol_cfg.g.dart';
@JsonSerializable()
@HiveType(typeId: 8) @HiveType(typeId: 8)
final class WakeOnLanCfg { final class WakeOnLanCfg {
@HiveField(0) @HiveField(0)
@@ -54,22 +56,8 @@ final class WakeOnLanCfg {
); );
} }
static WakeOnLanCfg fromJson(Map<String, dynamic> json) { factory WakeOnLanCfg.fromJson(Map<String, dynamic> json) =>
return WakeOnLanCfg( _$WakeOnLanCfgFromJson(json);
mac: json['mac'] as String,
ip: json['ip'] as String,
pwd: json['pwd'] as String?,
);
}
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() => _$WakeOnLanCfgToJson(this);
final map = <String, dynamic>{
'mac': mac,
'ip': ip,
};
if (pwd != null) {
map['pwd'] = pwd;
}
return map;
}
} }

View File

@@ -45,3 +45,20 @@ class WakeOnLanCfgAdapter extends TypeAdapter<WakeOnLanCfg> {
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
typeId == other.typeId; 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

@@ -1,37 +0,0 @@
import 'package:fl_lib/fl_lib.dart';
class AbsolutePath {
String _path;
String get path => _path;
final List<String> _prePath;
AbsolutePath(this._path) : _prePath = ['/'];
void update(String newPath) {
_prePath.add(_path);
if (newPath == '..') {
_path = _path.substring(0, _path.lastIndexOf('/'));
if (_path == '') {
_path = '/';
}
return;
}
if (newPath == '/') {
_path = '/';
return;
}
if (newPath.startsWith('/')) {
_path = newPath;
return;
}
_path = _path.joinPath(newPath, seperator: '/');
}
bool undo() {
if (_prePath.isEmpty) {
return false;
}
_path = _prePath.removeLast();
return true;
}
}

View File

@@ -1,10 +1,49 @@
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:server_box/data/model/sftp/absolute_path.dart'; import 'package:fl_lib/fl_lib.dart';
/// Remote server only can be linux-like system, so use '/' as seperator
const _sep = '/';
class SftpBrowserStatus { class SftpBrowserStatus {
List<SftpName>? files; final List<SftpName> files = [];
AbsolutePath? path; final path = _AbsolutePath(_sep);
SftpClient? client; SftpClient? client;
SftpBrowserStatus(); SftpBrowserStatus(SSHClient client) {
client.sftp().then((value) => this.client = value);
}
}
class _AbsolutePath {
String _path;
final _prePath = <String>[];
_AbsolutePath(this._path);
String get path => _path;
/// Update path, not set path
set path(String newPath) {
_prePath.add(_path);
if (newPath == '..') {
_path = _path.substring(0, _path.lastIndexOf(_sep));
if (_path == '') {
_path = _sep;
}
return;
}
if (newPath.startsWith(_sep)) {
_path = newPath;
return;
}
_path = _path.joinPath(newPath, separator: _sep);
}
bool undo() {
if (_prePath.isEmpty) {
return false;
}
_path = _prePath.removeLast();
return true;
}
} }

View File

@@ -1,12 +1,12 @@
part of 'worker.dart'; part of 'worker.dart';
class SftpReq { class SftpReq {
final ServerPrivateInfo spi; final Spi spi;
final String remotePath; final String remotePath;
final String localPath; final String localPath;
final SftpReqType type; final SftpReqType type;
String? privateKey; String? privateKey;
ServerPrivateInfo? jumpSpi; Spi? jumpSpi;
String? jumpPrivateKey; String? jumpPrivateKey;
SftpReq( SftpReq(
@@ -21,7 +21,7 @@ class SftpReq {
} }
if (spi.jumpId != null) { if (spi.jumpId != null) {
jumpSpi = Stores.server.box.get(spi.jumpId); jumpSpi = Stores.server.box.get(spi.jumpId);
jumpPrivateKey = Stores.key.get(jumpSpi?.keyId)?.key; jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key;
} }
} }
} }

View File

@@ -60,8 +60,6 @@ Future<void> isolateMessageHandler(
case SftpReqType.upload: case SftpReqType.upload:
await _upload(data, mainSendPort, sendError); await _upload(data, mainSendPort, sendError);
break; break;
default:
sendError(Exception('unknown type'));
} }
break; break;
default: default:

View File

@@ -1,10 +1,14 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
import 'package:xterm/core.dart'; import 'package:xterm/core.dart';
part 'virtual_key.g.dart'; part 'virtual_key.g.dart';
enum VirtualKeyFunc { toggleIME, backspace, clipboard, snippet, file }
@HiveType(typeId: 4) @HiveType(typeId: 4)
enum VirtKey { enum VirtKey {
@HiveField(0) @HiveField(0)
@@ -38,23 +42,103 @@ enum VirtKey {
@HiveField(14) @HiveField(14)
pgup, pgup,
@HiveField(15) @HiveField(15)
pgdn; pgdn,
@HiveField(16)
slash,
@HiveField(17)
backSlash,
@HiveField(18)
underscore,
@HiveField(19)
plus,
@HiveField(20)
equal,
@HiveField(21)
minus,
@HiveField(22)
parenLeft,
@HiveField(23)
parenRight,
@HiveField(24)
bracketLeft,
@HiveField(25)
bracketRight,
@HiveField(26)
braceLeft,
@HiveField(27)
braceRight,
@HiveField(28)
chevronLeft,
@HiveField(29)
chevronRight,
@HiveField(30)
colon,
@HiveField(31)
semicolon,
@HiveField(32)
f1,
@HiveField(33)
f2,
@HiveField(34)
f3,
@HiveField(35)
f4,
@HiveField(36)
f5,
@HiveField(37)
f6,
@HiveField(38)
f7,
@HiveField(39)
f8,
@HiveField(40)
f9,
@HiveField(41)
f10,
@HiveField(42)
f11,
@HiveField(43)
f12;
}
extension VirtKeyX on VirtKey {
/// Used for input to terminal
String? get inputRaw => switch (this) {
VirtKey.slash => '/',
VirtKey.backSlash => '\\',
VirtKey.underscore => '_',
VirtKey.plus => '+',
VirtKey.equal => '=',
VirtKey.minus => '-',
VirtKey.parenLeft => '(',
VirtKey.parenRight => ')',
VirtKey.bracketLeft => '[',
VirtKey.bracketRight => ']',
VirtKey.braceLeft => '{',
VirtKey.braceRight => '}',
VirtKey.chevronLeft => '<',
VirtKey.chevronRight => '>',
VirtKey.colon => ':',
VirtKey.semicolon => ';',
_ => null,
};
/// Used for displaying on UI
String get text { String get text {
switch (this) { final t = inputRaw;
case VirtKey.pgdn: if (t != null) return t;
return 'PgDn';
case VirtKey.pgup: if (this == VirtKey.pgdn) return 'PgDn';
return 'PgUp'; if (this == VirtKey.pgup) return 'PgUp';
default:
if (name.length > 1) { if (name.length > 1) {
return name.substring(0, 1).toUpperCase() + name.substring(1); return name.substring(0, 1).toUpperCase() + name.substring(1);
}
return name;
} }
return name;
} }
static final defaultOrder = [ /// Default order of virtual keys
static const defaultOrder = [
VirtKey.esc, VirtKey.esc,
VirtKey.alt, VirtKey.alt,
VirtKey.home, VirtKey.home,
@@ -71,99 +155,68 @@ enum VirtKey {
VirtKey.ime, VirtKey.ime,
]; ];
TerminalKey? get key { /// Corresponding [TerminalKey]
switch (this) { TerminalKey? get key => switch (this) {
case VirtKey.esc: VirtKey.esc => TerminalKey.escape,
return TerminalKey.escape; VirtKey.alt => TerminalKey.alt,
case VirtKey.alt: VirtKey.home => TerminalKey.home,
return TerminalKey.alt; VirtKey.up => TerminalKey.arrowUp,
case VirtKey.home: VirtKey.end => TerminalKey.end,
return TerminalKey.home; VirtKey.tab => TerminalKey.tab,
case VirtKey.up: VirtKey.ctrl => TerminalKey.control,
return TerminalKey.arrowUp; VirtKey.left => TerminalKey.arrowLeft,
case VirtKey.end: VirtKey.down => TerminalKey.arrowDown,
return TerminalKey.end; VirtKey.right => TerminalKey.arrowRight,
case VirtKey.tab: VirtKey.pgup => TerminalKey.pageUp,
return TerminalKey.tab; VirtKey.pgdn => TerminalKey.pageDown,
case VirtKey.ctrl: VirtKey.f1 => TerminalKey.f1,
return TerminalKey.control; VirtKey.f2 => TerminalKey.f2,
case VirtKey.left: VirtKey.f3 => TerminalKey.f3,
return TerminalKey.arrowLeft; VirtKey.f4 => TerminalKey.f4,
case VirtKey.down: VirtKey.f5 => TerminalKey.f5,
return TerminalKey.arrowDown; VirtKey.f6 => TerminalKey.f6,
case VirtKey.right: VirtKey.f7 => TerminalKey.f7,
return TerminalKey.arrowRight; VirtKey.f8 => TerminalKey.f8,
case VirtKey.pgup: VirtKey.f9 => TerminalKey.f9,
return TerminalKey.pageUp; VirtKey.f10 => TerminalKey.f10,
case VirtKey.pgdn: VirtKey.f11 => TerminalKey.f11,
return TerminalKey.pageDown; VirtKey.f12 => TerminalKey.f12,
default: _ => null,
return null; };
}
}
IconData? get icon { /// Icons for virtual keys
switch (this) { IconData? get icon => switch (this) {
case VirtKey.up: VirtKey.up => Icons.arrow_upward,
return Icons.arrow_upward; VirtKey.left => Icons.arrow_back,
case VirtKey.left: VirtKey.down => Icons.arrow_downward,
return Icons.arrow_back; VirtKey.right => Icons.arrow_forward,
case VirtKey.down: VirtKey.sftp => Icons.file_open,
return Icons.arrow_downward; VirtKey.snippet => Icons.code,
case VirtKey.right: VirtKey.clipboard => Icons.paste,
return Icons.arrow_forward; VirtKey.ime => Icons.keyboard,
case VirtKey.sftp: _ => null,
return Icons.file_open; };
case VirtKey.snippet:
return Icons.code;
case VirtKey.clipboard:
return Icons.paste;
case VirtKey.ime:
return Icons.keyboard;
default:
return null;
}
}
// Use [VirtualKeyFunc] instead of [VirtKey] // Use [VirtualKeyFunc] instead of [VirtKey]
// This can help linter to enum all [VirtualKeyFunc] // This can help linter to enum all [VirtualKeyFunc]
// and make sure all [VirtualKeyFunc] are handled // and make sure all [VirtualKeyFunc] are handled
VirtualKeyFunc? get func { VirtualKeyFunc? get func => switch (this) {
switch (this) { VirtKey.sftp => VirtualKeyFunc.file,
case VirtKey.sftp: VirtKey.snippet => VirtualKeyFunc.snippet,
return VirtualKeyFunc.file; VirtKey.clipboard => VirtualKeyFunc.clipboard,
case VirtKey.snippet: VirtKey.ime => VirtualKeyFunc.toggleIME,
return VirtualKeyFunc.snippet; _ => null,
case VirtKey.clipboard: };
return VirtualKeyFunc.clipboard;
case VirtKey.ime:
return VirtualKeyFunc.toggleIME;
default:
return null;
}
}
bool get toggleable { bool get toggleable => switch (this) {
switch (this) { VirtKey.alt || VirtKey.ctrl => true,
case VirtKey.alt: _ => false,
case VirtKey.ctrl: };
return true;
default:
return false;
}
}
bool get canLongPress { bool get canLongPress => switch (this) {
switch (this) { VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,
case VirtKey.up: _ => false,
case VirtKey.left: };
case VirtKey.down:
case VirtKey.right:
return true;
default:
return false;
}
}
String? get help => switch (this) { String? get help => switch (this) {
VirtKey.sftp => l10n.virtKeyHelpSFTP, VirtKey.sftp => l10n.virtKeyHelpSFTP,
@@ -171,6 +224,18 @@ enum VirtKey {
VirtKey.ime => l10n.virtKeyHelpIME, VirtKey.ime => l10n.virtKeyHelpIME,
_ => null, _ => null,
}; };
}
enum VirtualKeyFunc { toggleIME, backspace, clipboard, snippet, file } /// - [saveDefaultIfErr] if the stored raw values is invalid, save default order to store
static List<VirtKey> loadFromStore({bool saveDefaultIfErr = true}) {
try {
final ints = Stores.setting.sshVirtKeys.fetch();
return ints.map((e) => VirtKey.values[e]).toList();
} on RangeError {
final ints = defaultOrder.map((e) => e.index).toList();
Stores.setting.sshVirtKeys.put(ints);
} catch (e, s) {
Loggers.app.warning('Failed to load sshVirtKeys', e, s);
}
return defaultOrder;
}
}

View File

@@ -45,6 +45,62 @@ class VirtKeyAdapter extends TypeAdapter<VirtKey> {
return VirtKey.pgup; return VirtKey.pgup;
case 15: case 15:
return VirtKey.pgdn; return VirtKey.pgdn;
case 16:
return VirtKey.slash;
case 17:
return VirtKey.backSlash;
case 18:
return VirtKey.underscore;
case 19:
return VirtKey.plus;
case 20:
return VirtKey.equal;
case 21:
return VirtKey.minus;
case 22:
return VirtKey.parenLeft;
case 23:
return VirtKey.parenRight;
case 24:
return VirtKey.bracketLeft;
case 25:
return VirtKey.bracketRight;
case 26:
return VirtKey.braceLeft;
case 27:
return VirtKey.braceRight;
case 28:
return VirtKey.chevronLeft;
case 29:
return VirtKey.chevronRight;
case 30:
return VirtKey.colon;
case 31:
return VirtKey.semicolon;
case 32:
return VirtKey.f1;
case 33:
return VirtKey.f2;
case 34:
return VirtKey.f3;
case 35:
return VirtKey.f4;
case 36:
return VirtKey.f5;
case 37:
return VirtKey.f6;
case 38:
return VirtKey.f7;
case 39:
return VirtKey.f8;
case 40:
return VirtKey.f9;
case 41:
return VirtKey.f10;
case 42:
return VirtKey.f11;
case 43:
return VirtKey.f12;
default: default:
return VirtKey.esc; return VirtKey.esc;
} }
@@ -101,6 +157,90 @@ class VirtKeyAdapter extends TypeAdapter<VirtKey> {
case VirtKey.pgdn: case VirtKey.pgdn:
writer.writeByte(15); writer.writeByte(15);
break; break;
case VirtKey.slash:
writer.writeByte(16);
break;
case VirtKey.backSlash:
writer.writeByte(17);
break;
case VirtKey.underscore:
writer.writeByte(18);
break;
case VirtKey.plus:
writer.writeByte(19);
break;
case VirtKey.equal:
writer.writeByte(20);
break;
case VirtKey.minus:
writer.writeByte(21);
break;
case VirtKey.parenLeft:
writer.writeByte(22);
break;
case VirtKey.parenRight:
writer.writeByte(23);
break;
case VirtKey.bracketLeft:
writer.writeByte(24);
break;
case VirtKey.bracketRight:
writer.writeByte(25);
break;
case VirtKey.braceLeft:
writer.writeByte(26);
break;
case VirtKey.braceRight:
writer.writeByte(27);
break;
case VirtKey.chevronLeft:
writer.writeByte(28);
break;
case VirtKey.chevronRight:
writer.writeByte(29);
break;
case VirtKey.colon:
writer.writeByte(30);
break;
case VirtKey.semicolon:
writer.writeByte(31);
break;
case VirtKey.f1:
writer.writeByte(32);
break;
case VirtKey.f2:
writer.writeByte(33);
break;
case VirtKey.f3:
writer.writeByte(34);
break;
case VirtKey.f4:
writer.writeByte(35);
break;
case VirtKey.f5:
writer.writeByte(36);
break;
case VirtKey.f6:
writer.writeByte(37);
break;
case VirtKey.f7:
writer.writeByte(38);
break;
case VirtKey.f8:
writer.writeByte(39);
break;
case VirtKey.f9:
writer.writeByte(40);
break;
case VirtKey.f10:
writer.writeByte(41);
break;
case VirtKey.f11:
writer.writeByte(42);
break;
case VirtKey.f12:
writer.writeByte(43);
break;
} }
} }

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'; import 'package:flutter/material.dart';
class AppProvider extends ChangeNotifier { final class AppProvider {
BuildContext? ctx; const AppProvider._();
bool isWearOS = false; static BuildContext? ctx;
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;
}
}
} }

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