Compare commits

..

73 Commits

Author SHA1 Message Date
Noo6
b56e033773 fix: sftp open file on windows 2024-11-14 14:18:32 +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
lollipopkit🏳️‍⚧️
aaa69f0f95 chore: bump version 2024-08-04 19:26:31 +08:00
lollipopkit🏳️‍⚧️
64676bc5cb chore: l10n 2024-08-04 13:05:29 +08:00
lollipopkit🏳️‍⚧️
a15c04956c opt.: TipText 2024-08-04 12:02:57 +08:00
lollipopkit🏳️‍⚧️
e3c885483b opt.: use ssh term to decompress (#519) 2024-08-04 11:40:38 +08:00
lollipopkit🏳️‍⚧️
493c86cacb fix: home widget url (#517) 2024-08-04 00:17:21 +08:00
lollipopkit🏳️‍⚧️
ea7c8caf14 bug: color seed setting not working (#516) 2024-08-03 23:17:18 +08:00
lollipopkit🏳️‍⚧️
9db04a60c2 opt.: l10n & fix: write script (#514) 2024-08-03 22:44:21 +08:00
lollipopkit🏳️‍⚧️
610f46da0d opt.: bulk import servers (#512) 2024-08-03 14:52:39 +08:00
lollipopkit🏳️‍⚧️
b8e5418ff2 feat: import snippets from network (#510)
Fixes #507
2024-08-03 14:25:58 +08:00
lollipopkit🏳️‍⚧️
0e21755acb opt.: remove internal SharePreference keys while using KvEditor (#509)
Fixes #508
2024-08-03 12:37:16 +08:00
lollipopkit🏳️‍⚧️
73248011a1 migrate: fl_lib 2024-08-01 13:44:32 +08:00
lollipopkit🏳️‍⚧️
969643d3df opt.: debug page copy logs 2024-07-28 22:12:07 +08:00
lollipopkit🏳️‍⚧️
c90d0e4b3b fix: manual restore 2024-07-28 20:37:34 +08:00
lollipopkit🏳️‍⚧️
f9aadc6b0f opt.: Btn 2024-07-28 20:26:08 +08:00
lollipopkit🏳️‍⚧️
8fd4cc1fe1 opt.: TagsEditor & Btn 2024-07-28 19:05:31 +08:00
lollipopkit🏳️‍⚧️
432d76f024 fix: builtin editor (#503) 2024-07-28 15:06:34 +08:00
𝗦𝗵𝗟𝗲𝗿𝗣
ca8211e1a4 Add TR Locales (#497) 2024-07-27 16:42:09 +08:00
186 changed files with 6084 additions and 6673 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

@@ -9,8 +9,8 @@ permissions:
contents: write contents: write
jobs: jobs:
releaseAL: releaseAndroid:
name: Release android and linux name: Release android
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: Checkout
@@ -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.2' flutter-version: '3.24.1'
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: 'zulu'
@@ -29,7 +29,7 @@ jobs:
curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key
curl -u ${{ secrets.BASIC_AUTH }} -o android/key.properties ${{ secrets.URL_PREFIX }}key.properties curl -u ${{ secrets.BASIC_AUTH }} -o android/key.properties ${{ secrets.URL_PREFIX }}key.properties
- name: Build - name: Build
run: dart run fl_build -p android,linux run: dart run fl_build -p android
- name: Rename for fdroid - name: Rename for fdroid
run: | run: |
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
@@ -42,6 +42,24 @@ jobs:
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
releaseLinux:
name: Release linux
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Build
run: |
dart run fl_build -p linux
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -2,10 +2,11 @@ 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="../../issues/43">Linux</a> server status and tools to manage server.
@@ -14,6 +15,17 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
</p> </p>
## 🏙️ Screenshots
<table>
<tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
</tr>
</table>
## 📥 Install ## 📥 Install
Platform | From Platform | From
@@ -22,34 +34,22 @@ iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) 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) Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
**Please only download pkgs from the source that you trust!** Please only download pkgs from the source that **you trust**!
- `AppStore` & `CDN` packages are built by myself
- Github releases are built by Github Actions
- Other sources are built by themselves
## 🔖 Feature ## 🔖 Feature
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process`... - `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
- Platform specific: `Bio auth``Msg push``Home widget``watchOS App`... - 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); Español, Русский язык, Português, 日本語 (Generated by GPT) - English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)
## 🏙️ ScreenShots
<table>
<tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/1.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/2.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/3.png"></td>
</tr>
<tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/4.png"> </td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/5.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/6.png"></td>
</tr>
</table>
## 🆘 Help ## 🆘 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,10 +2,11 @@
<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="../../issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
@@ -13,6 +14,18 @@
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a> 特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>
</p> </p>
## 🏙️ 截屏
<table>
<tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
</tr>
</table>
## 📥 安装 ## 📥 安装
平台 | 下载 平台 | 下载
@@ -21,49 +34,34 @@ iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) 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) Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
**请不要从不受信任的来源下载!** 请从 **信任** 的来源下载!
- `AppStore` & `CDN` 的包由我构建
- Github 的包由 Github Actions 构建
- 其他来源由其所有者构建
## 🔖 特点 ## 🔖 特点
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程` 管理... - `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
- 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`... - 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`...
- 本地化 - 本地化
- English, 简体中文 - English, 简体中文
- Español, Русский язык, Português, 日本語 (Generated by GPT) - Español, Русский язык, Português, 日本語 (Generated by GPT)
- Deutsch (@its-tom) / 繁體中文 (@kalashnikov) / Indonesian (@azkadev) / Français (@FrancXPT) / Dutch (@QazCetelic) - 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);
- 感谢贡献者们!
## 🏙️ 截屏
<table>
<tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/1.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/2.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/3.png"></td>
</tr>
<tr>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/4.png"> </td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/5.png"></td>
<td><img width="277px" src="https://cdn.lolli.tech/serverbox/screenshot/6.png"></td>
</tr>
</table>
## 🆘 帮助 ## 🆘 帮助
- 吹水、参与开发、了解如何使用QQ群 **762870488** <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) 中反馈。
## 🧱 贡献 ## 🧱 贡献
任何正面的贡献都欢迎。 任何正面的贡献都欢迎。

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.

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" />
@@ -15,7 +16,8 @@
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,88 @@
package tech.lolli.toolbox
import android.app.*
import android.content.Intent
import android.os.Build
import android.os.IBinder
class ForegroundService : Service() {
private val chanId = "ForegroundServiceChannel"
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
"ACTION_STOP_FOREGROUND" -> {
stopForegroundService()
return START_NOT_STICKY
}
else -> {
val notification = createNotification()
startForeground(1, notification)
return START_STICKY
}
}
}
override fun onBind(intent: Intent): IBinder? {
return null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
chanId,
chanId,
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
}
private fun createNotification(): Notification {
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
)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
.setContentTitle("Server Box")
.setContentText("Open the app")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
.build()
} else {
Notification.Builder(this)
.setContentTitle("Server Box")
.setContentText("Open the app")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
.build()
}
}
fun stopForegroundService() {
stopForeground(true)
stopSelf()
}
}

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,6 +1,11 @@
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
@@ -18,8 +23,18 @@ class MainActivity: FlutterFragmentActivity() {
result.success(null) result.success(null)
} }
"startService" -> { "startService" -> {
val intent = Intent(this@MainActivity, KeepAliveService::class.java) reqPerm()
startService(intent) val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
}
"stopService" -> {
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
stopService(serviceIntent)
result.success(null)
} }
else -> { else -> {
result.notImplemented() result.notImplemented()
@@ -28,4 +43,16 @@ class MainActivity: FlutterFragmentActivity() {
} }
} }
} }
private fun reqPerm() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
123,
)
}
}
} }

