Compare commits

...

137 Commits

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

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

* opt: `local.dart` fmt

---------

Co-authored-by: Noo6 <72285529+No06@users.noreply.github.com>
2024-11-14 14:42:15 +08:00
Noo6
d706886343 fix: sftp open file on windows (#633) 2024-11-14 14:24:57 +08:00
Noo6
7dda63af8a fix: add file on windows 2024-11-14 11:20:45 +08:00
lollipopkit🏳️‍⚧️
00d303ac36 fix: editing pref store (#618) 2024-10-29 19:37:19 +08:00
lollipopkit🏳️‍⚧️
229983d82e chore: bump version 2024-10-17 14:06:45 +08:00
Mased
4928ca600d [Translation] some Russian fixes (#604) 2024-10-07 21:04:36 +08:00
lollipopkit🏳️‍⚧️
89ec2d94d6 opt.: virt keys loading 2024-10-05 10:18:00 +08:00
lollipopkit🏳️‍⚧️
393c3e6388 Merge branch 'main' of github.com:lollipopkit/flutter_server_box 2024-10-05 10:00:41 +08:00
Gitro
dee458e926 new: material you icon (#599) 2024-10-04 12:19:10 +08:00
lollipopkit🏳️‍⚧️
f89228db40 opt.: ssh page 2024-09-28 17:09:35 +08:00
lollipopkit🏳️‍⚧️
3b6fb6194b opt.: more virtual keys (#596) 2024-09-28 14:40:22 +08:00
lollipopkit🏳️‍⚧️
02444fc2f0 new: ios18 dark app icon (#594)
Fixes #593
2024-09-25 19:16:14 +08:00
lollipopkit🏳️‍⚧️
aef317a140 opt.: dismiss notification if no ssh conn (#592) 2024-09-24 22:01:35 +08:00
lollipopkit🏳️‍⚧️
47aedb2f2e fix: rename file 2024-09-22 16:06:09 +08:00
lollipopkit🏳️‍⚧️
eab06abcaf opt. 2024-09-21 23:12:15 +08:00
lollipopkit🏳️‍⚧️
c062c12a0e opt.: redesigned settings page (#587) 2024-09-21 22:37:42 +08:00
lollipopkit🏳️‍⚧️
d7669c94b8 opt.: spi with same id (#585) 2024-09-21 11:54:56 +08:00
lollipopkit🏳️‍⚧️
50af289574 migrate: fl_lib 2024-09-21 11:01:41 +08:00
lollipopkit🏳️‍⚧️
90b88ed795 opt.: sync immediately after changes (#577) 2024-09-14 17:08:51 +08:00
CakesTwix
d611fdcd50 l10n: Added Ukrainian lang (#574) 2024-09-14 14:48:23 +08:00
lollipopkit🏳️‍⚧️
db9b2dd818 fix: snippet fmt (#570) 2024-09-01 21:07:32 +08:00
lollipopkit🏳️‍⚧️
edb49ead67 opt.: split webdav & other settings (#569) 2024-08-31 21:45:09 +08:00
lollipopkit🏳️‍⚧️
7f0dc656b8 Merge pull request #568 from lollipopkit/lollipopkit/issue564 2024-08-31 19:36:50 +08:00
lollipopkit🏳️‍⚧️
b33d0bbc3e opt.: back btn on scan page
Fixes #564
2024-08-31 19:33:57 +08:00
lollipopkit🏳️‍⚧️
7d0ea8a58b Merge branch 'android_service' 2024-08-31 19:28:04 +08:00
Noo6
c18732d8f3 fix: backup on windows (#563) 2024-08-30 22:44:08 +08:00
Shin
157af0a354 l10n: fixed Japanese translations. (#558) 2024-08-30 11:35:13 +08:00
lollipopkit🏳️‍⚧️
2d9dc044f9 new: custom foreground service on Android (#556) 2024-08-28 16:42:09 +08:00
lollipopkit🏳️‍⚧️
479250c207 init 2024-08-28 13:44:54 +08:00
Noo6
aef7ec911f opt: ssh tab page 2024-08-28 13:18:21 +08:00
lollipopkit🏳️‍⚧️
4f9ee7781f fix: ssh tab page UI (#555) 2024-08-27 17:20:47 +08:00
lollipopkit🏳️‍⚧️
eb83d05c81 fix: ssh alter url (#554) 2024-08-27 15:22:26 +08:00
lollipopkit🏳️‍⚧️
329fd33b69 fix: lang switch (#553) 2024-08-27 14:36:00 +08:00
lollipopkit🏳️‍⚧️
931c5f0bf6 opt.: alterUrl (#550) 2024-08-25 22:52:47 +08:00
lollipopkit🏳️‍⚧️
bcbf1fbc17 fix: fdroid build (#548) 2024-08-25 22:06:55 +08:00
Noo6
3e7315dac6 fix: ssh page displays the CustomAppBar on desktop 2024-08-18 20:04:39 +08:00
Noo6
4cecfdf7a8 opt: ssh card text overflow 2024-08-18 13:57:20 +08:00
Noo6
0346821cf5 opt: windows and linux drag area 2024-08-18 13:27:16 +08:00
lollipopkit🏳️‍⚧️
966a60a82d migrate: appleos 2024-08-17 22:59:32 +08:00
lollipopkit🏳️‍⚧️
76e98c6468 feat: custom shell script install path (#545) 2024-08-17 22:44:35 +08:00
lollipopkit🏳️‍⚧️
d7ae8b75b8 feat: custom net dev (#543) 2024-08-17 21:57:39 +08:00
lollipopkit🏳️‍⚧️
b5329e2692 fix: tags dialog 2024-08-16 21:02:05 +08:00
lollipopkit🏳️‍⚧️
ef297673f3 fix: privatekey update actually creates a new key (#541)
Fixes #540
2024-08-16 21:00:34 +08:00
lollipopkit🏳️‍⚧️
7558b4806d opt.: TagSwitcher related 2024-08-16 19:09:54 +08:00
Noo6
f7ef8a3915 opt: hidden at launch on linux and macos 2024-08-16 16:19:58 +08:00
lollipopkit🏳️‍⚧️
38366a2ef3 refactors (#539) 2024-08-16 01:24:43 +08:00
lollipopkit🏳️‍⚧️
7e5bb54c98 opt.: hide logo if distribution == null (#536) 2024-08-15 18:02:31 +08:00
lollipopkit🏳️‍⚧️
7ce3854392 opt.: use json_serializable 2024-08-15 16:44:13 +08:00
lollipopkit🏳️‍⚧️
195ddd2bcc refactor: SSHClientX.exec 2024-08-15 11:27:22 +08:00
lollipopkit🏳️‍⚧️
267b0b0a69 opt.: sftp home & back (#533) 2024-08-14 19:01:44 +08:00
lollipopkit🏳️‍⚧️
41e3fcb23a feat: systemd management (#532) 2024-08-14 14:29:03 +08:00
lollipopkit🏳️‍⚧️
46d5840276 feat: share server via qrcode (#530) 2024-08-13 20:04:01 +08:00
lollipopkit🏳️‍⚧️
fe566e97ca chore: README 2024-08-11 23:29:11 +08:00
lollipopkit🏳️‍⚧️
ddd1524d63 opt.: macos icon (#527) 2024-08-11 22:40:11 +08:00
lollipopkit🏳️‍⚧️
4d8268c614 fix: ssh tab focus mgmt (#525)
Fixes #522
2024-08-11 22:36:52 +08:00
lollipopkit🏳️‍⚧️
568b97606a opt.: split single list into multiples on desktop (#524) 2024-08-11 20:53:25 +08:00
lollipopkit🏳️‍⚧️
42cc2416a1 chore: README 2024-08-09 11:59:45 +08:00
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
226 changed files with 19181 additions and 7829 deletions

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

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

View File

@@ -9,19 +9,17 @@ permissions:
contents: write contents: write
jobs: jobs:
releaseAL: releaseAndroid:
name: Release android and linux name: Release android
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: '0'
- name: Install Flutter - name: Install Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.22.2' flutter-version: '3.27.3'
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: 'zulu'
@@ -31,7 +29,7 @@ jobs:
curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key curl -u ${{ secrets.BASIC_AUTH }} -o android/app/app.key ${{ secrets.URL_PREFIX }}app.key
curl -u ${{ secrets.BASIC_AUTH }} -o android/key.properties ${{ secrets.URL_PREFIX }}key.properties curl -u ${{ secrets.BASIC_AUTH }} -o android/key.properties ${{ secrets.URL_PREFIX }}key.properties
- name: Build - name: Build
run: dart run fl_build -p android,linux run: dart run fl_build -p android
- name: Rename for fdroid - name: Rename for fdroid
run: | run: |
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
@@ -44,7 +42,42 @@ jobs:
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage 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: Install dependencies
run: |
sudo apt update
# Basic
sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev libvulkan-dev desktop-file-utils
# App Specific
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev
# Packaging
sudo apt install -y rpm patchelf
- name: Build
run: |
dart run fl_build
dart run flutter_distributor:main release --name linux --skip-clean
- name: Rename artifacts
run: |
deb_name=$(ls dist/*/*.deb)
mv $deb_name ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.deb
rpm_name=$(ls dist/*/*.rpm)
mv $rpm_name ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.rpm
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.deb
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.rpm
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -54,8 +87,6 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: '0'
- name: Install Flutter - name: Install Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
- name: Build - name: Build

1
.gitignore vendored
View File

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

View File

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

4
.vscode/launch.json vendored
View File

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

View File

@@ -2,51 +2,54 @@ English | [简体中文](README_zh.md)
<h2 align="center">Flutter Server Box</h2> <h2 align="center">Flutter Server Box</h2>
<p align="center"> <div align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink"> <a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink"> <img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
</p> <img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow">
</div>
<p align="center"> <p align="center">
A Flutter project which provide charts to display <a href="../../issues/43">Linux</a> server status and tools to manage server. A Flutter project which provide charts to display <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> server status and tools to manage server.
<br> <br>
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>. Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
</p> </p>
## Download ## 🏙 Screenshots
🎉 **The `Android / Linux / Windows` version are now built via GitHub Actions** <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](https://apps.apple.com/app/id1586449703) / [Android & Linux & Windows](https://github.com/lollipopkit/flutter_server_box/releases)
- All deprecated versions before `v930` can be found in [here](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid). ## 📥 Install
- To prevent injection attacks and etc., please don't download from untrusted sources. eg: Gitee release is not related to this project.
| Platform | From |
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) |
| Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) |
| Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) |
Please only download pkgs from the source that **you trust**!
## 🔖 Feature ## 🔖 Feature
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Pkg & Process`... - `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
- Platform specific: `Bio auth``Msg push``Home widget``watchOS App`... - Platform specific: `Bio auth``Msg push``Home widget``watchOS App`...
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic); Español, Русский язык, Português, 日本語 (Generated by GPT) - English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)
## 🏙️ ScreenShots
<table>
<tr>
<td><img width="277px" src="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>
## 🆘 Help ## 🆘 Help
<div align="center">
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div>
- In order to push server status to your portable device without opening ServerBox app (Such as **message push** and **home widget**), you need to install [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor) on your servers, and config it correctly. See [wiki](https://github.com/lollipopkit/server_box_monitor/wiki) for more details. - In order to push server status to your portable device without opening ServerBox app (Such as **message push** and **home widget**), you need to install [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor) on your servers, and config it correctly. See [wiki](https://github.com/lollipopkit/server_box_monitor/wiki) for more details.
- **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki). - **Common issues** can be found in [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki).
@@ -59,8 +62,16 @@ After you read the above, you can open an [issue](https://github.com/lollipopkit
## 🧱 Contribution ## 🧱 Contribution
- Any positive contribution is welcome. Any positive contribution is welcome.
- [l10n guide](https://blog.lolli.tech/faq/) can be found in my blog.
### 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.
### 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 ## 💡 My other apps

View File

@@ -2,71 +2,77 @@
<h2 align="center">Flutter Server Box</h2> <h2 align="center">Flutter Server Box</h2>
<p align="center"> <div align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink"> <a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink"> <img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
</p> <img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow">
</div>
<p align="center"> <p align="center">
使用 Flutter 开发的 <a href="../../issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。 使用 Flutter 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
<br> <br>
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a> 特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>
</p> </p>
## Download ## 🏙 截屏
🎉 **现在 `Android / Linux / Windows` 版本使用 GitHub Actions 构建** <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](https://apps.apple.com/app/id1586449703) / [Android & Linux & Windows](https://github.com/lollipopkit/flutter_server_box/releases)
- 所有 `v930` 之前的版本可以在 [这里](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid) 找到。 ## 📥 安装
- 为了防止注入攻击等请不要从不受信任的来源下载。例如Gitee 的发行包与该项目无关。
平台 | 下载
----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)
请从 **信任** 的来源下载!
## 🔖 特点 ## 🔖 特点
- `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & & 进程` 管理... - `状态图表`CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
- 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`... - 特殊支持:`生物认证``推送``桌面小部件``watchOS App``跟随系统颜色`...
- 本地化 - 本地化
- English, 简体中文 - English, 简体中文
- Español, Русский язык, Português, 日本語 (Generated by GPT) - Español, Русский язык, Português, 日本語 (Generated by GPT)
- Deutsch (@its-tom) / 繁體中文 (@kalashnikov) / Indonesian (@azkadev) / Français (@FrancXPT) / Dutch (@QazCetelic) - Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix);
- 感谢贡献者们!
## 🏙️ 截屏
<table>
<tr>
<td><img width="277px" src="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>
## 🆘 帮助 ## 🆘 帮助
- 吹水、参与开发、了解如何使用QQ群 **762870488** <div align="center">
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),并且正确配置,详情可见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。 <a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
- **常见问题**可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。 <a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
</div>
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),详情见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
反馈前须知: 反馈前须知:
1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。 1. 反馈问题请附带 log点击首页右上角并以 bug 模版提交。
2. 反馈问题前请检查是否是 serverbox 的问题。 2. 反馈问题前请检查是否是 serverbox 的问题。
3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受 3. 欢迎所有有效、正面的反馈主观比如你觉得其他UI更好看的反馈不一定会接受
确认了解上述内容后,请在 [问题](https://github.com/lollipopkit/flutter_server_box/issues/new) 中反馈。
## 🧱 贡献 ## 🧱 贡献
- 任何正面的贡献都欢迎。 任何正面的贡献都欢迎。
- [本地化翻译指南](https://blog.lolli.tech/faq/) 可在我的博客中找到。
### 开发
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
2. 克隆这个仓库, 运行 `flutter run` 启动应用
3. 运行 `dart run fl_build -p PLATFORM` 构建应用
### 翻译
[指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。
## 💡 我的其它 Apps ## 💡 我的其它 Apps
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。 - [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

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

View File

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

View File

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

View File

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

13
distribute_options.yaml Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:fl_lib/l10n/gen_l10n/lib_l10n.dart'; import 'package:fl_lib/generated/l10n/lib_l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/build_data.dart'; import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/rebuild.dart'; import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/generated/l10n/l10n.dart';
import 'package:server_box/view/page/home/home.dart'; import 'package:server_box/view/page/home/home.dart';
import 'package:icons_plus/icons_plus.dart'; import 'package:icons_plus/icons_plus.dart';
@@ -22,8 +23,22 @@ class MyApp extends StatelessWidget {
listenable: RNodes.app, listenable: RNodes.app,
builder: (context, _) { builder: (context, _) {
if (!Stores.setting.useSystemPrimaryColor.fetch()) { if (!Stores.setting.useSystemPrimaryColor.fetch()) {
UIs.colorSeed = Color(Stores.setting.primaryColor.fetch()); final colorSeed = Color(Stores.setting.colorSeed.fetch());
return _buildApp(context); 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( return DynamicColorBuilder(
builder: (light, dark) { builder: (light, dark) {
@@ -36,10 +51,10 @@ class MyApp extends StatelessWidget {
brightness: Brightness.dark, brightness: Brightness.dark,
colorScheme: dark, colorScheme: dark,
); );
if (context.isDark && light != null) { if (context.isDark && dark != null) {
UIs.primaryColor = light.primary;
} else if (!context.isDark && dark != null) {
UIs.primaryColor = dark.primary; UIs.primaryColor = dark.primary;
} else if (!context.isDark && light != null) {
UIs.primaryColor = light.primary;
} }
return _buildApp(context, light: lightTheme, dark: darkTheme); return _buildApp(context, light: lightTheme, dark: darkTheme);
}, },
@@ -48,7 +63,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(); final tMode = Stores.setting.themeMode.fetch();
// Issue #57 // Issue #57
final themeMode = switch (tMode) { final themeMode = switch (tMode) {
@@ -58,17 +74,8 @@ class MyApp extends StatelessWidget {
}; };
final locale = Stores.setting.locale.fetch().toLocale; 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( return MaterialApp(
key: ValueKey(locale),
locale: locale, locale: locale,
localizationsDelegates: const [ localizationsDelegates: const [
LibLocalizations.delegate, LibLocalizations.delegate,
@@ -76,41 +83,29 @@ class MyApp extends StatelessWidget {
], ],
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
localeListResolutionCallback: LocaleUtil.resolve, localeListResolutionCallback: LocaleUtil.resolve,
navigatorObservers: [AppRouteObserver.instance],
title: BuildData.name, title: BuildData.name,
themeMode: themeMode, themeMode: themeMode,
theme: light, theme: light.fixWindowsFont,
darkTheme: tMode < 3 ? dark : dark.toAmoled, darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
home: _buildAppContent(ctx), home: VirtualWindowFrame(
child: Builder(
builder: (context) {
context.setLibL10n();
final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
final intros = _IntroPage.builders;
if (intros.isNotEmpty) {
return _IntroPage(intros);
}
return const HomePage();
},
),
),
); );
} }
Widget _buildAppContent(BuildContext ctx) {
//if (Pros.app.isWearOS) return const WearHome();
return const _AppContent(
intro: _IntroPage(),
child: HomePage(),
);
}
}
/// It's used for init settings related to [BuildContext]
final class _AppContent extends StatelessWidget {
final Widget child;
final Widget intro;
const _AppContent({required this.child, required this.intro});
@override
Widget build(BuildContext context) {
context.setLibL10n();
final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
final showIntro = Stores.setting.showIntro.fetch();
if (showIntro) return intro;
return child;
}
} }
void _setup(BuildContext context) async { void _setup(BuildContext context) async {

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,13 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/backup.dart';
import 'package:server_box/view/page/container.dart'; import 'package:server_box/view/page/container.dart';
import 'package:server_box/view/page/home/home.dart'; import 'package:server_box/view/page/home/home.dart';
import 'package:server_box/view/page/iperf.dart'; import 'package:server_box/view/page/iperf.dart';
import 'package:server_box/view/page/ping.dart'; import 'package:server_box/view/page/ping.dart';
import 'package:server_box/view/page/private_key/edit.dart'; import 'package:server_box/view/page/private_key/edit.dart';
import 'package:server_box/view/page/private_key/list.dart';
import 'package:server_box/view/page/pve.dart'; import 'package:server_box/view/page/pve.dart';
import 'package:server_box/view/page/server/detail/view.dart'; import 'package:server_box/view/page/server/detail/view.dart';
import 'package:server_box/view/page/setting/platform/android.dart'; import 'package:server_box/view/page/setting/platform/android.dart';
@@ -21,20 +16,14 @@ import 'package:server_box/view/page/setting/seq/srv_func_seq.dart';
import 'package:server_box/view/page/snippet/result.dart'; import 'package:server_box/view/page/snippet/result.dart';
import 'package:server_box/view/page/ssh/page.dart'; import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/setting/seq/virt_key.dart'; import 'package:server_box/view/page/setting/seq/virt_key.dart';
import 'package:server_box/view/page/storage/local.dart'; import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/view/page/process.dart';
import '../data/model/server/snippet.dart'; import 'package:server_box/view/page/server/tab.dart';
import '../view/page/editor.dart'; import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
import '../view/page/process.dart'; import 'package:server_box/view/page/setting/seq/srv_seq.dart';
import '../view/page/server/edit.dart'; import 'package:server_box/view/page/snippet/edit.dart';
import '../view/page/server/tab.dart'; import 'package:server_box/view/page/storage/sftp.dart';
import '../view/page/setting/entry.dart'; import 'package:server_box/view/page/storage/sftp_mission.dart';
import '../view/page/setting/seq/srv_detail_seq.dart';
import '../view/page/setting/seq/srv_seq.dart';
import '../view/page/snippet/edit.dart';
import '../view/page/snippet/list.dart';
import '../view/page/storage/sftp.dart';
import '../view/page/storage/sftp_mission.dart';
class AppRoutes { class AppRoutes {
final Widget page; final Widget page;
@@ -61,7 +50,7 @@ class AppRoutes {
return Future.value(null); return Future.value(null);
} }
static AppRoutes serverDetail({Key? key, required ServerPrivateInfo spi}) { static AppRoutes serverDetail({Key? key, required Spi spi}) {
return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail'); return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail');
} }
@@ -69,13 +58,6 @@ class AppRoutes {
return AppRoutes(ServerPage(key: key), 'server_tab'); return AppRoutes(ServerPage(key: key), 'server_tab');
} }
static AppRoutes serverEdit({Key? key, ServerPrivateInfo? spi}) {
return AppRoutes(
ServerEditPage(spi: spi),
'server_${spi == null ? 'add' : 'edit'}',
);
}
static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) { static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) {
return AppRoutes( return AppRoutes(
PrivateKeyEditPage(pki: pki), PrivateKeyEditPage(pki: pki),
@@ -83,10 +65,6 @@ class AppRoutes {
); );
} }
static AppRoutes keyList({Key? key}) {
return AppRoutes(PrivateKeysListPage(key: key), 'key_detail');
}
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) { static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
return AppRoutes( return AppRoutes(
SnippetEditPage(snippet: snippet), SnippetEditPage(snippet: snippet),
@@ -94,13 +72,9 @@ class AppRoutes {
); );
} }
static AppRoutes snippetList({Key? key}) {
return AppRoutes(SnippetListPage(key: key), 'snippet_detail');
}
static AppRoutes ssh({ static AppRoutes ssh({
Key? key, Key? key,
required ServerPrivateInfo spi, required Spi spi,
String? initCmd, String? initCmd,
Snippet? initSnippet, Snippet? initSnippet,
}) { }) {
@@ -119,26 +93,12 @@ class AppRoutes {
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting'); return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
} }
static AppRoutes localStorage(
{Key? key, bool isPickFile = false, String? initDir}) {
return AppRoutes(
LocalStoragePage(
key: key,
isPickFile: isPickFile,
initDir: initDir,
),
'local_storage');
}
static AppRoutes sftpMission({Key? key}) { static AppRoutes sftpMission({Key? key}) {
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission'); return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
} }
static AppRoutes sftp( static AppRoutes sftp(
{Key? key, {Key? key, required Spi spi, String? initPath, bool isSelect = false}) {
required ServerPrivateInfo spi,
String? initPath,
bool isSelect = false}) {
return AppRoutes( return AppRoutes(
SftpPage( SftpPage(
key: key, key: key,
@@ -149,48 +109,10 @@ class AppRoutes {
'sftp'); 'sftp');
} }
static AppRoutes backup({Key? key}) { static AppRoutes docker({Key? key, required Spi spi}) {
return AppRoutes(BackupPage(key: key), 'backup');
}
static AppRoutes debug({Key? key}) {
return AppRoutes(
DebugPage(
key: key,
args: DebugPageArgs(
notifier: Pros.debug.widgets,
onClear: Pros.debug.clear,
title: 'Logs(${BuildData.build})',
),
),
'debug',
);
}
static AppRoutes docker({Key? key, required ServerPrivateInfo spi}) {
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker'); return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
} }
/// - Pop true if the text is changed & [path] is not null
/// - Pop text if [path] is null
static AppRoutes editor({
Key? key,
String? path,
String? text,
String? langCode,
String? title,
}) {
return AppRoutes(
EditorPage(
key: key,
path: path,
text: text,
langCode: langCode,
title: title,
),
'editor');
}
// static AppRoutes fullscreen({Key? key}) { // static AppRoutes fullscreen({Key? key}) {
// return AppRoutes(FullScreenPage(key: key), 'fullscreen'); // return AppRoutes(FullScreenPage(key: key), 'fullscreen');
// } // }
@@ -203,14 +125,10 @@ class AppRoutes {
return AppRoutes(PingPage(key: key), 'ping'); return AppRoutes(PingPage(key: key), 'ping');
} }
static AppRoutes process({Key? key, required ServerPrivateInfo spi}) { static AppRoutes process({Key? key, required Spi spi}) {
return AppRoutes(ProcessPage(key: key, spi: spi), 'process'); return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
} }
static AppRoutes settings({Key? key}) {
return AppRoutes(SettingPage(key: key), 'setting');
}
static AppRoutes serverOrder({Key? key}) { static AppRoutes serverOrder({Key? key}) {
return AppRoutes(ServerOrderPage(key: key), 'server_order'); return AppRoutes(ServerOrderPage(key: key), 'server_order');
} }
@@ -237,7 +155,7 @@ class AppRoutes {
'snippet_result'); 'snippet_result');
} }
static AppRoutes iperf({Key? key, required ServerPrivateInfo spi}) { static AppRoutes iperf({Key? key, required Spi spi}) {
return AppRoutes(IPerfPage(key: key, spi: spi), 'iperf'); return AppRoutes(IPerfPage(key: key, spi: spi), 'iperf');
} }
@@ -245,11 +163,7 @@ class AppRoutes {
return AppRoutes(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq'); return AppRoutes(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
} }
static AppRoutes pve({Key? key, required ServerPrivateInfo spi}) { static AppRoutes pve({Key? key, required Spi spi}) {
return AppRoutes(PvePage(key: key, spi: spi), 'pve'); return AppRoutes(PvePage(key: key, spi: spi), 'pve');
} }
static AppRoutes kvEditor({Key? key, required Map<String, String> data}) {
return AppRoutes(KvEditor(key: key, data: data), 'kv_editor');
}
} }

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,29 +2,33 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/rebuild.dart'; import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
part 'backup.g.dart';
const backupFormatVersion = 1; const backupFormatVersion = 1;
final _logger = Logger('Backup'); final _logger = Logger('Backup');
class Backup { @JsonSerializable()
class Backup implements Mergeable {
// backup format version // backup format version
final int version; final int version;
final String date; final String date;
final List<ServerPrivateInfo> spis; final List<Spi> spis;
final List<Snippet> snippets; final List<Snippet> snippets;
final List<PrivateKeyInfo> keys; final List<PrivateKeyInfo> keys;
final Map<String, dynamic> container; final Map<String, dynamic> container;
final Map<String, dynamic> history; final Map<String, dynamic> history;
final int? lastModTime; final int? lastModTime;
final Map<String, dynamic>? settings;
const Backup({ const Backup({
required this.version, required this.version,
@@ -34,54 +38,40 @@ class Backup {
required this.keys, required this.keys,
required this.container, required this.container,
required this.history, required this.history,
required this.settings,
this.lastModTime, this.lastModTime,
}); });
Backup.fromJson(Map<String, dynamic> json) factory Backup.fromJson(Map<String, dynamic> json) => _$BackupFromJson(json);
: version = json['version'] as int,
date = json['date'],
spis = (json['spis'] as List)
.map((e) => ServerPrivateInfo.fromJson(e))
.toList(),
snippets =
(json['snippets'] as List).map((e) => Snippet.fromJson(e)).toList(),
keys = (json['keys'] as List)
.map((e) => PrivateKeyInfo.fromJson(e))
.toList(),
container = json['container'] ?? {},
lastModTime = json['lastModTime'],
history = json['history'] ?? {};
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$BackupToJson(this);
'version': version,
'date': date,
'spis': spis,
'snippets': snippets,
'keys': keys,
'container': container,
'lastModTime': lastModTime,
'history': history,
};
Backup.loadFromStore() static Future<Backup> loadFromStore() async {
: version = backupFormatVersion, final lastModTime = Stores.lastModTime?.millisecondsSinceEpoch;
date = DateTime.now().toString().split('.').firstOrNull ?? '', return Backup(
spis = Stores.server.fetch(), version: backupFormatVersion,
snippets = Stores.snippet.fetch(), date: DateTime.now().toString().split('.').firstOrNull ?? '',
keys = Stores.key.fetch(), spis: Stores.server.fetch(),
container = Stores.container.box.toJson(), snippets: Stores.snippet.fetch(),
lastModTime = Stores.lastModTime, keys: Stores.key.fetch(),
history = Stores.history.box.toJson(); container: await Stores.container.getAllMap(),
lastModTime: lastModTime,
history: await Stores.history.getAllMap(),
settings: await Stores.setting.getAllMap(),
);
}
static Future<String> backup([String? name]) async { static Future<String> backup([String? name]) async {
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson())); final bak = await Backup.loadFromStore();
final path = '${Paths.doc}/${name ?? Miscs.bakFileName}'; final result = _diyEncrypt(json.encode(bak.toJson()));
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result); await File(path).writeAsString(result);
return path; return path;
} }
Future<void> restore({bool force = false}) async { @override
final curTime = Stores.lastModTime ?? 0; Future<void> merge({bool force = false}) async {
final curTime = Stores.lastModTime?.millisecondsSinceEpoch ?? 0;
final bakTime = lastModTime ?? 0; final bakTime = lastModTime ?? 0;
final shouldRestore = force || curTime < bakTime; final shouldRestore = force || curTime < bakTime;
if (!shouldRestore) { if (!shouldRestore) {
@@ -90,93 +80,142 @@ class Backup {
} }
// Snippets // Snippets
final nowSnippets = Stores.snippet.box.keys.toSet(); if (force) {
final bakSnippets = snippets.map((e) => e.name).toSet(); for (final s in snippets) {
final newSnippets = bakSnippets.difference(nowSnippets); Stores.snippet.box.put(s.name, s);
final delSnippets = nowSnippets.difference(bakSnippets); }
final updateSnippets = nowSnippets.intersection(bakSnippets); } else {
for (final s in newSnippets) { final nowSnippets = Stores.snippet.box.keys.toSet();
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s)); final bakSnippets = snippets.map((e) => e.name).toSet();
} final newSnippets = bakSnippets.difference(nowSnippets);
for (final s in delSnippets) { final delSnippets = nowSnippets.difference(bakSnippets);
Stores.snippet.box.delete(s); final updateSnippets = nowSnippets.intersection(bakSnippets);
} for (final s in newSnippets) {
for (final s in updateSnippets) { Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s)); }
for (final s in delSnippets) {
Stores.snippet.box.delete(s);
}
for (final s in updateSnippets) {
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
}
} }
// ServerPrivateInfo // ServerPrivateInfo
final nowSpis = Stores.server.box.keys.toSet(); if (force) {
final bakSpis = spis.map((e) => e.id).toSet(); for (final s in spis) {
final newSpis = bakSpis.difference(nowSpis); Stores.server.box.put(s.id, s);
final delSpis = nowSpis.difference(bakSpis); }
final updateSpis = nowSpis.intersection(bakSpis); } else {
for (final s in newSpis) { final nowSpis = Stores.server.box.keys.toSet();
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s)); final bakSpis = spis.map((e) => e.id).toSet();
} final newSpis = bakSpis.difference(nowSpis);
for (final s in delSpis) { final delSpis = nowSpis.difference(bakSpis);
Stores.server.box.delete(s); final updateSpis = nowSpis.intersection(bakSpis);
} for (final s in newSpis) {
for (final s in updateSpis) { Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s)); }
for (final s in delSpis) {
Stores.server.box.delete(s);
}
for (final s in updateSpis) {
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
}
} }
// PrivateKeyInfo // PrivateKeyInfo
final nowKeys = Stores.key.box.keys.toSet(); if (force) {
final bakKeys = keys.map((e) => e.id).toSet(); for (final s in keys) {
final newKeys = bakKeys.difference(nowKeys); Stores.key.box.put(s.id, s);
final delKeys = nowKeys.difference(bakKeys); }
final updateKeys = nowKeys.intersection(bakKeys); } else {
for (final s in newKeys) { final nowKeys = Stores.key.box.keys.toSet();
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s)); final bakKeys = keys.map((e) => e.id).toSet();
} final newKeys = bakKeys.difference(nowKeys);
for (final s in delKeys) { final delKeys = nowKeys.difference(bakKeys);
Stores.key.box.delete(s); final updateKeys = nowKeys.intersection(bakKeys);
} for (final s in newKeys) {
for (final s in updateKeys) { Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s)); }
for (final s in delKeys) {
Stores.key.box.delete(s);
}
for (final s in updateKeys) {
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
}
} }
// History // History
final nowHistory = Stores.history.box.keys.toSet(); if (force) {
final bakHistory = history.keys.toSet(); Stores.history.box.putAll(history);
final newHistory = bakHistory.difference(nowHistory); } else {
final delHistory = nowHistory.difference(bakHistory); final nowHistory = Stores.history.box.keys.toSet();
final updateHistory = nowHistory.intersection(bakHistory); final bakHistory = history.keys.toSet();
for (final s in newHistory) { final newHistory = bakHistory.difference(nowHistory);
Stores.history.box.put(s, history[s]); final delHistory = nowHistory.difference(bakHistory);
} final updateHistory = nowHistory.intersection(bakHistory);
for (final s in delHistory) { for (final s in newHistory) {
Stores.history.box.delete(s); Stores.history.box.put(s, history[s]);
} }
for (final s in updateHistory) { for (final s in delHistory) {
Stores.history.box.put(s, history[s]); Stores.history.box.delete(s);
}
for (final s in updateHistory) {
Stores.history.box.put(s, history[s]);
}
} }
// Container // Container
final nowContainer = Stores.container.box.keys.toSet(); if (force) {
final bakContainer = container.keys.toSet(); Stores.container.box.putAll(container);
final newContainer = bakContainer.difference(nowContainer); } else {
final delContainer = nowContainer.difference(bakContainer); final nowContainer = Stores.container.box.keys.toSet();
final updateContainer = nowContainer.intersection(bakContainer); final bakContainer = container.keys.toSet();
for (final s in newContainer) { final newContainer = bakContainer.difference(nowContainer);
Stores.container.box.put(s, container[s]); final delContainer = nowContainer.difference(bakContainer);
} final updateContainer = nowContainer.intersection(bakContainer);
for (final s in delContainer) { for (final s in newContainer) {
Stores.container.box.delete(s); Stores.container.box.put(s, container[s]);
} }
for (final s in updateContainer) { for (final s in delContainer) {
Stores.container.box.put(s, container[s]); Stores.container.box.delete(s);
}
for (final s in updateContainer) {
Stores.container.box.put(s, container[s]);
}
} }
Pros.reload(); // Settings
RNodes.app.build(); final settings_ = settings;
if (settings_ != null) {
if (force) {
Stores.setting.box.putAll(settings_);
} else {
final nowSettings = Stores.setting.box.keys.toSet();
final bakSettings = settings_.keys.toSet();
final newSettings = bakSettings.difference(nowSettings);
final delSettings = nowSettings.difference(bakSettings);
final updateSettings = nowSettings.intersection(bakSettings);
for (final s in newSettings) {
Stores.setting.box.put(s, settings_[s]);
}
for (final s in delSettings) {
Stores.setting.box.delete(s);
}
for (final s in updateSettings) {
Stores.setting.box.put(s, settings_[s]);
}
}
}
Provider.reload();
RNodes.app.notify();
_logger.info('Restore success'); _logger.info('Restore success');
} }
Backup.fromJsonString(String raw) factory Backup.fromJsonString(String raw) =>
: this.fromJson(json.decode(_diyDecrypt(raw))); Backup.fromJson(json.decode(_diyDecrypt(raw)));
} }
String _diyEncrypt(String raw) => json.encode( String _diyEncrypt(String raw) => json.encode(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
final _seperator = Pfs.seperator;
/// It's used on platform's file system. /// It's used on platform's file system.
/// So use [Platform.pathSeparator] to join path. /// So use [Platform.pathSeparator] to join path.
class LocalPath { class LocalPath {
final String _prefixPath; final String _prefixPath;
String _path = '/'; String _path = _seperator;
String? _prePath; String? _prePath;
String get path => _prefixPath + _path; String get path => _prefixPath + _path;
@@ -13,20 +15,20 @@ class LocalPath {
void update(String newPath) { void update(String newPath) {
_prePath = _path; _prePath = _path;
if (newPath == '..') { if (newPath == '..') {
_path = _path.substring(0, _path.lastIndexOf('/')); _path = _path.substring(0, _path.lastIndexOf(_seperator));
if (_path == '') { if (_path == '') {
_path = '/'; _path = _seperator;
} }
return; return;
} }
if (newPath == '/') { if (newPath == _seperator) {
_path = '/'; _path = _seperator;
return; return;
} }
_path = _path.joinPath(newPath); _path = _path.joinPath(newPath);
} }
bool get canBack => path != '$_prefixPath/'; bool get canBack => path != '$_prefixPath$_seperator';
bool undo() { bool undo() {
if (_prePath == null || _path == _prePath) { if (_prePath == null || _path == _prePath) {
@@ -38,7 +40,7 @@ class LocalPath {
} }
String _trimSuffix(String prefixPath) { String _trimSuffix(String prefixPath) {
if (prefixPath.endsWith('/')) { if (prefixPath.endsWith(_seperator)) {
return prefixPath.substring(0, prefixPath.length - 1); return prefixPath.substring(0, prefixPath.length - 1);
} }
return prefixPath; return prefixPath;

View File

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

View File

@@ -1,7 +1,8 @@
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/provider/server.dart';
import '../../res/build_data.dart'; import 'package:server_box/data/res/build_data.dart';
import '../server/system.dart'; import 'package:server_box/data/model/server/system.dart';
enum ShellFunc { enum ShellFunc {
status, status,
@@ -12,52 +13,61 @@ enum ShellFunc {
suspend, suspend,
; ;
static const _homeVar = '\$HOME';
static const seperator = 'SrvBoxSep'; static const seperator = 'SrvBoxSep';
/// The suffix `\t` is for formatting /// The suffix `\t` is for formatting
static const cmdDivider = '\necho $seperator\n\t'; 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. /// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// /// it will be changed to [scriptDirHome]/[scriptFile].
/// So different version of app can run at the same time. static String getScriptDir(String id) {
/// final customScriptDir =
/// **Can't** use it in SFTP, because SFTP can't recognize `$HOME` ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
static String getShellPath(String home) => '$home/$_srvBoxDir/$scriptFile'; if (customScriptDir != null) return customScriptDir;
return _scriptDirMap.putIfAbsent(id, () {
static const srvBoxDir = '$_homeVar/$_srvBoxDir'; return scriptDirTmp;
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';
}
} }
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 755 $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 { String get name {
switch (this) { switch (this) {
@@ -213,6 +223,7 @@ enum StatusCmdType {
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'), 'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
nvidia._('nvidia-smi -q -x'), nvidia._('nvidia-smi -q -x'),
sensors._('sensors'), sensors._('sensors'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"'),
; ;
final String cmd; final String cmd;
@@ -231,6 +242,7 @@ enum BSDStatusCmdType {
mem._('top -l 1 | grep PhysMem'), mem._('top -l 1 | grep PhysMem'),
//temp, //temp,
host._('hostname'), host._('hostname'),
cpuBrand._('sysctl -n machdep.cpu.brand_string'),
; ;
final String cmd; final String cmd;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,16 @@
import 'dart:collection';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart'; import 'package:server_box/data/model/server/time_seq.dart';
import 'package:server_box/data/res/status.dart'; import 'package:server_box/data/res/status.dart';
/// Capacity of the FIFO queue
const _kCap = 30; const _kCap = 30;
class Cpus extends TimeSeq<List<SingleCpuCore>> { class Cpus extends TimeSeq<List<SingleCpuCore>> {
Cpus(super.init1, super.init2); Cpus(super.init1, super.init2);
final Map<String, int> brand = {};
@override @override
void onUpdate() { void onUpdate() {
_coresCount = now.length; _coresCount = now.length;
@@ -23,10 +25,16 @@ class Cpus extends TimeSeq<List<SingleCpuCore>> {
double usedPercent({int coreIdx = 0}) { double usedPercent({int coreIdx = 0}) {
if (now.length != pre.length) return 0; if (now.length != pre.length) return 0;
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle; if (now.isEmpty) return 0;
final totalDelta = now[coreIdx].total - pre[coreIdx].total; try {
final used = idleDelta / totalDelta; final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
return used.isNaN ? 0 : 100 - used * 100; 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; 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+)%'); final _bsdCpuPercentReg = RegExp(r'(\d+\.\d+)%');
/// TODO: Change this implementation to parse cpu status on BSD system /// 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:hive_flutter/adapters.dart';
import 'package:json_annotation/json_annotation.dart';
part 'custom.g.dart'; part 'custom.g.dart';
@JsonSerializable()
@HiveType(typeId: 7) @HiveType(typeId: 7)
final class ServerCustom { final class ServerCustom {
// @HiveField(0) // @HiveField(0)
@@ -19,6 +21,14 @@ final class ServerCustom {
@HiveField(5) @HiveField(5)
final String? logoUrl; final String? logoUrl;
/// The device name of the network interface displayed in the home server card.
@HiveField(6)
final String? netDev;
/// The directory where the script is stored.
@HiveField(7)
final String? scriptDir;
const ServerCustom({ const ServerCustom({
//this.temperature, //this.temperature,
this.pveAddr, this.pveAddr,
@@ -26,51 +36,14 @@ final class ServerCustom {
this.cmds, this.cmds,
this.preferTempDev, this.preferTempDev,
this.logoUrl, this.logoUrl,
this.netDev,
this.scriptDir,
}); });
static ServerCustom fromJson(Map<String, dynamic> json) { factory ServerCustom.fromJson(Map<String, dynamic> json) =>
//final temperature = json["temperature"] as String?; _$ServerCustomFromJson(json);
final pveAddr = json["pveAddr"] as String?;
final pveIgnoreCert = json["pveIgnoreCert"] as bool;
final cmds = json["cmds"] as Map<String, dynamic>?;
final preferTempDev = json["preferTempDev"] as String?;
final logoUrl = json["logoUrl"] as String?;
return ServerCustom(
//temperature: temperature,
pveAddr: pveAddr,
pveIgnoreCert: pveIgnoreCert,
cmds: cmds?.cast<String, String>(),
preferTempDev: preferTempDev,
logoUrl: logoUrl,
);
}
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() => _$ServerCustomToJson(this);
final json = <String, dynamic>{};
// if (temperature != null) {
// json["temperature"] = temperature;
// }
if (pveAddr != null) {
json["pveAddr"] = pveAddr;
}
json["pveIgnoreCert"] = pveIgnoreCert;
if (cmds != null) {
json["cmds"] = cmds;
}
if (preferTempDev != null) {
json["preferTempDev"] = preferTempDev;
}
if (logoUrl != null) {
json["logoUrl"] = logoUrl;
}
return json;
}
@override
String toString() {
return toJson().toString();
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -80,7 +53,9 @@ final class ServerCustom {
other.pveIgnoreCert == pveIgnoreCert && other.pveIgnoreCert == pveIgnoreCert &&
other.cmds == cmds && other.cmds == cmds &&
other.preferTempDev == preferTempDev && other.preferTempDev == preferTempDev &&
other.logoUrl == logoUrl; other.logoUrl == logoUrl &&
other.netDev == netDev &&
other.scriptDir == scriptDir;
} }
@override @override
@@ -90,5 +65,7 @@ final class ServerCustom {
pveIgnoreCert.hashCode ^ pveIgnoreCert.hashCode ^
cmds.hashCode ^ cmds.hashCode ^
preferTempDev.hashCode ^ preferTempDev.hashCode ^
logoUrl.hashCode; logoUrl.hashCode ^
netDev.hashCode ^
scriptDir.hashCode;
} }

View File

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

View File

@@ -1,7 +1,7 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart'; import 'package:server_box/data/model/server/time_seq.dart';
import '../../res/misc.dart'; import 'package:server_box/data/res/misc.dart';
class Disk { class Disk {
final String fs; final String fs;
@@ -104,7 +104,9 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
!item.dev.startsWith('vd') && !item.dev.startsWith('vd') &&
!item.dev.startsWith('hd') && !item.dev.startsWith('hd') &&
!item.dev.startsWith('mmcblk') && !item.dev.startsWith('mmcblk') &&
!item.dev.startsWith('sr')) continue; !item.dev.startsWith('sr')) {
continue;
}
final (read_, write_) = _getSpeed(item.dev); final (read_, write_) = _getSpeed(item.dev);
read += read_ ?? 0; read += read_ ?? 0;
write += write_ ?? 0; write += write_ ?? 0;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/shell_func.dart'; import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/battery.dart'; import 'package:server_box/data/model/server/battery.dart';
@@ -15,12 +13,8 @@ import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/temp.dart'; import 'package:server_box/data/model/server/temp.dart';
import '../app/tag_pickable.dart'; class Server {
Spi spi;
part 'server.ext.dart';
class Server implements TagPickable {
ServerPrivateInfo spi;
ServerStatus status; ServerStatus status;
SSHClient? client; SSHClient? client;
ServerConn conn; ServerConn conn;
@@ -32,14 +26,6 @@ class Server implements TagPickable {
this.client, this.client,
}); });
@override
bool containsTag(String tag) {
return spi.tags?.contains(tag) ?? false;
}
@override
String get tagName => spi.id;
bool get needGenClient => conn < ServerConn.connecting; bool get needGenClient => conn < ServerConn.connecting;
bool get canViewDetails => conn == ServerConn.finished; bool get canViewDetails => conn == ServerConn.finished;
@@ -95,5 +81,5 @@ enum ServerConn {
/// Status parsing finished /// Status parsing finished
finished; finished;
operator <(ServerConn other) => index < other.index; bool operator <(ServerConn other) => index < other.index;
} }

View File

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

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