Compare commits
197 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c556c0f1b5 | ||
|
|
c42c701ffc | ||
|
|
e6db2db320 | ||
|
|
66ecb02d9e | ||
|
|
8e7de604ee | ||
|
|
6f2a58ce18 | ||
|
|
066629d7e0 | ||
|
|
4b3953e0d2 | ||
|
|
b5aec55106 | ||
|
|
ba686db847 | ||
|
|
4d52023982 | ||
|
|
7a71a96442 | ||
|
|
79c515c903 | ||
|
|
4701757857 | ||
|
|
176cb7da03 | ||
|
|
741a6442e0 | ||
|
|
f6d394c71e | ||
|
|
7127c960f7 | ||
|
|
1084c49a5f | ||
|
|
bc824691e0 | ||
|
|
0c1ada0067 | ||
|
|
9547d92ac5 | ||
|
|
7e16d2f159 | ||
|
|
d88e97e699 | ||
|
|
d29bd1d806 | ||
|
|
2b2f1ddb60 | ||
|
|
4f16d510c8 | ||
|
|
94cded39a6 | ||
|
|
12082e1235 | ||
|
|
28e34e2183 | ||
|
|
4d45d01074 | ||
|
|
f6b3ec2a62 | ||
|
|
d6cf33fb70 | ||
|
|
1eea133b69 | ||
|
|
2b46cb6dcc | ||
|
|
8627ff823f | ||
|
|
e520929411 | ||
|
|
8f09085cf3 | ||
|
|
9e66071cb0 | ||
|
|
fa90c1ef31 | ||
|
|
ede238c647 | ||
|
|
6e7fee20b8 | ||
|
|
391e4f6b65 | ||
|
|
e185414355 | ||
|
|
2a2f348063 | ||
|
|
95ca6bcfc9 | ||
|
|
275041247a | ||
|
|
24d64b835d | ||
|
|
dd5fea09b1 | ||
|
|
0a404e035e | ||
|
|
b5ab5b1cab | ||
|
|
5cb83001c6 | ||
|
|
20a39f0292 | ||
|
|
900686f955 | ||
|
|
a10321e3de | ||
|
|
0691ab2213 | ||
|
|
ef05203ea3 | ||
|
|
28410707a8 | ||
|
|
06b966caa8 | ||
|
|
11b0806083 | ||
|
|
749fd4d800 | ||
|
|
bec4a3b314 | ||
|
|
9e5babec76 | ||
|
|
dbbb10364b | ||
|
|
16948c3e0f | ||
|
|
e39fb23b66 | ||
|
|
4777166dd9 | ||
|
|
0ae0241800 | ||
|
|
e7a5f43cc4 | ||
|
|
7f58237589 | ||
|
|
0bbd0b43b3 | ||
|
|
aaa1eddeaf | ||
|
|
2f6db2961f | ||
|
|
831efa833b | ||
|
|
867fcbfc0d | ||
|
|
41886be649 | ||
|
|
029b4e0dba | ||
|
|
3a3c29764a | ||
|
|
4ace4af7da | ||
|
|
ddd32e82d4 | ||
|
|
b882baeafa | ||
|
|
046f2c06d0 | ||
|
|
d706886343 | ||
|
|
7dda63af8a | ||
|
|
00d303ac36 | ||
|
|
229983d82e | ||
|
|
4928ca600d | ||
|
|
89ec2d94d6 | ||
|
|
393c3e6388 | ||
|
|
dee458e926 | ||
|
|
f89228db40 | ||
|
|
3b6fb6194b | ||
|
|
02444fc2f0 | ||
|
|
aef317a140 | ||
|
|
47aedb2f2e | ||
|
|
eab06abcaf | ||
|
|
c062c12a0e | ||
|
|
d7669c94b8 | ||
|
|
50af289574 | ||
|
|
90b88ed795 | ||
|
|
d611fdcd50 | ||
|
|
db9b2dd818 | ||
|
|
edb49ead67 | ||
|
|
7f0dc656b8 | ||
|
|
b33d0bbc3e | ||
|
|
7d0ea8a58b | ||
|
|
c18732d8f3 | ||
|
|
157af0a354 | ||
|
|
2d9dc044f9 | ||
|
|
479250c207 | ||
|
|
aef7ec911f | ||
|
|
4f9ee7781f | ||
|
|
eb83d05c81 | ||
|
|
329fd33b69 | ||
|
|
931c5f0bf6 | ||
|
|
bcbf1fbc17 | ||
|
|
3e7315dac6 | ||
|
|
4cecfdf7a8 | ||
|
|
0346821cf5 | ||
|
|
966a60a82d | ||
|
|
76e98c6468 | ||
|
|
d7ae8b75b8 | ||
|
|
b5329e2692 | ||
|
|
ef297673f3 | ||
|
|
7558b4806d | ||
|
|
f7ef8a3915 | ||
|
|
38366a2ef3 | ||
|
|
7e5bb54c98 | ||
|
|
7ce3854392 | ||
|
|
195ddd2bcc | ||
|
|
267b0b0a69 | ||
|
|
41e3fcb23a | ||
|
|
46d5840276 | ||
|
|
fe566e97ca | ||
|
|
ddd1524d63 | ||
|
|
4d8268c614 | ||
|
|
568b97606a | ||
|
|
42cc2416a1 | ||
|
|
aaa69f0f95 | ||
|
|
64676bc5cb | ||
|
|
a15c04956c | ||
|
|
e3c885483b | ||
|
|
493c86cacb | ||
|
|
ea7c8caf14 | ||
|
|
9db04a60c2 | ||
|
|
610f46da0d | ||
|
|
b8e5418ff2 | ||
|
|
0e21755acb | ||
|
|
73248011a1 | ||
|
|
969643d3df | ||
|
|
c90d0e4b3b | ||
|
|
f9aadc6b0f | ||
|
|
8fd4cc1fe1 | ||
|
|
432d76f024 | ||
|
|
ca8211e1a4 | ||
|
|
a3b48fc01c | ||
|
|
8be94aa09c | ||
|
|
5db1253ab8 | ||
|
|
ceedd86310 | ||
|
|
6a0254623f | ||
|
|
1c6ec56032 | ||
|
|
287869ed45 | ||
|
|
e4dbc3ba12 | ||
|
|
426e5689f8 | ||
|
|
afda5fd4a4 | ||
|
|
0a21b2820c | ||
|
|
87b3b76b0b | ||
|
|
41ec46f1d3 | ||
|
|
7a359588db | ||
|
|
255abe8b11 | ||
|
|
b0936c5e6e | ||
|
|
2907ac74d4 | ||
|
|
ea678f37b0 | ||
|
|
076082c945 | ||
|
|
5ee98f90e8 | ||
|
|
c988dd88d7 | ||
|
|
f7d6c461dc | ||
|
|
14771ae946 | ||
|
|
7e9086b20e | ||
|
|
4b3c4870ba | ||
|
|
43cebd0c04 | ||
|
|
5ce13109b0 | ||
|
|
6e428c91d1 | ||
|
|
4430045550 | ||
|
|
282cb06091 | ||
|
|
772c2743b5 | ||
|
|
90199b89a5 | ||
|
|
e1d2e3f3e5 | ||
|
|
3f9fe1f2c6 | ||
|
|
c79bbc5756 | ||
|
|
63e1bec2b9 | ||
|
|
d26b7c6f75 | ||
|
|
da8dc4fa54 | ||
|
|
413b81c47f | ||
|
|
9ef419e3df | ||
|
|
39893912d9 | ||
|
|
49456ca6c3 |
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
custom: ['https://cdn.lpkt.cn/donate']
|
||||||
59
.github/workflows/release.yml
vendored
@@ -1,6 +1,7 @@
|
|||||||
name: Flutter Release
|
name: Flutter Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
@@ -9,29 +10,27 @@ 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.32.2"
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: "zulu"
|
||||||
java-version: '17'
|
java-version: "17"
|
||||||
- name: Fetch secrets
|
- name: Fetch secrets
|
||||||
run: |
|
run: |
|
||||||
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 +43,39 @@ 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 wget
|
||||||
|
# App Specific
|
||||||
|
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev
|
||||||
|
# Packaging
|
||||||
|
sudo wget https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage -O /bin/appimagetool
|
||||||
|
sudo chmod +x /bin/appimagetool
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
dart run fl_build -p linux
|
||||||
|
- name: Rename artifacts
|
||||||
|
run: |
|
||||||
|
appimage_name=$(ls dist/*/*.AppImage)
|
||||||
|
mv $appimage_name ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -54,8 +85,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
|
||||||
@@ -76,9 +105,9 @@ jobs:
|
|||||||
# uses: actions/checkout@v4
|
# uses: actions/checkout@v4
|
||||||
# - 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.32.1'
|
||||||
# - name: Build
|
# - name: Build
|
||||||
# run: dart run fl_build -p ios,mac
|
# run: dart run fl_build -p ios,mac
|
||||||
# - name: Create Release
|
# - name: Create Release
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -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
|
||||||
|
|
||||||
@@ -64,3 +65,4 @@ untranlated.json
|
|||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
more_build_data.json
|
more_build_data.json
|
||||||
trans.txt
|
trans.txt
|
||||||
|
android/app/.cxx
|
||||||
|
|||||||
30
.metadata
@@ -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
@@ -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"
|
||||||
// ]
|
// ]
|
||||||
|
|||||||
82
README.md
@@ -2,71 +2,85 @@ 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">
|
||||||
|
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
## 🏙️ Screenshots
|
||||||
|
|
||||||
## ⬇️ Download
|
<table>
|
||||||
🎉 **The `Android / Linux / Windows` version are now built via GitHub Actions**
|
<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)
|
## 📥 Install
|
||||||
|
|
||||||
- All deprecated versions before `v930` can be found in [here](https://cdn.lolli.tech/serverbox/?sort=time&order=desc&layout=grid).
|
|Platform| From|
|
||||||
- To prevent injection attacks and etc., please don't download from untrusted sources. eg: Gitee release is not related to this project.
|
|--|--|
|
||||||
|
| 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`, `S.M.A.R.T`...
|
||||||
- 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://qm.qq.com/q/daCGa7eShG"><img alt="qq" src="https://img.shields.io/badge/QQ-Group-pink"></a>
|
||||||
|
<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).
|
||||||
|
|
||||||
Before you open an issue, please read the following:
|
Before you open an issue, please read the following:
|
||||||
|
|
||||||
1. Paste the **entire log** (click the top right of the home page) in the issue template.
|
1. Paste the **entire log** (click the top right of the home page) in the issue template.
|
||||||
2. Make sure whether the issue is caused by ServerBox app.
|
2. Make sure whether the issue is caused by ServerBox app.
|
||||||
3. Welcome all valid and positive feedback, subjective feedback (such as you think other UI is better) may not be accepted.
|
3. Welcome all valid and positive feedback, subjective feedback (such as you think other UI is better) may not be accepted.
|
||||||
|
|
||||||
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
|
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
|
||||||
|
|
||||||
|
|
||||||
## 🧱 Contribution
|
## 🧱 Contribution
|
||||||
- Any positive contribution is welcome.
|
|
||||||
- [l10n guide](https://blog.lolli.tech/faq/) can be found in my blog.
|
|
||||||
|
|
||||||
|
Any positive contribution is welcome.
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
|
||||||
|
2. Clone this repo, run `flutter run` to start the app.
|
||||||
|
3. Run `dart run fl_build -p PLATFORM` to build the app.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms.
|
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - A third-party GPT Client for OpenAI API on all platforms.
|
||||||
- [More](https://github.com/lollipopkit) - Tools & etc.
|
- [More](https://github.com/lollipopkit) - Tools & etc.
|
||||||
|
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
`GPL v3 lollipopkit`
|
`GPL v3 lollipopkit`
|
||||||
|
|||||||
88
README_zh.md
@@ -2,76 +2,86 @@
|
|||||||
|
|
||||||
<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">
|
||||||
|
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||||
|
</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
|
<table>
|
||||||
🎉 **现在 `Android / Linux / Windows` 版本使用 GitHub Actions 构建**。
|
<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` 管理,`S.M.A.R.T`...
|
||||||
- 特殊支持:`生物认证`、`推送`、`桌面小部件`、`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://qm.qq.com/q/daCGa7eShG"><img alt="qq" src="https://img.shields.io/badge/QQ-群-pink"></a>
|
||||||
- **常见问题**可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
|
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
|
||||||
|
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
- 为了可以在不使用 ServerBox app 时获取服务器状态(例如:桌面小部件、推送服务),你需要在你的服务器上安装 [ServerBoxMonitor](https://github.com/lollipopkit/server_box_monitor),详情见 [wiki](https://github.com/lollipopkit/server_box_monitor/wiki/%E4%B8%BB%E9%A1%B5)。
|
||||||
|
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
|
||||||
|
|
||||||
反馈前须知:
|
反馈前须知:
|
||||||
|
|
||||||
1. 反馈问题请附带 log(点击首页右上角),并以 bug 模版提交。
|
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 的 第三方全平台客户端。
|
||||||
- [更多](https://github.com/lollipopkit) - 工具 & etc.
|
- [更多](https://github.com/lollipopkit) - 工具 & etc.
|
||||||
|
|
||||||
|
|
||||||
## 📝 协议
|
## 📝 协议
|
||||||
|
|
||||||
`GPL v3 lollipopkit`
|
`GPL v3 lollipopkit`
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ include: package:flutter_lints/flutter.yaml
|
|||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
- '**/*.g.dart'
|
- "**/*.g.dart"
|
||||||
language:
|
language:
|
||||||
# strict-casts: true
|
# strict-casts: true
|
||||||
# strict-inference: true
|
# strict-inference: true
|
||||||
# strict-raw-types: true
|
# strict-raw-types: true
|
||||||
|
errors:
|
||||||
|
invalid_annotation_target: ignore
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
@@ -30,14 +32,20 @@ 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
|
||||||
|
directives_ordering: true # Enable sorting of imports
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId "tech.lolli.toolbox"
|
applicationId "tech.lolli.toolbox"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
@@ -86,11 +85,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix '.debug'
|
// No applicationIdSuffix or resValue here
|
||||||
}
|
}
|
||||||
|
|
||||||
profile {
|
profile {
|
||||||
applicationIdSuffix '.debug'
|
// No applicationIdSuffix or resValue here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,18 +11,19 @@
|
|||||||
<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"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|layoutDirection|fontScale|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
@@ -29,12 +31,12 @@
|
|||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme"
|
||||||
/>
|
/>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
@@ -43,11 +45,6 @@
|
|||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="id.flutter.flutter_background_service.BackgroundService"
|
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.HomeWidget"
|
android:name=".widget.HomeWidget"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -67,7 +64,12 @@
|
|||||||
android:resource="@xml/home_widget" />
|
android:resource="@xml/home_widget" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service android:name=".KeepAliveService"/>
|
<service
|
||||||
|
android:name=".ForegroundService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility?hl=en and
|
https://developer.android.com/training/package-visibility?hl=en and
|
||||||
@@ -76,8 +78,8 @@
|
|||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package tech.lolli.toolbox
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
|
|
||||||
import android.os.IBinder
|
|
||||||
import org.jetbrains.annotations.Nullable
|
|
||||||
|
|
||||||
class KeepAliveService : Service() {
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 761 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 411 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 895 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
4
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">ServerBox</string>
|
||||||
|
</resources>
|
||||||
@@ -9,6 +9,23 @@ rootProject.buildDir = '../build'
|
|||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subprojects { subproject ->
|
||||||
|
// Only works on com.android.application(the main app module)
|
||||||
|
if (subproject.plugins.hasPlugin('com.android.application')) {
|
||||||
|
subproject.afterEvaluate {
|
||||||
|
android.buildTypes.matching { it.name == 'profile' }.all { buildType ->
|
||||||
|
buildType.applicationIdSuffix = ".profile"
|
||||||
|
buildTypes.profile.resValue 'string', 'app_name', 'SrvBxP'
|
||||||
|
}
|
||||||
|
android.buildTypes.matching { it.name == 'debug' }.all { buildType ->
|
||||||
|
buildType.applicationIdSuffix = ".debug"
|
||||||
|
buildTypes.debug.resValue 'string', 'app_name', 'SrvBxD'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ 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 "2.1.21" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|||||||
13
distribute_options.yaml
Normal 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
|
||||||
BIN
imgs/detail.png
|
Before Width: | Height: | Size: 135 KiB |
BIN
imgs/docker.png
|
Before Width: | Height: | Size: 115 KiB |
BIN
imgs/editor.png
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 112 KiB |
BIN
imgs/server.png
|
Before Width: | Height: | Size: 135 KiB |
BIN
imgs/sftp.png
|
Before Width: | Height: | Size: 135 KiB |
BIN
imgs/ssh.png
|
Before Width: | Height: | Size: 144 KiB |
@@ -1,24 +1,23 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- device_info_plus (0.0.1):
|
- app_links (0.0.2):
|
||||||
|
- Flutter
|
||||||
|
- camera_avfoundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_background_service_ios (0.0.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
|
||||||
- flutter_native_splash (0.0.1):
|
|
||||||
- Flutter
|
- Flutter
|
||||||
- icloud_storage (0.0.1):
|
- icloud_storage (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- local_auth_darwin (0.0.1):
|
- local_auth_darwin (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.3.0):
|
|
||||||
- Flutter
|
|
||||||
- plain_notification_token (0.0.1):
|
- plain_notification_token (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
@@ -34,16 +33,15 @@ 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_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`)
|
||||||
@@ -52,14 +50,14 @@ DEPENDENCIES:
|
|||||||
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
|
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
device_info_plus:
|
app_links:
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/app_links/ios"
|
||||||
|
camera_avfoundation:
|
||||||
|
:path: ".symlinks/plugins/camera_avfoundation/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_background_service_ios:
|
|
||||||
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
|
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
icloud_storage:
|
icloud_storage:
|
||||||
@@ -70,8 +68,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,23 +82,22 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/watch_connectivity/ios"
|
:path: ".symlinks/plugins/watch_connectivity/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
|
||||||
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
|
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
|
||||||
|
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
|
||||||
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
|
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||||
local_auth_darwin: 4d56c90c2683319835a61274b57620df9c4520ab
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
plain_notification_token: 047876b9d80a5b93565ddcc13a487a7e7b906f7d
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
|
||||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
|
||||||
watch_connectivity: 715eb484685e05846eab74795348a44bb2809b82
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8
|
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -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 = 1189;
|
||||||
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.1189;
|
||||||
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 = 1189;
|
||||||
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.1189;
|
||||||
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 = 1189;
|
||||||
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.1189;
|
||||||
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 = 1189;
|
||||||
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.1189;
|
||||||
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 = 1189;
|
||||||
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.1189;
|
||||||
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 = 1189;
|
||||||
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.1189;
|
||||||
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 = 1189;
|
||||||
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.1189;
|
||||||
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 = 1189;
|
||||||
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.1189;
|
||||||
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 = 1189;
|
||||||
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.1189;
|
||||||
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;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
@@ -43,11 +44,13 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import UIKit
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
@UIApplicationMain
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -1 +1,37 @@
|
|||||||
{"images":[{"scale":"3x","idiom":"universal","filename":"AppIcon-29.0x29.0@3x.png","size":"29x29","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-29.0x29.0@2x.png","size":"29x29","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-64.0x64.0@3x.png","size":"64x64","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-20.0x20.0@2x.png","size":"20x20","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-60.0x60.0@2x.png","size":"60x60","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-40.0x40.0@3x.png","size":"40x40","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-76.0x76.0@2x.png","size":"76x76","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-38.0x38.0@3x.png","size":"38x38","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-68.0x68.0@2x.png","size":"68x68","platform":"ios"},{"scale":"1x","idiom":"universal","filename":"AppIcon-1024.0x1024.0@1x.png","size":"1024x1024","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-64.0x64.0@2x.png","size":"64x64","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-40.0x40.0@2x.png","size":"40x40","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-83.5x83.5@2x.png","size":"83.5x83.5","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-20.0x20.0@3x.png","size":"20x20","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-38.0x38.0@2x.png","size":"38x38","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-60.0x60.0@3x.png","size":"60x60","platform":"ios"}],"info":{"version":1,"author":"appicon"}}
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Icon-1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "icon-1024 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024 1.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
@@ -1,76 +1,81 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<string>zh</string>
|
<string>zh</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>ServerBox</string>
|
<string>ServerBox</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false />
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_dartobservatory._tcp</string>
|
<string>_dartobservatory._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<string>Required for auth</string>
|
<array>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<string>ConfigurationIntent</string>
|
||||||
<string>ServerBox needs to access your local network to discover and connect to your server.</string>
|
</array>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<array>
|
<true />
|
||||||
<string>ConfigurationIntent</string>
|
<key>UIBackgroundModes</key>
|
||||||
</array>
|
<array>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<string>fetch</string>
|
||||||
<true/>
|
</array>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<array>
|
<string>LaunchScreen</string>
|
||||||
<string>fetch</string>
|
<key>UIMainStoryboardFile</key>
|
||||||
</array>
|
<string>Main</string>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<string>LaunchScreen</string>
|
<false />
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<string>Main</string>
|
<array>
|
||||||
<key>UIStatusBarHidden</key>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<false/>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<array>
|
</array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<array>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
</array>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<false />
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<string>Access your local network to discover and connect to your server.</string>
|
||||||
<false/>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
</dict>
|
<string>Required for auth</string>
|
||||||
</plist>
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Scan QR codes and etc.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Get QR code and etc.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -64,9 +64,14 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>_dartobservatory._tcp</string>
|
<string>_dartobservatory._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
|
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>ServerBox needs to access your local network to discover and connect to your server.</string>
|
<string>Access your local network to discover and connect to your server.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Required for auth</string>
|
<string>Required for auth</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Scan QR codes and etc.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Get QR code and etc.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
@@ -28,13 +28,13 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false />
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<false/>
|
<false />
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
@@ -59,8 +59,13 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false />
|
||||||
|
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Required for auth</string>
|
<string>Required for auth</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Scan QR codes and etc.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Get QR code and etc.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -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
|
||||||
161
lib/app.dart
@@ -1,14 +1,14 @@
|
|||||||
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:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.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/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/home/home.dart';
|
import 'package:server_box/generated/l10n/l10n.dart';
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:server_box/view/page/home.dart';
|
||||||
|
|
||||||
part 'intro.dart';
|
part 'intro.dart';
|
||||||
|
|
||||||
@@ -22,33 +22,67 @@ 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());
|
return _build(context);
|
||||||
return _buildApp(context);
|
|
||||||
}
|
}
|
||||||
return DynamicColorBuilder(
|
|
||||||
builder: (light, dark) {
|
return _buildDynamicColor(context);
|
||||||
final lightTheme = ThemeData(
|
|
||||||
useMaterial3: true,
|
|
||||||
colorScheme: light,
|
|
||||||
);
|
|
||||||
final darkTheme = ThemeData(
|
|
||||||
useMaterial3: true,
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
colorScheme: dark,
|
|
||||||
);
|
|
||||||
if (context.isDark && light != null) {
|
|
||||||
UIs.primaryColor = light.primary;
|
|
||||||
} else if (!context.isDark && dark != null) {
|
|
||||||
UIs.primaryColor = dark.primary;
|
|
||||||
}
|
|
||||||
return _buildApp(context, light: lightTheme, dark: darkTheme);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildApp(BuildContext ctx, {ThemeData? light, ThemeData? dark}) {
|
Widget _build(BuildContext context) {
|
||||||
|
final colorSeed = Color(Stores.setting.colorSeed.fetch());
|
||||||
|
UIs.colorSeed = colorSeed;
|
||||||
|
UIs.primaryColor = colorSeed;
|
||||||
|
|
||||||
|
return _buildApp(
|
||||||
|
context,
|
||||||
|
light: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorSchemeSeed: UIs.colorSeed,
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
scrolledUnderElevation: 0.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dark: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorSchemeSeed: UIs.colorSeed,
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
scrolledUnderElevation: 0.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDynamicColor(BuildContext context) {
|
||||||
|
return DynamicColorBuilder(
|
||||||
|
builder: (light, dark) {
|
||||||
|
final lightTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: light,
|
||||||
|
);
|
||||||
|
final darkTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorScheme: dark,
|
||||||
|
);
|
||||||
|
if (context.isDark && dark != null) {
|
||||||
|
UIs.primaryColor = dark.primary;
|
||||||
|
} else if (!context.isDark && light != null) {
|
||||||
|
UIs.primaryColor = light.primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildApp(context, light: lightTheme, dark: darkTheme);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +92,16 @@ 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),
|
||||||
|
builder: (context, child) => ResponsiveBreakpoints.builder(
|
||||||
|
child: child ?? UIs.placeholder,
|
||||||
|
breakpoints: const [
|
||||||
|
Breakpoint(start: 0, end: 450, name: MOBILE),
|
||||||
|
Breakpoint(start: 451, end: 800, name: TABLET),
|
||||||
|
Breakpoint(start: 801, end: 1920, name: DESKTOP),
|
||||||
|
],
|
||||||
|
),
|
||||||
locale: locale,
|
locale: locale,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
LibLocalizations.delegate,
|
LibLocalizations.delegate,
|
||||||
@@ -76,41 +109,33 @@ 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: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
context.setLibL10n();
|
||||||
|
final appL10n = AppLocalizations.of(context);
|
||||||
|
if (appL10n != null) l10n = appL10n;
|
||||||
|
|
||||||
|
Widget child;
|
||||||
|
final intros = _IntroPage.builders;
|
||||||
|
if (intros.isNotEmpty) {
|
||||||
|
child = _IntroPage(intros);
|
||||||
|
}
|
||||||
|
|
||||||
|
child = const HomePage();
|
||||||
|
|
||||||
|
return VirtualWindowFrame(
|
||||||
|
title: BuildData.name,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
30
lib/core/chan.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:server_box/data/res/misc.dart';
|
||||||
|
import 'package:server_box/data/res/store.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 (!isIOS || !isAndroid) return;
|
||||||
|
if (!Stores.setting.autoUpdateHomeWidget.fetch()) return;
|
||||||
|
await _channel.invokeMethod('updateHomeWidget');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import 'package:server_box/data/res/build_data.dart';
|
|
||||||
|
|
||||||
extension BuildDataX on BuildData {
|
|
||||||
static const versionStr = 'v1.0.${BuildData.build}';
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n_en.dart';
|
import 'package:server_box/generated/l10n/l10n.dart';
|
||||||
|
import 'package:server_box/generated/l10n/l10n_en.dart';
|
||||||
|
|
||||||
AppLocalizations l10n = AppLocalizationsEn();
|
AppLocalizations l10n = AppLocalizationsEn();
|
||||||
|
|
||||||
|
extension LocaleX on BuildContext {
|
||||||
|
AppLocalizations get l10n => AppLocalizations.of(this)!;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: UnixPermOp(
|
||||||
|
r: userRead,
|
||||||
|
w: userWrite,
|
||||||
|
x: userExecute,
|
||||||
|
),
|
||||||
|
group: UnixPermOp(
|
||||||
|
r: groupRead,
|
||||||
|
w: groupWrite,
|
||||||
|
x: groupExecute,
|
||||||
|
),
|
||||||
|
other: UnixPermOp(
|
||||||
|
r: otherRead,
|
||||||
|
w: otherWrite,
|
||||||
|
x: otherExecute,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getRoleMode(bool r, bool w, bool x) {
|
String _getRoleMode(bool r, bool w, bool x) {
|
||||||
|
|||||||
@@ -5,72 +5,74 @@ import 'package:dartssh2/dartssh2.dart';
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import '../../data/res/misc.dart';
|
import 'package:server_box/data/res/misc.dart';
|
||||||
|
|
||||||
typedef _OnStdout = void Function(String data, SSHSession session);
|
typedef OnStdout = void Function(String data, SSHSession session);
|
||||||
typedef _OnStdin = void Function(SSHSession session);
|
typedef OnStdin = void Function(SSHSession session);
|
||||||
|
|
||||||
typedef PwdRequestFunc = Future<String?> Function(String? user);
|
typedef PwdRequestFunc = Future<String?> Function(String? user);
|
||||||
|
|
||||||
extension SSHClientX on SSHClient {
|
extension SSHClientX on SSHClient {
|
||||||
Future<SSHSession> exec(
|
Future<(SSHSession, String)> exec(
|
||||||
String cmd, {
|
OnStdin onStdin, {
|
||||||
_OnStdout? onStderr,
|
String? entry,
|
||||||
_OnStdout? onStdout,
|
SSHPtyConfig? pty,
|
||||||
_OnStdin? stdin,
|
OnStdout? onStdout,
|
||||||
bool redirectToBash = false, // not working yet. do not use
|
OnStdout? onStderr,
|
||||||
|
bool stdout = true,
|
||||||
|
bool stderr = true,
|
||||||
|
Map<String, String>? env,
|
||||||
}) async {
|
}) async {
|
||||||
final session = await execute(redirectToBash ? "head -1 | bash" : cmd);
|
final session = await execute(
|
||||||
|
entry ?? 'cat | sh',
|
||||||
if (redirectToBash) {
|
pty: pty,
|
||||||
session.stdin.add("$cmd\n".uint8List);
|
environment: env,
|
||||||
}
|
);
|
||||||
|
|
||||||
|
final result = BytesBuilder(copy: false);
|
||||||
final stdoutDone = Completer<void>();
|
final stdoutDone = Completer<void>();
|
||||||
final stderrDone = Completer<void>();
|
final stderrDone = Completer<void>();
|
||||||
|
|
||||||
if (onStdout != null) {
|
session.stdout.listen(
|
||||||
session.stdout.listen(
|
(e) {
|
||||||
(e) => onStdout(e.string, session),
|
onStdout?.call(e.string, session);
|
||||||
onDone: stdoutDone.complete,
|
if (stdout) result.add(e);
|
||||||
);
|
},
|
||||||
} else {
|
onDone: stdoutDone.complete,
|
||||||
stdoutDone.complete();
|
onError: stderrDone.completeError,
|
||||||
}
|
);
|
||||||
|
|
||||||
if (onStderr != null) {
|
session.stderr.listen(
|
||||||
session.stderr.listen(
|
(e) {
|
||||||
(e) => onStderr(e.string, session),
|
onStderr?.call(e.string, session);
|
||||||
onDone: stderrDone.complete,
|
if (stderr) result.add(e);
|
||||||
);
|
},
|
||||||
} else {
|
onDone: stderrDone.complete,
|
||||||
stderrDone.complete();
|
onError: stderrDone.completeError,
|
||||||
}
|
);
|
||||||
|
|
||||||
if (stdin != null) {
|
onStdin(session);
|
||||||
stdin(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
await stdoutDone.future;
|
await stdoutDone.future;
|
||||||
await stderrDone.future;
|
await stderrDone.future;
|
||||||
|
|
||||||
session.close();
|
return (session, result.takeBytes().string);
|
||||||
return session;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int?> execWithPwd(
|
Future<int?> execWithPwd(
|
||||||
String cmd, {
|
String script, {
|
||||||
|
String? entry,
|
||||||
BuildContext? context,
|
BuildContext? context,
|
||||||
_OnStdout? onStdout,
|
OnStdout? onStdout,
|
||||||
_OnStdout? onStderr,
|
OnStdout? onStderr,
|
||||||
_OnStdin? stdin,
|
|
||||||
bool redirectToBash = false, // not working yet. do not use
|
|
||||||
required String id,
|
required String id,
|
||||||
}) async {
|
}) async {
|
||||||
var isRequestingPwd = false;
|
var isRequestingPwd = false;
|
||||||
final session = await exec(
|
final (session, _) = await exec(
|
||||||
cmd,
|
(sess) {
|
||||||
redirectToBash: redirectToBash,
|
sess.stdin.add('$script\n'.uint8List);
|
||||||
|
sess.stdin.close();
|
||||||
|
},
|
||||||
onStderr: (data, session) async {
|
onStderr: (data, session) async {
|
||||||
onStderr?.call(data, session);
|
onStderr?.call(data, session);
|
||||||
if (isRequestingPwd) return;
|
if (isRequestingPwd) return;
|
||||||
@@ -83,56 +85,38 @@ extension SSHClientX on SSHClient {
|
|||||||
? await context.showPwdDialog(title: user, id: id)
|
? await context.showPwdDialog(title: user, id: id)
|
||||||
: null;
|
: null;
|
||||||
if (pwd == null || pwd.isEmpty) {
|
if (pwd == null || pwd.isEmpty) {
|
||||||
session.kill(SSHSignal.TERM);
|
session.stdin.close();
|
||||||
} else {
|
} else {
|
||||||
session.stdin.add('$pwd\n'.uint8List);
|
session.stdin.add('$pwd\n'.uint8List);
|
||||||
}
|
}
|
||||||
isRequestingPwd = false;
|
isRequestingPwd = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStdout: (data, sink) async {
|
onStdout: onStdout,
|
||||||
onStdout?.call(data, sink);
|
entry: entry,
|
||||||
},
|
|
||||||
stdin: stdin,
|
|
||||||
);
|
);
|
||||||
return session.exitCode;
|
return session.exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> runForOutput(
|
Future<String> execForOutput(
|
||||||
String command, {
|
String script, {
|
||||||
bool runInPty = false,
|
SSHPtyConfig? pty,
|
||||||
bool stdout = true,
|
bool stdout = true,
|
||||||
bool stderr = true,
|
bool stderr = true,
|
||||||
Map<String, String>? environment,
|
String? entry,
|
||||||
Future<void> Function(SSHSession)? action,
|
Map<String, String>? env,
|
||||||
}) async {
|
}) async {
|
||||||
final session = await execute(
|
final ret = await exec(
|
||||||
command,
|
(session) {
|
||||||
pty: runInPty ? const SSHPtyConfig() : null,
|
session.stdin.add('$script\n'.uint8List);
|
||||||
environment: environment,
|
session.stdin.close();
|
||||||
|
},
|
||||||
|
pty: pty,
|
||||||
|
env: env,
|
||||||
|
stdout: stdout,
|
||||||
|
stderr: stderr,
|
||||||
|
entry: entry,
|
||||||
);
|
);
|
||||||
|
return ret.$2;
|
||||||
final result = BytesBuilder(copy: false);
|
|
||||||
final stdoutDone = Completer<void>();
|
|
||||||
final stderrDone = Completer<void>();
|
|
||||||
|
|
||||||
session.stdout.listen(
|
|
||||||
stdout ? result.add : (_) {},
|
|
||||||
onDone: stdoutDone.complete,
|
|
||||||
onError: stderrDone.completeError,
|
|
||||||
);
|
|
||||||
|
|
||||||
session.stderr.listen(
|
|
||||||
stderr ? result.add : (_) {},
|
|
||||||
onDone: stderrDone.complete,
|
|
||||||
onError: stderrDone.completeError,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (action != null) await action(session);
|
|
||||||
|
|
||||||
await stdoutDone.future;
|
|
||||||
await stderrDone.future;
|
|
||||||
|
|
||||||
return result.takeBytes();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,255 +1,9 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.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/view/page/backup.dart';
|
|
||||||
import 'package:server_box/view/page/container.dart';
|
|
||||||
import 'package:server_box/view/page/home/home.dart';
|
|
||||||
import 'package:server_box/view/page/iperf.dart';
|
|
||||||
import 'package:server_box/view/page/ping.dart';
|
|
||||||
import 'package:server_box/view/page/private_key/edit.dart';
|
|
||||||
import 'package:server_box/view/page/private_key/list.dart';
|
|
||||||
import 'package:server_box/view/page/pve.dart';
|
|
||||||
import 'package:server_box/view/page/server/detail/view.dart';
|
|
||||||
import 'package:server_box/view/page/setting/platform/android.dart';
|
|
||||||
import 'package:server_box/view/page/setting/platform/ios.dart';
|
|
||||||
import 'package:server_box/view/page/setting/seq/srv_func_seq.dart';
|
|
||||||
import 'package:server_box/view/page/snippet/result.dart';
|
|
||||||
import 'package:server_box/view/page/ssh/page.dart';
|
|
||||||
import 'package:server_box/view/page/setting/seq/virt_key.dart';
|
|
||||||
import 'package:server_box/view/page/storage/local.dart';
|
|
||||||
|
|
||||||
import '../data/model/server/snippet.dart';
|
/// The args class for [AppRoute].
|
||||||
import '../view/page/editor.dart';
|
final class SpiRequiredArgs {
|
||||||
import '../view/page/process.dart';
|
/// The only required argument for this class.
|
||||||
import '../view/page/server/edit.dart';
|
final Spi spi;
|
||||||
import '../view/page/server/tab.dart';
|
|
||||||
import '../view/page/setting/entry.dart';
|
|
||||||
import '../view/page/setting/seq/srv_detail_seq.dart';
|
|
||||||
import '../view/page/setting/seq/srv_seq.dart';
|
|
||||||
import '../view/page/snippet/edit.dart';
|
|
||||||
import '../view/page/snippet/list.dart';
|
|
||||||
import '../view/page/storage/sftp.dart';
|
|
||||||
import '../view/page/storage/sftp_mission.dart';
|
|
||||||
|
|
||||||
class AppRoutes {
|
const SpiRequiredArgs(this.spi);
|
||||||
final Widget page;
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
AppRoutes(this.page, this.title);
|
|
||||||
|
|
||||||
Future<T?> go<T>(BuildContext context) {
|
|
||||||
return Navigator.push<T>(
|
|
||||||
context,
|
|
||||||
Stores.setting.cupertinoRoute.fetch()
|
|
||||||
? CupertinoPageRoute(builder: (context) => page)
|
|
||||||
: MaterialPageRoute(builder: (context) => page),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<T?> checkGo<T>({
|
|
||||||
required BuildContext context,
|
|
||||||
required bool Function() check,
|
|
||||||
}) {
|
|
||||||
if (check()) {
|
|
||||||
return go(context);
|
|
||||||
}
|
|
||||||
return Future.value(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes serverDetail({Key? key, required ServerPrivateInfo spi}) {
|
|
||||||
return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes serverTab({Key? key}) {
|
|
||||||
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}) {
|
|
||||||
return AppRoutes(
|
|
||||||
PrivateKeyEditPage(pki: pki),
|
|
||||||
'key_${pki == null ? 'add' : 'edit'}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes keyList({Key? key}) {
|
|
||||||
return AppRoutes(PrivateKeysListPage(key: key), 'key_detail');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
|
|
||||||
return AppRoutes(
|
|
||||||
SnippetEditPage(snippet: snippet),
|
|
||||||
'snippet_${snippet == null ? 'add' : 'edit'}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes snippetList({Key? key}) {
|
|
||||||
return AppRoutes(SnippetListPage(key: key), 'snippet_detail');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes ssh({
|
|
||||||
Key? key,
|
|
||||||
required ServerPrivateInfo spi,
|
|
||||||
String? initCmd,
|
|
||||||
Snippet? initSnippet,
|
|
||||||
}) {
|
|
||||||
return AppRoutes(
|
|
||||||
SSHPage(
|
|
||||||
key: key,
|
|
||||||
spi: spi,
|
|
||||||
initCmd: initCmd,
|
|
||||||
initSnippet: initSnippet,
|
|
||||||
),
|
|
||||||
'ssh_term',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes sshVirtKeySetting({Key? key}) {
|
|
||||||
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}) {
|
|
||||||
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes sftp(
|
|
||||||
{Key? key,
|
|
||||||
required ServerPrivateInfo spi,
|
|
||||||
String? initPath,
|
|
||||||
bool isSelect = false}) {
|
|
||||||
return AppRoutes(
|
|
||||||
SftpPage(
|
|
||||||
key: key,
|
|
||||||
spi: spi,
|
|
||||||
initPath: initPath,
|
|
||||||
isSelect: isSelect,
|
|
||||||
),
|
|
||||||
'sftp');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes backup({Key? key}) {
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// - 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}) {
|
|
||||||
// return AppRoutes(FullScreenPage(key: key), 'fullscreen');
|
|
||||||
// }
|
|
||||||
|
|
||||||
static AppRoutes home({Key? key}) {
|
|
||||||
return AppRoutes(HomePage(key: key), 'home');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes ping({Key? key}) {
|
|
||||||
return AppRoutes(PingPage(key: key), 'ping');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes process({Key? key, required ServerPrivateInfo spi}) {
|
|
||||||
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes settings({Key? key}) {
|
|
||||||
return AppRoutes(SettingPage(key: key), 'setting');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes serverOrder({Key? key}) {
|
|
||||||
return AppRoutes(ServerOrderPage(key: key), 'server_order');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes serverDetailOrder({Key? key}) {
|
|
||||||
return AppRoutes(ServerDetailOrderPage(key: key), 'server_detail_order');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes iosSettings({Key? key}) {
|
|
||||||
return AppRoutes(IOSSettingsPage(key: key), 'ios_setting');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes androidSettings({Key? key}) {
|
|
||||||
return AppRoutes(AndroidSettingsPage(key: key), 'android_setting');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes snippetResult(
|
|
||||||
{Key? key, required List<SnippetResult?> results}) {
|
|
||||||
return AppRoutes(
|
|
||||||
SnippetResultPage(
|
|
||||||
key: key,
|
|
||||||
results: results,
|
|
||||||
),
|
|
||||||
'snippet_result');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes iperf({Key? key, required ServerPrivateInfo spi}) {
|
|
||||||
return AppRoutes(IPerfPage(key: key, spi: spi), 'iperf');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes serverFuncBtnsOrder({Key? key}) {
|
|
||||||
return AppRoutes(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes pve({Key? key, required ServerPrivateInfo spi}) {
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
lib/core/sync.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:server_box/data/model/app/bak/backup2.dart';
|
||||||
|
import 'package:server_box/data/model/app/bak/utils.dart';
|
||||||
|
|
||||||
|
const bakSync = BakSyncer._();
|
||||||
|
|
||||||
|
final icloud = ICloud(containerId: 'iCloud.tech.lolli.serverbox');
|
||||||
|
|
||||||
|
final class BakSyncer extends SyncIface {
|
||||||
|
const BakSyncer._() : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveToFile() => BackupV2.backup();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Mergeable> fromFile(String path) async {
|
||||||
|
final content = await File(path).readAsString();
|
||||||
|
return MergeableUtils.fromJsonString(content).$1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +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/model/server/server_private_info.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';
|
|
||||||
|
|
||||||
/// Must put this func out of any Class.
|
/// Must put this func out of any Class.
|
||||||
///
|
///
|
||||||
/// Because of this function is called by [compute].
|
/// Because of this function is called by [compute].
|
||||||
@@ -31,7 +31,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 +42,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 +52,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 +93,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 +115,7 @@ Future<SSHClient> genClient(
|
|||||||
onStatus?.call(GenSSHClientStatus.pwd);
|
onStatus?.call(GenSSHClientStatus.pwd);
|
||||||
return SSHClient(
|
return SSHClient(
|
||||||
socket,
|
socket,
|
||||||
username: spi.user,
|
username: alterUser ?? spi.user,
|
||||||
onPasswordRequest: () => spi.pwd,
|
onPasswordRequest: () => spi.pwd,
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
// printDebug: debugPrint,
|
// printDebug: debugPrint,
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/res/provider.dart';
|
import 'package:server_box/data/provider/app.dart';
|
||||||
|
|
||||||
abstract final class KeybordInteractive {
|
abstract final class KeybordInteractive {
|
||||||
static FutureOr<List<String>?> defaultHandle(
|
static FutureOr<List<String>?> defaultHandle(
|
||||||
ServerPrivateInfo spi, {
|
Spi spi, {
|
||||||
BuildContext? ctx,
|
BuildContext? ctx,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final res = await (ctx ?? Pros.app.ctx)?.showPwdDialog(
|
final res = await (ctx ?? AppProvider.ctx)?.showPwdDialog(
|
||||||
title: '2FA ${l10n.pwd}',
|
title: l10n.pwd,
|
||||||
id: spi.id,
|
id: spi.id,
|
||||||
label: spi.id,
|
label: spi.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:computer/computer.dart';
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:icloud_storage/icloud_storage.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:server_box/data/model/app/backup.dart';
|
|
||||||
import 'package:server_box/data/model/app/sync.dart';
|
|
||||||
import 'package:server_box/data/res/misc.dart';
|
|
||||||
|
|
||||||
import '../../../data/model/app/error.dart';
|
|
||||||
|
|
||||||
abstract final class ICloud {
|
|
||||||
static const _containerId = 'iCloud.tech.lolli.serverbox';
|
|
||||||
|
|
||||||
static final _logger = Logger('iCloud');
|
|
||||||
|
|
||||||
/// Upload file to iCloud
|
|
||||||
///
|
|
||||||
/// - [relativePath] is the path relative to [Paths.doc],
|
|
||||||
/// must not starts with `/`
|
|
||||||
/// - [localPath] has higher priority than [relativePath], but only apply
|
|
||||||
/// to the local path instead of iCloud path
|
|
||||||
///
|
|
||||||
/// Return [null] if upload success, [ICloudErr] otherwise
|
|
||||||
static Future<ICloudErr?> upload({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
final completer = Completer<ICloudErr?>();
|
|
||||||
try {
|
|
||||||
await ICloudStorage.upload(
|
|
||||||
containerId: _containerId,
|
|
||||||
filePath: localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
destinationRelativePath: relativePath,
|
|
||||||
onProgress: (stream) {
|
|
||||||
stream.listen(
|
|
||||||
null,
|
|
||||||
onDone: () => completer.complete(null),
|
|
||||||
onError: (e) => completer.complete(
|
|
||||||
ICloudErr(type: ICloudErrType.generic, message: '$e'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Upload $relativePath failed', e, s);
|
|
||||||
completer.complete(ICloudErr(type: ICloudErrType.generic, message: '$e'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<ICloudFile>> getAll() async {
|
|
||||||
return await ICloudStorage.gather(
|
|
||||||
containerId: _containerId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> delete(String relativePath) async {
|
|
||||||
try {
|
|
||||||
await ICloudStorage.delete(
|
|
||||||
containerId: _containerId,
|
|
||||||
relativePath: relativePath,
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Delete $relativePath failed', e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Download file from iCloud
|
|
||||||
///
|
|
||||||
/// - [relativePath] is the path relative to [Paths.doc],
|
|
||||||
/// must not starts with `/`
|
|
||||||
/// - [localPath] has higher priority than [relativePath], but only apply
|
|
||||||
/// to the local path instead of iCloud path
|
|
||||||
///
|
|
||||||
/// Return `null` if upload success, [ICloudErr] otherwise
|
|
||||||
static Future<ICloudErr?> download({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
final completer = Completer<ICloudErr?>();
|
|
||||||
try {
|
|
||||||
await ICloudStorage.download(
|
|
||||||
containerId: _containerId,
|
|
||||||
relativePath: relativePath,
|
|
||||||
destinationFilePath: localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
onProgress: (stream) {
|
|
||||||
stream.listen(
|
|
||||||
null,
|
|
||||||
onDone: () => completer.complete(null),
|
|
||||||
onError: (e) => completer.complete(
|
|
||||||
ICloudErr(type: ICloudErrType.generic, message: '$e'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Download $relativePath failed', e, s);
|
|
||||||
completer.complete(ICloudErr(type: ICloudErrType.generic, message: '$e'));
|
|
||||||
}
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sync file between iCloud and local
|
|
||||||
///
|
|
||||||
/// - [relativePaths] is the path relative to [Paths.doc],
|
|
||||||
/// must not starts with `/`
|
|
||||||
/// - [bakPrefix] is the suffix of backup file, default to [null].
|
|
||||||
/// All files downloaded from cloud will be suffixed with [bakPrefix].
|
|
||||||
///
|
|
||||||
/// Return `null` if upload success, [ICloudErr] otherwise
|
|
||||||
static Future<SyncResult<String, ICloudErr>> syncFiles({
|
|
||||||
required Iterable<String> relativePaths,
|
|
||||||
String? bakPrefix,
|
|
||||||
}) async {
|
|
||||||
final uploadFiles = <String>[];
|
|
||||||
final downloadFiles = <String>[];
|
|
||||||
|
|
||||||
try {
|
|
||||||
final errs = <String, ICloudErr>{};
|
|
||||||
|
|
||||||
final allFiles = await getAll();
|
|
||||||
|
|
||||||
/// remove files not in relativePaths
|
|
||||||
allFiles.removeWhere((e) => !relativePaths.contains(e.relativePath));
|
|
||||||
|
|
||||||
final missions = <Future<void>>[];
|
|
||||||
|
|
||||||
/// upload files not in iCloud
|
|
||||||
final missed = relativePaths.where((e) {
|
|
||||||
return !allFiles.any((f) => f.relativePath == e);
|
|
||||||
});
|
|
||||||
missions.addAll(missed.map((e) async {
|
|
||||||
final err = await upload(relativePath: e);
|
|
||||||
if (err != null) {
|
|
||||||
errs[e] = err;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
final docPath = Paths.doc;
|
|
||||||
|
|
||||||
/// compare files in iCloud and local
|
|
||||||
missions.addAll(allFiles.map((file) async {
|
|
||||||
final relativePath = file.relativePath;
|
|
||||||
|
|
||||||
/// Check date
|
|
||||||
final localFile = File('$docPath/$relativePath');
|
|
||||||
if (!localFile.existsSync()) {
|
|
||||||
/// Local file not found, download remote file
|
|
||||||
final err = await download(relativePath: relativePath);
|
|
||||||
if (err != null) {
|
|
||||||
errs[relativePath] = err;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final localDate = await localFile.lastModified();
|
|
||||||
final remoteDate = file.contentChangeDate;
|
|
||||||
|
|
||||||
/// Same date, skip
|
|
||||||
if (remoteDate.difference(localDate) == Duration.zero) return;
|
|
||||||
|
|
||||||
/// Local is newer than remote, so upload local file
|
|
||||||
if (remoteDate.isBefore(localDate)) {
|
|
||||||
await delete(relativePath);
|
|
||||||
final err = await upload(relativePath: relativePath);
|
|
||||||
if (err != null) {
|
|
||||||
errs[relativePath] = err;
|
|
||||||
}
|
|
||||||
uploadFiles.add(relativePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remote is newer than local, so download remote
|
|
||||||
final localPath = '$docPath/${bakPrefix ?? ''}$relativePath';
|
|
||||||
final err = await download(
|
|
||||||
relativePath: relativePath,
|
|
||||||
localPath: localPath,
|
|
||||||
);
|
|
||||||
if (err != null) {
|
|
||||||
errs[relativePath] = err;
|
|
||||||
}
|
|
||||||
downloadFiles.add(relativePath);
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Future.wait(missions);
|
|
||||||
|
|
||||||
return SyncResult(up: uploadFiles, down: downloadFiles, err: errs);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Sync: $relativePaths failed', e, s);
|
|
||||||
return SyncResult(up: uploadFiles, down: downloadFiles, err: {
|
|
||||||
'Generic': ICloudErr(type: ICloudErrType.generic, message: '$e')
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
_logger.info('Sync, up: $uploadFiles, down: $downloadFiles');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> sync() async {
|
|
||||||
final result = await download(relativePath: Miscs.bakFileName);
|
|
||||||
if (result != null) {
|
|
||||||
await backup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final dlFile = await File(Paths.bak).readAsString();
|
|
||||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
|
||||||
await dlBak.restore();
|
|
||||||
|
|
||||||
await backup();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> backup() async {
|
|
||||||
await Backup.backup();
|
|
||||||
final uploadResult = await upload(relativePath: Miscs.bakFileName);
|
|
||||||
if (uploadResult != null) {
|
|
||||||
_logger.warning('Upload backup failed: $uploadResult');
|
|
||||||
} else {
|
|
||||||
_logger.info('Upload backup success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:computer/computer.dart';
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:server_box/data/model/app/backup.dart';
|
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
|
||||||
import 'package:server_box/data/res/misc.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
|
||||||
import 'package:webdav_client/webdav_client.dart';
|
|
||||||
|
|
||||||
abstract final class Webdav {
|
|
||||||
/// Some WebDAV provider only support non-root path
|
|
||||||
static const _prefix = 'srvbox/';
|
|
||||||
|
|
||||||
static var _client = WebdavClient(
|
|
||||||
url: Stores.setting.webdavUrl.fetch(),
|
|
||||||
user: Stores.setting.webdavUser.fetch(),
|
|
||||||
pwd: Stores.setting.webdavPwd.fetch(),
|
|
||||||
);
|
|
||||||
|
|
||||||
static final _logger = Logger('Webdav');
|
|
||||||
|
|
||||||
static Future<String?> test(String url, String user, String pwd) async {
|
|
||||||
final client = WebdavClient(url: url, user: user, pwd: pwd);
|
|
||||||
try {
|
|
||||||
await client.ping();
|
|
||||||
return null;
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Test failed', e, s);
|
|
||||||
return e.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> upload({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
await _client.writeFile(
|
|
||||||
localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
_prefix + relativePath,
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Upload $relativePath failed', e, s);
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> delete(String relativePath) async {
|
|
||||||
try {
|
|
||||||
await _client.remove(_prefix + relativePath);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Delete $relativePath failed', e, s);
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> download({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
await _client.readFile(
|
|
||||||
_prefix + relativePath,
|
|
||||||
localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_logger.warning('Download $relativePath failed');
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<String>> list() async {
|
|
||||||
try {
|
|
||||||
final list = await _client.readDir(_prefix);
|
|
||||||
final names = <String>[];
|
|
||||||
for (final item in list) {
|
|
||||||
if ((item.isDir ?? true) || item.name == null) continue;
|
|
||||||
names.add(item.name!);
|
|
||||||
}
|
|
||||||
return names;
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('List failed', e, s);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void changeClient(String url, String user, String pwd) {
|
|
||||||
_client = WebdavClient(url: url, user: user, pwd: pwd);
|
|
||||||
Stores.setting.webdavUrl.put(url);
|
|
||||||
Stores.setting.webdavUser.put(user);
|
|
||||||
Stores.setting.webdavPwd.put(pwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> sync() async {
|
|
||||||
final result = await download(relativePath: Miscs.bakFileName);
|
|
||||||
if (result != null) {
|
|
||||||
await backup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final dlFile = await File(Paths.bak).readAsString();
|
|
||||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
|
||||||
await dlBak.restore();
|
|
||||||
} catch (e) {
|
|
||||||
_logger.warning('Restore failed: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
await backup();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a local backup and upload it to WebDAV
|
|
||||||
static Future<void> backup() async {
|
|
||||||
await Backup.backup();
|
|
||||||
final uploadResult = await upload(relativePath: Miscs.bakFileName);
|
|
||||||
if (uploadResult != null) {
|
|
||||||
_logger.warning('Upload failed: $uploadResult');
|
|
||||||
} else {
|
|
||||||
_logger.info('Upload success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
||||||
import 'package:server_box/data/model/server/snippet.dart';
|
|
||||||
import 'package:server_box/data/res/misc.dart';
|
|
||||||
import 'package:server_box/data/res/provider.dart';
|
|
||||||
import 'package:server_box/data/res/rebuild.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
|
||||||
|
|
||||||
const backupFormatVersion = 1;
|
|
||||||
|
|
||||||
final _logger = Logger('Backup');
|
|
||||||
|
|
||||||
class Backup {
|
|
||||||
// backup format version
|
|
||||||
final int version;
|
|
||||||
final String date;
|
|
||||||
final List<ServerPrivateInfo> spis;
|
|
||||||
final List<Snippet> snippets;
|
|
||||||
final List<PrivateKeyInfo> keys;
|
|
||||||
final Map<String, dynamic> container;
|
|
||||||
final Map<String, dynamic> history;
|
|
||||||
final int? lastModTime;
|
|
||||||
|
|
||||||
const Backup({
|
|
||||||
required this.version,
|
|
||||||
required this.date,
|
|
||||||
required this.spis,
|
|
||||||
required this.snippets,
|
|
||||||
required this.keys,
|
|
||||||
required this.container,
|
|
||||||
required this.history,
|
|
||||||
this.lastModTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
Backup.fromJson(Map<String, dynamic> json)
|
|
||||||
: version = json['version'] as int,
|
|
||||||
date = json['date'],
|
|
||||||
spis = (json['spis'] as List)
|
|
||||||
.map((e) => ServerPrivateInfo.fromJson(e))
|
|
||||||
.toList(),
|
|
||||||
snippets =
|
|
||||||
(json['snippets'] as List).map((e) => Snippet.fromJson(e)).toList(),
|
|
||||||
keys = (json['keys'] as List)
|
|
||||||
.map((e) => PrivateKeyInfo.fromJson(e))
|
|
||||||
.toList(),
|
|
||||||
container = json['container'] ?? {},
|
|
||||||
lastModTime = json['lastModTime'],
|
|
||||||
history = json['history'] ?? {};
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'version': version,
|
|
||||||
'date': date,
|
|
||||||
'spis': spis,
|
|
||||||
'snippets': snippets,
|
|
||||||
'keys': keys,
|
|
||||||
'container': container,
|
|
||||||
'lastModTime': lastModTime,
|
|
||||||
'history': history,
|
|
||||||
};
|
|
||||||
|
|
||||||
Backup.loadFromStore()
|
|
||||||
: version = backupFormatVersion,
|
|
||||||
date = DateTime.now().toString().split('.').firstOrNull ?? '',
|
|
||||||
spis = Stores.server.fetch(),
|
|
||||||
snippets = Stores.snippet.fetch(),
|
|
||||||
keys = Stores.key.fetch(),
|
|
||||||
container = Stores.container.box.toJson(),
|
|
||||||
lastModTime = Stores.lastModTime,
|
|
||||||
history = Stores.history.box.toJson();
|
|
||||||
|
|
||||||
static Future<String> backup([String? name]) async {
|
|
||||||
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson()));
|
|
||||||
final path = '${Paths.doc}/${name ?? Miscs.bakFileName}';
|
|
||||||
await File(path).writeAsString(result);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> restore({bool force = false}) async {
|
|
||||||
final curTime = Stores.lastModTime ?? 0;
|
|
||||||
final bakTime = lastModTime ?? 0;
|
|
||||||
final shouldRestore = force || curTime < bakTime;
|
|
||||||
if (!shouldRestore) {
|
|
||||||
_logger.info('No need to restore, local is newer');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snippets
|
|
||||||
final nowSnippets = Stores.snippet.box.keys.toSet();
|
|
||||||
final bakSnippets = snippets.map((e) => e.name).toSet();
|
|
||||||
final newSnippets = bakSnippets.difference(nowSnippets);
|
|
||||||
final delSnippets = nowSnippets.difference(bakSnippets);
|
|
||||||
final updateSnippets = nowSnippets.intersection(bakSnippets);
|
|
||||||
for (final s in newSnippets) {
|
|
||||||
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
|
|
||||||
}
|
|
||||||
for (final s in delSnippets) {
|
|
||||||
Stores.snippet.box.delete(s);
|
|
||||||
}
|
|
||||||
for (final s in updateSnippets) {
|
|
||||||
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerPrivateInfo
|
|
||||||
final nowSpis = Stores.server.box.keys.toSet();
|
|
||||||
final bakSpis = spis.map((e) => e.id).toSet();
|
|
||||||
final newSpis = bakSpis.difference(nowSpis);
|
|
||||||
final delSpis = nowSpis.difference(bakSpis);
|
|
||||||
final updateSpis = nowSpis.intersection(bakSpis);
|
|
||||||
for (final s in newSpis) {
|
|
||||||
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
|
|
||||||
}
|
|
||||||
for (final s in delSpis) {
|
|
||||||
Stores.server.box.delete(s);
|
|
||||||
}
|
|
||||||
for (final s in updateSpis) {
|
|
||||||
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrivateKeyInfo
|
|
||||||
final nowKeys = Stores.key.box.keys.toSet();
|
|
||||||
final bakKeys = keys.map((e) => e.id).toSet();
|
|
||||||
final newKeys = bakKeys.difference(nowKeys);
|
|
||||||
final delKeys = nowKeys.difference(bakKeys);
|
|
||||||
final updateKeys = nowKeys.intersection(bakKeys);
|
|
||||||
for (final s in newKeys) {
|
|
||||||
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
|
|
||||||
}
|
|
||||||
for (final s in delKeys) {
|
|
||||||
Stores.key.box.delete(s);
|
|
||||||
}
|
|
||||||
for (final s in updateKeys) {
|
|
||||||
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
|
|
||||||
}
|
|
||||||
|
|
||||||
// History
|
|
||||||
final nowHistory = Stores.history.box.keys.toSet();
|
|
||||||
final bakHistory = history.keys.toSet();
|
|
||||||
final newHistory = bakHistory.difference(nowHistory);
|
|
||||||
final delHistory = nowHistory.difference(bakHistory);
|
|
||||||
final updateHistory = nowHistory.intersection(bakHistory);
|
|
||||||
for (final s in newHistory) {
|
|
||||||
Stores.history.box.put(s, history[s]);
|
|
||||||
}
|
|
||||||
for (final s in delHistory) {
|
|
||||||
Stores.history.box.delete(s);
|
|
||||||
}
|
|
||||||
for (final s in updateHistory) {
|
|
||||||
Stores.history.box.put(s, history[s]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container
|
|
||||||
final nowContainer = Stores.container.box.keys.toSet();
|
|
||||||
final bakContainer = container.keys.toSet();
|
|
||||||
final newContainer = bakContainer.difference(nowContainer);
|
|
||||||
final delContainer = nowContainer.difference(bakContainer);
|
|
||||||
final updateContainer = nowContainer.intersection(bakContainer);
|
|
||||||
for (final s in newContainer) {
|
|
||||||
Stores.container.box.put(s, container[s]);
|
|
||||||
}
|
|
||||||
for (final s in delContainer) {
|
|
||||||
Stores.container.box.delete(s);
|
|
||||||
}
|
|
||||||
for (final s in updateContainer) {
|
|
||||||
Stores.container.box.put(s, container[s]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pros.reload();
|
|
||||||
RNodes.app.build();
|
|
||||||
|
|
||||||
_logger.info('Restore success');
|
|
||||||
}
|
|
||||||
|
|
||||||
Backup.fromJsonString(String raw)
|
|
||||||
: this.fromJson(json.decode(_diyDecrypt(raw)));
|
|
||||||
}
|
|
||||||
|
|
||||||
String _diyEncrypt(String raw) => json.encode(
|
|
||||||
raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false),
|
|
||||||
);
|
|
||||||
|
|
||||||
String _diyDecrypt(String raw) {
|
|
||||||
try {
|
|
||||||
final list = json.decode(raw);
|
|
||||||
final sb = StringBuffer();
|
|
||||||
for (final e in list) {
|
|
||||||
sb.writeCharCode((e - 1) ~/ 2);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
} catch (e, trace) {
|
|
||||||
Loggers.app.warning('Backup decrypt failed', e, trace);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
237
lib/data/model/app/bak/backup.dart
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||||
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
|
import 'package:server_box/data/res/misc.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
|
part 'backup.g.dart';
|
||||||
|
|
||||||
|
const backupFormatVersion = 1;
|
||||||
|
|
||||||
|
final _logger = Logger('Backup');
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Backup implements Mergeable {
|
||||||
|
// backup format version
|
||||||
|
final int version;
|
||||||
|
final String date;
|
||||||
|
final List<Spi> spis;
|
||||||
|
final List<Snippet> snippets;
|
||||||
|
final List<PrivateKeyInfo> keys;
|
||||||
|
final Map<String, dynamic> container;
|
||||||
|
final Map<String, dynamic> history;
|
||||||
|
final int? lastModTime;
|
||||||
|
final Map<String, dynamic>? settings;
|
||||||
|
|
||||||
|
const Backup({
|
||||||
|
required this.version,
|
||||||
|
required this.date,
|
||||||
|
required this.spis,
|
||||||
|
required this.snippets,
|
||||||
|
required this.keys,
|
||||||
|
required this.container,
|
||||||
|
required this.history,
|
||||||
|
required this.settings,
|
||||||
|
this.lastModTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Backup.fromJson(Map<String, dynamic> json) => _$BackupFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$BackupToJson(this);
|
||||||
|
|
||||||
|
static Future<Backup> loadFromStore() async {
|
||||||
|
final lastModTime = Stores.lastModTime;
|
||||||
|
return Backup(
|
||||||
|
version: backupFormatVersion,
|
||||||
|
date: DateTime.now().toString().split('.').firstOrNull ?? '',
|
||||||
|
spis: Stores.server.fetch(),
|
||||||
|
snippets: Stores.snippet.fetch(),
|
||||||
|
keys: Stores.key.fetch(),
|
||||||
|
container: Stores.container.getAllMap(),
|
||||||
|
lastModTime: lastModTime,
|
||||||
|
history: Stores.history.getAllMap(),
|
||||||
|
settings: Stores.setting.getAllMap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> backup([String? name]) async {
|
||||||
|
final bak = await Backup.loadFromStore();
|
||||||
|
final result = _diyEncrypt(json.encode(bak.toJson()));
|
||||||
|
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
|
||||||
|
await File(path).writeAsString(result);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> merge({bool force = false}) async {
|
||||||
|
final curTime = Stores.lastModTime;
|
||||||
|
final bakTime = lastModTime ?? 0;
|
||||||
|
final shouldRestore = force || curTime < bakTime;
|
||||||
|
if (!shouldRestore) {
|
||||||
|
_logger.info('No need to restore, local is newer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snippets
|
||||||
|
if (force) {
|
||||||
|
for (final s in snippets) {
|
||||||
|
Stores.snippet.box.put(s.name, s);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final nowSnippets = Stores.snippet.box.keys.toSet();
|
||||||
|
final bakSnippets = snippets.map((e) => e.name).toSet();
|
||||||
|
final newSnippets = bakSnippets.difference(nowSnippets);
|
||||||
|
final delSnippets = nowSnippets.difference(bakSnippets);
|
||||||
|
final updateSnippets = nowSnippets.intersection(bakSnippets);
|
||||||
|
for (final s in newSnippets) {
|
||||||
|
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
|
||||||
|
}
|
||||||
|
for (final s in delSnippets) {
|
||||||
|
Stores.snippet.box.delete(s);
|
||||||
|
}
|
||||||
|
for (final s in updateSnippets) {
|
||||||
|
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerPrivateInfo
|
||||||
|
if (force) {
|
||||||
|
for (final s in spis) {
|
||||||
|
Stores.server.box.put(s.id, s);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final nowSpis = Stores.server.box.keys.toSet();
|
||||||
|
final bakSpis = spis.map((e) => e.id).toSet();
|
||||||
|
final newSpis = bakSpis.difference(nowSpis);
|
||||||
|
final delSpis = nowSpis.difference(bakSpis);
|
||||||
|
final updateSpis = nowSpis.intersection(bakSpis);
|
||||||
|
for (final s in newSpis) {
|
||||||
|
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
|
||||||
|
}
|
||||||
|
for (final s in delSpis) {
|
||||||
|
Stores.server.box.delete(s);
|
||||||
|
}
|
||||||
|
for (final s in updateSpis) {
|
||||||
|
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKeyInfo
|
||||||
|
if (force) {
|
||||||
|
for (final s in keys) {
|
||||||
|
Stores.key.box.put(s.id, s);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final nowKeys = Stores.key.box.keys.toSet();
|
||||||
|
final bakKeys = keys.map((e) => e.id).toSet();
|
||||||
|
final newKeys = bakKeys.difference(nowKeys);
|
||||||
|
final delKeys = nowKeys.difference(bakKeys);
|
||||||
|
final updateKeys = nowKeys.intersection(bakKeys);
|
||||||
|
for (final s in newKeys) {
|
||||||
|
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
|
||||||
|
}
|
||||||
|
for (final s in delKeys) {
|
||||||
|
Stores.key.box.delete(s);
|
||||||
|
}
|
||||||
|
for (final s in updateKeys) {
|
||||||
|
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// History
|
||||||
|
if (force) {
|
||||||
|
Stores.history.box.putAll(history);
|
||||||
|
} else {
|
||||||
|
final nowHistory = Stores.history.box.keys.toSet();
|
||||||
|
final bakHistory = history.keys.toSet();
|
||||||
|
final newHistory = bakHistory.difference(nowHistory);
|
||||||
|
final delHistory = nowHistory.difference(bakHistory);
|
||||||
|
final updateHistory = nowHistory.intersection(bakHistory);
|
||||||
|
for (final s in newHistory) {
|
||||||
|
Stores.history.box.put(s, history[s]);
|
||||||
|
}
|
||||||
|
for (final s in delHistory) {
|
||||||
|
Stores.history.box.delete(s);
|
||||||
|
}
|
||||||
|
for (final s in updateHistory) {
|
||||||
|
Stores.history.box.put(s, history[s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container
|
||||||
|
if (force) {
|
||||||
|
Stores.container.box.putAll(container);
|
||||||
|
} else {
|
||||||
|
final nowContainer = Stores.container.box.keys.toSet();
|
||||||
|
final bakContainer = container.keys.toSet();
|
||||||
|
final newContainer = bakContainer.difference(nowContainer);
|
||||||
|
final delContainer = nowContainer.difference(bakContainer);
|
||||||
|
final updateContainer = nowContainer.intersection(bakContainer);
|
||||||
|
for (final s in newContainer) {
|
||||||
|
Stores.container.box.put(s, container[s]);
|
||||||
|
}
|
||||||
|
for (final s in delContainer) {
|
||||||
|
Stores.container.box.delete(s);
|
||||||
|
}
|
||||||
|
for (final s in updateContainer) {
|
||||||
|
Stores.container.box.put(s, container[s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
final settings_ = settings;
|
||||||
|
if (settings_ != null) {
|
||||||
|
if (force) {
|
||||||
|
Stores.setting.box.putAll(settings_);
|
||||||
|
} else {
|
||||||
|
final nowSettings = Stores.setting.box.keys.toSet();
|
||||||
|
final bakSettings = settings_.keys.toSet();
|
||||||
|
final newSettings = bakSettings.difference(nowSettings);
|
||||||
|
final delSettings = nowSettings.difference(bakSettings);
|
||||||
|
final updateSettings = nowSettings.intersection(bakSettings);
|
||||||
|
for (final s in newSettings) {
|
||||||
|
Stores.setting.box.put(s, settings_[s]);
|
||||||
|
}
|
||||||
|
for (final s in delSettings) {
|
||||||
|
Stores.setting.box.delete(s);
|
||||||
|
}
|
||||||
|
for (final s in updateSettings) {
|
||||||
|
Stores.setting.box.put(s, settings_[s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Provider.reload();
|
||||||
|
RNodes.app.notify();
|
||||||
|
|
||||||
|
_logger.info('Restore success');
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Backup.fromJsonString(String raw) =>
|
||||||
|
Backup.fromJson(json.decode(_diyDecrypt(raw)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String _diyEncrypt(String raw) => json.encode(
|
||||||
|
raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false),
|
||||||
|
);
|
||||||
|
|
||||||
|
String _diyDecrypt(String raw) {
|
||||||
|
try {
|
||||||
|
final list = json.decode(raw);
|
||||||
|
final sb = StringBuffer();
|
||||||
|
for (final e in list) {
|
||||||
|
sb.writeCharCode((e - 1) ~/ 2);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (e, trace) {
|
||||||
|
Loggers.app.warning('Backup decrypt failed', e, trace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
37
lib/data/model/app/bak/backup.g.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'backup.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Backup _$BackupFromJson(Map<String, dynamic> json) => Backup(
|
||||||
|
version: (json['version'] as num).toInt(),
|
||||||
|
date: json['date'] as String,
|
||||||
|
spis: (json['spis'] as List<dynamic>)
|
||||||
|
.map((e) => Spi.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
snippets: (json['snippets'] as List<dynamic>)
|
||||||
|
.map((e) => Snippet.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
keys: (json['keys'] as List<dynamic>)
|
||||||
|
.map((e) => PrivateKeyInfo.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
container: json['container'] as Map<String, dynamic>,
|
||||||
|
history: json['history'] as Map<String, dynamic>,
|
||||||
|
settings: json['settings'] as Map<String, dynamic>?,
|
||||||
|
lastModTime: (json['lastModTime'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$BackupToJson(Backup instance) => <String, dynamic>{
|
||||||
|
'version': instance.version,
|
||||||
|
'date': instance.date,
|
||||||
|
'spis': instance.spis,
|
||||||
|
'snippets': instance.snippets,
|
||||||
|
'keys': instance.keys,
|
||||||
|
'container': instance.container,
|
||||||
|
'history': instance.history,
|
||||||
|
'lastModTime': instance.lastModTime,
|
||||||
|
'settings': instance.settings,
|
||||||
|
};
|
||||||
89
lib/data/model/app/bak/backup2.dart
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:server_box/data/res/misc.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
|
part 'backup2.freezed.dart';
|
||||||
|
part 'backup2.g.dart';
|
||||||
|
|
||||||
|
final _loggerV2 = Logger('BackupV2');
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class BackupV2 with _$BackupV2 implements Mergeable {
|
||||||
|
const BackupV2._();
|
||||||
|
|
||||||
|
/// Construct a backup with the latest format (v2).
|
||||||
|
///
|
||||||
|
/// All `Map<String, dynamic>` are:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "key1": Model{},
|
||||||
|
/// "_lastModTime": {
|
||||||
|
/// "key1": 1234567890,
|
||||||
|
/// },
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
const factory BackupV2({
|
||||||
|
required int version,
|
||||||
|
required int date,
|
||||||
|
required Map<String, Object?> spis,
|
||||||
|
required Map<String, Object?> snippets,
|
||||||
|
required Map<String, Object?> keys,
|
||||||
|
required Map<String, Object?> container,
|
||||||
|
required Map<String, Object?> history,
|
||||||
|
required Map<String, Object?> settings,
|
||||||
|
}) = _BackupV2;
|
||||||
|
|
||||||
|
factory BackupV2.fromJson(Map<String, dynamic> json) => _$BackupV2FromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> merge({bool force = false}) async {
|
||||||
|
_loggerV2.info('Merging...');
|
||||||
|
|
||||||
|
// Merge each store
|
||||||
|
await Mergeable.mergeStore(backupData: spis, store: Stores.server, force: force);
|
||||||
|
await Mergeable.mergeStore(backupData: snippets, store: Stores.snippet, force: force);
|
||||||
|
await Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force);
|
||||||
|
await Mergeable.mergeStore(backupData: container, store: Stores.container, force: force);
|
||||||
|
await Mergeable.mergeStore(backupData: history, store: Stores.history, force: force);
|
||||||
|
await Mergeable.mergeStore(backupData: settings, store: Stores.setting, force: force);
|
||||||
|
|
||||||
|
// Reload providers and notify listeners
|
||||||
|
Provider.reload();
|
||||||
|
RNodes.app.notify();
|
||||||
|
|
||||||
|
_loggerV2.info('Merge completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
static const formatVer = 2;
|
||||||
|
|
||||||
|
static Future<BackupV2> loadFromStore() async {
|
||||||
|
return BackupV2(
|
||||||
|
version: formatVer,
|
||||||
|
date: DateTimeX.timestamp,
|
||||||
|
spis: Stores.server.getAllMap(includeInternalKeys: true),
|
||||||
|
snippets: Stores.snippet.getAllMap(includeInternalKeys: true),
|
||||||
|
keys: Stores.key.getAllMap(includeInternalKeys: true),
|
||||||
|
container: Stores.container.getAllMap(includeInternalKeys: true),
|
||||||
|
history: Stores.history.getAllMap(includeInternalKeys: true),
|
||||||
|
settings: Stores.setting.getAllMap(includeInternalKeys: true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> backup([String? name]) async {
|
||||||
|
final bak = await BackupV2.loadFromStore();
|
||||||
|
final result = json.encode(bak.toJson());
|
||||||
|
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
|
||||||
|
await File(path).writeAsString(result);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BackupV2.fromJsonString(String jsonString) {
|
||||||
|
final map = json.decode(jsonString) as Map<String, dynamic>;
|
||||||
|
return BackupV2.fromJson(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
205
lib/data/model/app/bak/backup2.freezed.dart
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'backup2.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$BackupV2 {
|
||||||
|
|
||||||
|
int get version; int get date; Map<String, Object?> get spis; Map<String, Object?> get snippets; Map<String, Object?> get keys; Map<String, Object?> get container; Map<String, Object?> get history; Map<String, Object?> get settings;
|
||||||
|
/// Create a copy of BackupV2
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$BackupV2CopyWith<BackupV2> get copyWith => _$BackupV2CopyWithImpl<BackupV2>(this as BackupV2, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this BackupV2 to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other.spis, spis)&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.keys, keys)&&const DeepCollectionEquality().equals(other.container, container)&&const DeepCollectionEquality().equals(other.history, history)&&const DeepCollectionEquality().equals(other.settings, settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(spis),const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(keys),const DeepCollectionEquality().hash(container),const DeepCollectionEquality().hash(history),const DeepCollectionEquality().hash(settings));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $BackupV2CopyWith<$Res> {
|
||||||
|
factory $BackupV2CopyWith(BackupV2 value, $Res Function(BackupV2) _then) = _$BackupV2CopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
int version, int date, Map<String, Object?> spis, Map<String, Object?> snippets, Map<String, Object?> keys, Map<String, Object?> container, Map<String, Object?> history, Map<String, Object?> settings
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$BackupV2CopyWithImpl<$Res>
|
||||||
|
implements $BackupV2CopyWith<$Res> {
|
||||||
|
_$BackupV2CopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final BackupV2 _self;
|
||||||
|
final $Res Function(BackupV2) _then;
|
||||||
|
|
||||||
|
/// Create a copy of BackupV2
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,spis: null == spis ? _self.spis : spis // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,snippets: null == snippets ? _self.snippets : snippets // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,keys: null == keys ? _self.keys : keys // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,container: null == container ? _self.container : container // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,history: null == history ? _self.history : history // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,settings: null == settings ? _self.settings : settings // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _BackupV2 extends BackupV2 {
|
||||||
|
const _BackupV2({required this.version, required this.date, required final Map<String, Object?> spis, required final Map<String, Object?> snippets, required final Map<String, Object?> keys, required final Map<String, Object?> container, required final Map<String, Object?> history, required final Map<String, Object?> settings}): _spis = spis,_snippets = snippets,_keys = keys,_container = container,_history = history,_settings = settings,super._();
|
||||||
|
factory _BackupV2.fromJson(Map<String, dynamic> json) => _$BackupV2FromJson(json);
|
||||||
|
|
||||||
|
@override final int version;
|
||||||
|
@override final int date;
|
||||||
|
final Map<String, Object?> _spis;
|
||||||
|
@override Map<String, Object?> get spis {
|
||||||
|
if (_spis is EqualUnmodifiableMapView) return _spis;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_spis);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object?> _snippets;
|
||||||
|
@override Map<String, Object?> get snippets {
|
||||||
|
if (_snippets is EqualUnmodifiableMapView) return _snippets;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_snippets);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object?> _keys;
|
||||||
|
@override Map<String, Object?> get keys {
|
||||||
|
if (_keys is EqualUnmodifiableMapView) return _keys;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object?> _container;
|
||||||
|
@override Map<String, Object?> get container {
|
||||||
|
if (_container is EqualUnmodifiableMapView) return _container;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_container);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object?> _history;
|
||||||
|
@override Map<String, Object?> get history {
|
||||||
|
if (_history is EqualUnmodifiableMapView) return _history;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_history);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object?> _settings;
|
||||||
|
@override Map<String, Object?> get settings {
|
||||||
|
if (_settings is EqualUnmodifiableMapView) return _settings;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of BackupV2
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$BackupV2CopyWith<_BackupV2> get copyWith => __$BackupV2CopyWithImpl<_BackupV2>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$BackupV2ToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupV2&&(identical(other.version, version) || other.version == version)&&(identical(other.date, date) || other.date == date)&&const DeepCollectionEquality().equals(other._spis, _spis)&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._keys, _keys)&&const DeepCollectionEquality().equals(other._container, _container)&&const DeepCollectionEquality().equals(other._history, _history)&&const DeepCollectionEquality().equals(other._settings, _settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,version,date,const DeepCollectionEquality().hash(_spis),const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_keys),const DeepCollectionEquality().hash(_container),const DeepCollectionEquality().hash(_history),const DeepCollectionEquality().hash(_settings));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$BackupV2CopyWith<$Res> implements $BackupV2CopyWith<$Res> {
|
||||||
|
factory _$BackupV2CopyWith(_BackupV2 value, $Res Function(_BackupV2) _then) = __$BackupV2CopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
int version, int date, Map<String, Object?> spis, Map<String, Object?> snippets, Map<String, Object?> keys, Map<String, Object?> container, Map<String, Object?> history, Map<String, Object?> settings
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$BackupV2CopyWithImpl<$Res>
|
||||||
|
implements _$BackupV2CopyWith<$Res> {
|
||||||
|
__$BackupV2CopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _BackupV2 _self;
|
||||||
|
final $Res Function(_BackupV2) _then;
|
||||||
|
|
||||||
|
/// Create a copy of BackupV2
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? date = null,Object? spis = null,Object? snippets = null,Object? keys = null,Object? container = null,Object? history = null,Object? settings = null,}) {
|
||||||
|
return _then(_BackupV2(
|
||||||
|
version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,spis: null == spis ? _self._spis : spis // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,snippets: null == snippets ? _self._snippets : snippets // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,keys: null == keys ? _self._keys : keys // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,container: null == container ? _self._container : container // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,history: null == history ? _self._history : history // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,settings: null == settings ? _self._settings : settings // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, Object?>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
29
lib/data/model/app/bak/backup2.g.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'backup2.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_BackupV2 _$BackupV2FromJson(Map<String, dynamic> json) => _BackupV2(
|
||||||
|
version: (json['version'] as num).toInt(),
|
||||||
|
date: (json['date'] as num).toInt(),
|
||||||
|
spis: json['spis'] as Map<String, dynamic>,
|
||||||
|
snippets: json['snippets'] as Map<String, dynamic>,
|
||||||
|
keys: json['keys'] as Map<String, dynamic>,
|
||||||
|
container: json['container'] as Map<String, dynamic>,
|
||||||
|
history: json['history'] as Map<String, dynamic>,
|
||||||
|
settings: json['settings'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$BackupV2ToJson(_BackupV2 instance) => <String, dynamic>{
|
||||||
|
'version': instance.version,
|
||||||
|
'date': instance.date,
|
||||||
|
'spis': instance.spis,
|
||||||
|
'snippets': instance.snippets,
|
||||||
|
'keys': instance.keys,
|
||||||
|
'container': instance.container,
|
||||||
|
'history': instance.history,
|
||||||
|
'settings': instance.settings,
|
||||||
|
};
|
||||||
15
lib/data/model/app/bak/utils.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:server_box/data/model/app/bak/backup.dart';
|
||||||
|
import 'package:server_box/data/model/app/bak/backup2.dart';
|
||||||
|
|
||||||
|
abstract final class MergeableUtils {
|
||||||
|
static (Mergeable, String) fromJsonString(String json) {
|
||||||
|
try {
|
||||||
|
final bak = BackupV2.fromJsonString(json);
|
||||||
|
return (bak, DateTime.fromMillisecondsSinceEpoch(bak.date).hms());
|
||||||
|
} catch (e) {
|
||||||
|
final bak = Backup.fromJsonString(json);
|
||||||
|
return (bak, bak.date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,6 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
|
||||||
enum ErrFrom {
|
|
||||||
unknown,
|
|
||||||
apt,
|
|
||||||
docker,
|
|
||||||
sftp,
|
|
||||||
ssh,
|
|
||||||
status,
|
|
||||||
icloud,
|
|
||||||
webdav,
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class Err<T> {
|
|
||||||
final ErrFrom from;
|
|
||||||
final T type;
|
|
||||||
final String? message;
|
|
||||||
|
|
||||||
String? get solution;
|
|
||||||
|
|
||||||
Err({required this.from, required this.type, this.message});
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SSHErrType {
|
enum SSHErrType {
|
||||||
unknown,
|
unknown,
|
||||||
connect,
|
connect,
|
||||||
@@ -35,7 +14,7 @@ enum SSHErrType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SSHErr extends Err<SSHErrType> {
|
class SSHErr extends Err<SSHErrType> {
|
||||||
SSHErr({required super.type, super.message}) : super(from: ErrFrom.ssh);
|
SSHErr({required super.type, super.message});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get solution => switch (type) {
|
String? get solution => switch (type) {
|
||||||
@@ -45,11 +24,6 @@ class SSHErr extends Err<SSHErrType> {
|
|||||||
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
|
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'SSHErr<$type>: $message';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ContainerErrType {
|
enum ContainerErrType {
|
||||||
@@ -65,16 +39,10 @@ enum ContainerErrType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ContainerErr extends Err<ContainerErrType> {
|
class ContainerErr extends Err<ContainerErrType> {
|
||||||
ContainerErr({required super.type, super.message})
|
ContainerErr({required super.type, super.message});
|
||||||
: super(from: ErrFrom.docker);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get solution => null;
|
String? get solution => null;
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ContainerErr<$type>: $message';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ICloudErrType {
|
enum ICloudErrType {
|
||||||
@@ -84,15 +52,10 @@ enum ICloudErrType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ICloudErr extends Err<ICloudErrType> {
|
class ICloudErr extends Err<ICloudErrType> {
|
||||||
ICloudErr({required super.type, super.message}) : super(from: ErrFrom.icloud);
|
ICloudErr({required super.type, super.message});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get solution => null;
|
String? get solution => null;
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ICloudErr<$type>: $message';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WebdavErrType {
|
enum WebdavErrType {
|
||||||
@@ -102,15 +65,10 @@ enum WebdavErrType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WebdavErr extends Err<WebdavErrType> {
|
class WebdavErr extends Err<WebdavErrType> {
|
||||||
WebdavErr({required super.type, super.message}) : super(from: ErrFrom.webdav);
|
WebdavErr({required super.type, super.message});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get solution => null;
|
String? get solution => null;
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'WebdavErr<$type>: $message';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PveErrType {
|
enum PveErrType {
|
||||||
@@ -121,13 +79,8 @@ enum PveErrType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PveErr extends Err<PveErrType> {
|
class PveErr extends Err<PveErrType> {
|
||||||
PveErr({required super.type, super.message}) : super(from: ErrFrom.status);
|
PveErr({required super.type, super.message});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get solution => null;
|
String? get solution => null;
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'PveErr<$type>: $message';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,64 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
|
||||||
|
|
||||||
@HiveType(typeId: 6)
|
|
||||||
enum ServerFuncBtn {
|
enum ServerFuncBtn {
|
||||||
@HiveField(0)
|
terminal(),
|
||||||
terminal,
|
sftp(),
|
||||||
@HiveField(1)
|
container(),
|
||||||
sftp,
|
process(),
|
||||||
@HiveField(2)
|
//pkg(),
|
||||||
container,
|
snippet(),
|
||||||
@HiveField(3)
|
iperf(),
|
||||||
process,
|
// pve(),
|
||||||
@HiveField(4)
|
systemd(1058),
|
||||||
pkg,
|
|
||||||
@HiveField(5)
|
|
||||||
snippet,
|
|
||||||
@HiveField(6)
|
|
||||||
iperf,
|
|
||||||
// @HiveField(7)
|
|
||||||
// pve,
|
|
||||||
;
|
;
|
||||||
|
|
||||||
|
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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'server_func.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
|
||||||
@override
|
|
||||||
final int typeId = 6;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ServerFuncBtn read(BinaryReader reader) {
|
|
||||||
switch (reader.readByte()) {
|
|
||||||
case 0:
|
|
||||||
return ServerFuncBtn.terminal;
|
|
||||||
case 1:
|
|
||||||
return ServerFuncBtn.sftp;
|
|
||||||
case 2:
|
|
||||||
return ServerFuncBtn.container;
|
|
||||||
case 3:
|
|
||||||
return ServerFuncBtn.process;
|
|
||||||
case 4:
|
|
||||||
return ServerFuncBtn.pkg;
|
|
||||||
case 5:
|
|
||||||
return ServerFuncBtn.snippet;
|
|
||||||
case 6:
|
|
||||||
return ServerFuncBtn.iperf;
|
|
||||||
default:
|
|
||||||
return ServerFuncBtn.terminal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, ServerFuncBtn obj) {
|
|
||||||
switch (obj) {
|
|
||||||
case ServerFuncBtn.terminal:
|
|
||||||
writer.writeByte(0);
|
|
||||||
break;
|
|
||||||
case ServerFuncBtn.sftp:
|
|
||||||
writer.writeByte(1);
|
|
||||||
break;
|
|
||||||
case ServerFuncBtn.container:
|
|
||||||
writer.writeByte(2);
|
|
||||||
break;
|
|
||||||
case ServerFuncBtn.process:
|
|
||||||
writer.writeByte(3);
|
|
||||||
break;
|
|
||||||
case ServerFuncBtn.pkg:
|
|
||||||
writer.writeByte(4);
|
|
||||||
break;
|
|
||||||
case ServerFuncBtn.snippet:
|
|
||||||
writer.writeByte(5);
|
|
||||||
break;
|
|
||||||
case ServerFuncBtn.iperf:
|
|
||||||
writer.writeByte(6);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is ServerFuncBtnAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,93 +1,73 @@
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:fl_lib/fl_lib.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';
|
|
||||||
|
|
||||||
@HiveType(typeId: 5)
|
|
||||||
enum NetViewType {
|
enum NetViewType {
|
||||||
@HiveField(0)
|
|
||||||
conn,
|
conn,
|
||||||
@HiveField(1)
|
|
||||||
speed,
|
speed,
|
||||||
@HiveField(2)
|
|
||||||
traffic;
|
traffic;
|
||||||
|
|
||||||
NetViewType get next {
|
NetViewType get next => switch (this) {
|
||||||
switch (this) {
|
conn => speed,
|
||||||
case conn:
|
speed => traffic,
|
||||||
return speed;
|
traffic => conn,
|
||||||
case speed:
|
};
|
||||||
return traffic;
|
|
||||||
case traffic:
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String get toStr {
|
String get toStr => switch (this) {
|
||||||
switch (this) {
|
NetViewType.conn => l10n.conn,
|
||||||
case NetViewType.conn:
|
NetViewType.traffic => l10n.traffic,
|
||||||
return l10n.conn;
|
NetViewType.speed => l10n.speed,
|
||||||
case NetViewType.traffic:
|
};
|
||||||
return l10n.traffic;
|
|
||||||
case NetViewType.speed:
|
|
||||||
return l10n.speed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(String, String) build(ServerStatus ss) {
|
/// If no device is specified, return the cached value (only real devices,
|
||||||
final ignoreLocal = Stores.setting.ignoreLocalNet.fetch();
|
/// such as ethX, wlanX...).
|
||||||
switch (this) {
|
(String, String) build(ServerStatus ss, {String? dev}) {
|
||||||
case NetViewType.conn:
|
final notSepcifyDev = dev == null || dev.isEmpty;
|
||||||
return (
|
try {
|
||||||
'${l10n.conn}:\n${ss.tcp.maxConn}',
|
switch (this) {
|
||||||
'${l10n.failed}:\n${ss.tcp.fail}',
|
case NetViewType.conn:
|
||||||
);
|
|
||||||
case NetViewType.speed:
|
|
||||||
if (ignoreLocal) {
|
|
||||||
return (
|
return (
|
||||||
'↓:\n${ss.netSpeed.cachedRealVals.speedIn}',
|
'${l10n.conn}:\n${ss.tcp.maxConn}',
|
||||||
'↑:\n${ss.netSpeed.cachedRealVals.speedOut}',
|
'${libL10n.fail}:\n${ss.tcp.fail}',
|
||||||
);
|
);
|
||||||
}
|
case NetViewType.speed:
|
||||||
return (
|
if (notSepcifyDev) {
|
||||||
'↓:\n${ss.netSpeed.speedIn()}',
|
return (
|
||||||
'↑:\n${ss.netSpeed.speedOut()}',
|
'↓:\n${ss.netSpeed.cachedVals.speedIn}',
|
||||||
);
|
'↑:\n${ss.netSpeed.cachedVals.speedOut}',
|
||||||
case NetViewType.traffic:
|
);
|
||||||
if (ignoreLocal) {
|
}
|
||||||
return (
|
return (
|
||||||
'↓:\n${ss.netSpeed.cachedRealVals.sizeIn}',
|
'↓:\n${ss.netSpeed.speedIn(device: dev)}',
|
||||||
'↑:\n${ss.netSpeed.cachedRealVals.sizeOut}',
|
'↑:\n${ss.netSpeed.speedOut(device: dev)}',
|
||||||
);
|
);
|
||||||
}
|
case NetViewType.traffic:
|
||||||
return (
|
if (notSepcifyDev) {
|
||||||
'↓:\n${ss.netSpeed.sizeIn()}',
|
return (
|
||||||
'↑:\n${ss.netSpeed.sizeOut()}',
|
'↓:\n${ss.netSpeed.cachedVals.sizeIn}',
|
||||||
);
|
'↑:\n${ss.netSpeed.cachedVals.sizeOut}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
'↓:\n${ss.netSpeed.sizeIn(device: dev)}',
|
||||||
|
'↑:\n${ss.netSpeed.sizeOut(device: dev)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('NetViewType.build', e, s);
|
||||||
|
return ('N/A', 'N/A');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int toJson() {
|
int toJson() => switch (this) {
|
||||||
switch (this) {
|
NetViewType.conn => 0,
|
||||||
case NetViewType.conn:
|
NetViewType.speed => 1,
|
||||||
return 0;
|
NetViewType.traffic => 2,
|
||||||
case NetViewType.speed:
|
};
|
||||||
return 1;
|
|
||||||
case NetViewType.traffic:
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static NetViewType fromJson(int json) {
|
static NetViewType fromJson(int json) => switch (json) {
|
||||||
switch (json) {
|
0 => NetViewType.conn,
|
||||||
case 0:
|
1 => NetViewType.speed,
|
||||||
return NetViewType.conn;
|
_ => NetViewType.traffic,
|
||||||
case 2:
|
};
|
||||||
return NetViewType.traffic;
|
|
||||||
default:
|
|
||||||
return NetViewType.speed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'net_view.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class NetViewTypeAdapter extends TypeAdapter<NetViewType> {
|
|
||||||
@override
|
|
||||||
final int typeId = 5;
|
|
||||||
|
|
||||||
@override
|
|
||||||
NetViewType read(BinaryReader reader) {
|
|
||||||
switch (reader.readByte()) {
|
|
||||||
case 0:
|
|
||||||
return NetViewType.conn;
|
|
||||||
case 1:
|
|
||||||
return NetViewType.speed;
|
|
||||||
case 2:
|
|
||||||
return NetViewType.traffic;
|
|
||||||
default:
|
|
||||||
return NetViewType.conn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, NetViewType obj) {
|
|
||||||
switch (obj) {
|
|
||||||
case NetViewType.conn:
|
|
||||||
writer.writeByte(0);
|
|
||||||
break;
|
|
||||||
case NetViewType.speed:
|
|
||||||
writer.writeByte(1);
|
|
||||||
break;
|
|
||||||
case NetViewType.traffic:
|
|
||||||
writer.writeByte(2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is NetViewTypeAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ enum ServerDetailCards {
|
|||||||
swap(Icons.swap_horiz),
|
swap(Icons.swap_horiz),
|
||||||
gpu(Bootstrap.gpu_card),
|
gpu(Bootstrap.gpu_card),
|
||||||
disk(Bootstrap.device_hdd_fill),
|
disk(Bootstrap.device_hdd_fill),
|
||||||
|
smart(Icons.health_and_safety, sinceBuild: 1174),
|
||||||
net(ZondIcons.network),
|
net(ZondIcons.network),
|
||||||
sensor(MingCute.dashboard_4_line),
|
sensor(MingCute.dashboard_4_line),
|
||||||
temp(FontAwesome.temperature_empty_solid),
|
temp(FontAwesome.temperature_empty_solid),
|
||||||
battery(Icons.battery_full),
|
battery(Icons.battery_full),
|
||||||
pve(BoxIcons.bxs_dashboard, sinceBuild: 818),
|
pve(BoxIcons.bxs_dashboard, sinceBuild: 818),
|
||||||
custom(Icons.code, sinceBuild: 825),
|
custom(Icons.code, sinceBuild: 825);
|
||||||
;
|
|
||||||
|
|
||||||
final int? sinceBuild;
|
final int? sinceBuild;
|
||||||
|
|
||||||
@@ -31,19 +31,20 @@ 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',
|
||||||
gpu => 'GPU',
|
gpu => 'GPU',
|
||||||
disk => l10n.disk,
|
disk => l10n.disk,
|
||||||
net => l10n.net,
|
smart => l10n.diskHealth,
|
||||||
sensor => l10n.sensors,
|
net => l10n.net,
|
||||||
temp => l10n.temperature,
|
sensor => l10n.sensors,
|
||||||
battery => l10n.battery,
|
temp => l10n.temperature,
|
||||||
pve => 'PVE',
|
battery => l10n.battery,
|
||||||
custom => l10n.cmd,
|
pve => 'PVE',
|
||||||
};
|
custom => l10n.cmd,
|
||||||
|
};
|
||||||
|
|
||||||
/// If:
|
/// If:
|
||||||
/// Version 1 => user set [about], default is [about, cpu]
|
/// Version 1 => user set [about], default is [about, cpu]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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/system.dart';
|
||||||
import '../../res/build_data.dart';
|
import 'package:server_box/data/provider/server.dart';
|
||||||
import '../server/system.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
|
|
||||||
enum ShellFunc {
|
enum ShellFunc {
|
||||||
status,
|
status,
|
||||||
@@ -9,55 +9,64 @@ enum ShellFunc {
|
|||||||
process,
|
process,
|
||||||
shutdown,
|
shutdown,
|
||||||
reboot,
|
reboot,
|
||||||
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 = ServerProvider.pick(
|
||||||
/// **Can't** use it in SFTP, because SFTP can't recognize `$HOME`
|
id: id,
|
||||||
static String getShellPath(String home) => '$home/$_srvBoxDir/$scriptFile';
|
)?.value.spi.custom?.scriptDir;
|
||||||
|
if (customScriptDir != null) return customScriptDir;
|
||||||
static const srvBoxDir = '$_homeVar/$_srvBoxDir';
|
return _scriptDirMap.putIfAbsent(id, () {
|
||||||
static const _installShellPath = '$_homeVar/$_srvBoxDir/$scriptFile';
|
return scriptDirTmp;
|
||||||
|
});
|
||||||
// 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) {
|
||||||
@@ -86,14 +95,14 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
|||||||
else
|
else
|
||||||
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
||||||
fi''';
|
fi''';
|
||||||
// case ShellFunc.docker:
|
// case ShellFunc.docker:
|
||||||
// return '''
|
// return '''
|
||||||
// result=\$(docker version 2>&1 | grep "permission denied")
|
// result=\$(docker version 2>&1 | grep "permission denied")
|
||||||
// if [ "\$result" != "" ]; then
|
// if [ "\$result" != "" ]; then
|
||||||
// \t${_dockerCmds.join(_cmdDivider)}
|
// \t${_dockerCmds.join(_cmdDivider)}
|
||||||
// else
|
// else
|
||||||
// \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
|
// \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
|
||||||
// fi''';
|
// fi''';
|
||||||
case ShellFunc.process:
|
case ShellFunc.process:
|
||||||
return '''
|
return '''
|
||||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||||
@@ -199,21 +208,25 @@ enum StatusCmdType {
|
|||||||
echo._('echo ${SystemType.linuxSign}'),
|
echo._('echo ${SystemType.linuxSign}'),
|
||||||
time._('date +%s'),
|
time._('date +%s'),
|
||||||
net._('cat /proc/net/dev'),
|
net._('cat /proc/net/dev'),
|
||||||
sys._('cat /etc/*-release | grep PRETTY_NAME'),
|
sys._('cat /etc/*-release | grep ^PRETTY_NAME'),
|
||||||
cpu._('cat /proc/stat | grep cpu'),
|
cpu._('cat /proc/stat | grep cpu'),
|
||||||
uptime._('uptime'),
|
uptime._('uptime'),
|
||||||
conn._('cat /proc/net/snmp'),
|
conn._('cat /proc/net/snmp'),
|
||||||
disk._('df'),
|
disk._(
|
||||||
|
'lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
||||||
|
),
|
||||||
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||||
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
||||||
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||||
host._('cat /etc/hostname'),
|
host._('cat /etc/hostname'),
|
||||||
diskio._('cat /proc/diskstats'),
|
diskio._('cat /proc/diskstats'),
|
||||||
battery._(
|
battery._(
|
||||||
'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'),
|
||||||
;
|
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
||||||
|
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
||||||
|
|
||||||
final String cmd;
|
final String cmd;
|
||||||
|
|
||||||
@@ -227,11 +240,12 @@ enum BSDStatusCmdType {
|
|||||||
sys._('uname -or'),
|
sys._('uname -or'),
|
||||||
cpu._('top -l 1 | grep "CPU usage"'),
|
cpu._('top -l 1 | grep "CPU usage"'),
|
||||||
uptime._('uptime'),
|
uptime._('uptime'),
|
||||||
|
// Keep df -k for BSD systems as lsblk is not available on macOS/BSD
|
||||||
disk._('df -k'),
|
disk._('df -k'),
|
||||||
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;
|
||||||
|
|
||||||
@@ -240,10 +254,10 @@ enum BSDStatusCmdType {
|
|||||||
|
|
||||||
extension StatusCmdTypeX on StatusCmdType {
|
extension StatusCmdTypeX on StatusCmdType {
|
||||||
String get i18n => switch (this) {
|
String get i18n => switch (this) {
|
||||||
StatusCmdType.sys => l10n.system,
|
StatusCmdType.sys => l10n.system,
|
||||||
StatusCmdType.host => l10n.host,
|
StatusCmdType.host => l10n.host,
|
||||||
StatusCmdType.uptime => l10n.uptime,
|
StatusCmdType.uptime => l10n.uptime,
|
||||||
StatusCmdType.battery => l10n.battery,
|
StatusCmdType.battery => l10n.battery,
|
||||||
final val => val.name,
|
final val => val.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
class SyncResult<T, E> {
|
|
||||||
final List<T> up;
|
|
||||||
final List<T> down;
|
|
||||||
final Map<T, E> err;
|
|
||||||
|
|
||||||
const SyncResult({
|
|
||||||
required this.up,
|
|
||||||
required this.down,
|
|
||||||
required this.err,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'SyncResult{up: $up, down: $down, err: $err}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class SyncIface<T> {
|
|
||||||
/// Merge [other] into [this], return [this] after merge.
|
|
||||||
/// Data in [other] has higher priority than [this].
|
|
||||||
FutureOr<void> sync(T other);
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,96 @@
|
|||||||
|
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:icons_plus/icons_plus.dart';
|
||||||
import 'package:server_box/view/page/server/tab.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/view/page/server/tab/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: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),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationRailDestination get navRailDestination {
|
||||||
|
return switch (this) {
|
||||||
|
server => NavigationRailDestination(
|
||||||
|
icon: const Icon(BoxIcons.bx_server),
|
||||||
|
label: Text(l10n.server),
|
||||||
|
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||||
|
),
|
||||||
|
// settings => NavigationRailDestination(
|
||||||
|
// icon: const Icon(Icons.settings),
|
||||||
|
// label: libL10n.setting,
|
||||||
|
// selectedIcon: const Icon(Icons.settings),
|
||||||
|
// ),
|
||||||
|
ssh => const NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.terminal_outlined),
|
||||||
|
label: Text('SSH'),
|
||||||
|
selectedIcon: Icon(Icons.terminal),
|
||||||
|
),
|
||||||
|
snippet => NavigationRailDestination(
|
||||||
|
icon: const Icon(Icons.code),
|
||||||
|
label: Text(l10n.snippet),
|
||||||
|
selectedIcon: const Icon(Icons.code),
|
||||||
|
),
|
||||||
|
file => NavigationRailDestination(
|
||||||
|
icon: const Icon(Icons.folder_open),
|
||||||
|
label: Text(libL10n.file),
|
||||||
|
selectedIcon: const Icon(Icons.folder),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NavigationDestination> get navDestinations {
|
||||||
|
return AppTab.values.map((e) => e.navDestination).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NavigationRailDestination> get navRailDestinations {
|
||||||
|
return AppTab.values.map((e) => e.navRailDestination).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
abstract class TagPickable {
|
|
||||||
bool containsTag(String tag);
|
|
||||||
|
|
||||||
String get tagName;
|
|
||||||
}
|
|
||||||
@@ -45,21 +45,21 @@ final class PodmanImg implements ContainerImg {
|
|||||||
String toRawJson() => json.encode(toJson());
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
|
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
|
||||||
repository: json["repository"],
|
repository: json['repository'],
|
||||||
tag: json["tag"],
|
tag: json['tag'],
|
||||||
id: json["Id"],
|
id: json['Id'],
|
||||||
created: json["Created"],
|
created: json['Created'],
|
||||||
size: json["Size"],
|
size: json['Size'],
|
||||||
containers: json["Containers"],
|
containers: json['Containers'],
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"repository": repository,
|
'repository': repository,
|
||||||
"tag": tag,
|
'tag': tag,
|
||||||
"Id": id,
|
'Id': id,
|
||||||
"Created": created,
|
'Created': created,
|
||||||
"Size": size,
|
'Size': size,
|
||||||
"Containers": containers,
|
'Containers': containers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,36 +96,36 @@ final class DockerImg implements ContainerImg {
|
|||||||
String toRawJson() => json.encode(toJson());
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
factory DockerImg.fromJson(Map<String, dynamic> json) {
|
factory DockerImg.fromJson(Map<String, dynamic> json) {
|
||||||
final containers = switch (json["Containers"]) {
|
final containers = switch (json['Containers']) {
|
||||||
final String a => a,
|
final String a => a,
|
||||||
final Object? a => a.toString(),
|
final Object? a => a.toString(),
|
||||||
};
|
};
|
||||||
final repo = switch (json["Repository"] ?? json["Names"]) {
|
final repo = switch (json['Repository'] ?? json['Names']) {
|
||||||
final String a => a,
|
final String a => a,
|
||||||
final List a => a.firstOrNull.toString(),
|
final List a => a.firstOrNull.toString(),
|
||||||
final Object? a => a.toString(),
|
final Object? a => a.toString(),
|
||||||
};
|
};
|
||||||
final size = switch (json["Size"]) {
|
final size = switch (json['Size']) {
|
||||||
final String a => a,
|
final String a => a,
|
||||||
final int a => a.bytes2Str,
|
final int a => a.bytes2Str,
|
||||||
final Object? a => a.toString(),
|
final Object? a => a.toString(),
|
||||||
};
|
};
|
||||||
return DockerImg(
|
return DockerImg(
|
||||||
containers: containers,
|
containers: containers,
|
||||||
createdAt: json["CreatedAt"],
|
createdAt: json['CreatedAt'],
|
||||||
id: json["ID"] ?? json["Id"] ?? '',
|
id: json['ID'] ?? json['Id'] ?? '',
|
||||||
repository: repo,
|
repository: repo,
|
||||||
size: size,
|
size: size,
|
||||||
tag: json["Tag"],
|
tag: json['Tag'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"Containers": containers,
|
'Containers': containers,
|
||||||
"CreatedAt": createdAt,
|
'CreatedAt': createdAt,
|
||||||
"ID": id,
|
'ID': id,
|
||||||
"Repository": repository,
|
'Repository': repository,
|
||||||
"Size": size,
|
'Size': size,
|
||||||
"Tag": tag,
|
'Tag': tag,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,29 +84,29 @@ final class PodmanPs implements ContainerPs {
|
|||||||
String toRawJson() => json.encode(toJson());
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
|
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
|
||||||
command: json["Command"] == null
|
command: json['Command'] == null
|
||||||
? []
|
? []
|
||||||
: List<String>.from(json["Command"]!.map((x) => x)),
|
: List<String>.from(json['Command']!.map((x) => x)),
|
||||||
created:
|
created:
|
||||||
json["Created"] == null ? null : DateTime.parse(json["Created"]),
|
json['Created'] == null ? null : DateTime.parse(json['Created']),
|
||||||
exited: json["Exited"],
|
exited: json['Exited'],
|
||||||
id: json["Id"],
|
id: json['Id'],
|
||||||
image: json["Image"],
|
image: json['Image'],
|
||||||
names: json["Names"] == null
|
names: json['Names'] == null
|
||||||
? []
|
? []
|
||||||
: List<String>.from(json["Names"]!.map((x) => x)),
|
: List<String>.from(json['Names']!.map((x) => x)),
|
||||||
startedAt: json["StartedAt"],
|
startedAt: json['StartedAt'],
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"Command":
|
'Command':
|
||||||
command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
||||||
"Created": created?.toIso8601String(),
|
'Created': created?.toIso8601String(),
|
||||||
"Exited": exited,
|
'Exited': exited,
|
||||||
"Id": id,
|
'Id': id,
|
||||||
"Image": image,
|
'Image': image,
|
||||||
"Names": names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
|
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
|
||||||
"StartedAt": startedAt,
|
'StartedAt': startedAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +105,7 @@ enum PkgManager {
|
|||||||
return PkgManager.apt;
|
return PkgManager.apt;
|
||||||
case Dist.opensuse:
|
case Dist.opensuse:
|
||||||
return PkgManager.zypper;
|
return PkgManager.zypper;
|
||||||
|
case Dist.coreelec:
|
||||||
case Dist.wrt:
|
case Dist.wrt:
|
||||||
return PkgManager.opkg;
|
return PkgManager.opkg;
|
||||||
case Dist.arch:
|
case Dist.arch:
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ class UpgradePkgInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _parseApt(String raw) {
|
void _parseApt(String raw) {
|
||||||
final split1 = raw.split("/");
|
final split1 = raw.split('/');
|
||||||
package = split1[0];
|
package = split1[0];
|
||||||
final split2 = split1[1].split(" ");
|
final split2 = split1[1].split(' ');
|
||||||
newVersion = split2[1];
|
newVersion = split2[1];
|
||||||
arch = split2[2];
|
arch = split2[2];
|
||||||
nowVersion = split2[5].replaceFirst(']', '');
|
nowVersion = split2[5].replaceFirst(']', '');
|
||||||
@@ -53,7 +53,7 @@ class UpgradePkgInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _parseZypper(String raw) {
|
void _parseZypper(String raw) {
|
||||||
final cols = raw.split("|");
|
final cols = raw.split('|');
|
||||||
package = cols[2].trim();
|
package = cols[2].trim();
|
||||||
nowVersion = cols[3].trim();
|
nowVersion = cols[3].trim();
|
||||||
newVersion = cols[4].trim();
|
newVersion = cols[4].trim();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import '../../res/misc.dart';
|
import 'package:server_box/data/res/misc.dart';
|
||||||
|
|
||||||
class Conn {
|
class Conn {
|
||||||
final int maxConn;
|
final int maxConn;
|
||||||
|
|||||||
@@ -1,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
|
||||||
|
|||||||