View File

@@ -27,9 +27,12 @@ class HomeWidget : AppWidgetProvider() {
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)
var url = sp.getString("$appWidgetId", null) var url = sp.getString("widget_$appWidgetId", null)
val gUrl = sp.getString("*", null)
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
url = sp.getString("$appWidgetId", null)
}
if (url.isNullOrEmpty()) {
val gUrl = sp.getString("widget_*", null)
url = gUrl url = gUrl
} }

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

@@ -1,24 +1,23 @@
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
- flutter_native_splash (0.0.1): - flutter_native_splash (0.0.1):
- 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
- 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):
@@ -32,34 +31,36 @@ PODS:
- Flutter - Flutter
- watch_connectivity (0.0.1): - watch_connectivity (0.0.1):
- Flutter - Flutter
- webview_flutter_wkwebview (0.0.1):
- 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_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`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`) - watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
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:
:path: ".symlinks/plugins/flutter_background_service_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 +71,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:
@@ -84,24 +83,26 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
watch_connectivity: watch_connectivity:
:path: ".symlinks/plugins/watch_connectivity/ios" :path: ".symlinks/plugins/watch_connectivity/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
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_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
local_auth_darwin: 4d56c90c2683319835a61274b57620df9c4520ab local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
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: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
watch_connectivity: 715eb484685e05846eab74795348a44bb2809b82 watch_connectivity: 715eb484685e05846eab74795348a44bb2809b82
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8 PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8

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 = 1034; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -700,7 +682,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1034; MARKETING_VERSION = 1.0.1104;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -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 = 1034; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -836,7 +818,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1034; MARKETING_VERSION = 1.0.1104;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -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 = 1034; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -864,7 +846,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.1034; MARKETING_VERSION = 1.0.1104;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -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 = 1034; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -898,7 +880,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1034; MARKETING_VERSION = 1.0.1104;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -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 = 1034; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -937,7 +919,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1034; MARKETING_VERSION = 1.0.1104;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -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 = 1034; CURRENT_PROJECT_VERSION = 1104;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -973,7 +955,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.1034; MARKETING_VERSION = 1.0.1104;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -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 = 1034; CURRENT_PROJECT_VERSION = 1104;
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.1034; MARKETING_VERSION = 1.0.1104;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
@@ -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 = 1034; CURRENT_PROJECT_VERSION = 1104;
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.1034; MARKETING_VERSION = 1.0.1104;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;
@@ -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 = 1034; CURRENT_PROJECT_VERSION = 1104;
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.1034; MARKETING_VERSION = 1.0.1104;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;

