Compare commits

...

152 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
966a60a82d migrate: appleos 2024-08-17 22:59:32 +08:00
lollipopkit🏳️‍⚧️
76e98c6468 feat: custom shell script install path (#545) 2024-08-17 22:44:35 +08:00
lollipopkit🏳️‍⚧️
d7ae8b75b8 feat: custom net dev (#543) 2024-08-17 21:57:39 +08:00
lollipopkit🏳️‍⚧️
b5329e2692 fix: tags dialog 2024-08-16 21:02:05 +08:00
lollipopkit🏳️‍⚧️
ef297673f3 fix: privatekey update actually creates a new key (#541)
Fixes #540
2024-08-16 21:00:34 +08:00
lollipopkit🏳️‍⚧️
7558b4806d opt.: TagSwitcher related 2024-08-16 19:09:54 +08:00
Noo6
f7ef8a3915 opt: hidden at launch on linux and macos 2024-08-16 16:19:58 +08:00
lollipopkit🏳️‍⚧️
38366a2ef3 refactors (#539) 2024-08-16 01:24:43 +08:00
lollipopkit🏳️‍⚧️
7e5bb54c98 opt.: hide logo if distribution == null (#536) 2024-08-15 18:02:31 +08:00
lollipopkit🏳️‍⚧️
7ce3854392 opt.: use json_serializable 2024-08-15 16:44:13 +08:00
lollipopkit🏳️‍⚧️
195ddd2bcc refactor: SSHClientX.exec 2024-08-15 11:27:22 +08:00
lollipopkit🏳️‍⚧️
267b0b0a69 opt.: sftp home & back (#533) 2024-08-14 19:01:44 +08:00
lollipopkit🏳️‍⚧️
41e3fcb23a feat: systemd management (#532) 2024-08-14 14:29:03 +08:00
lollipopkit🏳️‍⚧️
46d5840276 feat: share server via qrcode (#530) 2024-08-13 20:04:01 +08:00
lollipopkit🏳️‍⚧️
fe566e97ca chore: README 2024-08-11 23:29:11 +08:00
lollipopkit🏳️‍⚧️
ddd1524d63 opt.: macos icon (#527) 2024-08-11 22:40:11 +08:00
lollipopkit🏳️‍⚧️
4d8268c614 fix: ssh tab focus mgmt (#525)
Fixes #522
2024-08-11 22:36:52 +08:00
lollipopkit🏳️‍⚧️
568b97606a opt.: split single list into multiples on desktop (#524) 2024-08-11 20:53:25 +08:00
lollipopkit🏳️‍⚧️
42cc2416a1 chore: README 2024-08-09 11:59:45 +08:00
lollipopkit🏳️‍⚧️
aaa69f0f95 chore: bump version 2024-08-04 19:26:31 +08:00
lollipopkit🏳️‍⚧️
64676bc5cb chore: l10n 2024-08-04 13:05:29 +08:00
lollipopkit🏳️‍⚧️
a15c04956c opt.: TipText 2024-08-04 12:02:57 +08:00
lollipopkit🏳️‍⚧️
e3c885483b opt.: use ssh term to decompress (#519) 2024-08-04 11:40:38 +08:00
lollipopkit🏳️‍⚧️
493c86cacb fix: home widget url (#517) 2024-08-04 00:17:21 +08:00
lollipopkit🏳️‍⚧️
ea7c8caf14 bug: color seed setting not working (#516) 2024-08-03 23:17:18 +08:00
lollipopkit🏳️‍⚧️
9db04a60c2 opt.: l10n & fix: write script (#514) 2024-08-03 22:44:21 +08:00
lollipopkit🏳️‍⚧️
610f46da0d opt.: bulk import servers (#512) 2024-08-03 14:52:39 +08:00
lollipopkit🏳️‍⚧️
b8e5418ff2 feat: import snippets from network (#510)
Fixes #507
2024-08-03 14:25:58 +08:00
lollipopkit🏳️‍⚧️
0e21755acb opt.: remove internal SharePreference keys while using KvEditor (#509)
Fixes #508
2024-08-03 12:37:16 +08:00
lollipopkit🏳️‍⚧️
73248011a1 migrate: fl_lib 2024-08-01 13:44:32 +08:00
lollipopkit🏳️‍⚧️
969643d3df opt.: debug page copy logs 2024-07-28 22:12:07 +08:00
lollipopkit🏳️‍⚧️
c90d0e4b3b fix: manual restore 2024-07-28 20:37:34 +08:00
lollipopkit🏳️‍⚧️
f9aadc6b0f opt.: Btn 2024-07-28 20:26:08 +08:00
lollipopkit🏳️‍⚧️
8fd4cc1fe1 opt.: TagsEditor & Btn 2024-07-28 19:05:31 +08:00
lollipopkit🏳️‍⚧️
432d76f024 fix: builtin editor (#503) 2024-07-28 15:06:34 +08:00
𝗦𝗵𝗟𝗲𝗿𝗣
ca8211e1a4 Add TR Locales (#497) 2024-07-27 16:42:09 +08:00
lollipopkit🏳️‍⚧️
a3b48fc01c chore: bump version 2024-07-26 23:35:59 +08:00
lollipopkit🏳️‍⚧️
8be94aa09c feat: use $EDITOR to edit files (#496)
Fixes #489
2024-07-26 23:32:57 +08:00
lollipopkit🏳️‍⚧️
5db1253ab8 fix: termux compatibility (#495) 2024-07-26 22:31:17 +08:00
lollipopkit🏳️‍⚧️
ceedd86310 rm: pkg (#494)
Fixes #470
2024-07-26 21:31:45 +08:00
lollipopkit🏳️‍⚧️
6a0254623f opt.: json input experience 2024-07-26 20:22:30 +08:00
lollipopkit🏳️‍⚧️
1c6ec56032 Create FUNDING.yml 2024-07-25 10:42:18 +08:00
lollipopkit🏳️‍⚧️
287869ed45 opt.: simplify settings page (#488) 2024-07-24 00:30:17 +08:00
lollipopkit🏳️‍⚧️
e4dbc3ba12 feat: set envs in term (#485) 2024-07-23 21:34:34 +08:00
lollipopkit🏳️‍⚧️
426e5689f8 fix: uploaded file's path on windows (#484) 2024-07-23 19:59:58 +08:00
lollipopkit🏳️‍⚧️
afda5fd4a4 fix: bio auth (#482) 2024-07-23 19:26:03 +08:00
lollipopkit🏳️‍⚧️
0a21b2820c fix: letterCacheTip translation 2024-07-23 12:14:31 +08:00
lollipopkit🏳️‍⚧️
87b3b76b0b feat: display cpu model (#477) 2024-07-23 12:03:10 +08:00
lollipopkit🏳️‍⚧️
41ec46f1d3 opt.: show loading dialog 2024-07-22 23:33:21 +08:00
lollipopkit🏳️‍⚧️
7a359588db opt.: input field suggestion 2024-07-22 22:03:56 +08:00
lollipopkit🏳️‍⚧️
255abe8b11 rollback: write script to /dev/shm (#474) 2024-07-21 17:58:14 +08:00
lollipopkit🏳️‍⚧️
b0936c5e6e fix: termux compatibility (#472) 2024-07-20 20:35:30 +08:00
lollipopkit🏳️‍⚧️
2907ac74d4 chore: bump version 2024-07-18 23:44:01 +08:00
lollipopkit🏳️‍⚧️
ea678f37b0 chore: bump version 2024-07-18 23:15:40 +08:00
lollipopkit🏳️‍⚧️
076082c945 feat: sftp perm setting & path copy (#467) 2024-07-18 21:40:40 +08:00
lollipopkit🏳️‍⚧️
5ee98f90e8 fix: update changelog (#466) 2024-07-18 20:53:22 +08:00
lollipopkit🏳️‍⚧️
c988dd88d7 fix: linux duplicated title bar (#462)
Fixes #459
2024-07-15 17:38:30 +08:00
lollipopkit🏳️‍⚧️
f7d6c461dc fix: version display (#458)
Fixes #457
2024-07-10 15:12:05 +08:00
lollipopkit🏳️‍⚧️
14771ae946 chore: README 2024-07-09 14:36:04 +08:00
lollipopkit🏳️‍⚧️
7e9086b20e fix: chmod perm 2024-07-09 14:02:23 +08:00
Noo6
4b3c4870ba fix: desktop window appbar (#450) 2024-07-05 12:42:38 +08:00
Noo6
43cebd0c04 fix: window blink on startup (#447) 2024-07-04 12:23:20 +08:00
Noo6
5ce13109b0 fix: ssh card tap area (#448) 2024-07-04 12:22:19 +08:00
lollipopkit🏳️‍⚧️
6e428c91d1 feat: disable letter cache (#446)
Fixes #445
2024-07-03 19:55:33 +08:00
lollipopkit🏳️‍⚧️
4430045550 feat: write script into /dev/shm (#444)
Fixes #443
2024-07-03 19:14:27 +08:00
lollipopkit🏳️‍⚧️
282cb06091 fix: picker dialog (#442)
Fixes #440
2024-07-03 18:19:09 +08:00
lollipopkit🏳️‍⚧️
772c2743b5 fix: no appBar in server detail page (#441)
Fixes #435
2024-07-03 16:42:52 +08:00
lollipopkit🏳️‍⚧️
90199b89a5 chore: README 2024-07-03 00:52:28 +08:00
Integral
e1d2e3f3e5 l10n: fix remaining translations (#439)
* l10n: Url -> URL

* l10n: Logo Address -> Logo URL

* l10n: fix ttl translation

* l10n: fix all zh-tw translations

* l10n: fix all zh-cn translations

* l10n: fix all en translations
2024-07-02 15:02:49 +00:00
Integral
3f9fe1f2c6 l10n: alterUrl -> fallbackSshDest (#437) 2024-07-02 13:15:19 +00:00
Integral
c79bbc5756 l10n: fix two acronyms (MAC, URL) (#436)
* l10n: Mac Address -> MAC Address

* l10n: PVE addr -> URL
2024-07-01 14:56:38 +00:00
Integral
63e1bec2b9 l10n: fix translation of net/network (#433) 2024-07-01 07:01:51 +00:00
Integral
d26b7c6f75 l10n: add noLineChartForCpu field (#431) 2024-07-01 05:24:31 +00:00
Integral
da8dc4fa54 docs(README): add f-droid & reorganize other installation methods (#429)
* docs(README): add f-droid & reorganize other installation methods

* docs(README): update Chinese README from English
2024-06-30 11:51:36 +00:00
Integral
413b81c47f l10n: fix Japanese translations (#425) 2024-06-25 16:00:42 +00:00
Integral
9ef419e3df l10n: fix English translations (#424) 2024-06-25 16:00:26 +00:00
Integral
39893912d9 Merge pull request #423 from Integral-Tech/fix-fdroid-typo
chore: fix typo of "F-Droid"
2024-06-25 14:04:16 +00:00
Integral
49456ca6c3 chore: fix typo of "F-Droid" 2024-06-25 22:00:46 +08:00
lollipopkit🏳️‍⚧️
6b9b8f0dbb chore: bump version 2024-06-25 13:15:29 +08:00
lollipopkit🏳️‍⚧️
5eb48b2717 fix: linux build (#422)
Fixes #421
2024-06-25 11:48:56 +08:00
lollipopkit🏳️‍⚧️
5339cfca70 chore: lib vers bump (#420)
Fixes #419
2024-06-25 11:16:34 +08:00
lollipopkit🏳️‍⚧️
1462b2d0b8 fix: always enter intro page (#418)
Fixes #417
2024-06-24 16:32:37 +08:00
lollipopkit🏳️‍⚧️
3798a23183 chore: bump version 2024-06-24 00:56:45 +08:00
lollipopkit🏳️‍⚧️
ddaf916170 feat: intro page (#416)
Fixes #415
2024-06-24 00:43:52 +08:00
lollipopkit🏳️‍⚧️
d6e37b058f fix: docker parse (#414)
Fixes #395
2024-06-23 18:37:05 +08:00
lollipopkit🏳️‍⚧️
2e9ad7d7cb chore: missing l10n noLineChart (#413)
Fixes #412
2024-06-23 15:39:30 +08:00
lollipopkit🏳️‍⚧️
190da74f66 chore: add participants (#410)
Fixes #409
2024-06-23 00:27:26 +08:00
lollipopkit🏳️‍⚧️
f1315dda7f fix: batch delete servers (#408)
Fixes #393
2024-06-23 00:24:30 +08:00
lollipopkit🏳️‍⚧️
43e6105eb3 fix: hideTitleBar doesn't work (#407)
Fixes #406
2024-06-22 22:58:22 +08:00
lollipopkit🏳️‍⚧️
d785209eb6 opt.: share on desktop (#405) 2024-06-22 22:50:17 +08:00
lollipopkit🏳️‍⚧️
da8b6a9010 feat: remember window size (#404)
Fixes #398
2024-06-22 21:52:48 +08:00
lollipopkit🏳️‍⚧️
1fd68722da fix: watchos settings (#403)
Fixes #401
2024-06-22 21:09:05 +08:00
lollipopkit🏳️‍⚧️
c9a2c1d0e4 Android - default to English (#402) 2024-06-18 14:39:18 +08:00
lollipopkit
161f536a62 fix: conform fdroid version fmt
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 23:51:10 +08:00
lollipopkit
1a32e9944e opt.: put version logic somewhere else (#381)
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 22:40:18 +08:00
lollipopkit
6deb753198 fix: version change logic (#381) 2024-06-13 21:29:03 +08:00
lollipopkit
4e33a98631 chore: fl_build 2024-06-13 21:22:44 +08:00
lollipopkit
dcb9464d8f fix
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 20:39:06 +08:00
lollipopkit
b94936b29f fix: reproducible (#381)
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 20:28:22 +08:00
lollipopkit
108d0a5a5b chore: bump version
Signed-off-by: lollipopkit <10864310+lollipopkit@users.noreply.github.com>
2024-06-13 00:45:24 +08:00
Integral
4814a2de28 Merge pull request #391 from Integral-Tech/add-abiCodes
Add abiCodes
2024-06-12 16:21:13 +00:00
Integral
5d8eeff502 Add abiCodes 2024-06-13 00:18:04 +08:00
lollipopkit
0bc4087266 chore: correct version for fdroid 2024-06-13 00:00:20 +08:00
lollipopkit
921209b901 chore: specify flutter version (#381) 2024-06-12 19:51:04 +08:00
lollipopkit
fa9d754470 rm: android wear settings 2024-06-12 19:42:38 +08:00
Integral
1f50a1f0f4 Merge pull request #389 from Integral-Tech/clean-up-proguard-rules
fix: clean up proguard-rules to remove depends on google libraries
2024-06-11 18:03:44 +00:00
Integral
80e84c0421 fix: clean up proguard-rules to remove depends on google libraries 2024-06-12 01:36:29 +08:00
lollipopkit
5059872c3f chore: README 2024-06-11 22:54:48 +08:00
lollipopkit
8add244776 fix: fdroid version fmt 2024-06-11 22:47:48 +08:00
lollipopkit
04e23fd7e4 fix: pubspec version 2024-06-11 22:18:27 +08:00
lollipopkit
336b11b808 chore: change dart pkg id 2024-06-11 22:06:29 +08:00
lollipopkit
8d9dba361c chore: completely rm countly 2024-06-11 22:03:17 +08:00
Integral
64ce3638cb chore: add distributionSha256Sum in gradle (#385) 2024-06-11 18:43:41 +08:00
lollipopkit
f3ceb73f0e fix: rm gms (#379 #381) 2024-06-11 16:40:05 +08:00
Integral
131dbe934c chore: setup fastlane for F-Droid submission (#384) 2024-06-11 16:18:39 +08:00
lollipopkit
58288e89bd Merge branch 'main' of github.com:lollipopkit/flutter_server_box 2024-06-10 21:35:59 +08:00
lollipopkit🏳️‍⚧️
22c43c7124 Lollipopkit/issue382 (#383) 2024-06-10 21:34:56 +08:00
lollipopkit
2bf0b25ee5 fix: podman term 2024-06-10 21:32:45 +08:00
lollipopkit
3bc03c1364 opt.: add tip (#380 #382) 2024-06-10 21:29:41 +08:00
lollipopkit
490d71f8c9 feat: snippet with term ctrl (#380 #382) 2024-06-10 21:08:33 +08:00
lollipopkit
edceb5900e opt.: write script to server tip 2024-06-09 13:57:02 +08:00
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
202 changed files with 6716 additions and 6492 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ['https://cdn.lpkt.cn/donate']

View File

@@ -9,44 +9,78 @@ permissions:
contents: write
jobs:
releaseAL:
name: Release android and linux
runs-on: ubuntu-latest
releaseAndroid:
name: Release android
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.24.0'
- 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
run: dart run fl_build -p android
- name: Rename for fdroid
run: |
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
releaseLinux:
name: Release linux
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Build
run: dart run fl_build -p android,linux
run: |
dart run fl_build -p linux
- name: Create Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: |
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_amd64.apk
${{ env.APP_NAME }}_amd64.AppImage
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# releaseWin:
# name: Release windows
# runs-on: windows-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# - name: Install Flutter
# uses: subosito/flutter-action@v2
# - name: Build
# run: dart run fl_build -p windows
# - name: Create Release
# uses: softprops/action-gh-release@v1
# with:
# files: |
# ${{ env.APP_NAME }}_amd64_windows.zip
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
releaseWin:
name: Release windows
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Build
run: dart run fl_build -p windows
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_windows_amd64.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# releaseApple:
# name: Release ios and macos
@@ -56,10 +90,13 @@ jobs:
# uses: actions/checkout@v4
# - name: Install Flutter
# uses: subosito/flutter-action@v2
# with:
# channel: 'stable'
# flutter-version: '3.22.2'
# - name: Build
# run: dart run fl_build -p ios,mac
# - name: Create Release
# uses: softprops/action-gh-release@v1
# uses: softprops/action-gh-release@v2
# with:
# files: |
# ${{ env.APP_NAME }}_universal_macos.zip

3
.gitignore vendored
View File

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

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4"
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
channel: "stable"
project_type: app
@@ -13,26 +13,26 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: android
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: ios
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: linux
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: macos
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: web
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: windows
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
# User provided section

4
.vscode/launch.json vendored
View File

@@ -8,6 +8,10 @@
"name": "debug",
"request": "launch",
"type": "dart",
"env": {
// Comment this line to use the default display
"DISPLAY": ":1"
}
// "args": [
// "-v"
// ]

View File

@@ -2,11 +2,11 @@ English | [简体中文](README_zh.md)
<h2 align="center">Flutter Server Box</h2>
<p align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink">
<img alt="countly" src="https://img.shields.io/badge/analysis-countly-pink">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink">
</p>
<div align="center">
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
<img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow">
</div>
<p align="center">
A Flutter project which provide charts to display <a href="../../issues/43">Linux</a> server status and tools to manage server.
@@ -14,50 +14,42 @@ A Flutter project which provide charts to display <a href="../../issues/43">Linu
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
</p>
## ⬇️ 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
[Linux](https://cdn.lolli.tech/serverbox/latest.AppImage) / [Windows](https://cdn.lolli.tech/serverbox/latest.win.zip): Basically tested, with debug certificate
## 🏙️ Screenshots
<table>
<tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
</tr>
</table>
## 📥 Install
Platform | From
--- | ---
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
Please only download pkgs from the source that **you trust**!
## 🔖 Feature
- Status chart, `SSH` Terminal, `SFTP`, `Docker & Pkg & Process`, Code editor...
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
- Platform specific: `Bio auth``Msg push``Home widget``watchOS App`...
- Localization
- English, 简体中文
- Español, Русский язык, Português, 日本語 (Generated by GPT)
- Deutsch (@its-tom) / 繁體中文 (@kalashnikov) / Indonesian (@azkadev) / Français (@FrancXPT) / Dutch (@QazCetelic)
## 🏙️ ScreenShots
<table>
<tr>
<td>
<img width="277px" src="imgs/server.png">
</td>
<td>
<img width="277px" src="imgs/detail.png">
</td>
<td>
<img width="277px" src="imgs/sftp.png">
</td>
</tr>
</table>
<table>
<tr>
<td>
<img width="277px" src="imgs/editor.png">
</td>
<td>
<img width="277px" src="imgs/ssh.png">
</td>
<td>
<img width="277px" src="imgs/docker.png">
</td>
</tr>
</table>
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft); Español, Русский язык, Português, 日本語 (Generated by GPT)
## 🆘 Help
<div align="center">
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div>
- In order to push server status to your portable device without opening ServerBox app (Such as **message push** and **home widget**), you need to install [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor) on your servers, and config it correctly. See [wiki](https://github.com/lollipopkit/server_box_monitor/wiki) for more details.
- **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki).
@@ -66,25 +58,24 @@ Before you open an issue, please read the following:
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.
After you read the above, you can:
- 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).
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
## 🧱 Contribution
- Any positive contribution is welcome.
- [l10n guide](https://blog.lolli.tech/faq/) can be found in my blog.
Any positive contribution is welcome.
### Development
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
2. Clone this repo, run `flutter run` to start the app.
3. Run `dart run fl_build -p PLATFORM` to build the app.
## 👏🏼 Contributors
<a href="https://github.com/lollipopkit/flutter_server_box/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lollipopkit/flutter_server_box" />
</a>
### Translation
- [Guide](https://blog.lpkt.cn/posts/faq/) can be found in my blog.
- We need your help! Just feel free to open a PR.
## 💡 My other apps
- [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.

View File

@@ -2,11 +2,11 @@
<h2 align="center">Flutter Server Box</h2>
<p align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink">
<img alt="countly" src="https://img.shields.io/badge/analysis-countly-pink">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink">
</p>
<div align="center">
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
<img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow">
</div>
<p align="center">
使用 Flutter 开发的 <a href="../../issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
@@ -15,79 +15,67 @@
</p>
## Download
[iOS](https://apps.apple.com/app/id1586449703) / [Android](https://cdn.lolli.tech/serverbox/latest.apk) / [macOS](https://apps.apple.com/app/id1586449703): 经过测试,使用自签名证书
[Linux](https://cdn.lolli.tech/serverbox/latest.AppImage) / [Windows](https://cdn.lolli.tech/serverbox/latest.win.zip): 经过不完全测试,使用调试证书
## 🏙 截屏
<table>
<tr>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/2.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/3.jpg"></td>
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/4.jpg"></td>
</tr>
</table>
## 📥 安装
平台 | 下载
--- | ---
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid)
请从 **信任** 的来源下载!
## 🔖 特点
- 状态图表, `SSH` 终端, `SFTP`, `Docker & & 进程` 管理器, 代码编辑器...
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
- 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`...
- 本地化
- English, 简体中文
- Español, Русский язык, Português, 日本語 (Generated by GPT)
- Deutsch (@its-tom) / 繁體中文 (@kalashnikov) / Indonesian (@azkadev) / Français (@FrancXPT) / Dutch (@QazCetelic)
## 🏙️ 截屏
<table>
<tr>
<td>
<img width="277px" src="imgs/server.png">
</td>
<td>
<img width="277px" src="imgs/detail.png">
</td>
<td>
<img width="277px" src="imgs/sftp.png">
</td>
</tr>
</table>
<table>
<tr>
<td>
<img width="277px" src="imgs/editor.png">
</td>
<td>
<img width="277px" src="imgs/ssh.png">
</td>
<td>
<img width="277px" src="imgs/docker.png">
</td>
</tr>
</table>
- Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft);
- 感谢贡献者们!
## 🆘 帮助
- 吹水、参与开发、了解如何使用QQ群 **762870488**
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),并且正确配置,详情可见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。
- **常见问题**可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
<div align="center">
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div>
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),详情见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
反馈前须知:
1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。
2. 反馈问题前请检查是否是 serverbox 的问题。
3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受
确认了解上述内容后:
- 如果你有**任何问题或者功能请求**,请在 [讨论](https://github.com/lollipopkit/flutter_server_box/discussions/new/choose) 中交流。
- 如果 ServerBox app 有**任何 bug**,请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
## 🧱 贡献
- 任何正面的贡献都欢迎。
- [本地化翻译指南](https://blog.lolli.tech/faq/) 可在我的博客中找到。
任何正面的贡献都欢迎。
### 开发
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
2. 克隆这个仓库, 运行 `flutter run` 启动应用
3. 运行 `dart run fl_build -p PLATFORM` 构建应用
## 👏🏼 贡献者
<a href="https://github.com/lollipopkit/flutter_server_box/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lollipopkit/flutter_server_box" />
</a>
### 翻译
[指南](https://blog.lolli.tech/faq/) 可在我的博客中找到。
## 💡 我的其它 Apps
- [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.

View File

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

View File

@@ -100,3 +100,14 @@ flutter {
}
dependencies {}
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
import com.android.build.OutputFile
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (abiVersionCode != null) {
output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
}
}

View File

@@ -1,7 +1 @@
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class com.jcraft.** { *; }

View File

@@ -27,9 +27,12 @@ class HomeWidget : AppWidgetProvider() {
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.home_widget)
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
var url = sp.getString("$appWidgetId", null)
val gUrl = sp.getString("*", null)
var url = sp.getString("widget_$appWidgetId", null)
if (url.isNullOrEmpty()) {
url = sp.getString("$appWidgetId", null)
}
if (url.isNullOrEmpty()) {
val gUrl = sp.getString("widget_*", null)
url = gUrl
}

View File

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

View File

@@ -0,0 +1,6 @@
A Flutter project which provide charts to display Linux server status and tools to manage server.
Especially thanks to dartssh2 & xterm.dart.
* Status chart (CPU, Sensors, GPU...), SSH Term, SFTP, Docker & Pkg & Process...
* Platform specific: Bio auth、Msg push、Home widget、watchOS App...
* English, 简体中文; Deutsch, 繁體中文, Indonesian, Français, Dutch; Español, Русский язык, Português, 日本語

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -0,0 +1 @@
A server status & toolbox app using Flutter

View File

@@ -0,0 +1 @@
ServerBox

View File

@@ -0,0 +1,7 @@
使用 Flutter 开发的 Linux 服务器工具箱,提供服务器状态图表和管理工具。
特别感谢 dartssh2 & xterm.dart。
特点:
* 状态图表CPU、传感器、GPU 等), SSH 终端, SFTP, Docker & 包 & 进程管理器...
* 特殊支持生物认证、推送、桌面小部件、watchOS App、跟随系统颜色...
* 本地化 (English, 简体中文, Español, Русский язык, Português, 日本語, Deutsch, 繁體中文, Indonesian, Français, Dutch

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -0,0 +1 @@
使用 Flutter 开发的服务器状态和工具箱应用

View File

@@ -0,0 +1 @@
ServerBox

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 112 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,16 @@
import 'package:dynamic_color/dynamic_color.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_gen/gen_l10n/l10n.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/rebuild.dart';
import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/view/page/home/home.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/home/home.dart';
import 'package:icons_plus/icons_plus.dart';
part 'intro.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@@ -15,11 +19,25 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
_setup(context);
return ListenableBuilder(
listenable: RebuildNodes.app,
builder: (_, __) {
listenable: RNodes.app,
builder: (context, _) {
if (!Stores.setting.useSystemPrimaryColor.fetch()) {
UIs.colorSeed = Color(Stores.setting.primaryColor.fetch());
return _buildApp(context);
final colorSeed = Color(Stores.setting.colorSeed.fetch());
UIs.colorSeed = colorSeed;
// Past code uses [UIs.primaryColor] as the primary color
UIs.primaryColor = colorSeed;
return _buildApp(
context,
light: ThemeData(
useMaterial3: true,
colorSchemeSeed: UIs.colorSeed,
),
dark: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: UIs.colorSeed,
),
);
}
return DynamicColorBuilder(
builder: (light, dark) {
@@ -32,10 +50,10 @@ class MyApp extends StatelessWidget {
brightness: Brightness.dark,
colorScheme: dark,
);
if (context.isDark && light != null) {
UIs.primaryColor = light.primary;
} else if (!context.isDark && dark != null) {
if (context.isDark && dark != null) {
UIs.primaryColor = dark.primary;
} else if (!context.isDark && light != null) {
UIs.primaryColor = light.primary;
}
return _buildApp(context, light: lightTheme, dark: darkTheme);
},
@@ -44,7 +62,8 @@ class MyApp extends StatelessWidget {
);
}
Widget _buildApp(BuildContext ctx, {ThemeData? light, ThemeData? dark}) {
Widget _buildApp(BuildContext ctx,
{required ThemeData light, required ThemeData dark}) {
final tMode = Stores.setting.themeMode.fetch();
// Issue #57
final themeMode = switch (tMode) {
@@ -54,16 +73,6 @@ class MyApp extends StatelessWidget {
};
final locale = Stores.setting.locale.fetch().toLocale;
light ??= ThemeData(
useMaterial3: true,
colorSchemeSeed: UIs.colorSeed,
);
dark ??= ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: UIs.colorSeed,
);
return MaterialApp(
locale: locale,
localizationsDelegates: const [
@@ -71,35 +80,30 @@ class MyApp extends StatelessWidget {
...AppLocalizations.localizationsDelegates,
],
supportedLocales: AppLocalizations.supportedLocales,
localeListResolutionCallback: LocaleUtil.resolve,
navigatorObservers: [AppRouteObserver.instance],
title: BuildData.name,
themeMode: themeMode,
theme: light,
darkTheme: tMode < 3 ? dark : _getAmoledTheme(dark),
home: _buildAppContent(ctx),
);
}
theme: light.fixWindowsFont,
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
home: Builder(
builder: (context) {
context.setLibL10n();
final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
Widget _buildAppContent(BuildContext ctx) {
//if (Pros.app.isWearOS) return const WearHome();
return const HomePage();
final intros = _IntroPage.builders;
if (intros.isNotEmpty) {
return _IntroPage(intros);
}
return const HomePage();
},
),
);
}
}
void _setup(BuildContext context) async {
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

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

View File

@@ -1,6 +1,6 @@
import 'package:flutter/services.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/res/store.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
abstract final class HomeWidgetMC {
static const _channel = MethodChannel('${Miscs.pkgName}/home_widget');

View File

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

View File

@@ -1,4 +1,5 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:server_box/view/widget/unix_perm.dart';
extension SftpFileX on SftpFileMode {
String get str {
@@ -8,6 +9,26 @@ extension SftpFileX on SftpFileMode {
return '$user$group$other';
}
UnixPerm toUnixPerm() {
return UnixPerm(
user: RWX(
r: userRead,
w: userWrite,
x: userExecute,
),
group: RWX(
r: groupRead,
w: groupWrite,
x: groupExecute,
),
other: RWX(
r: otherRead,
w: otherWrite,
x: otherExecute,
),
);
}
}
String _getRoleMode(bool r, bool w, bool x) {

View File

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

View File

@@ -1,40 +1,37 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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/res/build_data.dart';
import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/view/page/backup.dart';
import 'package:toolbox/view/page/container.dart';
import 'package:toolbox/view/page/home/home.dart';
import 'package:toolbox/view/page/iperf.dart';
import 'package:toolbox/view/page/ping.dart';
import 'package:toolbox/view/page/private_key/edit.dart';
import 'package:toolbox/view/page/private_key/list.dart';
import 'package:toolbox/view/page/pve.dart';
import 'package:toolbox/view/page/server/detail/view.dart';
import 'package:toolbox/view/page/setting/platform/android.dart';
import 'package:toolbox/view/page/setting/platform/ios.dart';
import 'package:toolbox/view/page/setting/seq/srv_func_seq.dart';
import 'package:toolbox/view/page/snippet/result.dart';
import 'package:toolbox/view/page/ssh/page.dart';
import 'package:toolbox/view/page/setting/seq/virt_key.dart';
import 'package:toolbox/view/page/storage/local.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/backup.dart';
import 'package:server_box/view/page/container.dart';
import 'package:server_box/view/page/home/home.dart';
import 'package:server_box/view/page/iperf.dart';
import 'package:server_box/view/page/ping.dart';
import 'package:server_box/view/page/private_key/edit.dart';
import 'package:server_box/view/page/private_key/list.dart';
import 'package:server_box/view/page/pve.dart';
import 'package:server_box/view/page/server/detail/view.dart';
import 'package:server_box/view/page/setting/platform/android.dart';
import 'package:server_box/view/page/setting/platform/ios.dart';
import 'package:server_box/view/page/setting/seq/srv_func_seq.dart';
import 'package:server_box/view/page/snippet/result.dart';
import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/setting/seq/virt_key.dart';
import 'package:server_box/view/page/storage/local.dart';
import '../data/model/server/snippet.dart';
import '../view/page/editor.dart';
import '../view/page/process.dart';
import '../view/page/server/edit.dart';
import '../view/page/server/tab.dart';
import '../view/page/setting/entry.dart';
import '../view/page/setting/seq/srv_detail_seq.dart';
import '../view/page/setting/seq/srv_seq.dart';
import '../view/page/snippet/edit.dart';
import '../view/page/snippet/list.dart';
import '../view/page/storage/sftp.dart';
import '../view/page/storage/sftp_mission.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/view/page/editor.dart';
import 'package:server_box/view/page/process.dart';
import 'package:server_box/view/page/server/edit.dart';
import 'package:server_box/view/page/server/tab.dart';
import 'package:server_box/view/page/setting/entry.dart';
import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
import 'package:server_box/view/page/setting/seq/srv_seq.dart';
import 'package:server_box/view/page/snippet/edit.dart';
import 'package:server_box/view/page/snippet/list.dart';
import 'package:server_box/view/page/storage/sftp.dart';
import 'package:server_box/view/page/storage/sftp_mission.dart';
class AppRoutes {
final Widget page;
@@ -61,7 +58,7 @@ class AppRoutes {
return Future.value(null);
}
static AppRoutes serverDetail({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes serverDetail({Key? key, required Spi spi}) {
return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail');
}
@@ -69,7 +66,7 @@ class AppRoutes {
return AppRoutes(ServerPage(key: key), 'server_tab');
}
static AppRoutes serverEdit({Key? key, ServerPrivateInfo? spi}) {
static AppRoutes serverEdit({Key? key, Spi? spi}) {
return AppRoutes(
ServerEditPage(spi: spi),
'server_${spi == null ? 'add' : 'edit'}',
@@ -100,14 +97,16 @@ class AppRoutes {
static AppRoutes ssh({
Key? key,
required ServerPrivateInfo spi,
required Spi spi,
String? initCmd,
Snippet? initSnippet,
}) {
return AppRoutes(
SSHPage(
key: key,
spi: spi,
initCmd: initCmd,
initSnippet: initSnippet,
),
'ssh_term',
);
@@ -133,10 +132,7 @@ class AppRoutes {
}
static AppRoutes sftp(
{Key? key,
required ServerPrivateInfo spi,
String? initPath,
bool isSelect = false}) {
{Key? key, required Spi spi, String? initPath, bool isSelect = false}) {
return AppRoutes(
SftpPage(
key: key,
@@ -151,21 +147,7 @@ class AppRoutes {
return AppRoutes(BackupPage(key: key), 'backup');
}
static AppRoutes debug({Key? key}) {
return AppRoutes(
DebugPage(
key: key,
args: DebugPageArgs(
notifier: Pros.debug.widgets,
onClear: Pros.debug.clear,
title: 'Logs(${BuildData.build})',
),
),
'debug',
);
}
static AppRoutes docker({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes docker({Key? key, required Spi spi}) {
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
}
@@ -201,7 +183,7 @@ class AppRoutes {
return AppRoutes(PingPage(key: key), 'ping');
}
static AppRoutes process({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes process({Key? key, required Spi spi}) {
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
}
@@ -235,7 +217,7 @@ class AppRoutes {
'snippet_result');
}
static AppRoutes iperf({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes iperf({Key? key, required Spi spi}) {
return AppRoutes(IPerfPage(key: key, spi: spi), 'iperf');
}
@@ -243,7 +225,7 @@ class AppRoutes {
return AppRoutes(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
}
static AppRoutes pve({Key? key, required ServerPrivateInfo spi}) {
static AppRoutes pve({Key? key, required Spi spi}) {
return AppRoutes(PvePage(key: key, spi: spi), 'pve');
}
}

View File

@@ -2,14 +2,9 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:plain_notification_token/plain_notification_token.dart';
Future<String?> getToken() async {
if (isIOS) {
final plainNotificationToken = PlainNotificationToken();
plainNotificationToken.requestPermission();
// If you want to wait until Permission dialog close,
// you need wait changing setting registered.
await plainNotificationToken.onIosSettingsRegistered.first;
return await plainNotificationToken.getToken();
}
return null;
if (!isIOS) return null;
final instance = ApnsToken()..requestPermission();
// Wait until Permission dialog closed
await instance.onIosSettingsRegistered.first;
return await instance.getToken();
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,23 +2,27 @@ import 'dart:convert';
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:logging/logging.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/rebuild.dart';
import 'package:toolbox/data/res/store.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart';
part 'backup.g.dart';
const backupFormatVersion = 1;
final _logger = Logger('Backup');
@JsonSerializable()
class Backup {
// backup format version
final int version;
final String date;
final List<ServerPrivateInfo> spis;
final List<Spi> spis;
final List<Snippet> snippets;
final List<PrivateKeyInfo> keys;
final Map<String, dynamic> container;
@@ -36,31 +40,9 @@ class Backup {
this.lastModTime,
});
Backup.fromJson(Map<String, dynamic> json)
: version = json['version'] as int,
date = json['date'],
spis = (json['spis'] as List)
.map((e) => ServerPrivateInfo.fromJson(e))
.toList(),
snippets =
(json['snippets'] as List).map((e) => Snippet.fromJson(e)).toList(),
keys = (json['keys'] as List)
.map((e) => PrivateKeyInfo.fromJson(e))
.toList(),
container = json['container'] ?? {},
lastModTime = json['lastModTime'],
history = json['history'] ?? {};
factory Backup.fromJson(Map<String, dynamic> json) => _$BackupFromJson(json);
Map<String, dynamic> toJson() => {
'version': version,
'date': date,
'spis': spis,
'snippets': snippets,
'keys': keys,
'container': container,
'lastModTime': lastModTime,
'history': history,
};
Map<String, dynamic> toJson() => _$BackupToJson(this);
Backup.loadFromStore()
: version = backupFormatVersion,
@@ -74,7 +56,7 @@ class Backup {
static Future<String> backup([String? name]) async {
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);
return path;
}
@@ -89,93 +71,119 @@ class Backup {
}
// Snippets
final nowSnippets = Stores.snippet.box.keys.toSet();
final bakSnippets = snippets.map((e) => e.name).toSet();
final newSnippets = bakSnippets.difference(nowSnippets);
final delSnippets = nowSnippets.difference(bakSnippets);
final updateSnippets = nowSnippets.intersection(bakSnippets);
for (final s in newSnippets) {
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
}
for (final s in delSnippets) {
Stores.snippet.box.delete(s);
}
for (final s in updateSnippets) {
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
if (force) {
for (final s in snippets) {
Stores.snippet.box.put(s.name, s);
}
} else {
final nowSnippets = Stores.snippet.box.keys.toSet();
final bakSnippets = snippets.map((e) => e.name).toSet();
final newSnippets = bakSnippets.difference(nowSnippets);
final delSnippets = nowSnippets.difference(bakSnippets);
final updateSnippets = nowSnippets.intersection(bakSnippets);
for (final s in newSnippets) {
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
}
for (final s in delSnippets) {
Stores.snippet.box.delete(s);
}
for (final s in updateSnippets) {
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
}
}
// ServerPrivateInfo
final nowSpis = Stores.server.box.keys.toSet();
final bakSpis = spis.map((e) => e.id).toSet();
final newSpis = bakSpis.difference(nowSpis);
final delSpis = nowSpis.difference(bakSpis);
final updateSpis = nowSpis.intersection(bakSpis);
for (final s in newSpis) {
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
}
for (final s in delSpis) {
Stores.server.box.delete(s);
}
for (final s in updateSpis) {
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
if (force) {
for (final s in spis) {
Stores.server.box.put(s.id, s);
}
} else {
final nowSpis = Stores.server.box.keys.toSet();
final bakSpis = spis.map((e) => e.id).toSet();
final newSpis = bakSpis.difference(nowSpis);
final delSpis = nowSpis.difference(bakSpis);
final updateSpis = nowSpis.intersection(bakSpis);
for (final s in newSpis) {
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
}
for (final s in delSpis) {
Stores.server.box.delete(s);
}
for (final s in updateSpis) {
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
}
}
// PrivateKeyInfo
final nowKeys = Stores.key.box.keys.toSet();
final bakKeys = keys.map((e) => e.id).toSet();
final newKeys = bakKeys.difference(nowKeys);
final delKeys = nowKeys.difference(bakKeys);
final updateKeys = nowKeys.intersection(bakKeys);
for (final s in newKeys) {
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
}
for (final s in delKeys) {
Stores.key.box.delete(s);
}
for (final s in updateKeys) {
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
if (force) {
for (final s in keys) {
Stores.key.box.put(s.id, s);
}
} else {
final nowKeys = Stores.key.box.keys.toSet();
final bakKeys = keys.map((e) => e.id).toSet();
final newKeys = bakKeys.difference(nowKeys);
final delKeys = nowKeys.difference(bakKeys);
final updateKeys = nowKeys.intersection(bakKeys);
for (final s in newKeys) {
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
}
for (final s in delKeys) {
Stores.key.box.delete(s);
}
for (final s in updateKeys) {
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
}
}
// History
final nowHistory = Stores.history.box.keys.toSet();
final bakHistory = history.keys.toSet();
final newHistory = bakHistory.difference(nowHistory);
final delHistory = nowHistory.difference(bakHistory);
final updateHistory = nowHistory.intersection(bakHistory);
for (final s in newHistory) {
Stores.history.box.put(s, history[s]);
}
for (final s in delHistory) {
Stores.history.box.delete(s);
}
for (final s in updateHistory) {
Stores.history.box.put(s, history[s]);
if (force) {
Stores.history.box.putAll(history);
} else {
final nowHistory = Stores.history.box.keys.toSet();
final bakHistory = history.keys.toSet();
final newHistory = bakHistory.difference(nowHistory);
final delHistory = nowHistory.difference(bakHistory);
final updateHistory = nowHistory.intersection(bakHistory);
for (final s in newHistory) {
Stores.history.box.put(s, history[s]);
}
for (final s in delHistory) {
Stores.history.box.delete(s);
}
for (final s in updateHistory) {
Stores.history.box.put(s, history[s]);
}
}
// Container
final nowContainer = Stores.container.box.keys.toSet();
final bakContainer = container.keys.toSet();
final newContainer = bakContainer.difference(nowContainer);
final delContainer = nowContainer.difference(bakContainer);
final updateContainer = nowContainer.intersection(bakContainer);
for (final s in newContainer) {
Stores.container.box.put(s, container[s]);
}
for (final s in delContainer) {
Stores.container.box.delete(s);
}
for (final s in updateContainer) {
Stores.container.box.put(s, container[s]);
if (force) {
Stores.container.box.putAll(container);
} else {
final nowContainer = Stores.container.box.keys.toSet();
final bakContainer = container.keys.toSet();
final newContainer = bakContainer.difference(nowContainer);
final delContainer = nowContainer.difference(bakContainer);
final updateContainer = nowContainer.intersection(bakContainer);
for (final s in newContainer) {
Stores.container.box.put(s, container[s]);
}
for (final s in delContainer) {
Stores.container.box.delete(s);
}
for (final s in updateContainer) {
Stores.container.box.put(s, container[s]);
}
}
Pros.reload();
RebuildNodes.app.rebuild();
Provider.reload();
RNodes.app.notify();
_logger.info('Restore success');
}
Backup.fromJsonString(String raw)
: this.fromJson(json.decode(_diyDecrypt(raw)));
factory Backup.fromJsonString(String raw) =>
Backup.fromJson(json.decode(_diyDecrypt(raw)));
}
String _diyEncrypt(String raw) => json.encode(

View File

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

View File

@@ -1,4 +1,4 @@
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:server_box/core/extension/context/locale.dart';
enum ErrFrom {
unknown,

View File

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

View File

@@ -1,5 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:server_box/core/extension/context/locale.dart';
enum ContainerMenu {
start,
@@ -21,46 +22,27 @@ enum ContainerMenu {
terminal,
//stats,
];
} else {
return [start, rm, logs];
}
return [start, rm, logs];
}
IconData get icon {
switch (this) {
case ContainerMenu.start:
return Icons.play_arrow;
case ContainerMenu.stop:
return Icons.stop;
case ContainerMenu.restart:
return Icons.restart_alt;
case ContainerMenu.rm:
return Icons.delete;
case ContainerMenu.logs:
return Icons.logo_dev;
case ContainerMenu.terminal:
return Icons.terminal;
// case DockerMenuType.stats:
// return Icons.bar_chart;
}
}
IconData get icon => switch (this) {
ContainerMenu.start => Icons.play_arrow,
ContainerMenu.stop => Icons.stop,
ContainerMenu.restart => Icons.restart_alt,
ContainerMenu.rm => Icons.delete,
ContainerMenu.logs => Icons.logo_dev,
ContainerMenu.terminal => Icons.terminal,
// DockerMenuType.stats => Icons.bar_chart,
};
String get toStr {
switch (this) {
case ContainerMenu.start:
return l10n.start;
case ContainerMenu.stop:
return l10n.stop;
case ContainerMenu.restart:
return l10n.restart;
case ContainerMenu.rm:
return l10n.delete;
case ContainerMenu.logs:
return l10n.log;
case ContainerMenu.terminal:
return l10n.terminal;
// case DockerMenuType.stats:
// return s.stats;
}
}
String get toStr => switch (this) {
ContainerMenu.start => l10n.start,
ContainerMenu.stop => l10n.stop,
ContainerMenu.restart => l10n.restart,
ContainerMenu.rm => libL10n.delete,
ContainerMenu.logs => libL10n.log,
ContainerMenu.terminal => l10n.terminal,
// DockerMenuType.stats => s.stats,
};
}

View File

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

View File

@@ -21,12 +21,12 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
return ServerFuncBtn.container;
case 3:
return ServerFuncBtn.process;
case 4:
return ServerFuncBtn.pkg;
case 5:
return ServerFuncBtn.snippet;
case 6:
return ServerFuncBtn.iperf;
case 8:
return ServerFuncBtn.systemd;
default:
return ServerFuncBtn.terminal;
}
@@ -47,15 +47,15 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
case ServerFuncBtn.process:
writer.writeByte(3);
break;
case ServerFuncBtn.pkg:
writer.writeByte(4);
break;
case ServerFuncBtn.snippet:
writer.writeByte(5);
break;
case ServerFuncBtn.iperf:
writer.writeByte(6);
break;
case ServerFuncBtn.systemd:
writer.writeByte(8);
break;
}
}

View File

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

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

@@ -1,8 +1,8 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/data/res/store.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
enum ServerDetailCards {
about(Icons.info),
@@ -31,7 +31,7 @@ enum ServerDetailCards {
static final names = values.map((e) => e.name).toList();
String get toStr => switch (this) {
about => l10n.about,
about => libL10n.about,
cpu => 'CPU',
mem => 'RAM',
swap => 'Swap',

View File

@@ -1,7 +1,8 @@
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/provider/server.dart';
import '../../res/build_data.dart';
import '../server/system.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/model/server/system.dart';
enum ShellFunc {
status,
@@ -12,52 +13,61 @@ enum ShellFunc {
suspend,
;
static const _homeVar = '\$HOME';
static const seperator = 'SrvBoxSep';
/// The suffix `\t` is for formatting
static const cmdDivider = '\necho $seperator\n\t';
static const _srvBoxDir = '.config/server_box';
static const scriptFile = 'mobile_v${BuildData.script}.sh';
/// Issue #159
/// srvboxm -> ServerBox Mobile
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
static const scriptDirHome = '~/.config/server_box';
static const scriptDirTmp = '/tmp/server_box';
static final _scriptDirMap = <String, String>{};
/// Get the script directory for the given [id].
///
/// Use script commit count as version of shell script.
///
/// So different version of app can run at the same time.
///
/// **Can't** use it in SFTP, because SFTP can't recognize `$HOME`
static String getShellPath(String home) => '$home/$_srvBoxDir/$scriptFile';
static const srvBoxDir = '$_homeVar/$_srvBoxDir';
static const _installShellPath = '$_homeVar/$_srvBoxDir/$scriptFile';
// Issue #299, chmod ~/.config to avoid permission issue
static const installShellCmd = """
chmod +x ~/.config &> /dev/null
mkdir -p $_homeVar/$_srvBoxDir
cat > $_installShellPath
chmod +x $_installShellPath
""";
String get flag {
switch (this) {
case ShellFunc.status:
return 's';
// case ShellFunc.docker:
// return 'd';
case ShellFunc.process:
return 'p';
case ShellFunc.shutdown:
return 'sd';
case ShellFunc.reboot:
return 'r';
case ShellFunc.suspend:
return 'sp';
}
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// it will be changed to [scriptDirHome]/[scriptFile].
static String getScriptDir(String id) {
final customScriptDir =
ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
return _scriptDirMap.putIfAbsent(id, () {
return scriptDirTmp;
});
}
String get exec => 'sh $_installShellPath -$flag';
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
_ => _scriptDirMap[id] = scriptDirHome,
};
static String getScriptPath(String id) {
return '${getScriptDir(id)}/$scriptFile';
}
static String getInstallShellCmd(String id) {
final scriptDir = getScriptDir(id);
final scriptPath = '$scriptDir/$scriptFile';
return '''
mkdir -p $scriptDir
cat > $scriptPath
chmod 744 $scriptPath
''';
}
String get flag => switch (this) {
ShellFunc.process => 'p',
ShellFunc.shutdown => 'sd',
ShellFunc.reboot => 'r',
ShellFunc.suspend => 'sp',
ShellFunc.status => 's',
// ShellFunc.docker=> 'd',
};
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
String get name {
switch (this) {
@@ -213,6 +223,7 @@ enum StatusCmdType {
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
nvidia._('nvidia-smi -q -x'),
sensors._('sensors'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"'),
;
final String cmd;
@@ -231,6 +242,7 @@ enum BSDStatusCmdType {
mem._('top -l 1 | grep PhysMem'),
//temp,
host._('hostname'),
cpuBrand._('sysctl -n machdep.cpu.brand_string'),
;
final String cmd;

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:toolbox/view/page/ping.dart';
import 'package:toolbox/view/page/server/tab.dart';
import 'package:toolbox/view/page/snippet/list.dart';
import 'package:toolbox/view/page/ssh/tab.dart';
import 'package:server_box/view/page/ping.dart';
import 'package:server_box/view/page/server/tab.dart';
import 'package:server_box/view/page/snippet/list.dart';
import 'package:server_box/view/page/ssh/tab.dart';
enum AppTab {
server,

View File

@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart';
import 'package:toolbox/data/model/container/type.dart';
import 'package:server_box/data/model/container/type.dart';
abstract final class ContainerImg {
final String? repository = null;
@@ -45,21 +45,21 @@ final class PodmanImg implements ContainerImg {
String toRawJson() => json.encode(toJson());
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
repository: json["repository"],
tag: json["tag"],
id: json["Id"],
created: json["Created"],
size: json["Size"],
containers: json["Containers"],
repository: json['repository'],
tag: json['tag'],
id: json['Id'],
created: json['Created'],
size: json['Size'],
containers: json['Containers'],
);
Map<String, dynamic> toJson() => {
"repository": repository,
"tag": tag,
"Id": id,
"Created": created,
"Size": size,
"Containers": containers,
'repository': repository,
'tag': tag,
'Id': id,
'Created': created,
'Size': size,
'Containers': containers,
};
}
@@ -72,7 +72,7 @@ final class DockerImg implements ContainerImg {
final String repository;
final String size;
@override
final String tag;
final String? tag;
DockerImg({
required this.containers,
@@ -95,21 +95,37 @@ final class DockerImg implements ContainerImg {
String toRawJson() => json.encode(toJson());
factory DockerImg.fromJson(Map<String, dynamic> json) => DockerImg(
containers: json["Containers"],
createdAt: json["CreatedAt"],
id: json["ID"],
repository: json["Repository"],
size: json["Size"],
tag: json["Tag"],
);
factory DockerImg.fromJson(Map<String, dynamic> json) {
final containers = switch (json['Containers']) {
final String a => a,
final Object? a => a.toString(),
};
final repo = switch (json['Repository'] ?? json['Names']) {
final String a => a,
final List a => a.firstOrNull.toString(),
final Object? a => a.toString(),
};
final size = switch (json['Size']) {
final String a => a,
final int a => a.bytes2Str,
final Object? a => a.toString(),
};
return DockerImg(
containers: containers,
createdAt: json['CreatedAt'],
id: json['ID'] ?? json['Id'] ?? '',
repository: repo,
size: size,
tag: json['Tag'],
);
}
Map<String, dynamic> toJson() => {
"Containers": containers,
"CreatedAt": createdAt,
"ID": id,
"Repository": repository,
"Size": size,
"Tag": tag,
'Containers': containers,
'CreatedAt': createdAt,
'ID': id,
'Repository': repository,
'Size': size,
'Tag': tag,
};
}

View File

@@ -1,10 +1,11 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/data/model/container/type.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/misc.dart';
abstract final class ContainerPs {
sealed class ContainerPs {
final String? id = null;
final String? image = null;
String? get name;
@@ -16,7 +17,7 @@ abstract final class ContainerPs {
String? net;
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);
}
@@ -83,35 +84,33 @@ final class PodmanPs implements ContainerPs {
String toRawJson() => json.encode(toJson());
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
command: json["Command"] == null
command: json['Command'] == null
? []
: List<String>.from(json["Command"]!.map((x) => x)),
: List<String>.from(json['Command']!.map((x) => x)),
created:
json["Created"] == null ? null : DateTime.parse(json["Created"]),
exited: json["Exited"],
id: json["Id"],
image: json["Image"],
names: json["Names"] == null
json['Created'] == null ? null : DateTime.parse(json['Created']),
exited: json['Exited'],
id: json['Id'],
image: json['Image'],
names: json['Names'] == null
? []
: List<String>.from(json["Names"]!.map((x) => x)),
startedAt: json["StartedAt"],
: List<String>.from(json['Names']!.map((x) => x)),
startedAt: json['StartedAt'],
);
Map<String, dynamic> toJson() => {
"Command":
'Command':
command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
"Created": created?.toIso8601String(),
"Exited": exited,
"Id": id,
"Image": image,
"Names": names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
"StartedAt": startedAt,
'Created': created?.toIso8601String(),
'Exited': exited,
'Id': id,
'Image': image,
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
'StartedAt': startedAt,
};
}
final class DockerPs implements ContainerPs {
final String? command;
final String? createdAt;
@override
final String? id;
@override
@@ -129,8 +128,6 @@ final class DockerPs implements ContainerPs {
String? disk;
DockerPs({
this.command,
this.createdAt,
this.id,
this.image,
this.names,
@@ -141,10 +138,13 @@ final class DockerPs implements ContainerPs {
String? get name => names;
@override
String? get cmd => command;
String? get cmd => null;
@override
bool get running => state == 'running';
bool get running {
if (state?.contains('Exited') == true) return false;
return true;
}
@override
void parseStats(String s) {
@@ -155,26 +155,15 @@ final class DockerPs implements ContainerPs {
disk = stats['BlockIO'];
}
factory DockerPs.fromRawJson(String str) =>
DockerPs.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory DockerPs.fromJson(Map<String, dynamic> json) => DockerPs(
command: json["Command"],
createdAt: json["CreatedAt"],
id: json["ID"],
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,
};
/// CONTAINER ID NAMES IMAGE STATUS
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
factory DockerPs.parse(String raw) {
final parts = raw.split(Miscs.multiBlankreg);
return DockerPs(
id: parts[0],
state: parts[1],
names: parts[2],
image: parts[3].trim(),
);
}
}

View File

@@ -1,5 +1,5 @@
import 'package:toolbox/data/model/container/image.dart';
import 'package:toolbox/data/model/container/ps.dart';
import 'package:server_box/data/model/container/image.dart';
import 'package:server_box/data/model/container/ps.dart';
enum ContainerType {
docker,
@@ -7,7 +7,7 @@ enum ContainerType {
;
ContainerPs Function(String str) get ps => switch (this) {
ContainerType.docker => DockerPs.fromRawJson,
ContainerType.docker => DockerPs.parse,
ContainerType.podman => PodmanPs.fromRawJson,
};

View File

@@ -1,4 +1,4 @@
import 'package:toolbox/data/model/server/dist.dart';
import 'package:server_box/data/model/server/dist.dart';
enum PkgManager {
apt,

View File

@@ -1,4 +1,4 @@
import 'package:toolbox/data/model/pkg/manager.dart';
import 'package:server_box/data/model/pkg/manager.dart';
class UpgradePkgInfo {
final PkgManager? _mgr;
@@ -34,9 +34,9 @@ class UpgradePkgInfo {
}
void _parseApt(String raw) {
final split1 = raw.split("/");
final split1 = raw.split('/');
package = split1[0];
final split2 = split1[1].split(" ");
final split2 = split1[1].split(' ');
newVersion = split2[1];
arch = split2[2];
nowVersion = split2[5].replaceFirst(']', '');
@@ -53,7 +53,7 @@ class UpgradePkgInfo {
}
void _parseZypper(String raw) {
final cols = raw.split("|");
final cols = raw.split('|');
package = cols[2].trim();
nowVersion = cols[3].trim();
newVersion = cols[4].trim();

View File

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

View File

@@ -1,14 +1,16 @@
import 'dart:collection';
import 'package:fl_chart/fl_chart.dart';
import 'package:toolbox/data/model/server/time_seq.dart';
import 'package:toolbox/data/res/status.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart';
import 'package:server_box/data/res/status.dart';
/// Capacity of the FIFO queue
const _kCap = 30;
class Cpus extends TimeSeq<List<SingleCpuCore>> {
Cpus(super.init1, super.init2);
final Map<String, int> brand = {};
@override
void onUpdate() {
_coresCount = now.length;
@@ -23,10 +25,16 @@ class Cpus extends TimeSeq<List<SingleCpuCore>> {
double usedPercent({int coreIdx = 0}) {
if (now.length != pre.length) return 0;
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
final totalDelta = now[coreIdx].total - pre[coreIdx].total;
final used = idleDelta / totalDelta;
return used.isNaN ? 0 : 100 - used * 100;
if (now.isEmpty) return 0;
try {
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
final totalDelta = now[coreIdx].total - pre[coreIdx].total;
final used = idleDelta / totalDelta;
return used.isNaN ? 0 : 100 - used * 100;
} catch (e, s) {
Loggers.app.warning('Cpus.usedPercent()', e, s);
return 0;
}
}
int _coresCount = 0;
@@ -175,6 +183,22 @@ class SingleCpuCore extends TimeSeqIface<SingleCpuCore> {
}
}
final class CpuBrand {
static Map<String, int> parse(String raw) {
final lines = raw.split('\n');
// {brand: count}
final brands = <String, int>{};
for (var line in lines) {
if (line.contains('model name')) {
final model = line.split(':').last.trim();
final count = brands[model] ?? 0;
brands[model] = count + 1;
}
}
return brands;
}
}
final _bsdCpuPercentReg = RegExp(r'(\d+\.\d+)%');
/// TODO: Change this implementation to parse cpu status on BSD system

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:server_box/core/extension/context/locale.dart';
enum PveResType {
lxc,

View File

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

View File

@@ -1,26 +1,21 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/data/model/app/error.dart';
import 'package:toolbox/data/model/app/shell_func.dart';
import 'package:toolbox/data/model/server/battery.dart';
import 'package:toolbox/data/model/server/conn.dart';
import 'package:toolbox/data/model/server/cpu.dart';
import 'package:toolbox/data/model/server/disk.dart';
import 'package:toolbox/data/model/server/memory.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
import 'package:toolbox/data/model/server/nvdia.dart';
import 'package:toolbox/data/model/server/sensors.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/system.dart';
import 'package:toolbox/data/model/server/temp.dart';
import '../app/tag_pickable.dart';
part 'server.ext.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/app/tag_pickable.dart';
import 'package:server_box/data/model/server/battery.dart';
import 'package:server_box/data/model/server/conn.dart';
import 'package:server_box/data/model/server/cpu.dart';
import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/net_speed.dart';
import 'package:server_box/data/model/server/nvdia.dart';
import 'package:server_box/data/model/server/sensors.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/temp.dart';
class Server implements TagPickable {
ServerPrivateInfo spi;
Spi spi;
ServerStatus status;
SSHClient? client;
ServerConn conn;
@@ -95,5 +90,5 @@ enum ServerConn {
/// Status parsing finished
finished;
operator <(ServerConn other) => index < other.index;
bool operator <(ServerConn other) => index < other.index;
}

View File

@@ -1,58 +0,0 @@
part of 'server.dart';
extension ServerX on Server {
String getTopRightStr(ServerPrivateInfo spi) {
switch (conn) {
case ServerConn.disconnected:
return l10n.disconnected;
case ServerConn.finished:
// Highest priority of temperature display
final cmdTemp = () {
final val = status.customCmds['server_card_top_right'];
if (val == null) return null;
// This returned value is used on server card top right, so it should
// be a single line string.
return val.split('\n').lastOrNull;
}();
final temperatureVal = () {
// Second priority
final preferTempDev = spi.custom?.preferTempDev;
if (preferTempDev != null) {
final preferTemp = status.sensors
.firstWhereOrNull((e) => e.device == preferTempDev)
?.summary
?.split(' ')
.firstOrNull;
if (preferTemp != null) {
return double.tryParse(preferTemp.replaceFirst('°C', ''));
}
}
// Last priority
final temp = status.temps.first;
if (temp != null) {
return temp;
}
return null;
}();
final upTime = status.more[StatusCmdType.uptime];
final items = [
cmdTemp ??
(temperatureVal != null
? '${temperatureVal.toStringAsFixed(1)}°C'
: null),
upTime
];
final str = items.where((e) => e != null && e.isNotEmpty).join(' | ');
if (str.isEmpty) return l10n.noResult;
return str;
case ServerConn.loading:
return l10n.serverTabLoading;
case ServerConn.connected:
return l10n.connected;
case ServerConn.connecting:
return l10n.serverTabConnecting;
case ServerConn.failed:
return status.err != null ? l10n.viewErr : l10n.serverTabFailed;
}
}
}

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:toolbox/data/model/server/battery.dart';
import 'package:toolbox/data/model/server/nvdia.dart';
import 'package:toolbox/data/model/server/sensors.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/system.dart';
import 'package:server_box/data/model/server/battery.dart';
import 'package:server_box/data/model/server/nvdia.dart';
import 'package:server_box/data/model/server/sensors.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/system.dart';
import '../app/shell_func.dart';
import 'cpu.dart';
import 'disk.dart';
import 'memory.dart';
import 'net_speed.dart';
import 'conn.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/cpu.dart';
import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/net_speed.dart';
import 'package:server_box/data/model/server/conn.dart';
class ServerStatusUpdateReq {
final ServerStatus ss;
@@ -71,6 +71,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
try {
final cpus = SingleCpuCore.parse(StatusCmdType.cpu.find(segments));
req.ss.cpu.update(cpus);
final brand = CpuBrand.parse(StatusCmdType.cpuBrand.find(segments));
req.ss.cpu.brand.clear();
req.ss.cpu.brand.addAll(brand);
} catch (e, s) {
Loggers.app.warning(e, s);
}

View File

@@ -1,10 +1,16 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'dart:async';
import '../app/tag_pickable.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:xterm/core.dart';
import 'package:server_box/data/model/app/tag_pickable.dart';
part 'snippet.g.dart';
@JsonSerializable()
@HiveType(typeId: 2)
class Snippet implements TagPickable {
@HiveField(0)
@@ -28,22 +34,10 @@ class Snippet implements TagPickable {
this.autoRunOn,
});
Snippet.fromJson(Map<String, dynamic> json)
: name = json['name'].toString(),
script = json['script'].toString(),
tags = json['tags']?.cast<String>(),
note = json['note']?.toString(),
autoRunOn = json['autoRunOn']?.cast<String>();
factory Snippet.fromJson(Map<String, dynamic> json) =>
_$SnippetFromJson(json);
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['name'] = name;
data['script'] = script;
data['tags'] = tags;
data['note'] = note;
data['autoRunOn'] = autoRunOn;
return data;
}
Map<String, dynamic> toJson() => _$SnippetToJson(this);
@override
bool containsTag(String tag) {
@@ -53,29 +47,140 @@ class Snippet implements TagPickable {
@override
String get tagName => name;
String fmtWith(ServerPrivateInfo spi) {
final fmted = script.replaceAllMapped(
RegExp(r'\${.+?}'),
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
String fmtWithSpi(Spi spi) {
return script.replaceAllMapped(
fmtFinder,
(match) {
final key = match.group(0);
final func = fmtArgs[key];
if (func == null) {
return key!;
}
return func(spi);
if (func != null) return func(spi);
// If not found, return the original content for further processing
return key ?? '';
},
);
return fmted;
}
Future<void> runInTerm(
Terminal terminal,
Spi spi, {
bool autoEnter = false,
}) async {
final argsFmted = fmtWithSpi(spi);
final matches = fmtFinder.allMatches(argsFmted);
/// There is no [TerminalKey] in the script
if (matches.isEmpty) {
terminal.textInput(argsFmted);
if (autoEnter) terminal.keyInput(TerminalKey.enter);
return;
}
// Records all start and end indexes of the matches
final (starts, ends) = matches.fold((<int>[], <int>[]), (pre, e) {
pre.$1.add(e.start);
pre.$2.add(e.end);
return pre;
});
// Check all indexes, `(idx + 1).start` must >= `idx.end`
for (var i = 0; i < starts.length - 1; i++) {
final lastEnd = ends[i];
final nextStart = starts[i + 1];
if (nextStart < lastEnd) {
throw 'Invalid format: $nextStart < $lastEnd';
}
}
// Start term input
if (starts.first > 0) {
terminal.textInput(argsFmted.substring(0, starts.first));
}
// Process matched
for (var idx = 0; idx < starts.length; idx++) {
final start = starts[idx];
final end = ends[idx];
final key = argsFmted.substring(start, end).toLowerCase();
// Special funcs
final special = _find(SnippetFuncs.specialCtrl, key);
if (special != null) {
final raw = key.substring(special.key.length + 1, key.length - 1);
await special.value((term: terminal, raw: raw));
}
// Term keys
final termKey = _find(fmtTermKeys, key);
if (termKey != null) await _doTermKeys(terminal, termKey, key);
}
// End term input
if (ends.last < argsFmted.length) {
terminal.textInput(argsFmted.substring(ends.last));
}
if (autoEnter) terminal.keyInput(TerminalKey.enter);
}
Future<void> _doTermKeys(
Terminal terminal,
MapEntry<String, TerminalKey> termKey,
String key,
) async {
if (termKey.value == TerminalKey.enter) {
terminal.keyInput(TerminalKey.enter);
return;
}
final ctrlAlt = switch (termKey.value) {
TerminalKey.control => (ctrl: true, alt: false),
TerminalKey.alt => (ctrl: false, alt: true),
_ => (ctrl: false, alt: false),
};
// `${ctrl+ad}` -> `ctrla + d`
final chars = key.substring(termKey.key.length + 1, key.length - 1);
if (chars.isEmpty) return;
final ok = terminal.charInput(
chars.codeUnitAt(0),
ctrl: ctrlAlt.ctrl,
alt: ctrlAlt.alt,
);
if (!ok) {
Loggers.app.warning('Failed to input: $key');
}
terminal.textInput(chars.substring(1));
}
MapEntry<String, T>? _find<T>(Map<String, T> map, String key) {
return map.entries.firstWhereOrNull((e) => key.startsWith(e.key));
}
static final fmtArgs = {
r'${host}': (ServerPrivateInfo spi) => spi.ip,
r'${port}': (ServerPrivateInfo spi) => spi.port.toString(),
r'${user}': (ServerPrivateInfo spi) => spi.user,
r'${pwd}': (ServerPrivateInfo spi) => spi.pwd ?? '',
r'${id}': (ServerPrivateInfo spi) => spi.id,
r'${name}': (ServerPrivateInfo spi) => spi.name,
r'${host}': (Spi spi) => spi.ip,
r'${port}': (Spi spi) => spi.port.toString(),
r'${user}': (Spi spi) => spi.user,
r'${pwd}': (Spi spi) => spi.pwd ?? '',
r'${id}': (Spi spi) => spi.id,
r'${name}': (Spi spi) => spi.name,
};
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
static final fmtTermKeys = {
r'${ctrl': TerminalKey.control,
r'${alt': TerminalKey.alt,
};
static const example = Snippet(
name: 'example',
script: 'echo hello',
tags: ['tag'],
note: 'note',
autoRunOn: ['server_id'],
);
}
class SnippetResult {
@@ -89,3 +194,32 @@ class SnippetResult {
required this.time,
});
}
typedef SnippetFuncCtx = ({Terminal term, String raw});
abstract final class SnippetFuncs {
static final specialCtrl = {
// `${sleep 3}` -> sleep 3 seconds
r'${sleep': SnippetFuncs.sleep,
r'${enter': SnippetFuncs.enter,
};
static const help = {
'sleep': 'Sleep for a few seconds',
'enter': 'Enter a few times',
};
static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
final seconds = int.tryParse(ctx.raw);
if (seconds == null) return;
final duration = Duration(seconds: seconds);
await Future.delayed(duration);
}
static FutureOr<void> enter(SnippetFuncCtx ctx) async {
final times = int.tryParse(ctx.raw) ?? 1;
for (var i = 0; i < times; i++) {
ctx.term.keyInput(TerminalKey.enter);
}
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import 'package:toolbox/data/model/app/shell_func.dart';
import 'package:server_box/data/model/app/shell_func.dart';
enum SystemType {
linux._(linuxSign),

View File

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

View File

@@ -1,4 +1,4 @@
import 'package:toolbox/data/res/store.dart';
import 'package:server_box/data/res/store.dart';
class TryLimiter {
final Map<String, int> _triedTimes = {};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,12 @@
import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:toolbox/data/res/store.dart';
import '../../../core/utils/server.dart';
import '../server/server_private_info.dart';
import 'worker.dart';
part of 'worker.dart';
class SftpReq {
final ServerPrivateInfo spi;
final Spi spi;
final String remotePath;
final String localPath;
final SftpReqType type;
String? privateKey;
ServerPrivateInfo? jumpSpi;
Spi? jumpSpi;
String? jumpPrivateKey;
SftpReq(
@@ -69,9 +62,8 @@ class SftpReqStatus {
int get hashCode => id ^ super.hashCode;
void dispose() {
// ignore: deprecated_member_use_from_same_package
worker.dispose();
completer?.complete();
worker._dispose();
completer?.complete(true);
}
void onNotify(dynamic event) {

View File

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

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:xterm/core.dart';
part 'virtual_key.g.dart';

View File

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

View File

@@ -4,13 +4,13 @@ import 'dart:convert';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/ssh_client.dart';
import 'package:toolbox/data/model/app/shell_func.dart';
import 'package:toolbox/data/model/container/image.dart';
import 'package:toolbox/data/model/container/ps.dart';
import 'package:toolbox/data/model/app/error.dart';
import 'package:toolbox/data/model/container/type.dart';
import 'package:toolbox/data/res/store.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/container/image.dart';
import 'package:server_box/data/model/container/ps.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/store.dart';
final _dockerNotFound =
RegExp(r"command not found|Unknown command|Command '\w+' not found");
@@ -26,7 +26,8 @@ class ContainerProvider extends ChangeNotifier {
ContainerErr? error;
String? runLog;
ContainerType type;
bool sudo = false;
var sudoCompleter = Completer<bool>();
bool isBusy = false;
ContainerProvider({
required this.client,
@@ -41,6 +42,7 @@ class ContainerProvider extends ChangeNotifier {
this.type = type;
Stores.container.setType(type, hostId);
error = runLog = items = images = version = null;
sudoCompleter = Completer<bool>();
notifyListeners();
await refresh();
}
@@ -60,17 +62,27 @@ class ContainerProvider extends ChangeNotifier {
// return value;
// }
Future<bool> _requiresSudo() async {
final psResult = await client?.run(_wrap(ContainerCmdType.ps.exec(type)));
if (psResult == null) return true;
if (psResult.string.toLowerCase().contains("permission denied")) {
return true;
void _requiresSudo() async {
/// Podman is rootless
if (type == ContainerType.podman) return sudoCompleter.complete(false);
if (!Stores.setting.containerTrySudo.fetch()) {
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 {
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.
/// Or this will ask for pwd again and again.
@@ -78,17 +90,22 @@ class ContainerProvider extends ChangeNotifier {
final includeStats = Stores.setting.containerParseStat.fetch();
var raw = '';
final cmd = _wrap(ContainerCmdType.execAll(
type,
sudo: sudo,
includeStats: includeStats,
));
final code = await client?.execWithPwd(
_wrap(ContainerCmdType.execAll(
type,
sudo: sudo,
includeStats: includeStats,
)),
cmd,
context: context,
onStdout: (data, _) => raw = '$raw$data',
id: hostId,
);
isBusy = false;
if (!context.mounted) return;
/// Code 127 means command not found
if (code == 127 || raw.contains(_dockerNotFound)) {
error = ContainerErr(type: ContainerErrType.notInstalled);
@@ -126,8 +143,12 @@ class ContainerProvider extends ChangeNotifier {
final psRaw = ContainerCmdType.ps.find(segments);
try {
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);
items = lines.map((e) => ContainerPs.fromRawJson(e, type)).toList();
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
} catch (e, trace) {
error = ContainerErr(
type: ContainerErrType.parsePs,
@@ -139,11 +160,18 @@ class ContainerProvider extends ChangeNotifier {
}
// Parse images
final imageRaw = ContainerCmdType.images.find(segments);
final imageRaw = ContainerCmdType.images.find(segments).trim();
final isEntireJson = imageRaw.startsWith('[') && imageRaw.endsWith(']');
try {
final imgLines = imageRaw.split('\n');
imgLines.removeWhere((element) => element.isEmpty);
images = imgLines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
if (isEntireJson) {
images = (json.decode(imageRaw) as List)
.map((e) => ContainerImg.fromRawJson(json.encode(e), type))
.toList();
} else {
final lines = imageRaw.split('\n');
lines.removeWhere((element) => element.isEmpty);
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
}
} catch (e, trace) {
error = ContainerErr(
type: ContainerErrType.parseImages,
@@ -203,7 +231,7 @@ class ContainerProvider extends ChangeNotifier {
runLog = '';
final errs = <String>[];
final code = await client?.execWithPwd(
_wrap(sudo ? 'sudo -S $cmd' : cmd),
_wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd),
context: context,
onStdout: (data, _) {
runLog = '$runLog$data';
@@ -254,7 +282,16 @@ enum ContainerCmdType {
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
return switch (this) {
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 =>
includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
@@ -268,6 +305,6 @@ enum ContainerCmdType {
}) {
return ContainerCmdType.values
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
.join(' && echo ${ShellFunc.seperator} && ');
.join('\necho ${ShellFunc.seperator}\n');
}
}

View File

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

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