Compare commits

...

31 Commits

Author SHA1 Message Date
lollipopkit
e5ef28415b opt.: ssh tab 2024-06-08 21:39:42 +08:00
lollipopkit
46b98df153 Merge branch 'dev' 2024-06-08 20:58:41 +08:00
lollipopkit
9f34021c90 fix: docker ps parse if id/name is too long 2024-06-08 20:57:56 +08:00
lollipopkit
8121eef839 opt.: RNode 2024-06-08 15:35:19 +08:00
lollipopkit
da48d1f66c opt.: IME popup after opening drawer if ssh term is focusing 2024-06-08 13:53:23 +08:00
lollipopkit
b167287c5b opt.: ssh tab page's tab bar 2024-06-07 23:53:13 +08:00
lollipopkit
41f9da6bf8 fix: ssh tab PageView animteToPage 2024-06-07 21:51:00 +08:00
lollipopkit
e7c7fc8186 fix: ssh tab name generaton 2024-06-07 21:43:38 +08:00
lollipopkit
b950dd2d68 fix: ssh tab 2024-06-07 21:09:17 +08:00
lollipopkit
6d34de14d3 Merge branch 'dev' 2024-06-06 21:26:03 +08:00
lollipopkit
a5a84c0cdd fix: podman log 2024-06-06 18:52:20 +08:00
lollipopkit
701b1b811f feat: beta program 2024-06-06 16:18:10 +08:00
lollipopkit
97267cdfbf fix: docker container status (#374) 2024-06-05 21:58:19 +08:00
lollipopkit
40ce37d230 opt.: sftp del dir 2024-06-05 18:48:44 +08:00
lollipopkit
8a9ade355c fix: update check 2024-06-05 18:26:02 +08:00
lollipopkit
9bffec64b5 fix: wol cfg (#373) 2024-06-05 18:16:17 +08:00
lollipopkit
a03ee2ae0e fix: term help 2024-06-05 11:12:43 +08:00
PaperCube
ee889235fe Fixed UI representation of server reorder page (#372) 2024-06-04 18:57:57 +01:00
lollipopkit
94d6d80497 chore: README 2024-06-04 23:17:32 +08:00
lollipopkit
413c45a559 opt.: backup 2024-06-04 22:33:59 +08:00
lollipopkit
6dc5536c48 fix: batch import 2024-06-04 19:57:56 +08:00
lollipopkit
76c4bf56fa fix: sync 2024-06-02 16:04:45 +08:00
lollipopkit
a0c6642230 fix: ios watch config 2024-06-02 15:50:01 +08:00
lollipopkit
4198d7bd13 opt.: ios watch config 2024-06-02 15:30:21 +08:00
lollipopkit
b06fddec07 new: windows release actions 2024-06-02 15:29:53 +08:00
lollipopkit
d1f14bee59 opt.: speed up docker page 2024-06-01 22:36:02 +08:00
lollipopkit
8953f63197 opt.: pve 2024-06-01 17:10:22 +08:00
lollipopkit🏳️‍⚧️
193d80d826 Merge pull request #370 from leganck/main
Add pve http proxy
2024-05-31 14:15:18 +08:00
leganck
9e308792aa Add pve http proxy 2024-05-30 23:30:45 +08:00
lollipopkit
fbabd8c351 new: wear settings (#358) & opt.: android widget edit 2024-05-27 20:52:46 +08:00
lollipopkit
1a3cb09ca2 fix: ssh term (#365) 2024-05-27 11:32:52 +08:00
71 changed files with 966 additions and 734 deletions

View File

@@ -11,42 +11,54 @@ permissions:
jobs: jobs:
releaseAL: releaseAL:
name: Release android and linux name: Release android and linux
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: '0'
- name: Install Flutter - name: Install Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Fetch secrets
run: |
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
- name: Build - name: Build
run: dart run fl_build -p android,linux run: dart run fl_build -p android,linux
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_arm.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_amd64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.apk
${{ env.APP_NAME }}_amd64.AppImage ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# releaseWin: releaseWin:
# name: Release windows name: Release windows
# runs-on: windows-latest runs-on: windows-latest
# steps: steps:
# - name: Checkout - name: Checkout
# uses: actions/checkout@v4 uses: actions/checkout@v4
# - name: Install Flutter with:
# uses: subosito/flutter-action@v2 fetch-depth: '0'
# - name: Build - name: Install Flutter
# run: dart run fl_build -p windows uses: subosito/flutter-action@v2
# - name: Create Release - name: Build
# uses: softprops/action-gh-release@v1 run: dart run fl_build -p windows
# with: - name: Create Release
# files: | uses: softprops/action-gh-release@v2
# ${{ env.APP_NAME }}_amd64_windows.zip with:
# env: files: |
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_windows_amd64.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# releaseApple: # releaseApple:
# name: Release ios and macos # name: Release ios and macos
@@ -59,7 +71,7 @@ jobs:
# - name: Build # - name: Build
# run: dart run fl_build -p ios,mac # run: dart run fl_build -p ios,mac
# - name: Create Release # - name: Create Release
# uses: softprops/action-gh-release@v1 # uses: softprops/action-gh-release@v2
# with: # with:
# files: | # files: |
# ${{ env.APP_NAME }}_universal_macos.zip # ${{ env.APP_NAME }}_universal_macos.zip

3
.gitignore vendored
View File

@@ -57,9 +57,10 @@ test.dart
# Linux release # Linux release
linux.AppDir linux.AppDir
ServerBox-x86_64.AppImage **/*.AppImage
untranlated.json untranlated.json
.vscode/settings.json .vscode/settings.json
more_build_data.json more_build_data.json
trans.txt

View File

@@ -15,44 +15,32 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
</p> </p>
## ⬇️ Download ## ⬇️ Download
[iOS](https://apps.apple.com/app/id1586449703) / [Android](https://cdn.lolli.tech/serverbox/latest.apk) / [macOS](https://apps.apple.com/app/id1586449703): Full support with my own certificate 🎉 **The `Android / Linux / Windows` version are now built via GitHub Actions**
[Linux](https://cdn.lolli.tech/serverbox/latest.AppImage) / [Windows](https://cdn.lolli.tech/serverbox/latest.win.zip): Basically tested, with debug certificate
[iOS & macOS](https://apps.apple.com/app/id1586449703) / [Android & Linux & Windows](https://github.com/lollipopkit/flutter_server_box/releases)
All deprecated versions before `v930` can be found in [here](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid).
## 🔖 Feature ## 🔖 Feature
- Status chart, `SSH` Terminal, `SFTP`, `Docker & Pkg & Process`, Code editor... - `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Pkg & Process`...
- Platform specific: `Bio auth``Msg push``Home widget``watchOS App`... - Platform specific: `Bio auth``Msg push``Home widget``watchOS App`...
- Localization - 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, 简体中文
- Español, Русский язык, Português, 日本語 (Generated by GPT)
- Deutsch (@its-tom) / 繁體中文 (@kalashnikov) / Indonesian (@azkadev) / Français (@FrancXPT) / Dutch (@QazCetelic)
## 🏙️ ScreenShots ## 🏙️ ScreenShots
<table> <table>
<tr> <tr>
<td> <td><img width="277px" src="imgs/server.png"></td>
<img width="277px" src="imgs/server.png"> <td><img width="277px" src="imgs/detail.png"></td>
</td> <td><img width="277px" src="imgs/sftp.png"></td>
<td>
<img width="277px" src="imgs/detail.png">
</td>
<td>
<img width="277px" src="imgs/sftp.png">
</td>
</tr> </tr>
</table> </table>
<table> <table>
<tr> <tr>
<td> <td><img width="277px" src="imgs/editor.png"> </td>
<img width="277px" src="imgs/editor.png"> <td><img width="277px" src="imgs/ssh.png"></td>
</td> <td><img width="277px" src="imgs/docker.png"></td>
<td>
<img width="277px" src="imgs/ssh.png">
</td>
<td>
<img width="277px" src="imgs/docker.png">
</td>
</tr> </tr>
</table> </table>
@@ -66,9 +54,7 @@ Before you open an issue, please read the following:
2. Make sure whether the issue is caused by ServerBox app. 2. Make sure whether the issue is caused by ServerBox app.
3. Welcome all valid and positive feedback, subjective feedback (such as you think other UI is better) may not be accepted. 3. Welcome all valid and positive feedback, subjective feedback (such as you think other UI is better) may not be accepted.
After you read the above, you can: After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
- If you have **any question or feature request**, please open a [discussion](https://github.com/lollipopkit/flutter_server_box/discussions/new/choose).
- If ServerBox app has **any bug**, please open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
## 🧱 Contribution ## 🧱 Contribution
@@ -76,15 +62,8 @@ After you read the above, you can:
- [l10n guide](https://blog.lolli.tech/faq/) can be found in my blog. - [l10n guide](https://blog.lolli.tech/faq/) can be found in my blog.
## 👏🏼 Contributors
<a href="https://github.com/lollipopkit/flutter_server_box/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lollipopkit/flutter_server_box" />
</a>
## 💡 My other apps ## 💡 My other apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms. - [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms.
- [2FA Box](https://github.com/lollipopkit/flutter_2fa) - Open source 2FA app for Android, iOS and the web.
- [More](https://github.com/lollipopkit) - Tools & etc. - [More](https://github.com/lollipopkit) - Tools & etc.

View File

@@ -16,12 +16,15 @@
## ⬇️ Download ## ⬇️ Download
[iOS](https://apps.apple.com/app/id1586449703) / [Android](https://cdn.lolli.tech/serverbox/latest.apk) / [macOS](https://apps.apple.com/app/id1586449703): 经过测试,使用自签名证书 🎉 **现在 `Android / Linux / Windows` 版本使用 GitHub Actions 构建**
[Linux](https://cdn.lolli.tech/serverbox/latest.AppImage) / [Windows](https://cdn.lolli.tech/serverbox/latest.win.zip): 经过不完全测试,使用调试证书
[iOS & macOS](https://apps.apple.com/app/id1586449703) / [Android & Linux & Windows](https://github.com/lollipopkit/flutter_server_box/releases)
所有 `v930` 之前的版本可以在 [这里](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) 找到。
## 🔖 特点 ## 🔖 特点
- 状态图表, `SSH` 终端, `SFTP`, `Docker & 包 & 进程` 管理器, 代码编辑器... - `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 包 & 进程` 管理器...
- 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`... - 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`...
- 本地化 - 本地化
- English, 简体中文 - English, 简体中文
@@ -32,28 +35,16 @@
## 🏙️ 截屏 ## 🏙️ 截屏
<table> <table>
<tr> <tr>
<td> <td><img width="277px" src="imgs/server.png"></td>
<img width="277px" src="imgs/server.png"> <td><img width="277px" src="imgs/detail.png"></td>
</td> <td><img width="277px" src="imgs/sftp.png"></td>
<td>
<img width="277px" src="imgs/detail.png">
</td>
<td>
<img width="277px" src="imgs/sftp.png">
</td>
</tr> </tr>
</table> </table>
<table> <table>
<tr> <tr>
<td> <td><img width="277px" src="imgs/editor.png"> </td>
<img width="277px" src="imgs/editor.png"> <td><img width="277px" src="imgs/ssh.png"></td>
</td> <td><img width="277px" src="imgs/docker.png"></td>
<td>
<img width="277px" src="imgs/ssh.png">
</td>
<td>
<img width="277px" src="imgs/docker.png">
</td>
</tr> </tr>
</table> </table>
@@ -69,9 +60,7 @@
2. 反馈问题前请检查是否是 serverbox 的问题。 2. 反馈问题前请检查是否是 serverbox 的问题。
3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受 3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受
确认了解上述内容后 确认了解上述内容后,请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
- 如果你有**任何问题或者功能请求**,请在 [讨论](https://github.com/lollipopkit/flutter_server_box/discussions/new/choose) 中交流。
- 如果 ServerBox app 有**任何 bug**,请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
## 🧱 贡献 ## 🧱 贡献
@@ -79,15 +68,8 @@
- [本地化翻译指南](https://blog.lolli.tech/faq/) 可在我的博客中找到。 - [本地化翻译指南](https://blog.lolli.tech/faq/) 可在我的博客中找到。
## 👏🏼 贡献者
<a href="https://github.com/lollipopkit/flutter_server_box/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lollipopkit/flutter_server_box" />
</a>
## 💡 我的其它 Apps ## 💡 我的其它 Apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。 - [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。
- [2FA Box](https://github.com/lollipopkit/flutter_2fa) - 开源的 2FA 应用。
- [更多](https://github.com/lollipopkit) - 工具 & etc. - [更多](https://github.com/lollipopkit) - 工具 & etc.

View File

@@ -1,5 +1,7 @@
PODS: PODS:
- countly_flutter (24.4.0): - countly_flutter (24.4.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter - Flutter
- file_picker (0.0.1): - file_picker (0.0.1):
- Flutter - Flutter
@@ -10,7 +12,7 @@ PODS:
- Flutter - Flutter
- icloud_storage (0.0.1): - icloud_storage (0.0.1):
- Flutter - Flutter
- local_auth_ios (0.0.1): - local_auth_darwin (0.0.1):
- Flutter - Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
@@ -21,8 +23,6 @@ PODS:
- Flutter - Flutter
- plain_notification_token (0.0.1): - plain_notification_token (0.0.1):
- Flutter - Flutter
- r_upgrade (0.0.1):
- Flutter
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@@ -37,17 +37,17 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`) - countly_flutter (from `.symlinks/plugins/countly_flutter/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/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_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_ios (from `.symlinks/plugins/local_auth_ios/ios`) - 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`) - 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`)
- r_upgrade (from `.symlinks/plugins/r_upgrade/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`)
@@ -57,6 +57,8 @@ DEPENDENCIES:
EXTERNAL SOURCES: EXTERNAL SOURCES:
countly_flutter: countly_flutter:
:path: ".symlinks/plugins/countly_flutter/ios" :path: ".symlinks/plugins/countly_flutter/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
Flutter: Flutter:
@@ -67,8 +69,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
icloud_storage: icloud_storage:
:path: ".symlinks/plugins/icloud_storage/ios" :path: ".symlinks/plugins/icloud_storage/ios"
local_auth_ios: local_auth_darwin:
:path: ".symlinks/plugins/local_auth_ios/ios" :path: ".symlinks/plugins/local_auth_darwin/darwin"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
@@ -77,8 +79,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :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"
r_upgrade:
:path: ".symlinks/plugins/r_upgrade/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
@@ -91,21 +91,21 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/watch_connectivity/ios" :path: ".symlinks/plugins/watch_connectivity/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
countly_flutter: 5d2febe00242796cf569662e5d47da241f31b115 countly_flutter: 56233d921c6b4e0a720774a39b8ee8110d6f8d91
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287 file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 local_auth_darwin: 4d56c90c2683319835a61274b57620df9c4520ab
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1 plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
watch_connectivity: 715eb484685e05846eab74795348a44bb2809b82 watch_connectivity: 715eb484685e05846eab74795348a44bb2809b82

View File

@@ -690,7 +690,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 = 918; CURRENT_PROJECT_VERSION = 948;
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 +700,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
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 +826,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 = 918; CURRENT_PROJECT_VERSION = 948;
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 +836,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
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 +854,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 = 918; CURRENT_PROJECT_VERSION = 948;
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 +864,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
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 +885,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 = 918; CURRENT_PROJECT_VERSION = 948;
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 +898,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
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 +924,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 = 918; CURRENT_PROJECT_VERSION = 948;
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 +937,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
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 +960,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 = 918; CURRENT_PROJECT_VERSION = 948;
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 +973,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
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 +996,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 = 918; CURRENT_PROJECT_VERSION = 948;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1008,7 +1008,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
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 +1037,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 = 918; CURRENT_PROJECT_VERSION = 948;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1049,7 +1049,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
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 +1075,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 = 918; CURRENT_PROJECT_VERSION = 948;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1087,7 +1087,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox; PRODUCT_NAME = ServerBox;

View File

@@ -1,6 +1,6 @@
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:fl_lib/l10n/gen/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:toolbox/data/res/build_data.dart'; import 'package:toolbox/data/res/build_data.dart';
@@ -15,8 +15,8 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
_setup(context); _setup(context);
return ListenableBuilder( return ListenableBuilder(
listenable: RebuildNodes.app, listenable: RNodes.app,
builder: (_, __) { builder: (context, _) {
if (!Stores.setting.useSystemPrimaryColor.fetch()) { if (!Stores.setting.useSystemPrimaryColor.fetch()) {
UIs.colorSeed = Color(Stores.setting.primaryColor.fetch()); UIs.colorSeed = Color(Stores.setting.primaryColor.fetch());
return _buildApp(context); return _buildApp(context);
@@ -74,7 +74,7 @@ class MyApp extends StatelessWidget {
title: BuildData.name, title: BuildData.name,
themeMode: themeMode, themeMode: themeMode,
theme: light, theme: light,
darkTheme: tMode < 3 ? dark : _getAmoledTheme(dark), darkTheme: tMode < 3 ? dark : dark.toAmoled,
home: _buildAppContent(ctx), home: _buildAppContent(ctx),
); );
} }
@@ -88,18 +88,3 @@ class MyApp extends StatelessWidget {
void _setup(BuildContext context) async { void _setup(BuildContext context) async {
SystemUIs.setTransparentNavigationBar(context); SystemUIs.setTransparentNavigationBar(context);
} }
ThemeData _getAmoledTheme(ThemeData darkTheme) => darkTheme.copyWith(
scaffoldBackgroundColor: Colors.black,
dialogBackgroundColor: Colors.black,
drawerTheme: const DrawerThemeData(backgroundColor: Colors.black),
appBarTheme: const AppBarTheme(backgroundColor: Colors.black),
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
bottomSheetTheme:
const BottomSheetThemeData(backgroundColor: Colors.black),
listTileTheme: const ListTileThemeData(tileColor: Colors.transparent),
cardTheme: const CardTheme(color: Colors.black12),
navigationBarTheme:
const NavigationBarThemeData(backgroundColor: Colors.black),
popupMenuTheme: const PopupMenuThemeData(color: Colors.black),
);

View File

@@ -79,7 +79,9 @@ extension SSHClientX on SSHClient {
isRequestingPwd = true; isRequestingPwd = true;
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1); final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
if (context == null) return; if (context == null) return;
final pwd = await context.showPwdDialog(title: user, id: id); final pwd = context.mounted
? await context.showPwdDialog(title: user, id: id)
: null;
if (pwd == null || pwd.isEmpty) { if (pwd == null || pwd.isEmpty) {
session.kill(SSHSignal.TERM); session.kill(SSHSignal.TERM);
} else { } else {

View File

@@ -246,4 +246,8 @@ class AppRoutes {
static AppRoutes pve({Key? key, required ServerPrivateInfo spi}) { static AppRoutes pve({Key? key, required ServerPrivateInfo 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, data: data), 'kv_editor');
}
} }

View File

@@ -7,6 +7,7 @@ import 'package:icloud_storage/icloud_storage.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:toolbox/data/model/app/backup.dart'; import 'package:toolbox/data/model/app/backup.dart';
import 'package:toolbox/data/model/app/sync.dart'; import 'package:toolbox/data/model/app/sync.dart';
import 'package:toolbox/data/res/misc.dart';
import '../../../data/model/app/error.dart'; import '../../../data/model/app/error.dart';
@@ -198,14 +199,13 @@ abstract final class ICloud {
} }
static Future<void> sync() async { static Future<void> sync() async {
final result = await download(relativePath: Paths.bakName); final result = await download(relativePath: Miscs.bakFileName);
if (result != null) { if (result != null) {
_logger.warning('Download backup failed: $result');
await backup(); await backup();
return; return;
} }
final dlFile = await File(Paths.bakPath).readAsString(); final dlFile = await File(Paths.bak).readAsString();
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile); final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
await dlBak.restore(); await dlBak.restore();
@@ -214,7 +214,7 @@ abstract final class ICloud {
static Future<void> backup() async { static Future<void> backup() async {
await Backup.backup(); await Backup.backup();
final uploadResult = await upload(relativePath: Paths.bakName); final uploadResult = await upload(relativePath: Miscs.bakFileName);
if (uploadResult != null) { if (uploadResult != null) {
_logger.warning('Upload backup failed: $uploadResult'); _logger.warning('Upload backup failed: $uploadResult');
} else { } else {

View File

@@ -5,6 +5,7 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:toolbox/data/model/app/backup.dart'; import 'package:toolbox/data/model/app/backup.dart';
import 'package:toolbox/data/model/app/error.dart'; import 'package:toolbox/data/model/app/error.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
import 'package:webdav_client/webdav_client.dart'; import 'package:webdav_client/webdav_client.dart';
@@ -96,15 +97,14 @@ abstract final class Webdav {
} }
static Future<void> sync() async { static Future<void> sync() async {
final result = await download(relativePath: Paths.bakName); final result = await download(relativePath: Miscs.bakFileName);
if (result != null) { if (result != null) {
_logger.warning('Download failed: $result');
await backup(); await backup();
return; return;
} }
try { try {
final dlFile = await File(Paths.bakPath).readAsString(); final dlFile = await File(Paths.bak).readAsString();
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile); final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
await dlBak.restore(); await dlBak.restore();
} catch (e) { } catch (e) {
@@ -117,7 +117,7 @@ abstract final class Webdav {
/// Create a local backup and upload it to WebDAV /// Create a local backup and upload it to WebDAV
static Future<void> backup() async { static Future<void> backup() async {
await Backup.backup(); await Backup.backup();
final uploadResult = await upload(relativePath: Paths.bakName); final uploadResult = await upload(relativePath: Miscs.bakFileName);
if (uploadResult != null) { if (uploadResult != null) {
_logger.warning('Upload failed: $uploadResult'); _logger.warning('Upload failed: $uploadResult');
} else { } else {

View File

@@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
import 'package:toolbox/data/model/server/private_key_info.dart'; import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/snippet.dart'; import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/rebuild.dart'; import 'package:toolbox/data/res/rebuild.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
@@ -74,7 +75,7 @@ class Backup {
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 ?? 'srvbox_bak.json'}'; final path = '${Paths.doc}/${name ?? Miscs.bakFileName}';
await File(path).writeAsString(result); await File(path).writeAsString(result);
return path; return path;
} }
@@ -169,7 +170,7 @@ class Backup {
} }
Pros.reload(); Pros.reload();
RebuildNodes.app.rebuild(); RNodes.app.build();
_logger.info('Restore success'); _logger.info('Restore success');
} }

View File

@@ -1,5 +0,0 @@
typedef GhId = String;
extension GhIdX on GhId {
String get url => 'https://github.com/$this';
}

View File

@@ -1,23 +0,0 @@
import 'package:flutter/foundation.dart';
class RebuildNode implements Listenable {
final List<VoidCallback> _listeners = [];
RebuildNode();
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
void rebuild() {
for (var listener in _listeners) {
listener();
}
}
}

View File

@@ -3,8 +3,9 @@ import 'dart:convert';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/data/model/container/type.dart'; import 'package:toolbox/data/model/container/type.dart';
import 'package:toolbox/data/res/misc.dart';
abstract final class ContainerPs { sealed class ContainerPs {
final String? id = null; final String? id = null;
final String? image = null; final String? image = null;
String? get name; String? get name;
@@ -16,7 +17,7 @@ abstract final class ContainerPs {
String? net; String? net;
String? disk; String? disk;
factory ContainerPs.fromRawJson(String s, ContainerType typ) => typ.ps(s); factory ContainerPs.fromRaw(String s, ContainerType typ) => typ.ps(s);
void parseStats(String s); void parseStats(String s);
} }
@@ -110,8 +111,6 @@ final class PodmanPs implements ContainerPs {
} }
final class DockerPs implements ContainerPs { final class DockerPs implements ContainerPs {
final String? command;
final String? createdAt;
@override @override
final String? id; final String? id;
@override @override
@@ -129,8 +128,6 @@ final class DockerPs implements ContainerPs {
String? disk; String? disk;
DockerPs({ DockerPs({
this.command,
this.createdAt,
this.id, this.id,
this.image, this.image,
this.names, this.names,
@@ -141,10 +138,13 @@ final class DockerPs implements ContainerPs {
String? get name => names; String? get name => names;
@override @override
String? get cmd => command; String? get cmd => null;
@override @override
bool get running => state == 'running'; bool get running {
if (state?.contains('Exited') == true) return false;
return true;
}
@override @override
void parseStats(String s) { void parseStats(String s) {
@@ -155,26 +155,15 @@ final class DockerPs implements ContainerPs {
disk = stats['BlockIO']; disk = stats['BlockIO'];
} }
factory DockerPs.fromRawJson(String str) => /// CONTAINER ID NAMES IMAGE STATUS
DockerPs.fromJson(json.decode(str)); /// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
factory DockerPs.parse(String raw) {
String toRawJson() => json.encode(toJson()); final parts = raw.split(Miscs.multiBlankreg);
return DockerPs(
factory DockerPs.fromJson(Map<String, dynamic> json) => DockerPs( id: parts[0],
command: json["Command"], state: parts[1],
createdAt: json["CreatedAt"], names: parts[2],
id: json["ID"], image: parts[3].trim(),
image: json["Image"], );
names: json["Names"], }
state: json["State"],
);
Map<String, dynamic> toJson() => {
"Command": command,
"CreatedAt": createdAt,
"ID": id,
"Image": image,
"Names": names,
"State": state,
};
} }

View File

@@ -7,7 +7,7 @@ enum ContainerType {
; ;
ContainerPs Function(String str) get ps => switch (this) { ContainerPs Function(String str) get ps => switch (this) {
ContainerType.docker => DockerPs.fromRawJson, ContainerType.docker => DockerPs.parse,
ContainerType.podman => PodmanPs.fromRawJson, ContainerType.podman => PodmanPs.fromRawJson,
}; };

View File

@@ -64,7 +64,7 @@ class ServerPrivateInfo {
final port = json["port"] as int? ?? 22; final port = json["port"] as int? ?? 22;
final user = json["user"] as String? ?? 'root'; final user = json["user"] as String? ?? 'root';
final name = json["name"] as String? ?? ''; final name = json["name"] as String? ?? '';
final pwd = json["authorization"] as String?; final pwd = json["pwd"] as String? ?? json["authorization"] as String?;
final keyId = json["pubKeyId"] as String?; final keyId = json["pubKeyId"] as String?;
final tags = (json["tags"] as List?)?.cast<String>(); final tags = (json["tags"] as List?)?.cast<String>();
final alterUrl = json["alterUrl"] as String?; final alterUrl = json["alterUrl"] as String?;
@@ -100,7 +100,7 @@ class ServerPrivateInfo {
data["port"] = port; data["port"] = port;
data["user"] = user; data["user"] = user;
if (pwd != null) { if (pwd != null) {
data["authorization"] = pwd; data["pwd"] = pwd;
} }
if (keyId != null) { if (keyId != null) {
data["pubKeyId"] = keyId; data["pubKeyId"] = keyId;

View File

@@ -26,7 +26,8 @@ class ContainerProvider extends ChangeNotifier {
ContainerErr? error; ContainerErr? error;
String? runLog; String? runLog;
ContainerType type; ContainerType type;
bool sudo = false; var sudoCompleter = Completer<bool>();
bool isBusy = false;
ContainerProvider({ ContainerProvider({
required this.client, required this.client,
@@ -41,6 +42,7 @@ class ContainerProvider extends ChangeNotifier {
this.type = type; this.type = type;
Stores.container.setType(type, hostId); Stores.container.setType(type, hostId);
error = runLog = items = images = version = null; error = runLog = items = images = version = null;
sudoCompleter = Completer<bool>();
notifyListeners(); notifyListeners();
await refresh(); await refresh();
} }
@@ -60,17 +62,27 @@ class ContainerProvider extends ChangeNotifier {
// return value; // return value;
// } // }
Future<bool> _requiresSudo() async { void _requiresSudo() async {
final psResult = await client?.run(_wrap(ContainerCmdType.ps.exec(type))); /// Podman is rootless
if (psResult == null) return true; if (type == ContainerType.podman) return sudoCompleter.complete(false);
if (psResult.string.toLowerCase().contains("permission denied")) { if (!Stores.setting.containerTrySudo.fetch()) {
return true; return sudoCompleter.complete(false);
} }
return false;
final res = await client?.run(_wrap(ContainerCmdType.images.exec(type)));
if (res?.string.toLowerCase().contains("permission denied") ?? false) {
return sudoCompleter.complete(true);
}
return sudoCompleter.complete(false);
} }
Future<void> refresh({bool isAuto = false}) async { Future<void> refresh({bool isAuto = false}) async {
sudo = await _requiresSudo() && Stores.setting.containerTrySudo.fetch(); if (isBusy) return;
isBusy = true;
if (!sudoCompleter.isCompleted) _requiresSudo();
final sudo = await sudoCompleter.future;
/// If sudo is required and auto refresh is enabled, skip the refresh. /// If sudo is required and auto refresh is enabled, skip the refresh.
/// Or this will ask for pwd again and again. /// Or this will ask for pwd again and again.
@@ -78,17 +90,22 @@ class ContainerProvider extends ChangeNotifier {
final includeStats = Stores.setting.containerParseStat.fetch(); final includeStats = Stores.setting.containerParseStat.fetch();
var raw = ''; var raw = '';
final cmd = _wrap(ContainerCmdType.execAll(
type,
sudo: sudo,
includeStats: includeStats,
));
final code = await client?.execWithPwd( final code = await client?.execWithPwd(
_wrap(ContainerCmdType.execAll( cmd,
type,
sudo: sudo,
includeStats: includeStats,
)),
context: context, context: context,
onStdout: (data, _) => raw = '$raw$data', onStdout: (data, _) => raw = '$raw$data',
id: hostId, id: hostId,
); );
isBusy = false;
if (!context.mounted) return;
/// Code 127 means command not found /// Code 127 means command not found
if (code == 127 || raw.contains(_dockerNotFound)) { if (code == 127 || raw.contains(_dockerNotFound)) {
error = ContainerErr(type: ContainerErrType.notInstalled); error = ContainerErr(type: ContainerErrType.notInstalled);
@@ -126,8 +143,12 @@ class ContainerProvider extends ChangeNotifier {
final psRaw = ContainerCmdType.ps.find(segments); final psRaw = ContainerCmdType.ps.find(segments);
try { try {
final lines = psRaw.split('\n'); final lines = psRaw.split('\n');
if (type == ContainerType.docker) {
/// Due to the fetched data is not in json format, skip table header
lines.removeWhere((element) => element.contains('CONTAINER ID'));
}
lines.removeWhere((element) => element.isEmpty); lines.removeWhere((element) => element.isEmpty);
items = lines.map((e) => ContainerPs.fromRawJson(e, type)).toList(); items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
} catch (e, trace) { } catch (e, trace) {
error = ContainerErr( error = ContainerErr(
type: ContainerErrType.parsePs, type: ContainerErrType.parsePs,
@@ -203,7 +224,7 @@ class ContainerProvider extends ChangeNotifier {
runLog = ''; runLog = '';
final errs = <String>[]; final errs = <String>[];
final code = await client?.execWithPwd( final code = await client?.execWithPwd(
_wrap(sudo ? 'sudo -S $cmd' : cmd), _wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd),
context: context, context: context,
onStdout: (data, _) { onStdout: (data, _) {
runLog = '$runLog$data'; runLog = '$runLog$data';
@@ -254,7 +275,16 @@ enum ContainerCmdType {
final prefix = sudo ? 'sudo -S ${type.name}' : type.name; final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
return switch (this) { return switch (this) {
ContainerCmdType.version => '$prefix version $_jsonFmt', ContainerCmdType.version => '$prefix version $_jsonFmt',
ContainerCmdType.ps => '$prefix ps -a $_jsonFmt', ContainerCmdType.ps => switch (type) {
/// TODO: Rollback to json format when permformance recovers.
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
ContainerType.docker => '$prefix ps -a --format "table {{printf \\"'
'%-15.15s '
'%-30.30s '
'${"%-50.50s " * 2}\\"'
' .ID .Status .Names .Image}}"',
ContainerType.podman => '$prefix ps -a $_jsonFmt',
},
ContainerCmdType.stats => ContainerCmdType.stats =>
includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS', includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
ContainerCmdType.images => '$prefix image ls $_jsonFmt', ContainerCmdType.images => '$prefix image ls $_jsonFmt',

View File

@@ -10,20 +10,24 @@ import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/data/model/app/error.dart'; import 'package:toolbox/data/model/app/error.dart';
import 'package:toolbox/data/model/server/pve.dart'; import 'package:toolbox/data/model/server/pve.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:dartssh2/dartssh2.dart';
typedef PveCtrlFunc = Future<bool> Function(String node, String id); typedef PveCtrlFunc = Future<bool> Function(String node, String id);
final class PveProvider extends ChangeNotifier { final class PveProvider extends ChangeNotifier {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;
late final String addr; late String addr;
//late final SSHClient _client; late final SSHClient _client;
late final ServerSocket _serverSocket;
final List<SSHForwardChannel> _forwards = [];
int _localPort = 0;
PveProvider({required this.spi}) { PveProvider({required this.spi}) {
// final client = _spi.server?.client; final client = spi.server?.client;
// if (client == null) { if (client == null) {
// throw Exception('Server client is null'); throw Exception('Server client is null');
// } }
// _client = client; _client = client;
final addr = spi.custom?.pveAddr; final addr = spi.custom?.pveAddr;
if (addr == null) { if (addr == null) {
err.value = 'PVE address is null'; err.value = 'PVE address is null';
@@ -41,6 +45,7 @@ final class PveProvider extends ChangeNotifier {
..httpClientAdapter = IOHttpClientAdapter( ..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () { createHttpClient: () {
final client = HttpClient(); final client = HttpClient();
client.connectionFactory = cf;
if (_ignoreCert) { if (_ignoreCert) {
client.badCertificateCallback = (_, __, ___) => true; client.badCertificateCallback = (_, __, ___) => true;
} }
@@ -50,55 +55,76 @@ final class PveProvider extends ChangeNotifier {
); );
final data = ValueNotifier<PveRes?>(null); final data = ValueNotifier<PveRes?>(null);
bool get onlyOneNode => data.value?.nodes.length == 1; bool get onlyOneNode => data.value?.nodes.length == 1;
String? release; String? release;
bool isBusy = false; bool isBusy = false;
// int _localPort = 0;
// String get addr => 'http://127.0.0.1:$_localPort';
Future<void> _init() async { Future<void> _init() async {
try { try {
//await _forward(); await _forward();
await _login(); await _login();
await _release; await _getRelease();
} on PveErr { } on PveErr {
err.value = l10n.pveLoginFailed; err.value = l10n.pveLoginFailed;
} catch (e) { } catch (e, s) {
Loggers.app.warning('PVE init failed', e); Loggers.app.warning('PVE init failed', e, s);
err.value = e.toString(); err.value = e.toString();
} finally { } finally {
connected.complete(); connected.complete();
} }
} }
// Future<void> _forward() async { Future<void> _forward() async {
// var retries = 0; final url = Uri.parse(addr);
// while (retries < 3) { if (_localPort == 0) {
// try { _serverSocket = await ServerSocket.bind('localhost', 0);
// _localPort = Random().nextInt(1000) + 37000; _localPort = _serverSocket.port;
// print('Forwarding local port $_localPort'); _serverSocket.listen((socket) async {
// final serverSocket = await ServerSocket.bind('localhost', _localPort); final forward = await _client.forwardLocal(url.host, url.port);
// final forward = await _client.forwardLocal('127.0.0.1', 8006); _forwards.add(forward);
// serverSocket.listen((socket) { forward.stream.cast<List<int>>().pipe(socket);
// forward.stream.cast<List<int>>().pipe(socket); socket.cast<List<int>>().pipe(forward.sink);
// socket.pipe(forward.sink); });
// }); final newUrl = Uri.parse(addr)
// return; .replace(host: 'localhost', port: _localPort)
// } on SocketException { .toString();
// retries++; debugPrint('Forwarding $newUrl to $addr');
// } }
// } }
// throw Exception('Failed to bind local port');
// } Future<ConnectionTask<Socket>> cf(
Uri url, String? proxyHost, int? proxyPort) async {
/* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
final _localPort = serverSocket.port;
serverSocket.listen((socket) async {
final forward = await _client.forwardLocal(url.host, url.port);
forwards.add(forward);
forward.stream.cast<List<int>>().pipe(socket);
socket.cast<List<int>>().pipe(forward.sink);
});*/
if (url.isScheme("https")) {
return SecureSocket.startConnect('localhost', _localPort,
onBadCertificate: (_) => true);
} else {
return Socket.startConnect('localhost', _localPort);
}
}
Future<void> _login() async { Future<void> _login() async {
final resp = await session.post('$addr/api2/extjs/access/ticket', data: { final resp = await session.post(
'username': spi.user, '$addr/api2/extjs/access/ticket',
'password': spi.pwd, data: {
'realm': 'pam', 'username': spi.user,
'new-format': '1' 'password': spi.pwd,
}); 'realm': 'pam',
'new-format': '1'
},
options: Options(
headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType},
),
);
try { try {
final ticket = resp.data['data']['ticket']; final ticket = resp.data['data']['ticket'];
session.options.headers['CSRFPreventionToken'] = session.options.headers['CSRFPreventionToken'] =
@@ -110,7 +136,7 @@ final class PveProvider extends ChangeNotifier {
} }
/// Returns true if the PVE version is 8.0 or later /// Returns true if the PVE version is 8.0 or later
Future<void> get _release async { Future<void> _getRelease() async {
final resp = await session.get('$addr/api2/extjs/version'); final resp = await session.get('$addr/api2/extjs/version');
final version = resp.data['data']['release'] as String?; final version = resp.data['data']['release'] as String?;
if (version != null) { if (version != null) {
@@ -167,4 +193,13 @@ final class PveProvider extends ChangeNotifier {
bool _isCtrlSuc(Response resp) { bool _isCtrlSuc(Response resp) {
return resp.statusCode == 200; return resp.statusCode == 200;
} }
@override
Future<void> dispose() async {
super.dispose();
await _serverSocket.close();
for (final forward in _forwards) {
forward.close();
}
}
} }

View File

@@ -1,10 +1,10 @@
// This file is generated by make script. Do not edit. // This file is generated by fl_build. Do not edit.
class BuildData { class BuildData {
static const String name = "ServerBox"; static const String name = "ServerBox";
static const int build = 918; static const int build = 948;
static const String engine = "3.22.1"; static const String engine = "3.22.1";
static const String buildAt = "2024-05-25 19:17:18"; static const String buildAt = "2024-06-08 21:21:36";
static const int modifications = 2; static const int modifications = 3;
static const int script = 48; static const int script = 48;
} }

View File

@@ -1,5 +1,3 @@
import 'package:toolbox/data/model/app/github_id.dart';
abstract final class GithubIds { abstract final class GithubIds {
// Thanks // Thanks
// If you want to change your Github ID, please open an issue. // If you want to change your Github ID, please open an issue.
@@ -75,3 +73,9 @@ abstract final class GithubIds {
'Jasonzhu1207', 'Jasonzhu1207',
}; };
} }
typedef GhId = String;
extension GhIdX on GhId {
String get url => 'https://github.com/$this';
}

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
abstract final class Miscs { abstract final class Miscs {
static final blankReg = RegExp(r'\s+'); static final blankReg = RegExp(r'\s+');
static final multiBlankreg = RegExp(r'\s{2,}');
/// RegExp for password request /// RegExp for password request
static final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):'); static final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):');
@@ -18,4 +19,6 @@ abstract final class Miscs {
static const pkgName = 'tech.lolli.toolbox'; static const pkgName = 'tech.lolli.toolbox';
static const jsonEncoder = JsonEncoder.withIndent(' '); static const jsonEncoder = JsonEncoder.withIndent(' ');
static const bakFileName = 'srvbox_bak.json';
} }

View File

@@ -1,5 +1,6 @@
import 'package:toolbox/data/model/app/rebuild.dart'; import 'package:fl_lib/fl_lib.dart';
abstract final class RebuildNodes { abstract final class RNodes {
static final app = RebuildNode(); static final app = RNode();
static final dark = false.vn;
} }

View File

@@ -1,6 +1,6 @@
abstract final class Urls { abstract final class Urls {
static const cdnBase = 'https://cdn.lolli.tech/serverbox'; static const cdnBase = 'https://cdn.lolli.tech/serverbox';
static const updateCfg = '$cdnBase/update.json'; static const updateCfg = '$cdnBase/update2.json';
static const myGithub = 'https://github.com/lollipopkit'; static const myGithub = 'https://github.com/lollipopkit';
static const appHelp = '$myGithub/flutter_server_box#-help'; static const appHelp = '$myGithub/flutter_server_box#-help';
static const appWiki = '$myGithub/flutter_server_box/wiki'; static const appWiki = '$myGithub/flutter_server_box/wiki';

View File

@@ -276,6 +276,8 @@ class SettingStore extends PersistentStore {
/// fmt: https://example.com/{DIST}-{BRIGHT}.png /// fmt: https://example.com/{DIST}-{BRIGHT}.png
late final serverLogoUrl = property('serverLogoUrl', ''); late final serverLogoUrl = property('serverLogoUrl', '');
late final betaTest = property('betaTest', false);
// Never show these settings for users // Never show these settings for users
// //
// ------BEGIN------ // ------BEGIN------

View File

@@ -2,6 +2,7 @@
"@@locale": "de", "@@locale": "de",
"about": "Über", "about": "Über",
"aboutThanks": "Vielen Dank an die folgenden Personen, die daran teilgenommen haben.\n", "aboutThanks": "Vielen Dank an die folgenden Personen, die daran teilgenommen haben.\n",
"acceptBeta": "Akzeptieren Sie Testversion-Updates",
"add": "Neu", "add": "Neu",
"addAServer": "Server hinzufügen", "addAServer": "Server hinzufügen",
"addPrivateKey": "Private key hinzufügen", "addPrivateKey": "Private key hinzufügen",
@@ -280,6 +281,7 @@
"suspend": "Suspend", "suspend": "Suspend",
"suspendTip": "Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.", "suspendTip": "Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.",
"switchTo": "Wechseln zu {val}", "switchTo": "Wechseln zu {val}",
"sync": "Sync",
"syncTip": "Damit einige Änderungen wirksam werden, kann ein Neustart erforderlich sein.", "syncTip": "Damit einige Änderungen wirksam werden, kann ein Neustart erforderlich sein.",
"system": "Systeme", "system": "Systeme",
"tag": "Tags", "tag": "Tags",

View File

@@ -2,6 +2,7 @@
"@@locale": "en", "@@locale": "en",
"about": "About", "about": "About",
"aboutThanks": "Thanks to the following people who participated in.", "aboutThanks": "Thanks to the following people who participated in.",
"acceptBeta": "Accept test version updates",
"add": "Add", "add": "Add",
"addAServer": "add a server", "addAServer": "add a server",
"addPrivateKey": "Add private key", "addPrivateKey": "Add private key",
@@ -280,6 +281,7 @@
"suspend": "Suspend", "suspend": "Suspend",
"suspendTip": "The suspend function requires root privileges and systemd support.", "suspendTip": "The suspend function requires root privileges and systemd support.",
"switchTo": "Switch to {val}", "switchTo": "Switch to {val}",
"sync": "Sync",
"syncTip": "A restart may be required for some changes to take effect.", "syncTip": "A restart may be required for some changes to take effect.",
"system": "System", "system": "System",
"tag": "Tags", "tag": "Tags",

View File

@@ -2,6 +2,7 @@
"@@locale": "es", "@@locale": "es",
"about": "Acerca de", "about": "Acerca de",
"aboutThanks": "Gracias a los siguientes participantes.", "aboutThanks": "Gracias a los siguientes participantes.",
"acceptBeta": "Aceptar actualizaciones de la versión de prueba",
"add": "Añadir", "add": "Añadir",
"addAServer": "Agregar un servidor", "addAServer": "Agregar un servidor",
"addPrivateKey": "Agregar una llave privada", "addPrivateKey": "Agregar una llave privada",
@@ -280,6 +281,7 @@
"suspend": "Suspender", "suspend": "Suspender",
"suspendTip": "La función de suspender necesita permisos de root y soporte de systemd.", "suspendTip": "La función de suspender necesita permisos de root y soporte de systemd.",
"switchTo": "Cambiar a {val}", "switchTo": "Cambiar a {val}",
"sync": "Sincronizar",
"syncTip": "Puede que necesites reiniciar para que algunos cambios tengan efecto.", "syncTip": "Puede que necesites reiniciar para que algunos cambios tengan efecto.",
"system": "Sistema", "system": "Sistema",
"tag": "Etiqueta", "tag": "Etiqueta",

View File

@@ -2,6 +2,7 @@
"@@locale": "fr", "@@locale": "fr",
"about": "À propos", "about": "À propos",
"aboutThanks": "Merci aux personnes suivantes qui ont participé.", "aboutThanks": "Merci aux personnes suivantes qui ont participé.",
"acceptBeta": "Accepter les mises à jour de la version de test",
"add": "Ajouter", "add": "Ajouter",
"addAServer": "Ajouter un serveur", "addAServer": "Ajouter un serveur",
"addPrivateKey": "Ajouter une clé privée", "addPrivateKey": "Ajouter une clé privée",
@@ -280,6 +281,7 @@
"suspend": "Suspendre", "suspend": "Suspendre",
"suspendTip": "La fonction de suspension nécessite des privilèges root et le support de systemd.", "suspendTip": "La fonction de suspension nécessite des privilèges root et le support de systemd.",
"switchTo": "Passer à {val}", "switchTo": "Passer à {val}",
"sync": "Sync",
"syncTip": "Un redémarrage peut être nécessaire pour que certains changements prennent effet.", "syncTip": "Un redémarrage peut être nécessaire pour que certains changements prennent effet.",
"system": "Système", "system": "Système",
"tag": "Étiquettes", "tag": "Étiquettes",

View File

@@ -2,6 +2,7 @@
"@@locale": "id", "@@locale": "id",
"about": "Tentang", "about": "Tentang",
"aboutThanks": "Terima kasih kepada orang -orang berikut yang berpartisipasi.", "aboutThanks": "Terima kasih kepada orang -orang berikut yang berpartisipasi.",
"acceptBeta": "Terima pembaruan versi uji coba",
"add": "Menambahkan", "add": "Menambahkan",
"addAServer": "tambahkan server", "addAServer": "tambahkan server",
"addPrivateKey": "Tambahkan kunci pribadi", "addPrivateKey": "Tambahkan kunci pribadi",
@@ -280,6 +281,7 @@
"suspend": "Suspend", "suspend": "Suspend",
"suspendTip": "Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.", "suspendTip": "Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.",
"switchTo": "Beralih ke {val}", "switchTo": "Beralih ke {val}",
"sync": "Sinkronisasi",
"syncTip": "Pengaktifan ulang mungkin diperlukan agar beberapa perubahan dapat diterapkan.", "syncTip": "Pengaktifan ulang mungkin diperlukan agar beberapa perubahan dapat diterapkan.",
"system": "Sistem", "system": "Sistem",
"tag": "Tag", "tag": "Tag",

View File

@@ -2,6 +2,7 @@
"@@locale": "ja", "@@locale": "ja",
"about": "約", "about": "約",
"aboutThanks": "以下の参加者に感謝します。", "aboutThanks": "以下の参加者に感謝します。",
"acceptBeta": "テストバージョンの更新を受け入れる",
"add": "追加", "add": "追加",
"addAServer": "サーバーを追加する", "addAServer": "サーバーを追加する",
"addPrivateKey": "プライベートキーを追加", "addPrivateKey": "プライベートキーを追加",
@@ -280,6 +281,7 @@
"suspend": "中断", "suspend": "中断",
"suspendTip": "suspend機能はroot権限とsystemdのサポートが必要です。", "suspendTip": "suspend機能はroot権限とsystemdのサポートが必要です。",
"switchTo": "{val}に切り替える", "switchTo": "{val}に切り替える",
"sync": "同期する",
"syncTip": "再起動が必要な場合があります。一部の変更はその後に有効になります。", "syncTip": "再起動が必要な場合があります。一部の変更はその後に有効になります。",
"system": "システム", "system": "システム",
"tag": "タグ", "tag": "タグ",

View File

@@ -2,6 +2,7 @@
"@@locale": "nl", "@@locale": "nl",
"about": "Over", "about": "Over",
"aboutThanks": "Met dank aan de volgende mensen die hebben deelgenomen aan.", "aboutThanks": "Met dank aan de volgende mensen die hebben deelgenomen aan.",
"acceptBeta": "Accepteer testversie-updates",
"add": "Toevoegen", "add": "Toevoegen",
"addAServer": "een server toevoegen", "addAServer": "een server toevoegen",
"addPrivateKey": "Privésleutel toevoegen", "addPrivateKey": "Privésleutel toevoegen",
@@ -279,6 +280,7 @@
"suspend": "Ophangen", "suspend": "Ophangen",
"suspendTip": "De opschortfunctie vereist rootrechten en systemd-ondersteuning.", "suspendTip": "De opschortfunctie vereist rootrechten en systemd-ondersteuning.",
"switchTo": "Overschakelen naar {val}", "switchTo": "Overschakelen naar {val}",
"sync": "Sync",
"syncTip": "Een herstart kan nodig zijn voor sommige wijzigingen om van kracht te worden.", "syncTip": "Een herstart kan nodig zijn voor sommige wijzigingen om van kracht te worden.",
"system": "Systeem", "system": "Systeem",
"tag": "Labels", "tag": "Labels",

View File

@@ -2,6 +2,7 @@
"@@locale": "pt", "@@locale": "pt",
"about": "Sobre", "about": "Sobre",
"aboutThanks": "Agradecimentos a todos os participantes.", "aboutThanks": "Agradecimentos a todos os participantes.",
"acceptBeta": "Aceitar atualizações da versão de teste",
"add": "Adicionar", "add": "Adicionar",
"addAServer": "Adicionar um servidor", "addAServer": "Adicionar um servidor",
"addPrivateKey": "Adicionar uma chave privada", "addPrivateKey": "Adicionar uma chave privada",
@@ -280,6 +281,7 @@
"suspend": "Suspender", "suspend": "Suspender",
"suspendTip": "A função de suspensão requer permissões de root e suporte do systemd.", "suspendTip": "A função de suspensão requer permissões de root e suporte do systemd.",
"switchTo": "Mudar para {val}", "switchTo": "Mudar para {val}",
"sync": "Sincronizar",
"syncTip": "Pode ser necessário reiniciar para algumas mudanças surtirem efeito.", "syncTip": "Pode ser necessário reiniciar para algumas mudanças surtirem efeito.",
"system": "Sistema", "system": "Sistema",
"tag": "Tag", "tag": "Tag",

View File

@@ -2,6 +2,7 @@
"@@locale": "ru", "@@locale": "ru",
"about": "о", "about": "о",
"aboutThanks": "Благодарности всем участникам.", "aboutThanks": "Благодарности всем участникам.",
"acceptBeta": "Принять обновления тестовой версии",
"add": "добавить", "add": "добавить",
"addAServer": "добавить сервер", "addAServer": "добавить сервер",
"addPrivateKey": "добавить приватный ключ", "addPrivateKey": "добавить приватный ключ",
@@ -280,6 +281,7 @@
"suspend": "приостановить", "suspend": "приостановить",
"suspendTip": "Функция приостановки требует прав root и поддержки systemd.", "suspendTip": "Функция приостановки требует прав root и поддержки systemd.",
"switchTo": "переключиться на {val}", "switchTo": "переключиться на {val}",
"sync": "Синхронизировать",
"syncTip": "Возможно, потребуется перезагрузка, чтобы некоторые изменения вступили в силу.", "syncTip": "Возможно, потребуется перезагрузка, чтобы некоторые изменения вступили в силу.",
"system": "система", "system": "система",
"tag": "тег", "tag": "тег",

View File

@@ -2,6 +2,7 @@
"@@locale": "zh", "@@locale": "zh",
"about": "关于", "about": "关于",
"aboutThanks": "感谢以下参与的各位。", "aboutThanks": "感谢以下参与的各位。",
"acceptBeta": "接受测试版更新推送",
"add": "新增", "add": "新增",
"addAServer": "添加服务器", "addAServer": "添加服务器",
"addPrivateKey": "添加一个私钥", "addPrivateKey": "添加一个私钥",
@@ -280,6 +281,7 @@
"suspend": "挂起", "suspend": "挂起",
"suspendTip": "suspend 功能需要 root 权限及 systemd 支持。", "suspendTip": "suspend 功能需要 root 权限及 systemd 支持。",
"switchTo": "切换到 {val}", "switchTo": "切换到 {val}",
"sync": "同步",
"syncTip": "可能需要重新启动,某些更改才能生效。", "syncTip": "可能需要重新启动,某些更改才能生效。",
"system": "系统", "system": "系统",
"tag": "标签", "tag": "标签",

View File

@@ -2,6 +2,7 @@
"@@locale": "zh_TW", "@@locale": "zh_TW",
"about": "關於", "about": "關於",
"aboutThanks": "感謝以下參與的各位。", "aboutThanks": "感謝以下參與的各位。",
"acceptBeta": "接受測試版更新推送",
"add": "新增", "add": "新增",
"addAServer": "新增服務器", "addAServer": "新增服務器",
"addPrivateKey": "新增一個私鑰", "addPrivateKey": "新增一個私鑰",
@@ -280,6 +281,7 @@
"suspend": "挂起", "suspend": "挂起",
"suspendTip": "suspend 功能需要 root 權限及 systemd 支持。", "suspendTip": "suspend 功能需要 root 權限及 systemd 支持。",
"switchTo": "切換到 {val}", "switchTo": "切換到 {val}",
"sync": "同步",
"syncTip": "可能需要重新啟動,某些更改才能生效。", "syncTip": "可能需要重新啟動,某些更改才能生效。",
"system": "系統", "system": "系統",
"tag": "标签", "tag": "标签",

View File

@@ -20,8 +20,10 @@ import 'package:toolbox/data/model/server/custom.dart';
import 'package:toolbox/data/model/server/private_key_info.dart'; import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/snippet.dart'; import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/model/server/wol_cfg.dart';
import 'package:toolbox/data/model/ssh/virtual_key.dart'; import 'package:toolbox/data/model/ssh/virtual_key.dart';
import 'package:toolbox/data/res/build_data.dart'; import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/data/res/url.dart'; import 'package:toolbox/data/res/url.dart';
@@ -64,7 +66,7 @@ void _runInZone(void Function() body) {
Future<void> _initApp() async { Future<void> _initApp() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Paths.init(BuildData.name, bakName: 'srvbox'); await Paths.init(BuildData.name, bakName: Miscs.bakFileName);
await _initData(); await _initData();
_setupDebug(); _setupDebug();
@@ -86,6 +88,7 @@ Future<void> _initData() async {
Hive.registerAdapter(NetViewTypeAdapter()); // 5 Hive.registerAdapter(NetViewTypeAdapter()); // 5
Hive.registerAdapter(ServerFuncBtnAdapter()); // 6 Hive.registerAdapter(ServerFuncBtnAdapter()); // 6
Hive.registerAdapter(ServerCustomAdapter()); // 7 Hive.registerAdapter(ServerCustomAdapter()); // 7
Hive.registerAdapter(WakeOnLanCfgAdapter()); // 8
await Stores.setting.init(); await Stores.setting.init();
await Stores.server.init(); await Stores.server.init();
@@ -97,6 +100,8 @@ Future<void> _initData() async {
Pros.snippet.load(); Pros.snippet.load();
Pros.key.load(); Pros.key.load();
await Pros.app.init(); await Pros.app.init();
if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta;
} }
void _setupDebug() { void _setupDebug() {

View File

@@ -10,6 +10,7 @@ import 'package:toolbox/core/utils/sync/icloud.dart';
import 'package:toolbox/core/utils/sync/webdav.dart'; import 'package:toolbox/core/utils/sync/webdav.dart';
import 'package:toolbox/data/model/app/backup.dart'; import 'package:toolbox/data/model/app/backup.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/data/res/url.dart'; import 'package:toolbox/data/res/url.dart';
@@ -259,9 +260,9 @@ class BackupPage extends StatelessWidget {
), ),
], ],
); );
} catch (e, trace) { } catch (e, s) {
Loggers.app.warning('Import backup failed', e, trace); Loggers.app.warning('Import backup failed', e, s);
context.showSnackBar(e.toString()); context.showErrDialog(e: e, s: s, operation: l10n.restore);
} }
} }
@@ -269,89 +270,52 @@ class BackupPage extends StatelessWidget {
webdavLoading.value = true; webdavLoading.value = true;
try { try {
final files = await Webdav.list(); final files = await Webdav.list();
if (files.isEmpty) { if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);
context.showSnackBar(l10n.dirEmpty);
webdavLoading.value = false;
return;
}
final fileName = await context.showRoundDialog<String>( final fileName = await context.showPickSingleDialog(
title: l10n.restore, title: l10n.restore,
child: SizedBox( items: files,
width: 300,
height: 300,
child: ListView.builder(
itemCount: files.length,
itemBuilder: (_, index) {
final file = files[index];
return ListTile(
title: Text(file),
onTap: () => context.pop(file),
);
},
),
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(l10n.cancel),
),
],
); );
if (fileName == null) { if (fileName == null) return;
webdavLoading.value = false;
return;
}
final result = await Webdav.download(relativePath: fileName); final result = await Webdav.download(relativePath: fileName);
if (result != null) { if (result != null) {
Loggers.app.warning('Download webdav backup failed: $result'); throw result;
webdavLoading.value = false;
return;
} }
final dlFile = await File('${Paths.doc}/$fileName').readAsString(); final dlFile = await File('${Paths.doc}/$fileName').readAsString();
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile); final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
await dlBak.restore(force: true); await dlBak.restore(force: true);
webdavLoading.value = false; } catch (e, s) {
} catch (e) { context.showErrDialog(e: e, s: s, operation: l10n.restore);
context.showSnackBar(e.toString()); Loggers.app.warning('Download webdav backup failed', e, s);
rethrow;
} finally { } finally {
webdavLoading.value = false; webdavLoading.value = false;
} }
} }
Future<void> _onTapWebdavUp(BuildContext context) async { Future<void> _onTapWebdavUp(BuildContext context) async {
webdavLoading.value = true;
final date = DateTime.now().ymdhms(ymdSep: "-", hmsSep: "-", sep: "-");
final bakName = '$date-${Miscs.bakFileName}';
try { try {
webdavLoading.value = true;
final bakName =
'${DateTime.now().ymdhms(ymdSep: "-", hmsSep: "-", sep: "-")}-${Paths.bakName}';
await Backup.backup(bakName); await Backup.backup(bakName);
final uploadResult = await Webdav.upload(relativePath: bakName); final uploadResult = await Webdav.upload(relativePath: bakName);
if (uploadResult != null) { if (uploadResult != null) {
Loggers.app.warning('Upload webdav backup failed: $uploadResult'); throw uploadResult;
context.showSnackBar(uploadResult.toString());
} else {
Loggers.app.info('Upload webdav backup success');
} }
} catch (e) { Loggers.app.info('Upload webdav backup success');
context.showSnackBar(e.toString()); } catch (e, s) {
rethrow; context.showErrDialog(e: e, s: s, operation: l10n.upload);
Loggers.app.warning('Upload webdav backup failed', e, s);
} finally { } finally {
webdavLoading.value = false; webdavLoading.value = false;
} }
} }
Future<void> _onTapWebdavSetting(BuildContext context) async { Future<void> _onTapWebdavSetting(BuildContext context) async {
final urlCtrl = TextEditingController( final url = TextEditingController(text: Stores.setting.webdavUrl.fetch());
text: Stores.setting.webdavUrl.fetch(), final user = TextEditingController(text: Stores.setting.webdavUser.fetch());
); final pwd = TextEditingController(text: Stores.setting.webdavPwd.fetch());
final userCtrl = TextEditingController(
text: Stores.setting.webdavUser.fetch(),
);
final pwdCtrl = TextEditingController(
text: Stores.setting.webdavPwd.fetch(),
);
final result = await context.showRoundDialog<bool>( final result = await context.showRoundDialog<bool>(
title: 'WebDAV', title: 'WebDAV',
child: Column( child: Column(
@@ -360,15 +324,15 @@ class BackupPage extends StatelessWidget {
Input( Input(
label: 'URL', label: 'URL',
hint: 'https://example.com/webdav/', hint: 'https://example.com/webdav/',
controller: urlCtrl, controller: url,
), ),
Input( Input(
label: l10n.user, label: l10n.user,
controller: userCtrl, controller: user,
), ),
Input( Input(
label: l10n.pwd, label: l10n.pwd,
controller: pwdCtrl, controller: pwd,
), ),
], ],
), ),
@@ -382,15 +346,13 @@ class BackupPage extends StatelessWidget {
], ],
); );
if (result == true) { if (result == true) {
final result = final result = await Webdav.test(url.text, user.text, pwd.text);
await Webdav.test(urlCtrl.text, userCtrl.text, pwdCtrl.text); if (result != null) {
if (result == null) {
context.showSnackBar(l10n.success);
} else {
context.showSnackBar(result); context.showSnackBar(result);
return; return;
} }
Webdav.changeClient(urlCtrl.text, userCtrl.text, pwdCtrl.text); context.showSnackBar(l10n.success);
Webdav.changeClient(url.text, user.text, pwd.text);
} }
} }
@@ -430,9 +392,9 @@ class BackupPage extends StatelessWidget {
), ),
], ],
); );
} catch (e, trace) { } catch (e, s) {
Loggers.app.warning('Import backup failed', e, trace); Loggers.app.warning('Import backup failed', e, s);
context.showSnackBar(e.toString()); context.showErrDialog(e: e, s: s, operation: l10n.restore);
} }
} }
@@ -467,11 +429,9 @@ class BackupPage extends StatelessWidget {
); );
context.showSnackBar(l10n.success); context.showSnackBar(l10n.success);
} }
} catch (e) { } catch (e, s) {
context.showRoundDialog( context.showErrDialog(e: e, s: s, operation: l10n.import);
title: l10n.error, Loggers.app.warning('Import servers failed', e, s);
child: Text(e.toString()),
);
} }
} }
} }