View File

@@ -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>
<key>NSCameraUsageDescription</key>
<string>Scan QR codes and etc.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Get QR code and etc.</string>
</dict>
</plist> </plist>

View File

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

@@ -3,7 +3,6 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:fl_lib/l10n/gen_l10n/lib_l10n.dart'; import 'package:fl_lib/l10n/gen_l10n/lib_l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:locale_names/locale_names.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';
@@ -23,7 +22,10 @@ class MyApp extends StatelessWidget {
listenable: RNodes.app, listenable: RNodes.app,
builder: (context, _) { builder: (context, _) {
if (!Stores.setting.useSystemPrimaryColor.fetch()) { if (!Stores.setting.useSystemPrimaryColor.fetch()) {
UIs.colorSeed = Color(Stores.setting.primaryColor.fetch()); final colorSeed = Color(Stores.setting.colorSeed.fetch());
UIs.colorSeed = colorSeed;
// Past code uses [UIs.primaryColor] as the primary color
UIs.primaryColor = colorSeed;
return _buildApp( return _buildApp(
context, context,
light: ThemeData( light: ThemeData(
@@ -72,6 +74,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,
@@ -84,19 +87,21 @@ class MyApp extends StatelessWidget {
themeMode: themeMode, themeMode: themeMode,
theme: light.fixWindowsFont, theme: light.fixWindowsFont,
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont, darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
home: Builder( home: VirtualWindowFrame(
builder: (context) { child: Builder(
context.setLibL10n(); builder: (context) {
final appL10n = AppLocalizations.of(context); context.setLibL10n();
if (appL10n != null) l10n = appL10n; final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
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();
}, },
),
), ),
); );
} }

View File

@@ -11,4 +11,8 @@ abstract final class BgRunMC {
static void startService() { static void startService() {
_channel.invokeMethod('startService'); _channel.invokeMethod('startService');
} }
static void stopService() {
_channel.invokeMethod('stopService');
}
} }

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

@@ -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,18 +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/provider.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';
@@ -21,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;
@@ -61,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');
} }
@@ -69,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),
@@ -83,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),
@@ -94,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,
}) { }) {
@@ -119,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,
@@ -149,48 +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: DebugPageArgs(
notifier: Pros.debug.widgets,
onClear: Pros.debug.clear,
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');
// } // }
@@ -203,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');
} }
@@ -237,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');
} }
@@ -245,14 +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');
} }
static AppRoutes kvEditor({Key? key, required Map<String, String> data}) {
return AppRoutes(
KvEditor(key: key, args: KvEditorArgs(data: data)),
'kv_editor',
);
}
} }

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

