Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b56e033773 | ||
|
|
7dda63af8a | ||
|
|
00d303ac36 | ||
|
|
229983d82e | ||
|
|
4928ca600d | ||
|
|
89ec2d94d6 | ||
|
|
393c3e6388 | ||
|
|
dee458e926 | ||
|
|
f89228db40 | ||
|
|
3b6fb6194b | ||
|
|
02444fc2f0 | ||
|
|
aef317a140 | ||
|
|
47aedb2f2e | ||
|
|
eab06abcaf | ||
|
|
c062c12a0e | ||
|
|
d7669c94b8 | ||
|
|
50af289574 | ||
|
|
90b88ed795 | ||
|
|
d611fdcd50 | ||
|
|
db9b2dd818 | ||
|
|
edb49ead67 | ||
|
|
7f0dc656b8 | ||
|
|
b33d0bbc3e | ||
|
|
7d0ea8a58b | ||
|
|
c18732d8f3 | ||
|
|
157af0a354 | ||
|
|
2d9dc044f9 | ||
|
|
479250c207 | ||
|
|
aef7ec911f | ||
|
|
4f9ee7781f | ||
|
|
eb83d05c81 | ||
|
|
329fd33b69 | ||
|
|
931c5f0bf6 | ||
|
|
bcbf1fbc17 | ||
|
|
3e7315dac6 | ||
|
|
4cecfdf7a8 | ||
|
|
0346821cf5 | ||
|
|
966a60a82d | ||
|
|
76e98c6468 | ||
|
|
d7ae8b75b8 | ||
|
|
b5329e2692 | ||
|
|
ef297673f3 | ||
|
|
7558b4806d | ||
|
|
f7ef8a3915 | ||
|
|
38366a2ef3 | ||
|
|
7e5bb54c98 | ||
|
|
7ce3854392 | ||
|
|
195ddd2bcc | ||
|
|
267b0b0a69 | ||
|
|
41e3fcb23a | ||
|
|
46d5840276 | ||
|
|
fe566e97ca | ||
|
|
ddd1524d63 | ||
|
|
4d8268c614 | ||
|
|
568b97606a | ||
|
|
42cc2416a1 | ||
|
|
aaa69f0f95 | ||
|
|
64676bc5cb | ||
|
|
a15c04956c | ||
|
|
e3c885483b | ||
|
|
493c86cacb | ||
|
|
ea7c8caf14 | ||
|
|
9db04a60c2 | ||
|
|
610f46da0d | ||
|
|
b8e5418ff2 | ||
|
|
0e21755acb | ||
|
|
73248011a1 | ||
|
|
969643d3df | ||
|
|
c90d0e4b3b | ||
|
|
f9aadc6b0f | ||
|
|
8fd4cc1fe1 | ||
|
|
432d76f024 | ||
|
|
ca8211e1a4 |
15
.github/FUNDING.yml
vendored
@@ -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
|
|
||||||
|
|||||||
26
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
50
README.md
@@ -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).
|
||||||
|
|
||||||
|
|||||||
58
README_zh.md
@@ -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) 中反馈。
|
|
||||||
|
|
||||||
|
|
||||||
## 🧱 贡献
|
## 🧱 贡献
|
||||||
任何正面的贡献都欢迎。
|
任何正面的贡献都欢迎。
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024 1.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
31
lib/app.dart
@@ -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();
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import 'package:server_box/data/res/build_data.dart';
|
|
||||||
|
|
||||||
extension BuildDataX on BuildData {
|
|
||||||
static const versionStr = 'v1.0.${BuildData.build}';
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(
|
||||||
|
|||||||
37
lib/data/model/app/backup.g.dart
Normal 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,
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
abstract class TagPickable {
|
|
||||||
bool containsTag(String tag);
|
|
||||||
|
|
||||||
String get tagName;
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
118
lib/data/model/server/systemd.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
167
lib/data/provider/systemd.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||