View File

@@ -206,7 +206,10 @@ class _ContainerPageState extends State<ContainerPage> {
], ],
), ),
Text( Text(
'${item.image ?? l10n.unknown} - ${item.running ? l10n.running : l10n.stopped}', '${item.image ?? l10n.unknown} - ${switch (item) {
final PodmanPs ps => ps.running ? l10n.running : l10n.stopped,
final DockerPs ps => ps.state,
}}',
style: UIs.text13Grey, style: UIs.text13Grey,
), ),
_buildPsItemStats(item), _buildPsItemStats(item),
@@ -550,7 +553,10 @@ class _ContainerPageState extends State<ContainerPage> {
case ContainerMenu.logs: case ContainerMenu.logs:
AppRoutes.ssh( AppRoutes.ssh(
spi: widget.spi, spi: widget.spi,
initCmd: 'docker logs -f --tail 100 ${dItem.id}', initCmd: '${switch (_container.type) {
ContainerType.podman => 'podman',
ContainerType.docker => 'docker',
}} logs -f --tail 100 ${dItem.id}',
).go(context); ).go(context);
break; break;
case ContainerMenu.terminal: case ContainerMenu.terminal:

View File

@@ -37,7 +37,7 @@ class EditorPage extends StatefulWidget {
}); });
@override @override
_EditorPageState createState() => _EditorPageState(); State<EditorPage> createState() => _EditorPageState();
} }
class _EditorPageState extends State<EditorPage> { class _EditorPageState extends State<EditorPage> {

View File

@@ -9,7 +9,6 @@ import 'package:toolbox/core/channel/home_widget.dart';
import 'package:toolbox/core/extension/build.dart'; import 'package:toolbox/core/extension/build.dart';
import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/app/github_id.dart';
import 'package:toolbox/data/model/app/tab.dart'; import 'package:toolbox/data/model/app/tab.dart';
import 'package:toolbox/data/res/build_data.dart'; import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/github_id.dart'; import 'package:toolbox/data/res/github_id.dart';
@@ -17,6 +16,7 @@ import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/data/res/url.dart'; import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/view/page/ssh/page.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
part 'appbar.dart'; part 'appbar.dart';
@@ -151,6 +151,7 @@ class _HomePageState extends State<HomePage>
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, index) => AppTab.values[index].page, itemBuilder: (_, index) => AppTab.values[index].page,
onPageChanged: (value) { onPageChanged: (value) {
SSHPage.focusNode.unfocus();
if (!_switchingPage) { if (!_switchingPage) {
_selectIndex.value = value; _selectIndex.value = value;
} }
@@ -218,7 +219,7 @@ class _HomePageState extends State<HomePage>
Widget _buildDrawer() { Widget _buildDrawer() {
return Drawer( return Drawer(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -341,7 +342,7 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
if (Stores.setting.autoCheckAppUpdate.fetch()) { if (Stores.setting.autoCheckAppUpdate.fetch()) {
AppUpdateIface.doUpdate( AppUpdateIface.doUpdate(
build: BuildData.build, build: BuildData.build,
url: '${Urls.cdnBase}/update.json', url: Urls.updateCfg,
context: context, context: context,
); );
} }

View File

@@ -16,7 +16,7 @@ final class WearHome extends StatefulWidget {
const WearHome({super.key}); const WearHome({super.key});
@override @override
_WearHomeState createState() => _WearHomeState(); State<WearHome> createState() => _WearHomeState();
} }
final class _WearHomeState extends State<WearHome> with AfterLayoutMixin { final class _WearHomeState extends State<WearHome> with AfterLayoutMixin {
@@ -61,11 +61,30 @@ final class _WearHomeState extends State<WearHome> with AfterLayoutMixin {
} }
Widget _buildEachSever(Server srv) { Widget _buildEachSever(Server srv) {
return const Padding( final mem = () {
padding: EdgeInsets.all(7), final total = srv.status.mem.total;
final used = srv.status.mem.total - srv.status.mem.avail;
return '${used.bytes2Str} / ${total.bytes2Str}';
}();
final disk = () {
final total = srv.status.diskUsage?.size.kb2Str;
final used = srv.status.diskUsage?.used.kb2Str;
return '$used / $total';
}();
final net = '${srv.status.netSpeed.cachedRealVals.speedIn}'
'${srv.status.netSpeed.cachedRealVals.speedOut}';
return Padding(
padding: const EdgeInsets.all(7),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [], children: [
Text(srv.spi.name, style: UIs.text15Bold),
UIs.height7,
KvRow(k: 'CPU', v: '${srv.status.cpu.usedPercent()}%'),
KvRow(k: 'Mem', v: mem),
KvRow(k: 'Disk', v: disk),
KvRow(k: 'Net', v: net)
],
), ),
); );
} }
@@ -75,7 +94,7 @@ final class _WearHomeState extends State<WearHome> with AfterLayoutMixin {
if (Stores.setting.autoCheckAppUpdate.fetch()) { if (Stores.setting.autoCheckAppUpdate.fetch()) {
AppUpdateIface.doUpdate( AppUpdateIface.doUpdate(
build: BuildData.build, build: BuildData.build,
url: '${Urls.cdnBase}/update.json', url: Urls.updateCfg,
context: context, context: context,
); );
} }

View File

@@ -14,7 +14,7 @@ class PingPage extends StatefulWidget {
const PingPage({super.key}); const PingPage({super.key});
@override @override
_PingPageState createState() => _PingPageState(); State<PingPage> createState() => _PingPageState();
} }
class _PingPageState extends State<PingPage> class _PingPageState extends State<PingPage>

View File

@@ -19,7 +19,7 @@ class PrivateKeyEditPage extends StatefulWidget {
final PrivateKeyInfo? pki; final PrivateKeyInfo? pki;
@override @override
_PrivateKeyEditPageState createState() => _PrivateKeyEditPageState(); State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
} }
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> { class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {

View File

@@ -15,7 +15,7 @@ class PrivateKeysListPage extends StatefulWidget {
const PrivateKeysListPage({super.key}); const PrivateKeysListPage({super.key});
@override @override
_PrivateKeyListState createState() => _PrivateKeyListState(); State<PrivateKeysListPage> createState() => _PrivateKeyListState();
} }
class _PrivateKeyListState extends State<PrivateKeysListPage> class _PrivateKeyListState extends State<PrivateKeysListPage>

View File

@@ -16,7 +16,7 @@ class ProcessPage extends StatefulWidget {
const ProcessPage({super.key, required this.spi}); const ProcessPage({super.key, required this.spi});
@override @override
_ProcessPageState createState() => _ProcessPageState(); State<ProcessPage> createState() => _ProcessPageState();
} }
class _ProcessPageState extends State<ProcessPage> { class _ProcessPageState extends State<ProcessPage> {

View File

@@ -19,7 +19,7 @@ final class PvePage extends StatefulWidget {
}); });
@override @override
_PvePageState createState() => _PvePageState(); State<PvePage> createState() => _PvePageState();
} }
const _kHorziPadding = 11.0; const _kHorziPadding = 11.0;
@@ -46,6 +46,7 @@ final class _PvePageState extends State<PvePage> {
void dispose() { void dispose() {
super.dispose(); super.dispose();
_timer?.cancel(); _timer?.cancel();
_pve.dispose();
} }
@override @override

View File

@@ -32,7 +32,7 @@ class ServerDetailPage extends StatefulWidget {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;
@override @override
_ServerDetailPageState createState() => _ServerDetailPageState(); State<ServerDetailPage> createState() => _ServerDetailPageState();
} }
class _ServerDetailPageState extends State<ServerDetailPage> class _ServerDetailPageState extends State<ServerDetailPage>

View File

@@ -20,7 +20,7 @@ class ServerEditPage extends StatefulWidget {
final ServerPrivateInfo? spi; final ServerPrivateInfo? spi;
@override @override
_ServerEditPageState createState() => _ServerEditPageState(); State<ServerEditPage> createState() => _ServerEditPageState();
} }
class _ServerEditPageState extends State<ServerEditPage> { class _ServerEditPageState extends State<ServerEditPage> {
@@ -403,15 +403,27 @@ class _ServerEditPageState extends State<ServerEditPage> {
} }
List<Widget> _buildPVEs() { List<Widget> _buildPVEs() {
const addr = 'https://127.0.0.1:8006';
return [ return [
const Text('PVE', style: UIs.text13Grey), const Text('PVE', style: UIs.text13Grey),
UIs.height7, UIs.height7,
Input( Autocomplete<String>(
controller: _pveAddrCtrl, optionsBuilder: (val) {
type: TextInputType.url, final v = val.text;
icon: MingCute.web_line, if (v.startsWith(addr.substring(0, v.length))) {
label: l10n.addr, return [addr];
hint: 'https://example.com:8006', }
return [];
},
onSelected: (val) => _pveAddrCtrl.text = val,
fieldViewBuilder: (_, ctrl, node, __) => Input(
controller: ctrl,
type: TextInputType.url,
icon: MingCute.web_line,
node: node,
label: l10n.addr,
hint: addr,
),
), ),
ListTile( ListTile(
leading: const Padding( leading: const Padding(

View File

@@ -25,7 +25,7 @@ class ServerPage extends StatefulWidget {
const ServerPage({super.key}); const ServerPage({super.key});
@override @override
_ServerPageState createState() => _ServerPageState(); State<ServerPage> createState() => _ServerPageState();
} }
class _ServerPageState extends State<ServerPage> class _ServerPageState extends State<ServerPage>

View File

@@ -23,7 +23,7 @@ class SettingPage extends StatefulWidget {
const SettingPage({super.key}); const SettingPage({super.key});
@override @override
_SettingPageState createState() => _SettingPageState(); State<SettingPage> createState() => _SettingPageState();
} }
class _SettingPageState extends State<SettingPage> { class _SettingPageState extends State<SettingPage> {
@@ -305,7 +305,7 @@ class _SettingPageState extends State<SettingPage> {
_setting.primaryColor.put(color.value); _setting.primaryColor.put(color.value);
context.pop(); context.pop();
context.pop(); context.pop();
RebuildNodes.app.rebuild(); RNodes.app.build();
} }
// Widget _buildLaunchPage() { // Widget _buildLaunchPage() {
@@ -393,7 +393,7 @@ class _SettingPageState extends State<SettingPage> {
); );
if (selected != null) { if (selected != null) {
_setting.themeMode.put(selected); _setting.themeMode.put(selected);
RebuildNodes.app.rebuild(); RNodes.app.build();
} }
}, },
trailing: ValBuilder( trailing: ValBuilder(
@@ -442,7 +442,7 @@ class _SettingPageState extends State<SettingPage> {
onPressed: () { onPressed: () {
_setting.fontPath.delete(); _setting.fontPath.delete();
context.pop(); context.pop();
RebuildNodes.app.rebuild(); RNodes.app.build();
}, },
child: Text(l10n.clear), child: Text(l10n.clear),
) )
@@ -461,14 +461,12 @@ class _SettingPageState extends State<SettingPage> {
_setting.fontPath.put(path); _setting.fontPath.put(path);
} else { } else {
final fontFile = File(path); final fontFile = File(path);
final newPath = '${Paths.fontPath}/${path.split('/').last}'; await fontFile.copy(Paths.font);
await fontFile.copy(newPath); _setting.fontPath.put(Paths.font);
_setting.fontPath.put(newPath);
} }
context.pop(); context.pop();
RebuildNodes.app.rebuild(); RNodes.app.build();
return;
} }
Widget _buildTermFontSize() { Widget _buildTermFontSize() {
@@ -538,7 +536,7 @@ class _SettingPageState extends State<SettingPage> {
if (selected != null) { if (selected != null) {
_setting.locale.put(selected.code); _setting.locale.put(selected.code);
context.pop(); context.pop();
RebuildNodes.app.rebuild(); RNodes.app.build();
} }
}, },
trailing: ListenBuilder( trailing: ListenBuilder(
@@ -611,7 +609,7 @@ class _SettingPageState extends State<SettingPage> {
subtitle: Text(l10n.fullScreenTip, style: UIs.textGrey), subtitle: Text(l10n.fullScreenTip, style: UIs.textGrey),
trailing: StoreSwitch( trailing: StoreSwitch(
prop: _setting.fullScreen, prop: _setting.fullScreen,
callback: (_) => RebuildNodes.app.rebuild(), callback: (_) => RNodes.app.build(),
), ),
); );
} }
@@ -822,7 +820,7 @@ class _SettingPageState extends State<SettingPage> {
return; return;
} }
_setting.textFactor.put(val); _setting.textFactor.put(val);
RebuildNodes.app.rebuild(); RNodes.app.build();
context.pop(); context.pop();
} }
@@ -1068,6 +1066,7 @@ class _SettingPageState extends State<SettingPage> {
leading: const Icon(MingCute.more_3_fill), leading: const Icon(MingCute.more_3_fill),
title: Text(l10n.more), title: Text(l10n.more),
children: [ children: [
_buildBeta(),
_buildWakeLock(), _buildWakeLock(),
if (isAndroid || isIOS) _buildCollectUsage(), if (isAndroid || isIOS) _buildCollectUsage(),
_buildCollapseUI(), _buildCollapseUI(),
@@ -1163,19 +1162,29 @@ class _SettingPageState extends State<SettingPage> {
return ListTile( return ListTile(
leading: const Icon(Icons.image), leading: const Icon(Icons.image),
title: Text('Logo ${l10n.addr}'), title: Text('Logo ${l10n.addr}'),
subtitle: SimpleMarkdown(data: '[${l10n.doc}](${Urls.appWiki})'),
trailing: const Icon(Icons.keyboard_arrow_right), trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () { onTap: () {
final ctrl = final ctrl =
TextEditingController(text: _setting.serverLogoUrl.fetch()); TextEditingController(text: _setting.serverLogoUrl.fetch());
context.showRoundDialog( context.showRoundDialog(
title: 'Logo ${l10n.addr}', title: 'Logo ${l10n.addr}',
child: Input( child: Column(
controller: ctrl, mainAxisSize: MainAxisSize.min,
autoFocus: true, children: [
hint: 'https://example.com/logo.png', Input(
icon: Icons.link, controller: ctrl,
onSubmitted: onSave, autoFocus: true,
hint: 'https://example.com/logo.png',
icon: Icons.link,
maxLines: 2,
onSubmitted: onSave,
),
ListTile(
title: Text(l10n.doc),
trailing: const Icon(Icons.open_in_new),
onTap: () => Urls.appWiki.launch(),
),
],
), ),
actions: [ actions: [
TextButton( TextButton(
@@ -1187,4 +1196,12 @@ class _SettingPageState extends State<SettingPage> {
}, },
); );
} }
Widget _buildBeta() {
return ListTile(
title: const Text('Beta Program'),
subtitle: Text(l10n.acceptBeta, style: UIs.textGrey),
trailing: StoreSwitch(prop: _setting.betaTest),
);
}
} }

View File

@@ -1,21 +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:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/view/page/setting/platform/platform_pub.dart'; import 'package:toolbox/view/page/setting/platform/platform_pub.dart';
import 'package:watch_connectivity/watch_connectivity.dart';
class AndroidSettingsPage extends StatefulWidget { class AndroidSettingsPage extends StatefulWidget {
const AndroidSettingsPage({super.key}); const AndroidSettingsPage({super.key});
@override @override
_AndroidSettingsPageState createState() => _AndroidSettingsPageState(); State<AndroidSettingsPage> createState() => _AndroidSettingsPageState();
} }
class _AndroidSettingsPageState extends State<AndroidSettingsPage> { class _AndroidSettingsPageState extends State<AndroidSettingsPage> {
late SharedPreferences _sp; late SharedPreferences _sp;
final wc = WatchConnectivity();
@override @override
void initState() { void initState() {
@@ -34,6 +35,7 @@ class _AndroidSettingsPageState extends State<AndroidSettingsPage> {
children: [ children: [
_buildBgRun(), _buildBgRun(),
_buildAndroidWidgetSharedPreference(), _buildAndroidWidgetSharedPreference(),
_buildWatch(),
if (BioAuth.isPlatformSupported) if (BioAuth.isPlatformSupported)
PlatformPublicSettings.buildBioAuth(), PlatformPublicSettings.buildBioAuth(),
].map((e) => CardX(child: e)).toList(), ].map((e) => CardX(child: e)).toList(),
@@ -49,10 +51,9 @@ class _AndroidSettingsPageState extends State<AndroidSettingsPage> {
); );
} }
void _saveWidgetSP(String data, Map<String, String> old) { void _saveWidgetSP(Map<String, String> map, Map<String, String> old) {
context.pop(); context.pop();
try { try {
final map = Map<String, String>.from(json.decode(data));
final keysDel = old.keys.toSet().difference(map.keys.toSet()); final keysDel = old.keys.toSet().difference(map.keys.toSet());
for (final key in keysDel) { for (final key in keysDel) {
_sp.remove(key); _sp.remove(key);
@@ -70,7 +71,7 @@ class _AndroidSettingsPageState extends State<AndroidSettingsPage> {
return ListTile( return ListTile(
title: Text(l10n.homeWidgetUrlConfig), title: Text(l10n.homeWidgetUrlConfig),
trailing: const Icon(Icons.keyboard_arrow_right), trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () { onTap: () async {
final data = <String, String>{}; final data = <String, String>{};
_sp.getKeys().forEach((key) { _sp.getKeys().forEach((key) {
final val = _sp.getString(key); final val = _sp.getString(key);
@@ -78,25 +79,57 @@ class _AndroidSettingsPageState extends State<AndroidSettingsPage> {
data[key] = val; data[key] = val;
} }
}); });
final ctrl = TextEditingController(text: json.encode(data)); final result = await AppRoutes.kvEditor(data: data).go(context);
context.showRoundDialog( if (result != null) {
title: l10n.homeWidgetUrlConfig, if (result is Map<String, String>) {
child: Input( _saveWidgetSP(result, data);
autoFocus: true, } else {
controller: ctrl, final err = 'Save Android widget SharedPreference failed: '
label: 'JSON', 'unexpected type: ${result.runtimeType}';
type: TextInputType.visiblePassword, Loggers.app.warning(err);
maxLines: 7, context.showRoundDialog(
onSubmitted: (p0) => _saveWidgetSP(p0, data), title: l10n.error,
), child: SingleChildScrollView(
actions: [ child: SimpleMarkdown(data: '$err\n\n```$result```'),
TextButton( ),
onPressed: () { );
_saveWidgetSP(ctrl.text, data); }
}, }
child: Text(l10n.ok), },
), );
], }
Widget _buildWatch() {
return FutureWidget(
future: wc.isReachable,
error: (e, s) {
Loggers.app.warning('WatchOS error', e, s);
return ListTile(
title: const Text('Watch app'),
subtitle: Text(l10n.viewErr, style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () {
context.showRoundDialog(
title: l10n.error,
child: SingleChildScrollView(
child: SimpleMarkdown(data: '${e.toString()}\n```$s```'),
),
);
},
);
},
success: (val) {
if (val == null) {
return ListTile(
title: const Text('Watch app'),
subtitle: Text(l10n.watchNotPaired, style: UIs.textGrey),
);
}
return ListTile(
title: const Text('Watch app'),
subtitle: Text(l10n.sync, style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () async {},
); );
}, },
); );

View File

@@ -1,11 +1,8 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils/misc.dart'; import 'package:toolbox/core/utils/misc.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/view/page/setting/platform/platform_pub.dart'; import 'package:toolbox/view/page/setting/platform/platform_pub.dart';
import 'package:watch_connectivity/watch_connectivity.dart'; import 'package:watch_connectivity/watch_connectivity.dart';
@@ -14,7 +11,7 @@ class IOSSettingsPage extends StatefulWidget {
const IOSSettingsPage({super.key}); const IOSSettingsPage({super.key});
@override @override
_IOSSettingsPageState createState() => _IOSSettingsPageState(); State<IOSSettingsPage> createState() => _IOSSettingsPageState();
} }
class _IOSSettingsPageState extends State<IOSSettingsPage> { class _IOSSettingsPageState extends State<IOSSettingsPage> {
@@ -83,7 +80,7 @@ class _IOSSettingsPageState extends State<IOSSettingsPage> {
} }
Widget _buildWatchApp() { Widget _buildWatchApp() {
return FutureWidget<Map<String, dynamic>?>( return FutureWidget(
future: () async { future: () async {
if (!await wc.isPaired) { if (!await wc.isPaired) {
return null; return null;
@@ -115,19 +112,12 @@ class _IOSSettingsPageState extends State<IOSSettingsPage> {
} }
void _onTapWatchApp(Map<String, dynamic> map) async { void _onTapWatchApp(Map<String, dynamic> map) async {
/// Encode [map] to String with indent `\t` final urls = Map<String, String>.from(map['urls'] as Map? ?? {});
final text = Miscs.jsonEncoder.convert(map); final result = await AppRoutes.kvEditor(data: urls).go(context);
final result = await AppRoutes.editor( if (result == null || result! is Map<String, String>) return;
text: text,
langCode: 'json',
title: 'Watch app',
).go<String>(context);
if (result == null) {
return;
}
try { try {
final newCtx = json.decode(result) as Map<String, dynamic>; await wc.updateApplicationContext({'urls': result});
await wc.updateApplicationContext(newCtx);
} catch (e, trace) { } catch (e, trace) {
context.showRoundDialog( context.showRoundDialog(
title: l10n.error, title: l10n.error,

View File

@@ -1,3 +1,4 @@
import 'dart:ui';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/locale.dart';
@@ -8,7 +9,7 @@ class ServerOrderPage extends StatefulWidget {
const ServerOrderPage({super.key}); const ServerOrderPage({super.key});
@override @override
_ServerOrderPageState createState() => _ServerOrderPageState(); State<ServerOrderPage> createState() => _ServerOrderPageState();
} }
class _ServerOrderPageState extends State<ServerOrderPage> { class _ServerOrderPageState extends State<ServerOrderPage> {
@@ -22,6 +23,29 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
); );
} }
Widget _proxyDecorator(Widget child, int index, Animation<double> animation) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double elevation = lerpDouble(1, 6, animValue)!;
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
// Create a Card based on the color and the content of the dragged one
// and set its elevation to the animated value.
child: Card(
elevation: elevation,
// color: cards[index].color,
// child: cards[index].child,
child: _buildCardTile(index),
),
);
},
// child: child,
);
}
Widget _buildBody() { Widget _buildBody() {
if (Pros.server.serverOrder.isEmpty) { if (Pros.server.serverOrder.isEmpty) {
return Center(child: Text(l10n.noServerAvailable)); return Center(child: Text(l10n.noServerAvailable));
@@ -37,28 +61,36 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
}), }),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
buildDefaultDragHandles: false, buildDefaultDragHandles: false,
itemBuilder: (_, idx) => _buildItem(idx, Pros.server.serverOrder[idx]), itemBuilder: (_, idx) => _buildItem(idx),
itemCount: Pros.server.serverOrder.length, itemCount: Pros.server.serverOrder.length,
proxyDecorator: _proxyDecorator,
); );
} }
Widget _buildItem(int index, String id) { Widget _buildItem(int index) {
return ReorderableDelayedDragStartListener(
key: ValueKey('$index'),
index: index,
child: CardX(child: _buildCardTile(index)),
);
}
Widget _buildCardTile(int index) {
final id = Pros.server.serverOrder[index];
final spi = Pros.server.pick(id: id)?.spi; final spi = Pros.server.pick(id: id)?.spi;
if (spi == null) { if (spi == null) {
return const SizedBox(); return const SizedBox();
} }
return ReorderableDelayedDragStartListener(
key: ValueKey('$index'), return ListTile(
index: index, title: Text(spi.name),
child: CardX( subtitle: Text(spi.id, style: UIs.textGrey),
child: ListTile( leading: CircleAvatar(
title: Text(spi.name), child: Text(spi.name[0]),
subtitle: Text(spi.id, style: UIs.textGrey), ),
leading: CircleAvatar( trailing: ReorderableDragStartListener(
child: Text(spi.name[0]), index: index,
), child: const Icon(Icons.drag_handle),
trailing: const Icon(Icons.drag_handle),
),
), ),
); );
} }

View File

@@ -8,7 +8,7 @@ class SSHVirtKeySettingPage extends StatefulWidget {
const SSHVirtKeySettingPage({super.key}); const SSHVirtKeySettingPage({super.key});
@override @override
_SSHVirtKeySettingPageState createState() => _SSHVirtKeySettingPageState(); State<SSHVirtKeySettingPage> createState() => _SSHVirtKeySettingPageState();
} }
class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> { class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {

View File

@@ -13,7 +13,7 @@ class SnippetEditPage extends StatefulWidget {
final Snippet? snippet; final Snippet? snippet;
@override @override
_SnippetEditPageState createState() => _SnippetEditPageState(); State<SnippetEditPage> createState() => _SnippetEditPageState();
} }
class _SnippetEditPageState extends State<SnippetEditPage> class _SnippetEditPageState extends State<SnippetEditPage>

View File

@@ -12,7 +12,7 @@ class SnippetListPage extends StatefulWidget {
const SnippetListPage({super.key}); const SnippetListPage({super.key});
@override @override
_SnippetListPageState createState() => _SnippetListPageState(); State<SnippetListPage> createState() => _SnippetListPageState();
} }
class _SnippetListPageState extends State<SnippetListPage> { class _SnippetListPageState extends State<SnippetListPage> {

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:after_layout/after_layout.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:flutter/material.dart';
@@ -30,7 +31,7 @@ class SSHPage extends StatefulWidget {
final String? initCmd; final String? initCmd;
final bool notFromTab; final bool notFromTab;
final Function()? onSessionEnd; final Function()? onSessionEnd;
final FocusNode? focus; final GlobalKey<TerminalViewState>? terminalKey;
const SSHPage({ const SSHPage({
super.key, super.key,
@@ -38,21 +39,24 @@ class SSHPage extends StatefulWidget {
this.initCmd, this.initCmd,
this.notFromTab = true, this.notFromTab = true,
this.onSessionEnd, this.onSessionEnd,
this.focus, this.terminalKey,
}); });
static final focusNode = FocusNode();
@override @override
_SSHPageState createState() => _SSHPageState(); State<SSHPage> createState() => SSHPageState();
} }
const _horizonPadding = 7.0; const _horizonPadding = 7.0;
class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin { class SSHPageState extends State<SSHPage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
final _keyboard = VirtKeyProvider(); final _keyboard = VirtKeyProvider();
late final _terminal = Terminal(inputHandler: _keyboard); late final _terminal = Terminal(inputHandler: _keyboard);
final TerminalController _terminalController = TerminalController(); final TerminalController _terminalController = TerminalController();
final List<List<VirtKey>> _virtKeysList = []; final List<List<VirtKey>> _virtKeysList = [];
late final _focus = widget.focus ?? FocusNode(); late final _termKey = widget.terminalKey ?? GlobalKey<TerminalViewState>();
late MediaQueryData _media; late MediaQueryData _media;
late TerminalStyle _terminalStyle; late TerminalStyle _terminalStyle;
@@ -71,13 +75,6 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
super.initState(); super.initState();
_initStoredCfg(); _initStoredCfg();
_initVirtKeys(); _initVirtKeys();
Future.delayed(const Duration(milliseconds: 77), () async {
_showHelp();
await _initTerminal();
if (Stores.setting.sshWakeLock.fetch()) WakelockPlus.enable();
});
} }
@override @override
@@ -144,14 +141,14 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
), ),
child: TerminalView( child: TerminalView(
_terminal, _terminal,
key: _termKey,
controller: _terminalController, controller: _terminalController,
focusNode: _focus,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
enableSuggestions: true, enableSuggestions: true,
textStyle: _terminalStyle, textStyle: _terminalStyle,
theme: _terminalTheme, theme: _terminalTheme,
deleteDetection: isMobile, deleteDetection: isMobile,
autofocus: true, autofocus: false,
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light, keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
showToolbar: isMobile, showToolbar: isMobile,
viewOffset: Offset( viewOffset: Offset(
@@ -159,6 +156,7 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
CustomAppBar.barHeight ?? _media.padding.top, CustomAppBar.barHeight ?? _media.padding.top,
), ),
hideScrollBar: false, hideScrollBar: false,
focusNode: SSHPage.focusNode,
), ),
), ),
); );
@@ -282,11 +280,7 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
Future<void> _doVirtualKeyFunc(VirtualKeyFunc type) async { Future<void> _doVirtualKeyFunc(VirtualKeyFunc type) async {
switch (type) { switch (type) {
case VirtualKeyFunc.toggleIME: case VirtualKeyFunc.toggleIME:
if (!_focus.hasFocus) { _termKey.currentState?.toggleFocus();
_focus.requestFocus();
} else {
_focus.unfocus();
}
break; break;
case VirtualKeyFunc.backspace: case VirtualKeyFunc.backspace:
_terminal.keyInput(TerminalKey.backspace); _terminal.keyInput(TerminalKey.backspace);
@@ -433,6 +427,8 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
} }
} }
SSHPage.focusNode.requestFocus();
await session.done; await session.done;
if (mounted && widget.notFromTab) { if (mounted && widget.notFromTab) {
context.pop(); context.pop();
@@ -520,21 +516,29 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
} }
Future<void> _showHelp() async { Future<void> _showHelp() async {
if (!Stores.setting.sshTermHelpShown.fetch()) { if (Stores.setting.sshTermHelpShown.fetch()) return;
await context.showRoundDialog(
title: l10n.doc, return await context.showRoundDialog(
child: Text(l10n.sshTermHelp), title: l10n.doc,
actions: [ child: Text(l10n.sshTermHelp),
TextButton( actions: [
onPressed: () { TextButton(
Stores.setting.sshTermHelpShown.put(true); onPressed: () {
context.pop(); Stores.setting.sshTermHelpShown.put(true);
}, context.pop();
child: Text(l10n.noPromptAgain), },
), child: Text(l10n.noPromptAgain),
], ),
); ],
} );
}
@override
FutureOr<void> afterFirstLayout(BuildContext context) async {
await _showHelp();
await _initTerminal();
if (Stores.setting.sshWakeLock.fetch()) WakelockPlus.enable();
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
@@ -15,38 +16,37 @@ class SSHTabPage extends StatefulWidget {
State<SSHTabPage> createState() => _SSHTabPageState(); State<SSHTabPage> createState() => _SSHTabPageState();
} }
typedef _TabMap = Map<String, ({Widget page, GlobalKey<SSHPageState>? key})>;
class _SSHTabPageState extends State<SSHTabPage> class _SSHTabPageState extends State<SSHTabPage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
late final _tabMap = <String, ({Widget page, FocusNode? focus})>{ late final _TabMap _tabMap = {
l10n.add: (page: _buildAddPage(), focus: null), l10n.add: (page: _buildAddPage(), key: null),
}; };
late var _tabController = TabController( final _pageCtrl = PageController();
length: _tabMap.length, final _fabVN = 0.vn;
vsync: this, final _tabRN = RNode();
);
final _fabRN = ValueNotifier(0);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return Scaffold( return Scaffold(
appBar: TabBar( appBar: PreferredSizeListenBuilder(
controller: _tabController, listenable: _tabRN,
tabs: _tabMap.keys.map(_buildTabItem).toList(), builder: () {
isScrollable: true, return _TabBar(
tabAlignment: TabAlignment.start, idxVN: _fabVN,
dividerColor: Colors.transparent, map: _tabMap,
onTap: (value) { onTap: _onTapTab,
_fabRN.value = value; onClose: _onTapClose,
final mapKey = _tabMap.keys.elementAt(value); );
_tabMap[mapKey]?.focus?.requestFocus();
}, },
), ),
body: _buildBody(), body: _buildBody(),
floatingActionButton: ListenableBuilder( floatingActionButton: ValBuilder(
listenable: _fabRN, listenable: _fabVN,
builder: (_, __) { builder: (idx) {
if (_fabRN.value != 0) return const SizedBox(); if (idx != 0) return const SizedBox();
return FloatingActionButton( return FloatingActionButton(
heroTag: 'sshAddServer', heroTag: 'sshAddServer',
onPressed: () => AppRoutes.serverEdit().go(context), onPressed: () => AppRoutes.serverEdit().go(context),
@@ -58,42 +58,40 @@ class _SSHTabPageState extends State<SSHTabPage>
); );
} }
Widget _buildTabItem(String e) { void _onTapTab(int idx) async {
if (e == l10n.add) { await _toPage(idx);
return Tab(child: Text(e)); SSHPage.focusNode.unfocus();
} }
return Tab(
child: Row( void _onTapClose(String name) async {
children: [ SSHPage.focusNode.unfocus();
Text(e),
UIs.width7, final confirm = await showDialog<bool>(
IconBtn( context: context,
icon: Icons.close, builder: (context) {
onTap: () async { return AlertDialog(
final confirm = await context.showRoundDialog<bool>( title: Text(l10n.attention),
title: l10n.attention, content: Text('${l10n.close} SSH ${l10n.conn}($name) ?'),
child: Text('${l10n.close} SSH ${l10n.conn}($e) ?'), actions: [
actions: [ TextButton(
TextButton( onPressed: () => context.pop(true),
onPressed: () => context.pop(true), child: Text(l10n.ok, style: UIs.textRed),
child: Text(l10n.ok, style: UIs.textRed), ),
), TextButton(
TextButton( onPressed: () => context.pop(false),
onPressed: () => context.pop(false), child: Text(l10n.cancel),
child: Text(l10n.cancel), ),
), ],
], );
); },
if (confirm != true) {
return;
}
_tabMap.remove(e);
_refreshTabs();
},
),
],
),
); );
Future.delayed(Durations.short1, FocusScope.of(context).unfocus);
if (confirm != true) return;
_tabMap.remove(name);
_tabRN.build();
_pageCtrl.previousPage(
duration: Durations.medium1, curve: Curves.fastEaseInToSlowEaseOut);
} }
Widget _buildAddPage() { Widget _buildAddPage() {
@@ -128,47 +126,157 @@ class _SSHTabPageState extends State<SSHTabPage>
} }
Widget _buildBody() { Widget _buildBody() {
return TabBarView( return ListenBuilder(
physics: const NeverScrollableScrollPhysics(), listenable: _tabRN,
controller: _tabController, builder: () {
children: _tabMap.values.map((e) => e.page).toList(), return PageView.builder(
physics: const NeverScrollableScrollPhysics(),
controller: _pageCtrl,
itemCount: _tabMap.length,
itemBuilder: (_, idx) {
final name = _tabMap.keys.elementAt(idx);
return _tabMap[name]?.page ?? UIs.placeholder;
},
onPageChanged: (value) => _fabVN.value = value,
);
},
); );
} }
void _onTapInitCard(ServerPrivateInfo spi) { void _onTapInitCard(ServerPrivateInfo spi) async {
final name = () { final name = () {
if (_tabMap.containsKey(spi.name)) { final reg = RegExp('${spi.name}\\((\\d+)\\)');
return '${spi.name}(${_tabMap.length + 1})'; final idxs = _tabMap.keys
.map((e) => reg.firstMatch(e))
.map((e) => e?.group(1))
.where((e) => e != null);
if (idxs.isEmpty) {
return _tabMap.keys.contains(spi.name) ? '${spi.name}(1)' : spi.name;
}
final biggest = idxs.reduce((a, b) => a!.length > b!.length ? a : b);
final biggestInt = int.tryParse(biggest ?? '0');
if (biggestInt != null && biggestInt > 0) {
return '${spi.name}(${biggestInt + 1})';
} }
return spi.name; return spi.name;
}(); }();
final focus = FocusNode(); final key = GlobalKey<SSHPageState>();
_tabMap[name] = ( _tabMap[name] = (
page: SSHPage( page: SSHPage(
// Keep it, or the Flutter will works unexpectedly
key: key,
spi: spi, spi: spi,
focus: focus,
notFromTab: false, notFromTab: false,
onSessionEnd: () { onSessionEnd: () {
_tabMap.remove(name); _tabMap.remove(name);
_refreshTabs();
}, },
), ),
focus: focus, key: key,
); );
_refreshTabs(); _tabRN.build();
final idx = _tabMap.length - 1; // Wait for the page to be built
_tabController.animateTo(idx); await Future.delayed(Durations.short3);
_fabRN.value = idx; final idx = _tabMap.keys.toList().indexOf(name);
await _toPage(idx);
} }
void _refreshTabs() { Future<void> _toPage(int idx) => _pageCtrl.animateToPage(idx,
_tabController = TabController( duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut);
length: _tabMap.length,
vsync: this,
);
setState(() {});
}
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
} }
final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
const _TabBar({
required this.idxVN,
required this.map,
required this.onTap,
required this.onClose,
});
final ValueNotifier<int> idxVN;
final _TabMap map;
final void Function(int idx) onTap;
final void Function(String name) onClose;
List<String> get names => map.keys.toList();
@override
Size get preferredSize => const Size.fromHeight(48);
@override
Widget build(BuildContext context) {
return ListenBuilder(
listenable: idxVN,
builder: () {
return ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5),
itemCount: names.length,
itemBuilder: (_, idx) => _buillItem(idx),
separatorBuilder: (_, __) => Padding(
padding: const EdgeInsets.symmetric(vertical: 17),
child: Container(
color: const Color.fromARGB(61, 158, 158, 158),
width: 3,
),
),
);
},
);
}
Widget _buillItem(int idx) {
final name = names[idx];
final selected = idxVN.value == idx;
final color = selected ? null : Colors.grey;
final Widget child;
if (idx == 0) {
child = Padding(
padding: const EdgeInsets.symmetric(horizontal: 13),
child: Icon(MingCute.add_circle_fill, size: 17, color: color),
);
} else {
final text = Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: color),
softWrap: false,
textAlign: TextAlign.center,
textWidthBasis: TextWidthBasis.parent,
);
child = AnimatedContainer(
width: selected ? 90 : 50,
duration: Durations.medium3,
curve: Curves.fastEaseInToSlowEaseOut,
child: switch (selected) {
true => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(width: 55, child: text),
if (selected)
FadeIn(
child: IconBtn(
icon: MingCute.close_circle_fill,
color: color,
onTap: () => onClose(name),
),
),
],
),
false => Center(child: text),
},
).paddingOnly(left: 3, right: 3);
}
return InkWell(
borderRadius: BorderRadius.circular(13),
onTap: () => onTap(idx),
child: child,
).paddingSymmetric(horizontal: 13);
}
}

View File

@@ -32,7 +32,7 @@ class SftpPage extends StatefulWidget {
}); });
@override @override
_SftpPageState createState() => _SftpPageState(); State<SftpPage> createState() => _SftpPageState();
} }
class _SftpPageState extends State<SftpPage> with AfterLayoutMixin { class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
@@ -472,19 +472,41 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
void _delete(SftpName file) { void _delete(SftpName file) {
context.pop(); context.pop();
final isDir = file.attr.isDirectory; final isDir = file.attr.isDirectory;
final useRmr = Stores.setting.sftpRmrDir.fetch(); var useRmr = Stores.setting.sftpRmrDir.fetch();
final text = () { final text = () {
if (isDir && !useRmr) { if (isDir && !useRmr) {
return l10n.askContinue( return l10n.askContinue('${l10n.delete} ${file.filename}');
'${l10n.dirEmpty}\n${l10n.delete} '
'${file.filename}',
);
} }
return l10n.askContinue('${l10n.delete} ${file.filename}'); return l10n.askContinue('${l10n.delete} ${file.filename}');
}(); }();
// Most users don't know that SFTP can't delete a directory which is not
// empty, so we provide a checkbox to let user choose to use `rm -r` or not
context.showRoundDialog( context.showRoundDialog(
child: Text(text),
title: l10n.attention, title: l10n.attention,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(text),
),
if (!useRmr)
StatefulBuilder(
builder: (_, setState) {
return CheckboxListTile(
title: Text(l10n.sftpRmrDirSummary),
value: useRmr,
onChanged: (val) {
setState(() {
useRmr = val ?? false;
});
},
);
},
),
],
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => context.pop(), onPressed: () => context.pop(),
@@ -494,19 +516,22 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
onPressed: () async { onPressed: () async {
context.pop(); context.pop();
try { try {
await context.showLoadingDialog(fn: () async { await context.showLoadingDialog(
final remotePath = _getRemotePath(file); fn: () async {
if (useRmr) { final remotePath = _getRemotePath(file);
await _client!.run('rm -r "$remotePath"'); if (useRmr) {
} else if (file.attr.isDirectory) { await _client!.run('rm -r "$remotePath"');
await _status.client!.rmdir(remotePath); } else if (file.attr.isDirectory) {
} else { await _status.client!.rmdir(remotePath);
await _status.client!.remove(remotePath); } else {
} await _status.client!.remove(remotePath);
}); }
},
onErr: (e, s) {},
);
_listDir(); _listDir();
} catch (e, s) { } catch (e, s) {
_showErrDialog(context, e, 'Delete', s); context.showErrDialog(e: e, s: s, operation: l10n.delete);
} }
}, },
child: Text(l10n.delete, style: UIs.textRed), child: Text(l10n.delete, style: UIs.textRed),
@@ -547,13 +572,16 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
} }
context.pop(); context.pop();
try { try {
await context.showLoadingDialog(fn: () async { await context.showLoadingDialog(
final dir = '${_status.path!.path}/${textController.text}'; fn: () async {
await _status.client!.mkdir(dir); final dir = '${_status.path!.path}/${textController.text}';
}); await _status.client!.mkdir(dir);
},
onErr: (e, s) {},
);
_listDir(); _listDir();
} catch (e, s) { } catch (e, s) {
_showErrDialog(context, e, 'Create folder', s); context.showErrDialog(e: e, s: s, operation: l10n.createFolder);
} }
}, },
child: Text(l10n.ok, style: UIs.textRed), child: Text(l10n.ok, style: UIs.textRed),
@@ -591,13 +619,16 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
} }
context.pop(); context.pop();
try { try {
await context.showLoadingDialog(fn: () async { await context.showLoadingDialog(
final path = '${_status.path!.path}/${textController.text}'; fn: () async {
await _client!.run('touch "$path"'); final path = '${_status.path!.path}/${textController.text}';
}); await _client!.run('touch "$path"');
},
onErr: (e, s) {},
);
_listDir(); _listDir();
} catch (e, s) { } catch (e, s) {
_showErrDialog(context, e, 'Create file', s); context.showErrDialog(e: e, s: s, operation: l10n.createFile);
} }
}, },
child: Text(l10n.ok, style: UIs.textRed), child: Text(l10n.ok, style: UIs.textRed),
@@ -636,13 +667,16 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
} }
context.pop(); context.pop();
try { try {
await context.showLoadingDialog(fn: () async { await context.showLoadingDialog(
final newName = textController.text; fn: () async {
await _status.client?.rename(file.filename, newName); final newName = textController.text;
}); await _status.client?.rename(file.filename, newName);
},
onErr: (e, s) {},
);
_listDir(); _listDir();
} catch (e, s) { } catch (e, s) {
_showErrDialog(context, e, 'Rename', s); context.showErrDialog(e: e, s: s, operation: l10n.rename);
} }
}, },
child: Text(l10n.rename, style: UIs.textRed), child: Text(l10n.rename, style: UIs.textRed),
@@ -672,29 +706,6 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
_listDir(); _listDir();
} }
Future<void> _showErrDialog(
BuildContext ctx, Object e, String op, StackTrace s) async {
Loggers.app.warning('$op failed', e, s);
return ctx.showRoundDialog(
title: l10n.error,
child: SingleChildScrollView(
child: Column(
children: [
Text(e.toString()),
const SizedBox(height: 7),
SimpleMarkdown(data: s.toString()),
],
),
),
actions: [
TextButton(
onPressed: () => ctx.pop(),
child: Text(l10n.ok),
),
],
);
}
String _getRemotePath(SftpName name) { String _getRemotePath(SftpName name) {
final prePath = _status.path!.path; final prePath = _status.path!.path;
// Only support Linux as remote now, so the seperator is '/' // Only support Linux as remote now, so the seperator is '/'

View File

@@ -12,7 +12,7 @@ class SftpMissionPage extends StatefulWidget {
const SftpMissionPage({super.key}); const SftpMissionPage({super.key});
@override @override
_SftpMissionPageState createState() => _SftpMissionPageState(); State<SftpMissionPage> createState() => _SftpMissionPageState();
} }
class _SftpMissionPageState extends State<SftpMissionPage> { class _SftpMissionPageState extends State<SftpMissionPage> {

View File

@@ -7,7 +7,6 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h> #include <dynamic_color/dynamic_color_plugin.h>
#include <gtk/gtk_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
@@ -16,9 +15,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar = g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar = g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);

View File

@@ -4,7 +4,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
gtk
screen_retriever screen_retriever
url_launcher_linux url_launcher_linux
window_manager window_manager

View File

@@ -5,7 +5,6 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import app_links
import device_info_plus import device_info_plus
import dynamic_color import dynamic_color
import icloud_storage import icloud_storage
@@ -19,7 +18,6 @@ import wakelock_plus
import window_manager import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
IcloudStoragePlugin.register(with: registry.registrar(forPlugin: "IcloudStoragePlugin")) IcloudStoragePlugin.register(with: registry.registrar(forPlugin: "IcloudStoragePlugin"))

View File

@@ -471,7 +471,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 918; CURRENT_PROJECT_VERSION = 948;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Server Box"; INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
@@ -481,7 +481,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box"; PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -608,7 +608,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 918; CURRENT_PROJECT_VERSION = 948;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Server Box"; INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
@@ -618,7 +618,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box"; PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -638,7 +638,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 918; CURRENT_PROJECT_VERSION = 948;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6; "DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -649,7 +649,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.918; MARKETING_VERSION = 1.0.948;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box"; PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -33,22 +33,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
app_links:
dependency: transitive
description:
name: app_links
sha256: "96e677810b83707ff5e10fac11e4839daa0ea4e0123c35864c092699165eb3db"
url: "https://pub.dev"
source: hosted
version: "6.1.1"
archive: archive:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.5.1" version: "3.6.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -101,10 +93,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build_daemon name: build_daemon
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.1" version: "4.0.2"
build_resolvers: build_resolvers:
dependency: transitive dependency: transitive
description: description:
@@ -117,10 +109,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.9" version: "2.4.10"
build_runner_core: build_runner_core:
dependency: transitive dependency: transitive
description: description:
@@ -231,10 +223,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: countly_flutter name: countly_flutter
sha256: "829853407896350bdb4881ddc92326cf87b8d70ad92a78c4775cd05666d5a965" sha256: "366f0c7769b998a06579235b0afd3797bc0bc7c8ce80bb673fca59c217f66012"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "24.4.0" version: "24.4.1"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@@ -336,10 +328,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: extended_image name: extended_image
sha256: d7f091d068fcac7246c4b22a84b8dac59a62e04d29a5c172710c696e67a22f94 sha256: "9786aab821aac117763d6e4419cd49f5031fbaacfe3fd212c5b313d0334c37a9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.2.0" version: "8.2.1"
extended_image_library: extended_image_library:
dependency: transitive dependency: transitive
description: description:
@@ -392,9 +384,9 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
path: "." path: "."
ref: "8e1e3500fac2a39a43bed68aeb3cbdf792a1bd35" ref: "v1.0.13"
resolved-ref: "8e1e3500fac2a39a43bed68aeb3cbdf792a1bd35" resolved-ref: baded8103bd046806929b8d6547d5bce38060b06
url: "https://github.com/lollipopkit/fl_build.git" url: "https://github.com/lppcg/fl_build.git"
source: git source: git
version: "1.0.0" version: "1.0.0"
fl_chart: fl_chart:
@@ -409,9 +401,9 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: main ref: "v1.0.39"
resolved-ref: a2aa5359253ad83000ff2612ed2c5729cb0dcfc5 resolved-ref: "49fc10b39e390f4ecc3ee4f16f0926460b77adac"
url: "https://github.com/lollipopkit/fl_lib" url: "https://github.com/lppcg/fl_lib"
source: git source: git
version: "0.0.1" version: "0.0.1"
flutter: flutter:
@@ -500,10 +492,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_markdown_latex name: flutter_markdown_latex
sha256: b07ade84e661a7a2e7b7e59bcfa95e481d38fd8782a8f9f2349a8cb1b056ae89 sha256: "6902b6774800fd5d7b594fa2080781586f1b851f0964d41bb7fd28c61e3ef2a7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.2" version: "0.3.3"
flutter_math_fork: flutter_math_fork:
dependency: transitive dependency: transitive
description: description:
@@ -570,14 +562,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.3.1"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
highlight: highlight:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -670,10 +654,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.7" version: "4.2.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -766,10 +750,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: local_auth_darwin name: local_auth_darwin
sha256: "959145a4cf6f0de745b9ec9ac60101270eb4c5b8b7c2a0470907014adc1c618d" sha256: e424ebf90d5233452be146d4a7da4bcd7a70278b67791592f3fde1bda8eef9e2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
local_auth_platform_interface: local_auth_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1166,10 +1150,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shelf_web_socket name: shelf_web_socket
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "2.0.0"
shortid: shortid:
dependency: transitive dependency: transitive
description: description:
@@ -1307,10 +1291,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.1" version: "6.3.2"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@@ -1434,20 +1418,19 @@ packages:
watch_connectivity: watch_connectivity:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." name: watch_connectivity
ref: master sha256: a4257a314601c8448662d1a91421321a2e8a3207937fec4792116f1270ea8766
resolved-ref: ad5f151a5591b64d384ef22b7855b7a2a53ad3d3 url: "https://pub.dev"
url: "https://github.com/lollipopkit/watch_connectivity" source: hosted
source: git version: "0.2.0"
version: "0.1.5"
watch_connectivity_platform_interface: watch_connectivity_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: watch_connectivity_platform_interface name: watch_connectivity_platform_interface
sha256: "9074115391bd764c08a17346fcbc4d5c0b555672defbe6928ac648503b54aa9c" sha256: "82e8f165eac779d71bff7f6269a8be530798311cf7f3c33f1594d93468747d11"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.2" version: "0.2.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@@ -1464,14 +1447,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.1" version: "0.5.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712"
url: "https://pub.dev"
source: hosted
version: "0.1.4"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
name: web_socket_channel name: web_socket_channel
sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.5" version: "3.0.0"
webdav_client: webdav_client:
dependency: transitive dependency: transitive
description: description:
@@ -1485,10 +1476,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.5.0" version: "5.5.1"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
@@ -1525,8 +1516,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: a343bc2fdc11fbc7dbfc1f170692426a6fe01cb9 ref: master
resolved-ref: a343bc2fdc11fbc7dbfc1f170692426a6fe01cb9 resolved-ref: "13a280e77dd077b439af24ad3d054d318ae5df4a"
url: "https://github.com/lollipopkit/xterm.dart" url: "https://github.com/lollipopkit/xterm.dart"
source: git source: git
version: "4.0.0" version: "4.0.0"

View File

@@ -15,56 +15,53 @@ dependencies:
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
dio: ^5.2.1 dio: ^5.2.1
after_layout: ^1.1.0 after_layout: ^1.1.0
dartssh2:
git:
ref: dev
url: https://github.com/lollipopkit/dartssh2
circle_chart:
git:
url: https://github.com/lollipopkit/circle_chart
ref: main
easy_isolate: ^1.3.0 easy_isolate: ^1.3.0
intl: ^0.19.0 intl: ^0.19.0
xterm:
git:
ref: a343bc2fdc11fbc7dbfc1f170692426a6fe01cb9
url: https://github.com/lollipopkit/xterm.dart
plain_notification_token: ^0.0.4 plain_notification_token: ^0.0.4
highlight: ^0.7.0 highlight: ^0.7.0
flutter_highlight: ^0.7.0 flutter_highlight: ^0.7.0
code_text_field: ^1.1.0 code_text_field: ^1.1.0
shared_preferences: ^2.1.1 shared_preferences: ^2.1.1
dynamic_color: ^1.6.6 dynamic_color: ^1.6.6
watch_connectivity: watch_connectivity: ^0.2.0
git:
ref: master
url: https://github.com/lollipopkit/watch_connectivity
#flutter_secure_storage: ^9.0.0 #flutter_secure_storage: ^9.0.0
xml: ^6.4.2 # for parsing nvidia-smi xml: ^6.4.2 # for parsing nvidia-smi
flutter_displaymode: ^0.6.0 flutter_displaymode: ^0.6.0
computer:
git:
ref: master
url: https://github.com/lollipopkit/dart_computer
flutter_background_service: ^5.0.5 flutter_background_service: ^5.0.5
fl_chart: ^0.67.0 fl_chart: ^0.67.0
wakelock_plus: ^1.2.4 wakelock_plus: ^1.2.4
extended_image: ^8.2.0
wake_on_lan: ^4.1.1+3 wake_on_lan: ^4.1.1+3
flutter_adaptive_scaffold: ^0.1.10+2 flutter_adaptive_scaffold: ^0.1.10+2
device_info_plus: ^10.1.0
extended_image: ^8.2.1
dartssh2:
git:
url: https://github.com/lollipopkit/dartssh2
ref: dev
circle_chart:
git:
url: https://github.com/lollipopkit/circle_chart
ref: main
xterm:
git:
url: https://github.com/lollipopkit/xterm.dart
ref: master
computer:
git:
url: https://github.com/lollipopkit/dart_computer
ref: master
fl_lib: fl_lib:
git: git:
url: https://github.com/lollipopkit/fl_lib url: https://github.com/lppcg/fl_lib
ref: main ref: v1.0.39
device_info_plus: ^10.1.0
dependency_overrides: dependency_overrides:
# dartssh2: # dartssh2:
# path: ../dartssh2 # path: ../dartssh2
# fl_lib: # fl_lib:
# path: ../fl_lib # path: ../fl_lib
# xterm: # xterm:
# path: ../xterm.dart # path: ../xterm.dart
dev_dependencies: dev_dependencies:
flutter_native_splash: ^2.1.6 flutter_native_splash: ^2.1.6
@@ -76,8 +73,8 @@ dev_dependencies:
fl_build: fl_build:
# path: ../fl_build # path: ../fl_build
git: git:
url: https://github.com/lollipopkit/fl_build.git url: https://github.com/lppcg/fl_build.git
ref: 8e1e3500fac2a39a43bed68aeb3cbdf792a1bd35 ref: v1.0.13
flutter: flutter:
generate: true generate: true

32
test/container_test.dart Normal file
View File

@@ -0,0 +1,32 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:toolbox/data/model/container/ps.dart';
void main() {
test('docker ps parse', () {
const raw = '''
CONTAINER ID STATUS NAMES IMAGE
0e9e2ef860d2 Up 2 hours hbbs rustdesk/rustdesk-server:latest
9a4df3ed340c Up 41 minutes hbbr rustdesk/rustdesk-server:latest
fa1215b4be74 Up 12 hours firefly uusec/firefly:latest
''';
final lines = raw.split('\n');
const ids = ['0e9e2ef860d2', '9a4df3ed340c', 'fa1215b4be74'];
const names = ['hbbs', 'hbbr', 'firefly'];
const images = [
'rustdesk/rustdesk-server:latest',
'rustdesk/rustdesk-server:latest',
'uusec/firefly:latest'
];
const states = ['Up 2 hours', 'Up 41 minutes', 'Up 12 hours'];
for (var idx = 1; idx < lines.length; idx++) {
final raw = lines[idx];
if (raw.isEmpty) continue;
final ps = DockerPs.parse(raw);
expect(ps.id, ids[idx - 1]);
expect(ps.names, names[idx - 1]);
expect(ps.image, images[idx - 1]);
expect(ps.state, states[idx - 1]);
expect(ps.running, true);
}
});
}

View File

@@ -6,7 +6,6 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h> #include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <local_auth_windows/local_auth_plugin.h> #include <local_auth_windows/local_auth_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
@@ -16,8 +15,6 @@
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
DynamicColorPluginCApiRegisterWithRegistrar( DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
LocalAuthPluginRegisterWithRegistrar( LocalAuthPluginRegisterWithRegistrar(

View File

@@ -3,7 +3,6 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
app_links
dynamic_color dynamic_color
local_auth_windows local_auth_windows
permission_handler_windows permission_handler_windows