@@ -0,0 +1,40 @@
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/backup.dart';
import 'package:server_box/data/store/no_backup.dart';
const bakSync = BakSyncer._();
final class BakSyncer extends SyncIface<Backup> {
const BakSyncer._() : super();
@override
Future<void> saveToFile() => Backup.backup();
@override
Future<Backup> fromFile(String path) async {
final content = await File(path).readAsString();
return Backup.fromJsonString(content);
}
@override
Future<RemoteStorage?> get remoteStorage async {
if (isMacOS || isIOS) await icloud.init('iCloud.tech.lolli.serverbox');
final settings = NoBackupStore.instance;
await webdav.init(WebdavInitArgs(
url: settings.webdavUrl.fetch(),
user: settings.webdavUser.fetch(),
pwd: settings.webdavPwd.fetch(),
prefix: 'serverbox/',
));
final icloudEnabled = settings.icloudSync.fetch();
if (icloudEnabled) return icloud;
final webdavEnabled = settings.webdavSync.fetch();
if (webdavEnabled) return webdav;
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.
/// ///
@@ -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,34 +38,13 @@ 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() Backup.loadFromStore()
: version = backupFormatVersion, : version = backupFormatVersion,
@@ -71,16 +54,18 @@ class Backup {
keys = Stores.key.fetch(), keys = Stores.key.fetch(),
container = Stores.container.box.toJson(), container = Stores.container.box.toJson(),
lastModTime = Stores.lastModTime, lastModTime = Stores.lastModTime,
history = Stores.history.box.toJson(); history = Stores.history.box.toJson(),
settings = Stores.setting.box.toJson();
static Future<String> backup([String? name]) async { static Future<String> backup([String? name]) async {
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson())); final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson()));
final path = '${Paths.doc}/${name ?? Miscs.bakFileName}'; final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result); await File(path).writeAsString(result);
return path; return path;
} }
Future<void> restore({bool force = false}) async { @override
Future<void> merge({bool force = false}) async {
final curTime = Stores.lastModTime ?? 0; final curTime = Stores.lastModTime ?? 0;
final bakTime = lastModTime ?? 0; final bakTime = lastModTime ?? 0;
final shouldRestore = force || curTime < bakTime; final shouldRestore = force || curTime < bakTime;
@@ -90,93 +75,142 @@ class Backup {
} }
// Snippets // Snippets
final nowSnippets = Stores.snippet.box.keys.toSet(); if (force) {
final bakSnippets = snippets.map((e) => e.name).toSet(); for (final s in snippets) {
final newSnippets = bakSnippets.difference(nowSnippets); Stores.snippet.box.put(s.name, s);
final delSnippets = nowSnippets.difference(bakSnippets); }
final updateSnippets = nowSnippets.intersection(bakSnippets); } else {
for (final s in newSnippets) { final nowSnippets = Stores.snippet.box.keys.toSet();
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s)); final bakSnippets = snippets.map((e) => e.name).toSet();
} final newSnippets = bakSnippets.difference(nowSnippets);
for (final s in delSnippets) { final delSnippets = nowSnippets.difference(bakSnippets);
Stores.snippet.box.delete(s); final updateSnippets = nowSnippets.intersection(bakSnippets);
} for (final s in newSnippets) {
for (final s in updateSnippets) { Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s)); }
for (final s in delSnippets) {
Stores.snippet.box.delete(s);
}
for (final s in updateSnippets) {
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
}
} }
// ServerPrivateInfo // ServerPrivateInfo
final nowSpis = Stores.server.box.keys.toSet(); if (force) {
final bakSpis = spis.map((e) => e.id).toSet(); for (final s in spis) {
final newSpis = bakSpis.difference(nowSpis); Stores.server.box.put(s.id, s);
final delSpis = nowSpis.difference(bakSpis); }
final updateSpis = nowSpis.intersection(bakSpis); } else {
for (final s in newSpis) { final nowSpis = Stores.server.box.keys.toSet();
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s)); final bakSpis = spis.map((e) => e.id).toSet();
} final newSpis = bakSpis.difference(nowSpis);
for (final s in delSpis) { final delSpis = nowSpis.difference(bakSpis);
Stores.server.box.delete(s); final updateSpis = nowSpis.intersection(bakSpis);
} for (final s in newSpis) {
for (final s in updateSpis) { Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s)); }
for (final s in delSpis) {
Stores.server.box.delete(s);
}
for (final s in updateSpis) {
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
}
} }
// PrivateKeyInfo // PrivateKeyInfo
final nowKeys = Stores.key.box.keys.toSet(); if (force) {
final bakKeys = keys.map((e) => e.id).toSet(); for (final s in keys) {
final newKeys = bakKeys.difference(nowKeys); Stores.key.box.put(s.id, s);
final delKeys = nowKeys.difference(bakKeys); }
final updateKeys = nowKeys.intersection(bakKeys); } else {
for (final s in newKeys) { final nowKeys = Stores.key.box.keys.toSet();
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s)); final bakKeys = keys.map((e) => e.id).toSet();
} final newKeys = bakKeys.difference(nowKeys);
for (final s in delKeys) { final delKeys = nowKeys.difference(bakKeys);
Stores.key.box.delete(s); final updateKeys = nowKeys.intersection(bakKeys);
} for (final s in newKeys) {
for (final s in updateKeys) { Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s)); }
for (final s in delKeys) {
Stores.key.box.delete(s);
}
for (final s in updateKeys) {
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
}
} }
// History // History
final nowHistory = Stores.history.box.keys.toSet(); if (force) {
final bakHistory = history.keys.toSet(); Stores.history.box.putAll(history);
final newHistory = bakHistory.difference(nowHistory); } else {
final delHistory = nowHistory.difference(bakHistory); final nowHistory = Stores.history.box.keys.toSet();
final updateHistory = nowHistory.intersection(bakHistory); final bakHistory = history.keys.toSet();
for (final s in newHistory) { final newHistory = bakHistory.difference(nowHistory);
Stores.history.box.put(s, history[s]); final delHistory = nowHistory.difference(bakHistory);
} final updateHistory = nowHistory.intersection(bakHistory);
for (final s in delHistory) { for (final s in newHistory) {
Stores.history.box.delete(s); Stores.history.box.put(s, history[s]);
} }
for (final s in updateHistory) { for (final s in delHistory) {
Stores.history.box.put(s, history[s]); Stores.history.box.delete(s);
}
for (final s in updateHistory) {
Stores.history.box.put(s, history[s]);
}
} }
// Container // Container
final nowContainer = Stores.container.box.keys.toSet(); if (force) {
final bakContainer = container.keys.toSet(); Stores.container.box.putAll(container);
final newContainer = bakContainer.difference(nowContainer); } else {
final delContainer = nowContainer.difference(bakContainer); final nowContainer = Stores.container.box.keys.toSet();
final updateContainer = nowContainer.intersection(bakContainer); final bakContainer = container.keys.toSet();
for (final s in newContainer) { final newContainer = bakContainer.difference(nowContainer);
Stores.container.box.put(s, container[s]); final delContainer = nowContainer.difference(bakContainer);
} final updateContainer = nowContainer.intersection(bakContainer);
for (final s in delContainer) { for (final s in newContainer) {
Stores.container.box.delete(s); Stores.container.box.put(s, container[s]);
} }
for (final s in updateContainer) { for (final s in delContainer) {
Stores.container.box.put(s, container[s]); Stores.container.box.delete(s);
}
for (final s in updateContainer) {
Stores.container.box.put(s, container[s]);
}
} }
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

@@ -1,3 +1,4 @@
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';
@@ -21,46 +22,27 @@ enum ContainerMenu {
terminal, terminal,
//stats, //stats,
]; ];
} else {
return [start, rm, logs];
} }
return [start, rm, logs];
} }
IconData get icon { IconData get icon => switch (this) {
switch (this) { ContainerMenu.start => Icons.play_arrow,
case ContainerMenu.start: ContainerMenu.stop => Icons.stop,
return Icons.play_arrow; ContainerMenu.restart => Icons.restart_alt,
case ContainerMenu.stop: ContainerMenu.rm => Icons.delete,
return Icons.stop; ContainerMenu.logs => Icons.logo_dev,
case ContainerMenu.restart: ContainerMenu.terminal => Icons.terminal,
return Icons.restart_alt; // DockerMenuType.stats => Icons.bar_chart,
case ContainerMenu.rm: };
return Icons.delete;
case ContainerMenu.logs:
return Icons.logo_dev;
case ContainerMenu.terminal:
return Icons.terminal;
// case DockerMenuType.stats:
// return Icons.bar_chart;
}
}
String get toStr { String get toStr => switch (this) {
switch (this) { ContainerMenu.start => l10n.start,
case ContainerMenu.start: ContainerMenu.stop => l10n.stop,
return l10n.start; ContainerMenu.restart => l10n.restart,
case ContainerMenu.stop: ContainerMenu.rm => libL10n.delete,
return l10n.stop; ContainerMenu.logs => libL10n.log,
case ContainerMenu.restart: ContainerMenu.terminal => l10n.terminal,
return l10n.restart; // DockerMenuType.stats => s.stats,
case ContainerMenu.rm: };
return l10n.delete;
case ContainerMenu.logs:
return l10n.log;
case ContainerMenu.terminal:
return l10n.terminal;
// case DockerMenuType.stats:
// return s.stats;
}
}
} }

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

@@ -1,7 +1,7 @@
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';
@@ -14,80 +14,67 @@ enum NetViewType {
@HiveField(2) @HiveField(2)
traffic; traffic;
NetViewType get next { NetViewType get next => switch (this) {
switch (this) { conn => speed,
case conn: speed => traffic,
return speed; traffic => conn,
case speed: };
return traffic;
case traffic:
return conn;
}
}
String get toStr { String get toStr => switch (this) {
switch (this) { NetViewType.conn => l10n.conn,
case NetViewType.conn: NetViewType.traffic => l10n.traffic,
return l10n.conn; NetViewType.speed => l10n.speed,
case NetViewType.traffic: };
return l10n.traffic;
case NetViewType.speed:
return 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) {
'${l10n.failed}:\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');
} }
} }
int toJson() { int toJson() => switch (this) {
switch (this) { NetViewType.conn => 0,
case NetViewType.conn: NetViewType.speed => 1,
return 0; NetViewType.traffic => 2,
case NetViewType.speed: };
return 1;
case NetViewType.traffic:
return 2;
}
}
static NetViewType fromJson(int json) { static NetViewType fromJson(int json) => switch (json) {
switch (json) { 0 => NetViewType.conn,
case 0: 1 => NetViewType.speed,
return NetViewType.conn; _ => NetViewType.traffic,
case 2: };
return NetViewType.traffic;
default:
return NetViewType.speed;
}
}
} }

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

@@ -31,7 +31,7 @@ enum ServerDetailCards {
static final names = values.map((e) => e.name).toList(); static final names = values.map((e) => e.name).toList();
String get toStr => switch (this) { String get toStr => switch (this) {
about => l10n.about, about => libL10n.about,
cpu => 'CPU', cpu => 'CPU',
mem => 'RAM', mem => 'RAM',
swap => 'Swap', swap => 'Swap',

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,
@@ -19,14 +20,43 @@ enum ShellFunc {
/// srvboxm -> ServerBox Mobile /// srvboxm -> ServerBox Mobile
static const scriptFile = 'srvboxm_v${BuildData.script}.sh'; static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
static const scriptDir = '~/.config/server_box'; static const scriptDirHome = '~/.config/server_box';
static const scriptPath = '$scriptDir/$scriptFile'; static const scriptDirTmp = '/tmp/server_box';
static const String installShellCmd = """ static final _scriptDirMap = <String, String>{};
/// Get the script directory for the given [id].
///
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// it will be changed to [scriptDirHome]/[scriptFile].
static String getScriptDir(String id) {
final customScriptDir =
ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
return _scriptDirMap.putIfAbsent(id, () {
return scriptDirTmp;
});
}
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
_ => _scriptDirMap[id] = scriptDirHome,
};
static String getScriptPath(String id) {
return '${getScriptDir(id)}/$scriptFile';
}
static String getInstallShellCmd(String id) {
final scriptDir = getScriptDir(id);
final scriptPath = '$scriptDir/$scriptFile';
return '''
mkdir -p $scriptDir mkdir -p $scriptDir
cat > $scriptPath cat > $scriptPath
chmod 744 $scriptPath chmod 755 $scriptPath
"""; ''';
}
String get flag => switch (this) { String get flag => switch (this) {
ShellFunc.process => 'p', ShellFunc.process => 'p',
@@ -37,7 +67,7 @@ chmod 744 $scriptPath
// ShellFunc.docker=> 'd', // ShellFunc.docker=> 'd',
}; };
String get exec => 'sh $scriptPath -$flag'; String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
String get name { String get name {
switch (this) { 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

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

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,5 +1,4 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
final class SensorAdaptor { final class SensorAdaptor {
final String raw; final String raw;
@@ -37,7 +36,7 @@ final class SensorItem {
String get toMarkdown { String get toMarkdown {
final sb = StringBuffer(); final sb = StringBuffer();
sb.writeln('| ${l10n.name} | ${l10n.content} |'); sb.writeln('| ${libL10n.name} | ${libL10n.content} |');
sb.writeln('| --- | --- |'); sb.writeln('| --- | --- |');
for (final entry in details.entries) { for (final entry in details.entries) {
sb.writeln('| ${entry.key} | ${entry.value} |'); sb.writeln('| ${entry.key} | ${entry.value} |');
@@ -80,9 +79,7 @@ final class SensorItem {
for (var idx = 2; idx < len; idx++) { for (var idx = 2; idx < len; idx++) {
final part = sensorLines[idx]; final part = sensorLines[idx];
final detailParts = part.split(':'); final detailParts = part.split(':');
if (detailParts.length < 2) { if (detailParts.length < 2) continue;
continue;
}
final key = detailParts[0].trim(); final key = detailParts[0].trim();
final value = detailParts[1].trim(); final value = detailParts[1].trim();
details[key] = value; details[key] = value;

View File

@@ -1,6 +1,4 @@
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart'; import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/battery.dart'; import 'package:server_box/data/model/server/battery.dart';
@@ -15,12 +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';
import '../app/tag_pickable.dart'; class Server {
Spi spi;
part 'server.ext.dart';
class Server implements TagPickable {
ServerPrivateInfo spi;
ServerStatus status; ServerStatus status;
SSHClient? client; SSHClient? client;
ServerConn conn; ServerConn conn;
@@ -32,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;
@@ -95,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,58 +0,0 @@
part of 'server.dart';
extension ServerX on Server {
String getTopRightStr(ServerPrivateInfo spi) {
switch (conn) {
case ServerConn.disconnected:
return l10n.disconnected;
case ServerConn.finished:
// Highest priority of temperature display
final cmdTemp = () {
final val = status.customCmds['server_card_top_right'];
if (val == null) return null;
// This returned value is used on server card top right, so it should
// be a single line string.
return val.split('\n').lastOrNull;
}();
final temperatureVal = () {
// Second priority
final preferTempDev = spi.custom?.preferTempDev;
if (preferTempDev != null) {
final preferTemp = status.sensors
.firstWhereOrNull((e) => e.device == preferTempDev)
?.summary
?.split(' ')
.firstOrNull;
if (preferTemp != null) {
return double.tryParse(preferTemp.replaceFirst('°C', ''));
}
}
// Last priority
final temp = status.temps.first;
if (temp != null) {
return temp;
}
return null;
}();
final upTime = status.more[StatusCmdType.uptime];
final items = [
cmdTemp ??
(temperatureVal != null
? '${temperatureVal.toStringAsFixed(1)}°C'
: null),
upTime
];
final str = items.where((e) => e != null && e.isNotEmpty).join(' | ');
if (str.isEmpty) return l10n.noResult;
return str;
case ServerConn.loading:
return l10n.serverTabLoading;
case ServerConn.connected:
return l10n.connected;
case ServerConn.connecting:
return l10n.serverTabConnecting;
case ServerConn.failed:
return status.err != null ? l10n.viewErr : l10n.serverTabFailed;
}
}
}

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,27 +106,43 @@ 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(
} name: 'name',
ip: 'ip',
port: 22,
user: 'root',
pwd: 'pwd',
keyId: 'private_key_id',
tags: ['tag1', 'tag2'],
alterUrl: 'user@ip:port',
autoConnect: true,
jumpId: 'jump_server_id',
custom: ServerCustom(
pveAddr: 'http://localhost:8006',
pveIgnoreCert: false,
cmds: {
'echo': 'echo hello',
},
preferTempDev: 'nvme-pci-0400',
logoUrl: 'https://example.com/logo.png',
),
);
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
@@ -183,6 +175,14 @@ class Snippet implements TagPickable {
r'${ctrl': TerminalKey.control, r'${ctrl': TerminalKey.control,
r'${alt': TerminalKey.alt, r'${alt': TerminalKey.alt,
}; };
static const example = Snippet(
name: 'example',
script: 'echo hello',
tags: ['tag'],
note: 'note',
autoRunOn: ['server_id'],
);
} }
class SnippetResult { class SnippetResult {

View File

@@ -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, seperator: _sep);
}
bool undo() {
if (_prePath.isEmpty) {
return false;
}
_path = _prePath.removeLast();
return true;
}
} }

View File

@@ -1,19 +1,12 @@
import 'dart:async'; part of 'worker.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/res/store.dart';
import '../../../core/utils/server.dart';
import '../server/server_private_info.dart';
import '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(
@@ -69,9 +62,8 @@ class SftpReqStatus {
int get hashCode => id ^ super.hashCode; int get hashCode => id ^ super.hashCode;
void dispose() { void dispose() {
// ignore: deprecated_member_use_from_same_package worker._dispose();
worker.dispose(); completer?.complete(true);
completer?.complete();
} }
void onNotify(dynamic event) { void onNotify(dynamic event) {

View File

@@ -5,9 +5,12 @@ import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:easy_isolate/easy_isolate.dart'; import 'package:easy_isolate/easy_isolate.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/store.dart';
import '../../../core/utils/server.dart'; part 'req.dart';
import 'req.dart';
class SftpWorker { class SftpWorker {
final Function(Object event) onNotify; final Function(Object event) onNotify;
@@ -20,14 +23,7 @@ class SftpWorker {
required this.req, required this.req,
}); });
/// Use [@Deprecated] to prevent calling [SftpWorker.dispose] directly void _dispose() {
///
/// Don't delete this method
@Deprecated(
"Use [SftpWorkerStatus.dispose] to dispose the worker, "
"instead of [SftpWorker.dispose]",
)
void dispose() {
worker.dispose(); worker.dispose();
} }

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,80 @@ 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,
;
}
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 +132,56 @@ 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: _ => null,
return TerminalKey.control; };
case VirtKey.left:
return TerminalKey.arrowLeft;
case VirtKey.down:
return TerminalKey.arrowDown;
case VirtKey.right:
return TerminalKey.arrowRight;
case VirtKey.pgup:
return TerminalKey.pageUp;
case VirtKey.pgdn:
return TerminalKey.pageDown;
default:
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 +189,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,38 @@ 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;
default: default:
return VirtKey.esc; return VirtKey.esc;
} }
@@ -101,6 +133,54 @@ 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;
} }
} }

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,22 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart'; import 'package:server_box/core/sync.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/store.dart'; import 'package:server_box/data/res/store.dart';
class SnippetProvider extends ChangeNotifier { class SnippetProvider extends Provider {
late List<Snippet> _snippets; const SnippetProvider._();
List<Snippet> get snippets => _snippets; static const instance = SnippetProvider._();
final _tags = ValueNotifier(<String>[]); static final snippets = <Snippet>[].vn;
ValueNotifier<List<String>> get tags => _tags; static final tags = <String>{}.vn;
@override
void load() { void load() {
_snippets = Stores.snippet.fetch(); super.load();
final snippets_ = Stores.snippet.fetch();
final order = Stores.setting.snippetOrder.fetch(); final order = Stores.setting.snippetOrder.fetch();
if (order.isNotEmpty) { if (order.isNotEmpty) {
final surplus = _snippets.reorder( final surplus = snippets_.reorder(
order: order, order: order,
finder: (n, name) => n.name == name, finder: (n, name) => n.name == name,
); );
@@ -25,47 +25,49 @@ class SnippetProvider extends ChangeNotifier {
Stores.setting.snippetOrder.put(order); Stores.setting.snippetOrder.put(order);
} }
} }
snippets.value = snippets_;
_updateTags(); _updateTags();
} }
void _updateTags() { static void _updateTags() {
_tags.value.clear(); final tags_ = <String>{};
final tags = <String>{}; for (final s in snippets.value) {
for (final s in _snippets) { final t = s.tags;
if (s.tags?.isEmpty ?? true) { if (t != null) {
continue; tags_.addAll(t);
} }
tags.addAll(s.tags!);
} }
_tags.value.addAll(tags); tags.value = tags_;
_tags.notifyListeners();
} }
void add(Snippet snippet) { static void add(Snippet snippet) {
_snippets.add(snippet); snippets.value.add(snippet);
snippets.notify();
Stores.snippet.put(snippet); Stores.snippet.put(snippet);
_updateTags(); _updateTags();
notifyListeners(); bakSync.sync(milliDelay: 1000);
} }
void del(Snippet snippet) { static void del(Snippet snippet) {
_snippets.remove(snippet); snippets.value.remove(snippet);
snippets.notify();
Stores.snippet.delete(snippet); Stores.snippet.delete(snippet);
_updateTags(); _updateTags();
notifyListeners(); bakSync.sync(milliDelay: 1000);
} }
void update(Snippet old, Snippet newOne) { static void update(Snippet old, Snippet newOne) {
snippets.value.remove(old);
snippets.value.add(newOne);
snippets.notify();
Stores.snippet.delete(old); Stores.snippet.delete(old);
Stores.snippet.put(newOne); Stores.snippet.put(newOne);
_snippets.remove(old);
_snippets.add(newOne);
_updateTags(); _updateTags();
notifyListeners(); bakSync.sync(milliDelay: 1000);
} }
void renameTag(String old, String newOne) { static void renameTag(String old, String newOne) {
for (final s in _snippets) { for (final s in snippets.value) {
if (s.tags?.contains(old) ?? false) { if (s.tags?.contains(old) ?? false) {
s.tags?.remove(old); s.tags?.remove(old);
s.tags?.add(newOne); s.tags?.add(newOne);
@@ -73,8 +75,6 @@ class SnippetProvider extends ChangeNotifier {
} }
} }
_updateTags(); _updateTags();
notifyListeners(); bakSync.sync(milliDelay: 1000);
} }
String get export => json.encode(snippets);
} }

View File

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

View File

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

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