Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d0a3c2ea9 | ||
|
|
dfcd8922e5 | ||
|
|
f55e880f53 | ||
|
|
e969746889 | ||
|
|
eb00b1ae35 | ||
|
|
9db350f368 | ||
|
|
bacea863bf | ||
|
|
0405277231 | ||
|
|
4191d8cbc1 | ||
|
|
999ac75324 | ||
|
|
d783b6c413 | ||
|
|
4ee668dc13 | ||
|
|
b3210882dd | ||
|
|
e2c0c3a515 | ||
|
|
19e9c26f59 | ||
|
|
9b1059acbd | ||
|
|
fa2d60e1b8 | ||
|
|
7394eee0f0 | ||
|
|
8b70f9c4a0 | ||
|
|
83e8ff3c0a | ||
|
|
bffce9e7fe | ||
|
|
5fdaf2fc5f | ||
|
|
23afcb31b8 | ||
|
|
ca7697d5d9 | ||
|
|
162014041d | ||
|
|
53e2fca297 |
@@ -0,0 +1,37 @@
|
|||||||
|
name: build app
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout zerotier-magisk
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jetbrains'
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
- name: build target apk
|
||||||
|
run: |
|
||||||
|
cd app
|
||||||
|
flutter pub get
|
||||||
|
flutter build apk
|
||||||
|
|
||||||
|
- id: commit
|
||||||
|
uses: prompt/actions-commit-hash@v3
|
||||||
|
|
||||||
|
- name: Upload App release
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-${{ steps.commit.outputs.short }}
|
||||||
|
path: app/build/app/outputs/flutter-apk/app-release.apk
|
||||||
@@ -16,6 +16,9 @@ jobs:
|
|||||||
repository: zerotier/ZeroTierOne
|
repository: zerotier/ZeroTierOne
|
||||||
path: ZeroTierOne
|
path: ZeroTierOne
|
||||||
|
|
||||||
|
- id: commit
|
||||||
|
uses: prompt/actions-commit-hash@v3
|
||||||
|
|
||||||
- name: Prepare GCC Toolchain
|
- name: Prepare GCC Toolchain
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -qqy gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf g++-aarch64-linux-gnu g++-arm-linux-gnueabihf
|
sudo apt-get install -qqy gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf g++-aarch64-linux-gnu g++-arm-linux-gnueabihf
|
||||||
@@ -29,7 +32,7 @@ jobs:
|
|||||||
- name: Patch ZeroTier for Cross-Compile
|
- name: Patch ZeroTier for Cross-Compile
|
||||||
run: |
|
run: |
|
||||||
pip3 install toml
|
pip3 install toml
|
||||||
python3 build/build_helper.py
|
python3 build/patch.py
|
||||||
|
|
||||||
- name: Build for AArch64
|
- name: Build for AArch64
|
||||||
run: |
|
run: |
|
||||||
@@ -43,8 +46,12 @@ jobs:
|
|||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
cp ZeroTierOne/zerotier-one magisk/zerotier
|
cp ZeroTierOne/zerotier-one magisk/zerotier
|
||||||
cd magisk
|
|
||||||
zip -q -r ../zerotier-magisk-aarch64-gcc.zip .
|
- name: Upload AArch64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: zerotier-magisk-aarch64-gcc-${{ steps.commit.outputs.short }}
|
||||||
|
path: magisk/
|
||||||
|
|
||||||
- name: Build for AArch64 with SSO
|
- name: Build for AArch64 with SSO
|
||||||
run: |
|
run: |
|
||||||
@@ -57,8 +64,12 @@ jobs:
|
|||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
cp ZeroTierOne/zerotier-one magisk/zerotier
|
cp ZeroTierOne/zerotier-one magisk/zerotier
|
||||||
cd magisk
|
|
||||||
zip -q -r ../zerotier-magisk-aarch64-gcc-SSO.zip .
|
- name: Upload AArch64 SSO
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: zerotier-magisk-aarch64-gcc-SSO-${{ steps.commit.outputs.short }}
|
||||||
|
path: magisk/
|
||||||
|
|
||||||
- name: Build for Arm
|
- name: Build for Arm
|
||||||
run: |
|
run: |
|
||||||
@@ -73,26 +84,9 @@ jobs:
|
|||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
cp ZeroTierOne/zerotier-one magisk/zerotier/
|
cp ZeroTierOne/zerotier-one magisk/zerotier/
|
||||||
cd magisk
|
|
||||||
zip -q -r ../zerotier-magisk-arm-gcc.zip .
|
|
||||||
|
|
||||||
- name: Get Date
|
- name: Upload Arm
|
||||||
id: date
|
uses: actions/upload-artifact@v4
|
||||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
files: |
|
name: zerotier-magisk-arm-gcc-${{ steps.commit.outputs.short }}
|
||||||
zerotier-magisk-aarch64-gcc-SSO.zip
|
path: magisk/
|
||||||
zerotier-magisk-aarch64-gcc.zip
|
|
||||||
zerotier-magisk-arm-gcc.zip
|
|
||||||
tag_name: SNAPSHOT-GCC-${{ steps.date.outputs.date }}
|
|
||||||
name: SNAPSHOT-GCC-${{ steps.date.outputs.date }}
|
|
||||||
draft: false
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
# - name: Debugging with tmate
|
|
||||||
# uses: mxschmitt/action-tmate@v3.18
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
api_version: 28 # min. 21
|
api_version: 28 # min. 21
|
||||||
ndk: r26d # android-ndk-$ndk-linux.zip
|
ndk: r28 # android-ndk-$ndk-linux.zip
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,6 +26,9 @@ jobs:
|
|||||||
repository: miniupnp/libnatpmp
|
repository: miniupnp/libnatpmp
|
||||||
path: libnatpmp
|
path: libnatpmp
|
||||||
|
|
||||||
|
- id: commit
|
||||||
|
uses: prompt/actions-commit-hash@v3
|
||||||
|
|
||||||
- name: Prepare NDK Toolchain
|
- name: Prepare NDK Toolchain
|
||||||
run: |
|
run: |
|
||||||
wget -qO ndk.zip https://dl.google.com/android/repository/android-ndk-$ndk-linux.zip
|
wget -qO ndk.zip https://dl.google.com/android/repository/android-ndk-$ndk-linux.zip
|
||||||
@@ -34,7 +37,7 @@ jobs:
|
|||||||
- name: Patch ZeroTier for Cross-Compile
|
- name: Patch ZeroTier for Cross-Compile
|
||||||
run: |
|
run: |
|
||||||
pip3 install toml
|
pip3 install toml
|
||||||
python3 build/build_helper.py
|
python3 build/patch.py
|
||||||
|
|
||||||
- name: Build for AArch64
|
- name: Build for AArch64
|
||||||
run: |
|
run: |
|
||||||
@@ -57,8 +60,12 @@ jobs:
|
|||||||
mkdir -p magisk/zerotier/lib
|
mkdir -p magisk/zerotier/lib
|
||||||
cp ZeroTierOne/zerotier-one magisk/zerotier
|
cp ZeroTierOne/zerotier-one magisk/zerotier
|
||||||
cp $NDK_ROOT/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so magisk/zerotier/lib
|
cp $NDK_ROOT/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so magisk/zerotier/lib
|
||||||
cd magisk
|
|
||||||
zip -q -r ../zerotier-magisk-aarch64-ndk.zip .
|
- name: Upload AArch64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: zerotier-magisk-aarch64-ndk-${{ steps.commit.outputs.short }}
|
||||||
|
path: magisk/
|
||||||
|
|
||||||
- name: Build for Arm
|
- name: Build for Arm
|
||||||
run: |
|
run: |
|
||||||
@@ -81,25 +88,9 @@ jobs:
|
|||||||
|
|
||||||
cp ZeroTierOne/zerotier-one magisk/zerotier/
|
cp ZeroTierOne/zerotier-one magisk/zerotier/
|
||||||
cp $NDK_ROOT/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so magisk/zerotier/lib
|
cp $NDK_ROOT/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so magisk/zerotier/lib
|
||||||
cd magisk
|
|
||||||
zip -q -r ../zerotier-magisk-arm-ndk.zip .
|
|
||||||
|
|
||||||
- name: Get Date
|
- name: Upload Arm
|
||||||
id: date
|
uses: actions/upload-artifact@v4
|
||||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
files: |
|
name: zerotier-magisk-arm-ndk-${{ steps.commit.outputs.short }}
|
||||||
zerotier-magisk-aarch64-ndk.zip
|
path: magisk/
|
||||||
zerotier-magisk-arm-ndk.zip
|
|
||||||
tag_name: SNAPSHOT-NDK-${{ steps.date.outputs.date }}
|
|
||||||
name: SNAPSHOT-NDK-${{ steps.date.outputs.date }}
|
|
||||||
draft: false
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
# - name: Debugging with tmate
|
|
||||||
# uses: mxschmitt/action-tmate@v3.18
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
|
|
||||||
**Run zerotier in the background after booting!**
|
**Run zerotier in the background after booting!**
|
||||||
|
|
||||||
**No conflicts with other android VPN service!**
|
**No conflicts with other Android VPN service!**
|
||||||
|
|
||||||
|
**Use Android App to control ZeroTier**
|
||||||
|
|
||||||
|
**Support for Private Root Servers**
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -24,39 +28,84 @@ AArch64 suppots ARMv8-A and above; Arm version supports ARMv7-A (compiling with
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. download release from github.
|
1. Download magisk module zip file from github release; install.
|
||||||
2. install with magisk.
|
2. Download controller app apk file from github release; install.
|
||||||
3. modify the config file at `/sdcard/Android/zerotier/network_id.txt` and put your 16-character network id in it.
|
3. Reboot your phone.
|
||||||
4. restart and enjoy.
|
4. Open controller app; enter your 16-character network id; join.
|
||||||
|
5. Enjoy 😋
|
||||||
|
|
||||||
To restart, execute (in root) `sh /data/adb/zerotier/zerotier.sh restart`; An android application to control zerotier (networkid, restart, ...) has been added to the todo list.
|
## Usage
|
||||||
|
|
||||||
### Files
|
### Use Private Root Servers
|
||||||
|
|
||||||
|
Replace `/data/adb/zerotier/home/planet` with your own `planet` file.
|
||||||
|
|
||||||
|
### Controller App
|
||||||
|
|
||||||
|
Does not need root privilege
|
||||||
|
|
||||||
|
| Feature | Supported? |
|
||||||
|
| :----------------- | :--------- |
|
||||||
|
| status | ✅ |
|
||||||
|
| start/stop | ✅ |
|
||||||
|
| join/leave network | ✅ |
|
||||||
|
| join/leave planet | ❎ |
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img alt="ZeroTier for Magisk Icon" src="https://github.com/eventlOwOp/zerotier-magisk/blob/master/images/app_home.jpg" width="192" />
|
||||||
|
<img alt="ZeroTier for Magisk Icon" src="https://github.com/eventlOwOp/zerotier-magisk/blob/master/images/app_network.jpg" width="192" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Command line tools
|
||||||
|
|
||||||
|
Use `zerotier.sh` to start/stop or inspect status.
|
||||||
|
|
||||||
|
`Usage: zerotier.sh {start|stop|restart|status}`
|
||||||
|
|
||||||
|
Use `zerotier-cli / zerotier-idtool` for ZeroTierOne command line operations.
|
||||||
|
|
||||||
|
(`zerotier-one` not copied to `/system/bin`)
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
```
|
```
|
||||||
/data/adb/zerotier/
|
/data/adb/zerotier
|
||||||
| - run/
|
├── home # zerotier-one home directory
|
||||||
| | - pipe # pipe to service.sh
|
│ ├── authtoken.secret # zerotier-one http interface authtoken
|
||||||
| | - daemon.log # service.sh log
|
│ ├── zerotier-one.pid # zerotier-one pid
|
||||||
| ` - zerotier.log # zerotier-one log
|
│ ├── zerotier-one.port # zerotier-one port
|
||||||
| - home/ # zerotier-one home directory
|
│ └── ...
|
||||||
| ` - ...
|
├── lib # only in NDK compiled module
|
||||||
| - lib/
|
│ └── libc++_shared.so # NDK dynamic library
|
||||||
| ` - libc++_shared.so # NDK dynamic library
|
├── run
|
||||||
| - zerotier.sh # tool to communicate with service.sh
|
│ ├── daemon.log # service.sh log
|
||||||
| - zerotier-one # zerotier-one executable
|
│ ├── pipe # named pipe to interact with service.sh
|
||||||
| - zerotier-cli -> zerotier-one
|
│ ├── zerotier.log # zerotier-one log
|
||||||
` - zerotier-idtool -> zerotier-one
|
│ └── ...
|
||||||
|
├── zerotier-cli -> zerotier-one # zerotier-one command line interface
|
||||||
|
├── zerotier-idtool -> zerotier-one # zerotier-one id tool
|
||||||
|
├── zerotier-one # zerotier-one executable
|
||||||
|
└── zerotier.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
all the scripts and binaries are placed in `/data/adb/zerotier/`.
|
all the scripts and binaries are placed in `/data/adb/zerotier/`, and all copied to `/system/bin` (in PATH) to be executed directly (except `zerotier-one`)
|
||||||
|
|
||||||
`zerotier.sh` writes commands to the pipe at `run/pipe` and `service.sh` read from it.
|
`zerotier.sh` uses named pipe to communicate with `service.sh`, preventing ZeroTier to start as a subprocess of Shell.
|
||||||
|
|
||||||
`Usage: sh zerotier.sh {start|stop|restart|join|leave}`
|
|
||||||
|
|
||||||
log files are placed in `run`, `daemon.log` for `service.sh` and `zerotier.log` for ZeroTierOne.
|
log files are placed in `run`, `daemon.log` for `service.sh` and `zerotier.log` for ZeroTierOne.
|
||||||
|
|
||||||
### Build binaries yourself
|
## Build binaries yourself
|
||||||
|
|
||||||
The binaries are built with Android NDK toolchains on Github Action, see `.github/workflow/build.yml` for detailed information.
|
refer to `.github/workflow/build-{gcc|ndk}.yml` for detailed information.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
After 1.14.0, ZeroTierOne has introduce `multi-core concurrent packet processing`, which requires `pthread_setaffinity_np`.
|
||||||
|
|
||||||
|
However, for NDK, `pthread_setaffinity_np` won't be available until API level 36, Android 16. (refer to https://android.googlesource.com/platform/bionic/+/master/libc/include/pthread.h)
|
||||||
|
|
||||||
|
So in the NDK bulid version, it is replaced by the combination of `pthread_gettid_np` from `<pthread.h>` and `sched_getaffinity` from `<sched.h>`.
|
||||||
|
|
||||||
|
## Using your phone as router, want LAN to LAN mapping?
|
||||||
|
|
||||||
|
See [ThermalEng/zerotier-magisk](https://github.com/ThermalEng/zerotier-magisk/). Inform me by creating issue. Future function and UI integration if many module users want it.
|
||||||
|
|||||||
@@ -8,7 +8,13 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
将 ZeroTier 保持后台运行,不与其他 Android VPN 应用程序冲突
|
**将 ZeroTier 保持后台运行**
|
||||||
|
|
||||||
|
**不与其他 Android VPN 应用程序冲突**
|
||||||
|
|
||||||
|
**使用 Android App 进行控制**
|
||||||
|
|
||||||
|
**支持自建 Planet**
|
||||||
|
|
||||||
## 运行要求
|
## 运行要求
|
||||||
|
|
||||||
@@ -22,39 +28,85 @@ AArch64 版本支持 ARMv8-A 及以上;Arm 版本均对 ARMv7-A 进行编译 (
|
|||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
1. 从 Release 下载压缩包
|
1. 从 Release 处下载 magisk 模块压缩包,并安装
|
||||||
2. 导入 magisk 进行安装
|
2. 从 Release 处下载控制器的 apk 安装包,并安装
|
||||||
3. 将你的 16 位 ZeroTier network id 写入 `/sdcard/Android/zerotier/network_id.txt`
|
3. 重启
|
||||||
4. 重启
|
4. 打开控制器 App,输入 16 位 network id 并加入
|
||||||
|
5. 享用 😋
|
||||||
|
|
||||||
你可以用 `su` 执行 `sh /data/adb/zerotier/zerotier.sh restart` 来重启,或者干脆重启手机,计划写一个 app 来完成
|
## 使用
|
||||||
|
|
||||||
### 文件
|
### 自建 Planet
|
||||||
|
|
||||||
|
将 `/data/adb/zerotier/home/planet` 替换为自己的 planet 文件即可
|
||||||
|
|
||||||
|
### 控制器 App
|
||||||
|
|
||||||
|
无需 root 授权
|
||||||
|
|
||||||
|
| 功能 | 支持状态 |
|
||||||
|
| :--------------- | :------- |
|
||||||
|
| 查看运行状态 | ✅ |
|
||||||
|
| 启动停止重启 | ✅ |
|
||||||
|
| 加入离开 network | ✅ |
|
||||||
|
| 加入离开 planet | ❎ |
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img alt="ZeroTier for Magisk Icon" src="https://github.com/eventlOwOp/zerotier-magisk/blob/master/images/app_home.jpg" width="192" />
|
||||||
|
<img alt="ZeroTier for Magisk Icon" src="https://github.com/eventlOwOp/zerotier-magisk/blob/master/images/app_network.jpg" width="192" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### 命令行
|
||||||
|
|
||||||
|
查看运行状态,或者启动重启停止,使用 `zerotier.sh`
|
||||||
|
|
||||||
|
`Usage: zerotier.sh {start|stop|restart|status}`
|
||||||
|
|
||||||
|
ZeroTierOne 支持的所有命令行操作:使用 `zerotier-cli / zerotier-idtool`
|
||||||
|
|
||||||
|
(`zerotier-one` 并未导出到 `/system/bin`)
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
```
|
```
|
||||||
/data/adb/zerotier/
|
/data/adb/zerotier
|
||||||
| - run/
|
├── home # zerotier-one home directory
|
||||||
| | - pipe # pipe to service.sh
|
│ ├── authtoken.secret # zerotier-one http interface authtoken
|
||||||
| | - daemon.log # service.sh log
|
│ ├── zerotier-one.pid # zerotier-one pid
|
||||||
| ` - zerotier.log # zerotier-one log
|
│ ├── zerotier-one.port # zerotier-one port
|
||||||
| - home/ # zerotier-one home directory
|
│ └── ...
|
||||||
| ` - ...
|
├── lib # only in NDK compiled module
|
||||||
| - lib/
|
│ └── libc++_shared.so # NDK dynamic library
|
||||||
| ` - libc++_shared.so # NDK dynamic library
|
├── run
|
||||||
| - zerotier.sh # tool to communicate with service.sh
|
│ ├── daemon.log # service.sh log
|
||||||
| - zerotier-one # zerotier-one executable
|
│ ├── pipe # named pipe to interact with service.sh
|
||||||
| - zerotier-cli -> zerotier-one
|
│ ├── zerotier.log # zerotier-one log
|
||||||
` - zerotier-idtool -> zerotier-one
|
│ └── ...
|
||||||
|
├── zerotier-cli -> zerotier-one # zerotier-one command line interface
|
||||||
|
├── zerotier-idtool -> zerotier-one # zerotier-one id tool
|
||||||
|
├── zerotier-one # zerotier-one executable
|
||||||
|
└── zerotier.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
ZeroTier 可执行文件和操作的 Shell 脚本放在 `/data/adb/zerotier/`
|
ZeroTier 可执行文件和操作的 Shell 脚本放在 `/data/adb/zerotier/` 下,同时复制到 `/system/bin`(PATH 中)以便于直接执行(除了 `zerotier-one`)
|
||||||
|
|
||||||
`zerotier.sh` 向管道 `run/pipe` 写入, `service.sh` 读取管道,然后进行操作
|
`zerotier.sh` 通过命名管道与 `service.sh` 交互,防止 ZeroTier 作为 Shell 的子进程运行
|
||||||
|
|
||||||
`Usage: sh zerotier.sh {start|stop|restart|join|leave}`
|
日志存放在 `/data/adb/zerotier/run` 下,`service.sh` 为 `daemon.log`,ZeroTier 为 `zerotier.log`.
|
||||||
|
|
||||||
log files are placed in `run`, `daemon.log` for `service.sh` and `zerotier.log` for ZeroTierOne.
|
## 自行编译
|
||||||
|
|
||||||
### 自行编译
|
参考 `.github/workflow/build-{gcc|ndk}.yml`
|
||||||
|
|
||||||
使用 NDK,参考 `.github/workflow/build.yml`
|
## 注意
|
||||||
|
|
||||||
|
1.14.0 后 ZeroTierOne 引入了 `multi-core concurrent packet processing` ,其中使用了 `pthread_setaffinity_np` 实现线程亲和性设置
|
||||||
|
|
||||||
|
但 `pthread_setaffinity_np` 在 NDK 的 API level 36, Android 16 才受支持。 (参考 https://android.googlesource.com/platform/bionic/+/master/libc/include/pthread.h)
|
||||||
|
|
||||||
|
所以 NDK 中他被替换成了 `<pthread.h>` 中 `pthread_gettid_np` 和 `<sched.h>` 中 `sched_getaffinity` 的组合,来实现相同的功能
|
||||||
|
|
||||||
|
## 手机做路由器,想要 LAN to LAN
|
||||||
|
|
||||||
|
参考 [ThermalEng/zerotier-magisk](https://github.com/ThermalEng/zerotier-magisk/)
|
||||||
|
开 issue 统计一下需求,人多就合并再做 UI
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
**/.cxx/
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
|
|
||||||
|
# Android related
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
**/android/.gradle
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/local.properties
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: android
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: web
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
|
if (flutterVersionCode == null) {
|
||||||
|
flutterVersionCode = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
|
if (flutterVersionName == null) {
|
||||||
|
flutterVersionName = '1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "com.eventlowop.zerotier_magisk_app"
|
||||||
|
compileSdk flutter.compileSdkVersion
|
||||||
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId "com.eventlowop.zerotier_magisk_app"
|
||||||
|
// 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.
|
||||||
|
minSdkVersion flutter.minSdkVersion
|
||||||
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
versionName flutterVersionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source '../..'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="@string/appName"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/launcher_icon">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility?hl=en and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.eventlowop.zerotier_magisk_app
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="appName">ZeroTier for Magisk</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
maven { url 'https://mirrors.tuna.tsinghua.edu.cn/flutter/download.flutter.io' }
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = '../build'
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(':app')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx4G
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonTransitiveRClass=false
|
||||||
|
android.nonFinalResIds=false
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}
|
||||||
|
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||||
|
|
||||||
|
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
|
id "com.android.application" version '8.10.0-beta01' apply false
|
||||||
|
id "org.jetbrains.kotlin.android" version "2.1.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
||||||
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
arb-dir: lib/l10n
|
||||||
|
template-arb-file: app_en.arb
|
||||||
|
output-dir: lib/l10n
|
||||||
|
output-localization-file: app_localizations.dart
|
||||||
|
|
||||||
|
synthetic-package: false
|
||||||
|
nullable-getter: false
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:quickalert/quickalert.dart';
|
||||||
|
import '../services/zerotier_service.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class NetworkPage extends StatefulWidget {
|
||||||
|
final ZerotierService zerotierService;
|
||||||
|
|
||||||
|
const NetworkPage({
|
||||||
|
super.key,
|
||||||
|
required this.zerotierService,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NetworkPage> createState() => _NetworkPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NetworkPageState extends State<NetworkPage> {
|
||||||
|
final _idInputController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
List<String> _networks = [];
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_idInputController.addListener(_onTextChanged);
|
||||||
|
_fetchNetworkList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTextChanged() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setLoading(bool loading) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = loading;
|
||||||
|
if (loading && _error != null) {
|
||||||
|
_error = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchNetworkList() async {
|
||||||
|
_setLoading(true);
|
||||||
|
try {
|
||||||
|
final networks = await widget.zerotierService.loadNetwork();
|
||||||
|
if (mounted) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
setState(() {
|
||||||
|
if (networks == null) {
|
||||||
|
// ZeroTier服务不可用
|
||||||
|
_error = l10n.serviceNotRunning;
|
||||||
|
_networks = [];
|
||||||
|
} else {
|
||||||
|
_networks = List.from(networks);
|
||||||
|
_error = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
// 存储原始错误信息
|
||||||
|
final rawError = e.toString();
|
||||||
|
setState(() {
|
||||||
|
_error = rawError; // 存储原始错误
|
||||||
|
_networks = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在界面上显示本地化错误信息
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.loadNetworksErrorText(rawError)),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _performJoinNetwork(String networkId) async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
_setLoading(true);
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await widget.zerotierService.joinNetwork(networkId);
|
||||||
|
if (mounted) {
|
||||||
|
if (!widget.zerotierService.runningStatus) {
|
||||||
|
// ZeroTier服务未运行
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.serviceNotRunning),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (result) {
|
||||||
|
QuickAlert.show(
|
||||||
|
context: context,
|
||||||
|
type: QuickAlertType.success,
|
||||||
|
text: l10n.joinNetworkSuccessText(networkId),
|
||||||
|
);
|
||||||
|
_idInputController.clear();
|
||||||
|
} else {
|
||||||
|
// 其他失败
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.joinNetworkErrorText("Unknown error")),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await _fetchNetworkList();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l10n.joinNetworkErrorText(e.toString())), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _performLeaveNetwork(String networkId) async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
_setLoading(true);
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await widget.zerotierService.leaveNetwork(networkId);
|
||||||
|
if (mounted) {
|
||||||
|
if (!widget.zerotierService.runningStatus) {
|
||||||
|
// ZeroTier服务未运行
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.serviceNotRunning),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (result) {
|
||||||
|
QuickAlert.show(
|
||||||
|
context: context,
|
||||||
|
type: QuickAlertType.success,
|
||||||
|
text: l10n.leaveNetworkSuccessText(networkId),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 其他失败
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.leaveNetworkErrorText("Unknown error")),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await _fetchNetworkList();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l10n.leaveNetworkErrorText(e.toString())), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showLeaveConfirmation(String networkId) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
QuickAlert.show(
|
||||||
|
context: context,
|
||||||
|
type: QuickAlertType.confirm,
|
||||||
|
title: l10n.leaveNetworkConfirmationTitle,
|
||||||
|
text: l10n.leaveNetworkConfirmationText(networkId),
|
||||||
|
confirmBtnText: l10n.confirmButton,
|
||||||
|
cancelBtnText: l10n.cancelButton,
|
||||||
|
confirmBtnColor: Colors.red,
|
||||||
|
onConfirmBtnTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_performLeaveNetwork(networkId);
|
||||||
|
},
|
||||||
|
onCancelBtnTap: () => Navigator.pop(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_idInputController.removeListener(_onTextChanged);
|
||||||
|
_idInputController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _idInputController,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
inputFormatters: <TextInputFormatter>[
|
||||||
|
FilteringTextInputFormatter.allow(RegExp('[0-9a-fA-F]'))
|
||||||
|
],
|
||||||
|
enabled: !_isLoading,
|
||||||
|
maxLength: 16,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.networkIdLabel,
|
||||||
|
hintText: l10n.networkIdHint,
|
||||||
|
prefixIcon: const Icon(Icons.lan_outlined),
|
||||||
|
suffixIcon: _idInputController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: !_isLoading ? () => _idInputController.clear() : null,
|
||||||
|
tooltip: l10n.clearInputTooltip,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
counterText: "",
|
||||||
|
border: const OutlineInputBorder()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
),
|
||||||
|
onPressed: _isLoading || _idInputController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final networkId = _idInputController.text;
|
||||||
|
if (networkId.length != 16) {
|
||||||
|
QuickAlert.show(
|
||||||
|
context: context,
|
||||||
|
type: QuickAlertType.warning,
|
||||||
|
title: l10n.invalidNetworkIdTitle,
|
||||||
|
text: l10n.invalidNetworkIdText,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_performJoinNetwork(networkId);
|
||||||
|
},
|
||||||
|
label: Text(l10n.joinButton),
|
||||||
|
icon: const Icon(Icons.add_link)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
title: Text(l10n.networksTitle, style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: l10n.refreshNetworksTooltip,
|
||||||
|
onPressed: _isLoading ? null : _fetchNetworkList
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading && _networks.isEmpty
|
||||||
|
? const Center(child: CircularProgressIndicator.adaptive())
|
||||||
|
: _error != null
|
||||||
|
? Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.loadNetworksErrorText(_error!),
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: Text(l10n.refreshButtonLabel ?? 'Retry'),
|
||||||
|
onPressed: _isLoading ? null : _fetchNetworkList,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: RefreshIndicator(
|
||||||
|
onRefresh: _fetchNetworkList,
|
||||||
|
child: _networks.isEmpty
|
||||||
|
? LayoutBuilder(
|
||||||
|
builder: (context, constraints) => SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
l10n.noNetworksJoined,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).disabledColor
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: _networks.length,
|
||||||
|
itemBuilder: (BuildContext ctx, int i) {
|
||||||
|
final networkId = _networks[i];
|
||||||
|
return Card(
|
||||||
|
elevation: 1.5,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 0),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
child: Icon(Icons.vpn_key, size: 18, color: Theme.of(context).colorScheme.secondary),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
),
|
||||||
|
title: Text(networkId, style: const TextStyle(fontFamily: 'monospace', letterSpacing: 0.8)),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _isLoading ? null : () {
|
||||||
|
Clipboard.setData(ClipboardData(text: networkId));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l10n.copiedToClipboard(networkId)))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: l10n.copyTooltip,
|
||||||
|
icon: const Icon(Icons.copy, size: 20)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _isLoading ? null : () => _showLeaveConfirmation(networkId),
|
||||||
|
tooltip: l10n.leaveTooltip,
|
||||||
|
icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error, size: 20)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../services/zerotier_service.dart';
|
||||||
|
import '../l10n/app_localizations.dart'; // Import localizations
|
||||||
|
import '../models/peer_info.dart'; // Import the model
|
||||||
|
|
||||||
|
// class PeersPage extends StatelessWidget { // Convert to StatefulWidget
|
||||||
|
class PeersPage extends StatefulWidget {
|
||||||
|
final ZerotierService zerotierService;
|
||||||
|
// Remove callback parameter
|
||||||
|
// final Future<void> Function()? onRefreshPeers;
|
||||||
|
|
||||||
|
const PeersPage({
|
||||||
|
super.key,
|
||||||
|
required this.zerotierService,
|
||||||
|
// Remove callback parameter from constructor
|
||||||
|
// this.onRefreshPeers,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
// State<PeersPage> createState() => _PeersPageState(); // Add createState
|
||||||
|
State<PeersPage> createState() => _PeersPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add State class
|
||||||
|
class _PeersPageState extends State<PeersPage> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
// Use the PeerInfo model for the list
|
||||||
|
List<PeerInfo> _peers = [];
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPeers(); // Call fetch method on init
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to manage loading state
|
||||||
|
void _setLoading(bool loading) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = loading;
|
||||||
|
if (loading) {
|
||||||
|
_error = null; // Clear error when starting load
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder method to fetch peers
|
||||||
|
Future<void> _fetchPeers() async {
|
||||||
|
_setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用服务方法并获取返回值
|
||||||
|
final peers = await widget.zerotierService.loadPeers();
|
||||||
|
if (mounted) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
setState(() {
|
||||||
|
if (peers == null) {
|
||||||
|
// ZeroTier服务不可用
|
||||||
|
_error = l10n.serviceNotRunning;
|
||||||
|
_peers = [];
|
||||||
|
} else {
|
||||||
|
_peers = List.from(peers);
|
||||||
|
_error = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
// 存储原始错误信息,不使用 l10n
|
||||||
|
final rawError = e.toString();
|
||||||
|
setState(() {
|
||||||
|
// 在 setState 内部构建错误显示文本
|
||||||
|
_error = rawError; // 存储原始错误,显示时再格式化
|
||||||
|
_peers = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在确认 mounted 后使用 l10n 和 context
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.loadPeersErrorText(rawError)),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
_setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to build list item display
|
||||||
|
Widget _buildPeerTile(PeerInfo peer, AppLocalizations l10n) {
|
||||||
|
IconData roleIcon = peer.isPlanet ? Icons.cloud_outlined : Icons.computer_outlined;
|
||||||
|
Color roleColor = peer.isPlanet ? Colors.blueGrey : Theme.of(context).colorScheme.secondary;
|
||||||
|
|
||||||
|
// 设置tunneled状态显示
|
||||||
|
final tunnelIcon = peer.tunneled ? Icons.settings_ethernet : Icons.wifi;
|
||||||
|
final tunnelColor = peer.tunneled ? Colors.orange.shade700 : Colors.green.shade600;
|
||||||
|
final tunnelText = peer.tunneled ? l10n.peerTunneled : l10n.peerDirect;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 1.5,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 8),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: roleColor.withOpacity(0.1),
|
||||||
|
child: Icon(roleIcon, size: 20, color: roleColor),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
peer.address,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 14),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 对于非PLANET节点显示版本
|
||||||
|
if (!peer.isPlanet && peer.version != null) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
peer.version!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontWeight: FontWeight.normal
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Chip(
|
||||||
|
avatar: Icon(roleIcon, size: 14, color: roleColor),
|
||||||
|
label: Text(peer.role, style: TextStyle(fontSize: 12, color: roleColor)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
backgroundColor: roleColor.withOpacity(0.1),
|
||||||
|
side: BorderSide.none,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// 添加tunneled状态显示
|
||||||
|
Chip(
|
||||||
|
avatar: Icon(tunnelIcon, size: 14, color: tunnelColor),
|
||||||
|
label: Text(tunnelText, style: TextStyle(fontSize: 12, color: tunnelColor)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
backgroundColor: tunnelColor.withOpacity(0.1),
|
||||||
|
side: BorderSide.none,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (peer.latency != null) ...[
|
||||||
|
Icon(Icons.timer_outlined, size: 14, color: Colors.orange.shade700),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('${peer.latency} ms', style: TextStyle(fontSize: 12, color: Colors.orange.shade700)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (peer.preferredPath != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
peer.preferredPath!,
|
||||||
|
style: TextStyle(fontSize: 11, color: Theme.of(context).disabledColor, fontFamily: 'monospace'),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
Widget bodyContent;
|
||||||
|
|
||||||
|
if (_isLoading && _peers.isEmpty) {
|
||||||
|
bodyContent = const Center(child: CircularProgressIndicator.adaptive());
|
||||||
|
} else if (_error != null) {
|
||||||
|
bodyContent = Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.loadPeersErrorText(_error!),
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: Text(l10n.refreshButtonLabel),
|
||||||
|
onPressed: _isLoading ? null : _fetchPeers,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (!_isLoading && _peers.isEmpty) {
|
||||||
|
// Show "No peers found" centered, allowing pull-to-refresh
|
||||||
|
bodyContent = LayoutBuilder(
|
||||||
|
builder: (context, constraints) => SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
l10n.noPeersFound, // Uses existing l10n key
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).disabledColor
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Display the list using ListView.builder
|
||||||
|
bodyContent = ListView.builder(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(), // Ensure scrollable even when few items
|
||||||
|
itemCount: _peers.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildPeerTile(_peers[index], l10n);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the main content with RefreshIndicator
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _fetchPeers,
|
||||||
|
child: bodyContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../services/zerotier_service.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class StatusPage extends StatefulWidget {
|
||||||
|
final ZerotierService zerotierService;
|
||||||
|
|
||||||
|
const StatusPage({
|
||||||
|
super.key,
|
||||||
|
required this.zerotierService,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatusPage> createState() => _StatusPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusPageState extends State<StatusPage> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
ZerotierStatus? _statusInfo;
|
||||||
|
bool _isModuleInstalled = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// 获取缓存的状态信息,如果有则直接使用
|
||||||
|
final cachedStatus = widget.zerotierService.statusInfo;
|
||||||
|
if (cachedStatus != null) {
|
||||||
|
_statusInfo = cachedStatus;
|
||||||
|
_isModuleInstalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后异步更新状态
|
||||||
|
_fetchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchStatus() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// 检查是否有缓存的状态
|
||||||
|
final cachedStatus = widget.zerotierService.statusInfo;
|
||||||
|
final bool hasCache = cachedStatus != null;
|
||||||
|
|
||||||
|
// 如果没有缓存或者当前状态为空,才显示加载中
|
||||||
|
if (!hasCache || _statusInfo == null) {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final status = await widget.zerotierService.loadStatus();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_statusInfo = status;
|
||||||
|
_isModuleInstalled = true;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to load status: $e')),
|
||||||
|
);
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _performServiceAction(String command) async {
|
||||||
|
if (!mounted || _isLoading) return;
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
String actionVerb = command;
|
||||||
|
if (command == 'start') actionVerb = 'Starting';
|
||||||
|
if (command == 'stop') actionVerb = 'Stopping';
|
||||||
|
if (command == 'restart') actionVerb = 'Restarting';
|
||||||
|
|
||||||
|
try {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('$actionVerb ZeroTier...'), duration: const Duration(seconds: 1)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final success = await widget.zerotierService.zerotierCommand(command);
|
||||||
|
|
||||||
|
if (!success && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isModuleInstalled = false;
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.moduleNotRunning),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('$command failed: $e'), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
await _fetchStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final cachedStatus = widget.zerotierService.statusInfo;
|
||||||
|
final bool runningStatus = (_statusInfo != null && _statusInfo!.online) ||
|
||||||
|
(cachedStatus != null && cachedStatus.online && _statusInfo == null);
|
||||||
|
final statusColor = runningStatus ? Colors.green.shade400 : Colors.red.shade400;
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _fetchStatus,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildStatusIcon(runningStatus),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildStatusText(context, runningStatus, theme, l10n),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.refresh, color: theme.colorScheme.secondary),
|
||||||
|
onPressed: _isLoading ? null : _fetchStatus,
|
||||||
|
tooltip: l10n.refreshStatusTooltip,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_ActionButtons(
|
||||||
|
l10n: l10n,
|
||||||
|
startFunction: () => _performServiceAction('start'),
|
||||||
|
restartFunction: () => _performServiceAction('restart'),
|
||||||
|
stopFunction: () => _performServiceAction('stop'),
|
||||||
|
isServiceRunning: runningStatus,
|
||||||
|
isModuleInstalled: _isModuleInstalled,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
if (_isLoading)
|
||||||
|
const Center(child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 32.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
))
|
||||||
|
else if (_statusInfo != null && runningStatus)
|
||||||
|
_buildStatusInfo(context, _statusInfo!)
|
||||||
|
else if (cachedStatus != null && cachedStatus.online)
|
||||||
|
_buildStatusInfo(context, cachedStatus)
|
||||||
|
else
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildErrorText(context, theme, l10n),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusIcon(bool runningStatus) {
|
||||||
|
IconData iconData;
|
||||||
|
Color iconColor;
|
||||||
|
|
||||||
|
if (!_isModuleInstalled) {
|
||||||
|
iconData = Icons.error;
|
||||||
|
iconColor = Colors.orange.shade700;
|
||||||
|
} else if (runningStatus) {
|
||||||
|
iconData = Icons.play_circle_fill;
|
||||||
|
iconColor = Colors.green.shade400;
|
||||||
|
} else {
|
||||||
|
iconData = Icons.stop_circle;
|
||||||
|
iconColor = Colors.red.shade400;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon(
|
||||||
|
iconData,
|
||||||
|
color: iconColor,
|
||||||
|
size: 64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusText(BuildContext context, bool runningStatus, ThemeData theme, AppLocalizations l10n) {
|
||||||
|
String statusText;
|
||||||
|
Color textColor;
|
||||||
|
|
||||||
|
if (!_isModuleInstalled) {
|
||||||
|
statusText = l10n.moduleNotRunning;
|
||||||
|
textColor = Colors.orange.shade700;
|
||||||
|
} else if (runningStatus) {
|
||||||
|
statusText = l10n.statusRunning;
|
||||||
|
textColor = Colors.green.shade400;
|
||||||
|
} else {
|
||||||
|
statusText = l10n.statusStopped;
|
||||||
|
textColor = Colors.red.shade400;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isLoading) {
|
||||||
|
textColor = textColor.withOpacity(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
statusText,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: textColor,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorText(BuildContext context, ThemeData theme, AppLocalizations l10n) {
|
||||||
|
String errorText;
|
||||||
|
Color textColor;
|
||||||
|
|
||||||
|
if (!_isModuleInstalled) {
|
||||||
|
errorText = l10n.moduleNotRunning;
|
||||||
|
textColor = Colors.orange.shade700;
|
||||||
|
} else {
|
||||||
|
errorText = l10n.statusStopped;
|
||||||
|
textColor = theme.colorScheme.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
errorText,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: textColor
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusInfo(BuildContext context, ZerotierStatus status) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.statusDetailsTitle,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)
|
||||||
|
),
|
||||||
|
const Divider(height: 20, thickness: 1),
|
||||||
|
_buildInfoTile(context, Icons.lan_outlined, l10n.nodeAddressLabel, status.address),
|
||||||
|
_buildInfoTile(context, Icons.info_outline, l10n.softwareVersionLabel, status.version),
|
||||||
|
_buildInfoTile(context, status.online ? Icons.cloud_done_outlined : Icons.cloud_off_outlined, l10n.onlineStatusLabel, status.online ? l10n.onlineStatusOnline : l10n.onlineStatusOffline),
|
||||||
|
_buildInfoTile(context, Icons.settings_ethernet, l10n.primaryPortLabel, status.primaryPort.toString()),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.list_alt, color: theme.colorScheme.secondary),
|
||||||
|
title: Text(l10n.listeningAddressesLabel, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 100,
|
||||||
|
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: theme.dividerColor),
|
||||||
|
),
|
||||||
|
child: status.listeningOn.isEmpty
|
||||||
|
? Center(child: Text(l10n.noListeningAddresses, style: theme.textTheme.bodySmall))
|
||||||
|
: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: status.listeningOn.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
|
child: Text(
|
||||||
|
status.listeningOn[index],
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoTile(BuildContext context, IconData icon, String label, String value) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon, color: theme.colorScheme.secondary),
|
||||||
|
title: Text(label, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)),
|
||||||
|
subtitle: Text(value, style: theme.textTheme.bodyMedium?.copyWith(fontFamily: 'monospace')),
|
||||||
|
dense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionButtons extends StatelessWidget {
|
||||||
|
final AppLocalizations l10n;
|
||||||
|
final Function()? startFunction;
|
||||||
|
final Function()? restartFunction;
|
||||||
|
final Function()? stopFunction;
|
||||||
|
final bool isServiceRunning;
|
||||||
|
final bool isModuleInstalled;
|
||||||
|
|
||||||
|
const _ActionButtons({
|
||||||
|
required this.l10n,
|
||||||
|
required this.startFunction,
|
||||||
|
required this.restartFunction,
|
||||||
|
required this.stopFunction,
|
||||||
|
required this.isServiceRunning,
|
||||||
|
required this.isModuleInstalled,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)),
|
||||||
|
onPressed: isServiceRunning || !isModuleInstalled ? null : startFunction,
|
||||||
|
label: Text(l10n.startButton),
|
||||||
|
icon: const Icon(Icons.play_arrow),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)),
|
||||||
|
onPressed: isServiceRunning && isModuleInstalled ? restartFunction : null,
|
||||||
|
label: Text(l10n.restartButton),
|
||||||
|
icon: const Icon(Icons.restart_alt),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: theme.colorScheme.error,
|
||||||
|
side: BorderSide(color: theme.colorScheme.error),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12)
|
||||||
|
),
|
||||||
|
onPressed: isServiceRunning && isModuleInstalled ? stopFunction : null,
|
||||||
|
label: Text(l10n.stopButton),
|
||||||
|
icon: const Icon(Icons.stop),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"@@locale": "en",
|
||||||
|
"statusRunning": "Running",
|
||||||
|
"statusStopped": "Stopped",
|
||||||
|
"refreshStatusTooltip": "Refresh Status",
|
||||||
|
"startButton": "Start",
|
||||||
|
"restartButton": "Restart",
|
||||||
|
"stopButton": "Stop",
|
||||||
|
"statusDetailsTitle": "ZeroTier Details",
|
||||||
|
"nodeAddressLabel": "Node Address",
|
||||||
|
"softwareVersionLabel": "Software Version",
|
||||||
|
"onlineStatusLabel": "Online Status",
|
||||||
|
"onlineStatusOnline": "Online",
|
||||||
|
"onlineStatusOffline": "Offline",
|
||||||
|
"primaryPortLabel": "Primary Port",
|
||||||
|
"listeningAddressesLabel": "Listening Addresses:",
|
||||||
|
"noListeningAddresses": "None",
|
||||||
|
|
||||||
|
"leaveNetworkConfirmationTitle": "Leave Network?",
|
||||||
|
"leaveNetworkConfirmationText": "Are you sure you want to leave network {networkId}?",
|
||||||
|
"@leaveNetworkConfirmationText": {
|
||||||
|
"placeholders": {
|
||||||
|
"networkId": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "1234567890abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmButton": "Confirm",
|
||||||
|
"cancelButton": "Cancel",
|
||||||
|
"networkIdLabel": "Network ID",
|
||||||
|
"networkIdHint": "Enter 16-character Network ID",
|
||||||
|
"invalidNetworkIdTitle": "Invalid Network ID",
|
||||||
|
"invalidNetworkIdText": "Please enter a 16-character network ID.",
|
||||||
|
"joinButton": "Join",
|
||||||
|
"networksTitle": "Networks",
|
||||||
|
"refreshNetworksTooltip": "Refresh Network List",
|
||||||
|
"noNetworksJoined": "No networks joined",
|
||||||
|
"copiedToClipboard": "Copied {networkId} to clipboard!",
|
||||||
|
"@copiedToClipboard": {
|
||||||
|
"placeholders": {
|
||||||
|
"networkId": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "1234567890abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"copyTooltip": "Copy",
|
||||||
|
"leaveTooltip": "Leave",
|
||||||
|
"clearInputTooltip": "Clear input",
|
||||||
|
"peersFeatureComingSoon": "Peers feature coming soon",
|
||||||
|
"noPeersFound": "No peers found",
|
||||||
|
|
||||||
|
"navBarLabelStatus": "Status",
|
||||||
|
"navBarLabelNetworks": "Networks",
|
||||||
|
"navBarLabelPeers": "Peers",
|
||||||
|
"appBarTitleStatus": "Status",
|
||||||
|
"appBarTitleNetworks": "Networks",
|
||||||
|
"appBarTitlePeers": "Peers",
|
||||||
|
|
||||||
|
"joinNetworkSuccessText": "Successfully joined network {networkId}!",
|
||||||
|
"@joinNetworkSuccessText": {
|
||||||
|
"placeholders": {
|
||||||
|
"networkId": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "1234567890abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"joinNetworkErrorText": "Failed to join network: {error}",
|
||||||
|
"@joinNetworkErrorText": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "Connection timed out"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"leaveNetworkSuccessText": "Successfully left network {networkId}.",
|
||||||
|
"@leaveNetworkSuccessText": {
|
||||||
|
"placeholders": {
|
||||||
|
"networkId": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "1234567890abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"leaveNetworkErrorText": "Failed to leave network: {error}",
|
||||||
|
"@leaveNetworkErrorText": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "Permission denied"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"refreshButtonLabel": "Retry",
|
||||||
|
|
||||||
|
"loadPeersErrorText": "Failed to load peers: {error}",
|
||||||
|
"@loadPeersErrorText": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "Network error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"loadNetworksErrorText": "Failed to load networks: {error}",
|
||||||
|
"@loadNetworksErrorText": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "Network error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"serviceNotRunning": "ZeroTier service is not running",
|
||||||
|
|
||||||
|
"moduleNotRunning": "ZeroTier Magisk module is not running. Please check if you have installed the ZeroTier Magisk module.",
|
||||||
|
|
||||||
|
"peerTunneled": "Relayed",
|
||||||
|
"peerDirect": "Direct"
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
|
||||||
|
import 'app_localizations_en.dart';
|
||||||
|
import 'app_localizations_zh.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||||
|
/// returned by `AppLocalizations.of(context)`.
|
||||||
|
///
|
||||||
|
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||||
|
/// `localizationDelegates` list, and the locales they support in the app's
|
||||||
|
/// `supportedLocales` list. For example:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// import 'l10n/app_localizations.dart';
|
||||||
|
///
|
||||||
|
/// return MaterialApp(
|
||||||
|
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
/// home: MyApplicationHome(),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Update pubspec.yaml
|
||||||
|
///
|
||||||
|
/// Please make sure to update your pubspec.yaml to include the following
|
||||||
|
/// packages:
|
||||||
|
///
|
||||||
|
/// ```yaml
|
||||||
|
/// dependencies:
|
||||||
|
/// # Internationalization support.
|
||||||
|
/// flutter_localizations:
|
||||||
|
/// sdk: flutter
|
||||||
|
/// intl: any # Use the pinned version from flutter_localizations
|
||||||
|
///
|
||||||
|
/// # Rest of dependencies
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## iOS Applications
|
||||||
|
///
|
||||||
|
/// iOS applications define key application metadata, including supported
|
||||||
|
/// locales, in an Info.plist file that is built into the application bundle.
|
||||||
|
/// To configure the locales supported by your app, you’ll need to edit this
|
||||||
|
/// file.
|
||||||
|
///
|
||||||
|
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||||
|
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||||
|
/// project’s Runner folder.
|
||||||
|
///
|
||||||
|
/// Next, select the Information Property List item, select Add Item from the
|
||||||
|
/// Editor menu, then select Localizations from the pop-up menu.
|
||||||
|
///
|
||||||
|
/// Select and expand the newly-created Localizations item then, for each
|
||||||
|
/// locale your application supports, add a new item and select the locale
|
||||||
|
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||||
|
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||||
|
/// property.
|
||||||
|
abstract class AppLocalizations {
|
||||||
|
AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||||
|
|
||||||
|
final String localeName;
|
||||||
|
|
||||||
|
static AppLocalizations of(BuildContext context) {
|
||||||
|
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
|
||||||
|
|
||||||
|
/// A list of this localizations delegate along with the default localizations
|
||||||
|
/// delegates.
|
||||||
|
///
|
||||||
|
/// Returns a list of localizations delegates containing this delegate along with
|
||||||
|
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||||
|
/// and GlobalWidgetsLocalizations.delegate.
|
||||||
|
///
|
||||||
|
/// Additional delegates can be added by appending to this list in
|
||||||
|
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||||
|
/// of delegates is preferred or required.
|
||||||
|
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
|
||||||
|
delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A list of this localizations delegate's supported locales.
|
||||||
|
static const List<Locale> supportedLocales = <Locale>[
|
||||||
|
Locale('en'),
|
||||||
|
Locale('zh')
|
||||||
|
];
|
||||||
|
|
||||||
|
/// No description provided for @statusRunning.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Running'**
|
||||||
|
String get statusRunning;
|
||||||
|
|
||||||
|
/// No description provided for @statusStopped.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Stopped'**
|
||||||
|
String get statusStopped;
|
||||||
|
|
||||||
|
/// No description provided for @refreshStatusTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Refresh Status'**
|
||||||
|
String get refreshStatusTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @startButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Start'**
|
||||||
|
String get startButton;
|
||||||
|
|
||||||
|
/// No description provided for @restartButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Restart'**
|
||||||
|
String get restartButton;
|
||||||
|
|
||||||
|
/// No description provided for @stopButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Stop'**
|
||||||
|
String get stopButton;
|
||||||
|
|
||||||
|
/// No description provided for @statusDetailsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'ZeroTier Details'**
|
||||||
|
String get statusDetailsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @nodeAddressLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Node Address'**
|
||||||
|
String get nodeAddressLabel;
|
||||||
|
|
||||||
|
/// No description provided for @softwareVersionLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Software Version'**
|
||||||
|
String get softwareVersionLabel;
|
||||||
|
|
||||||
|
/// No description provided for @onlineStatusLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Online Status'**
|
||||||
|
String get onlineStatusLabel;
|
||||||
|
|
||||||
|
/// No description provided for @onlineStatusOnline.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Online'**
|
||||||
|
String get onlineStatusOnline;
|
||||||
|
|
||||||
|
/// No description provided for @onlineStatusOffline.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Offline'**
|
||||||
|
String get onlineStatusOffline;
|
||||||
|
|
||||||
|
/// No description provided for @primaryPortLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Primary Port'**
|
||||||
|
String get primaryPortLabel;
|
||||||
|
|
||||||
|
/// No description provided for @listeningAddressesLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Listening Addresses:'**
|
||||||
|
String get listeningAddressesLabel;
|
||||||
|
|
||||||
|
/// No description provided for @noListeningAddresses.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'None'**
|
||||||
|
String get noListeningAddresses;
|
||||||
|
|
||||||
|
/// No description provided for @leaveNetworkConfirmationTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Leave Network?'**
|
||||||
|
String get leaveNetworkConfirmationTitle;
|
||||||
|
|
||||||
|
/// No description provided for @leaveNetworkConfirmationText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Are you sure you want to leave network {networkId}?'**
|
||||||
|
String leaveNetworkConfirmationText(String networkId);
|
||||||
|
|
||||||
|
/// No description provided for @confirmButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Confirm'**
|
||||||
|
String get confirmButton;
|
||||||
|
|
||||||
|
/// No description provided for @cancelButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cancel'**
|
||||||
|
String get cancelButton;
|
||||||
|
|
||||||
|
/// No description provided for @networkIdLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Network ID'**
|
||||||
|
String get networkIdLabel;
|
||||||
|
|
||||||
|
/// No description provided for @networkIdHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enter 16-character Network ID'**
|
||||||
|
String get networkIdHint;
|
||||||
|
|
||||||
|
/// No description provided for @invalidNetworkIdTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Invalid Network ID'**
|
||||||
|
String get invalidNetworkIdTitle;
|
||||||
|
|
||||||
|
/// No description provided for @invalidNetworkIdText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Please enter a 16-character network ID.'**
|
||||||
|
String get invalidNetworkIdText;
|
||||||
|
|
||||||
|
/// No description provided for @joinButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Join'**
|
||||||
|
String get joinButton;
|
||||||
|
|
||||||
|
/// No description provided for @networksTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Networks'**
|
||||||
|
String get networksTitle;
|
||||||
|
|
||||||
|
/// No description provided for @refreshNetworksTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Refresh Network List'**
|
||||||
|
String get refreshNetworksTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @noNetworksJoined.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No networks joined'**
|
||||||
|
String get noNetworksJoined;
|
||||||
|
|
||||||
|
/// No description provided for @copiedToClipboard.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Copied {networkId} to clipboard!'**
|
||||||
|
String copiedToClipboard(String networkId);
|
||||||
|
|
||||||
|
/// No description provided for @copyTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Copy'**
|
||||||
|
String get copyTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @leaveTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Leave'**
|
||||||
|
String get leaveTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @clearInputTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear input'**
|
||||||
|
String get clearInputTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @peersFeatureComingSoon.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Peers feature coming soon'**
|
||||||
|
String get peersFeatureComingSoon;
|
||||||
|
|
||||||
|
/// No description provided for @noPeersFound.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No peers found'**
|
||||||
|
String get noPeersFound;
|
||||||
|
|
||||||
|
/// No description provided for @navBarLabelStatus.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Status'**
|
||||||
|
String get navBarLabelStatus;
|
||||||
|
|
||||||
|
/// No description provided for @navBarLabelNetworks.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Networks'**
|
||||||
|
String get navBarLabelNetworks;
|
||||||
|
|
||||||
|
/// No description provided for @navBarLabelPeers.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Peers'**
|
||||||
|
String get navBarLabelPeers;
|
||||||
|
|
||||||
|
/// No description provided for @appBarTitleStatus.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Status'**
|
||||||
|
String get appBarTitleStatus;
|
||||||
|
|
||||||
|
/// No description provided for @appBarTitleNetworks.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Networks'**
|
||||||
|
String get appBarTitleNetworks;
|
||||||
|
|
||||||
|
/// No description provided for @appBarTitlePeers.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Peers'**
|
||||||
|
String get appBarTitlePeers;
|
||||||
|
|
||||||
|
/// No description provided for @joinNetworkSuccessText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Successfully joined network {networkId}!'**
|
||||||
|
String joinNetworkSuccessText(String networkId);
|
||||||
|
|
||||||
|
/// No description provided for @joinNetworkErrorText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to join network: {error}'**
|
||||||
|
String joinNetworkErrorText(String error);
|
||||||
|
|
||||||
|
/// No description provided for @leaveNetworkSuccessText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Successfully left network {networkId}.'**
|
||||||
|
String leaveNetworkSuccessText(String networkId);
|
||||||
|
|
||||||
|
/// No description provided for @leaveNetworkErrorText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to leave network: {error}'**
|
||||||
|
String leaveNetworkErrorText(String error);
|
||||||
|
|
||||||
|
/// No description provided for @refreshButtonLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Retry'**
|
||||||
|
String get refreshButtonLabel;
|
||||||
|
|
||||||
|
/// No description provided for @loadPeersErrorText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to load peers: {error}'**
|
||||||
|
String loadPeersErrorText(String error);
|
||||||
|
|
||||||
|
/// No description provided for @loadNetworksErrorText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to load networks: {error}'**
|
||||||
|
String loadNetworksErrorText(String error);
|
||||||
|
|
||||||
|
/// No description provided for @serviceNotRunning.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'ZeroTier service is not running'**
|
||||||
|
String get serviceNotRunning;
|
||||||
|
|
||||||
|
/// No description provided for @moduleNotRunning.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'ZeroTier Magisk module is not running. Please check if you have installed the ZeroTier Magisk module.'**
|
||||||
|
String get moduleNotRunning;
|
||||||
|
|
||||||
|
/// No description provided for @peerTunneled.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Relayed'**
|
||||||
|
String get peerTunneled;
|
||||||
|
|
||||||
|
/// No description provided for @peerDirect.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Direct'**
|
||||||
|
String get peerDirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||||
|
const _AppLocalizationsDelegate();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AppLocalizations> load(Locale locale) {
|
||||||
|
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||||
|
|
||||||
|
|
||||||
|
// Lookup logic when only language code is specified.
|
||||||
|
switch (locale.languageCode) {
|
||||||
|
case 'en': return AppLocalizationsEn();
|
||||||
|
case 'zh': return AppLocalizationsZh();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw FlutterError(
|
||||||
|
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||||
|
'an issue with the localizations generation tool. Please file an issue '
|
||||||
|
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||||
|
'that was used.'
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// ignore: unused_import
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'app_localizations.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// The translations for English (`en`).
|
||||||
|
class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get statusRunning => 'Running';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get statusStopped => 'Stopped';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshStatusTooltip => 'Refresh Status';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get startButton => 'Start';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get restartButton => 'Restart';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stopButton => 'Stop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get statusDetailsTitle => 'ZeroTier Details';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get nodeAddressLabel => 'Node Address';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get softwareVersionLabel => 'Software Version';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onlineStatusLabel => 'Online Status';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onlineStatusOnline => 'Online';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onlineStatusOffline => 'Offline';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get primaryPortLabel => 'Primary Port';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listeningAddressesLabel => 'Listening Addresses:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noListeningAddresses => 'None';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get leaveNetworkConfirmationTitle => 'Leave Network?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String leaveNetworkConfirmationText(String networkId) {
|
||||||
|
return 'Are you sure you want to leave network $networkId?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get confirmButton => 'Confirm';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get cancelButton => 'Cancel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get networkIdLabel => 'Network ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get networkIdHint => 'Enter 16-character Network ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidNetworkIdTitle => 'Invalid Network ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidNetworkIdText => 'Please enter a 16-character network ID.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get joinButton => 'Join';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get networksTitle => 'Networks';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshNetworksTooltip => 'Refresh Network List';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noNetworksJoined => 'No networks joined';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String copiedToClipboard(String networkId) {
|
||||||
|
return 'Copied $networkId to clipboard!';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get copyTooltip => 'Copy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get leaveTooltip => 'Leave';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearInputTooltip => 'Clear input';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get peersFeatureComingSoon => 'Peers feature coming soon';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noPeersFound => 'No peers found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get navBarLabelStatus => 'Status';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get navBarLabelNetworks => 'Networks';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get navBarLabelPeers => 'Peers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appBarTitleStatus => 'Status';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appBarTitleNetworks => 'Networks';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appBarTitlePeers => 'Peers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String joinNetworkSuccessText(String networkId) {
|
||||||
|
return 'Successfully joined network $networkId!';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String joinNetworkErrorText(String error) {
|
||||||
|
return 'Failed to join network: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String leaveNetworkSuccessText(String networkId) {
|
||||||
|
return 'Successfully left network $networkId.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String leaveNetworkErrorText(String error) {
|
||||||
|
return 'Failed to leave network: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshButtonLabel => 'Retry';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String loadPeersErrorText(String error) {
|
||||||
|
return 'Failed to load peers: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String loadNetworksErrorText(String error) {
|
||||||
|
return 'Failed to load networks: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serviceNotRunning => 'ZeroTier service is not running';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moduleNotRunning => 'ZeroTier Magisk module is not running. Please check if you have installed the ZeroTier Magisk module.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get peerTunneled => 'Relayed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get peerDirect => 'Direct';
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// ignore: unused_import
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'app_localizations.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// The translations for Chinese (`zh`).
|
||||||
|
class AppLocalizationsZh extends AppLocalizations {
|
||||||
|
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get statusRunning => '运行中';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get statusStopped => '已停止';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshStatusTooltip => '刷新状态';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get startButton => '启动';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get restartButton => '重启';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stopButton => '停止';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get statusDetailsTitle => 'ZeroTier 详细信息';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get nodeAddressLabel => '节点地址';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get softwareVersionLabel => '软件版本';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onlineStatusLabel => '在线状态';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onlineStatusOnline => '在线';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onlineStatusOffline => '离线';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get primaryPortLabel => '主端口';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listeningAddressesLabel => '监听地址:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noListeningAddresses => '无';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get leaveNetworkConfirmationTitle => '离开网络?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String leaveNetworkConfirmationText(String networkId) {
|
||||||
|
return '您确定要离开网络 $networkId 吗?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get confirmButton => '确认';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get cancelButton => '取消';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get networkIdLabel => '网络 ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get networkIdHint => '输入 16 位网络 ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidNetworkIdTitle => '无效的网络 ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidNetworkIdText => '请输入一个 16 位的网络 ID。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get joinButton => '加入';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get networksTitle => '网络';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshNetworksTooltip => '刷新网络列表';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noNetworksJoined => '未加入任何网络';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String copiedToClipboard(String networkId) {
|
||||||
|
return '已将 $networkId 复制到剪贴板!';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get copyTooltip => '复制';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get leaveTooltip => '离开';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearInputTooltip => '清除输入';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get peersFeatureComingSoon => '节点功能即将推出';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noPeersFound => '未找到节点';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get navBarLabelStatus => '状态';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get navBarLabelNetworks => '网络';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get navBarLabelPeers => '节点';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appBarTitleStatus => '状态';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appBarTitleNetworks => '网络';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appBarTitlePeers => '节点';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String joinNetworkSuccessText(String networkId) {
|
||||||
|
return '成功加入网络 $networkId!';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String joinNetworkErrorText(String error) {
|
||||||
|
return '加入网络失败: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String leaveNetworkSuccessText(String networkId) {
|
||||||
|
return '成功离开网络 $networkId。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String leaveNetworkErrorText(String error) {
|
||||||
|
return '离开网络失败: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get refreshButtonLabel => '重试';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String loadPeersErrorText(String error) {
|
||||||
|
return '加载节点列表失败: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String loadNetworksErrorText(String error) {
|
||||||
|
return '加载网络列表失败: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serviceNotRunning => 'ZeroTier服务未运行';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moduleNotRunning => 'ZeroTier Magisk模块未运行,请检查您是否安装Zerotier Magisk模块。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get peerTunneled => '中继';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get peerDirect => '直连';
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"@@locale": "zh",
|
||||||
|
"statusRunning": "运行中",
|
||||||
|
"statusStopped": "已停止",
|
||||||
|
"refreshStatusTooltip": "刷新状态",
|
||||||
|
"startButton": "启动",
|
||||||
|
"restartButton": "重启",
|
||||||
|
"stopButton": "停止",
|
||||||
|
"statusDetailsTitle": "ZeroTier 详细信息",
|
||||||
|
"nodeAddressLabel": "节点地址",
|
||||||
|
"softwareVersionLabel": "软件版本",
|
||||||
|
"onlineStatusLabel": "在线状态",
|
||||||
|
"onlineStatusOnline": "在线",
|
||||||
|
"onlineStatusOffline": "离线",
|
||||||
|
"primaryPortLabel": "主端口",
|
||||||
|
"listeningAddressesLabel": "监听地址:",
|
||||||
|
"noListeningAddresses": "无",
|
||||||
|
|
||||||
|
"leaveNetworkConfirmationTitle": "离开网络?",
|
||||||
|
"leaveNetworkConfirmationText": "您确定要离开网络 {networkId} 吗?",
|
||||||
|
"@leaveNetworkConfirmationText": {
|
||||||
|
"placeholders": {
|
||||||
|
"networkId": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "1234567890abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmButton": "确认",
|
||||||
|
"cancelButton": "取消",
|
||||||
|
"networkIdLabel": "网络 ID",
|
||||||
|
"networkIdHint": "输入 16 位网络 ID",
|
||||||
|
"invalidNetworkIdTitle": "无效的网络 ID",
|
||||||
|
"invalidNetworkIdText": "请输入一个 16 位的网络 ID。",
|
||||||
|
"joinButton": "加入",
|
||||||
|
"networksTitle": "网络",
|
||||||
|
"refreshNetworksTooltip": "刷新网络列表",
|
||||||
|
"noNetworksJoined": "未加入任何网络",
|
||||||
|
"copiedToClipboard": "已将 {networkId} 复制到剪贴板!",
|
||||||
|
"@copiedToClipboard": {
|
||||||
|
"placeholders": {
|
||||||
|
"networkId": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "1234567890abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"copyTooltip": "复制",
|
||||||
|
"leaveTooltip": "离开",
|
||||||
|
"clearInputTooltip": "清除输入",
|
||||||
|
"peersFeatureComingSoon": "节点功能即将推出",
|
||||||
|
"noPeersFound": "未找到节点",
|
||||||
|
|
||||||
|
"navBarLabelStatus": "状态",
|
||||||
|
"navBarLabelNetworks": "网络",
|
||||||
|
"navBarLabelPeers": "节点",
|
||||||
|
"appBarTitleStatus": "状态",
|
||||||
|
"appBarTitleNetworks": "网络",
|
||||||
|
"appBarTitlePeers": "节点",
|
||||||
|
|
||||||
|
"joinNetworkSuccessText": "成功加入网络 {networkId}!",
|
||||||
|
"@joinNetworkSuccessText": {
|
||||||
|
"placeholders": {
|
||||||
|
"networkId": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "1234567890abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"joinNetworkErrorText": "加入网络失败: {error}",
|
||||||
|
"@joinNetworkErrorText": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "连接超时"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"leaveNetworkSuccessText": "成功离开网络 {networkId}。",
|
||||||
|
"@leaveNetworkSuccessText": {
|
||||||
|
"placeholders": {
|
||||||
|
"networkId": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "1234567890abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"leaveNetworkErrorText": "离开网络失败: {error}",
|
||||||
|
"@leaveNetworkErrorText": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "权限不足"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"refreshButtonLabel": "重试",
|
||||||
|
|
||||||
|
"loadPeersErrorText": "加载节点列表失败: {error}",
|
||||||
|
"@loadPeersErrorText": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "网络错误"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"loadNetworksErrorText": "加载网络列表失败: {error}",
|
||||||
|
"@loadNetworksErrorText": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "网络错误"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"serviceNotRunning": "ZeroTier服务未运行",
|
||||||
|
|
||||||
|
"moduleNotRunning": "ZeroTier Magisk模块未运行,请检查您是否安装Zerotier Magisk模块。",
|
||||||
|
|
||||||
|
"peerTunneled": "中继",
|
||||||
|
"peerDirect": "直连"
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:developer' as developer;
|
||||||
|
import './screens/home_page.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import './l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Add global error handling
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
|
FlutterError.presentError(details);
|
||||||
|
developer.log('Application error', error: details.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'ZeroTier for Magisk',
|
||||||
|
localizationsDelegates: const [
|
||||||
|
AppLocalizations.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [
|
||||||
|
Locale('en'),
|
||||||
|
Locale('zh'),
|
||||||
|
],
|
||||||
|
theme: ThemeData(
|
||||||
|
colorSchemeSeed: Colors.lightBlue,
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
),
|
||||||
|
darkTheme: ThemeData(
|
||||||
|
colorSchemeSeed: Colors.lightBlue,
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
home: const MyHomePage(),
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import 'dart:math'; // For min function
|
||||||
|
|
||||||
|
class PeerInfo implements Comparable<PeerInfo> {
|
||||||
|
final String address;
|
||||||
|
final int latency;
|
||||||
|
final String? version;
|
||||||
|
final String role;
|
||||||
|
final String? preferredPath;
|
||||||
|
final bool isPlanet;
|
||||||
|
final bool tunneled; // 是否通过中继
|
||||||
|
|
||||||
|
PeerInfo({
|
||||||
|
required this.address,
|
||||||
|
required this.latency,
|
||||||
|
this.version,
|
||||||
|
required this.role,
|
||||||
|
this.preferredPath,
|
||||||
|
required this.tunneled,
|
||||||
|
}) : isPlanet = (role == 'PLANET');
|
||||||
|
|
||||||
|
factory PeerInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
String? bestPath;
|
||||||
|
List<dynamic> paths = json['paths'] as List<dynamic>? ?? [];
|
||||||
|
|
||||||
|
// Find the preferred active path
|
||||||
|
var preferred = paths.firstWhere(
|
||||||
|
(p) =>
|
||||||
|
p is Map<String, dynamic> &&
|
||||||
|
p['active'] == true &&
|
||||||
|
p['expired'] == false &&
|
||||||
|
p['preferred'] == true,
|
||||||
|
orElse: () => null);
|
||||||
|
|
||||||
|
if (preferred != null) {
|
||||||
|
bestPath = preferred['address'] as String?;
|
||||||
|
} else {
|
||||||
|
// If no preferred, find the first active path
|
||||||
|
var firstActive = paths.firstWhere(
|
||||||
|
(p) =>
|
||||||
|
p is Map<String, dynamic> &&
|
||||||
|
p['active'] == true &&
|
||||||
|
p['expired'] == false,
|
||||||
|
orElse: () => null);
|
||||||
|
if (firstActive != null) {
|
||||||
|
bestPath = firstActive['address'] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PeerInfo(
|
||||||
|
address: json['address'] as String? ?? 'Unknown',
|
||||||
|
latency: json['latency'] as int,
|
||||||
|
version: json['version'] as String?,
|
||||||
|
role: json['role'] as String? ?? 'UNKNOWN',
|
||||||
|
preferredPath: bestPath,
|
||||||
|
tunneled: json['tunneled'] as bool,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparison logic for sorting: PLANETs first, then by tunneled (直连优先), then by latency
|
||||||
|
@override
|
||||||
|
int compareTo(PeerInfo other) {
|
||||||
|
if (isPlanet != other.isPlanet) {
|
||||||
|
return isPlanet ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tunneled != other.tunneled) {
|
||||||
|
return tunneled ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后按延迟排序(如果有的话)
|
||||||
|
if (latency != null && other.latency != null) {
|
||||||
|
return latency!.compareTo(other.latency!);
|
||||||
|
} else if (latency != null) {
|
||||||
|
return -1; // 有延迟信息的优先
|
||||||
|
} else if (other.latency != null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果都没有延迟信息,按地址排序
|
||||||
|
return address.compareTo(other.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../services/zerotier_service.dart';
|
||||||
|
import '../components/status_page.dart';
|
||||||
|
import '../components/network_page.dart';
|
||||||
|
import '../components/peers_page.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class MyHomePage extends StatefulWidget {
|
||||||
|
const MyHomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyHomePage> createState() => _MyHomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
final ZerotierService _zerotierService = ZerotierService();
|
||||||
|
int currentPageIndex = 0;
|
||||||
|
// Remove state related to specific page data/futures
|
||||||
|
// Future<void>? _networkFuture;
|
||||||
|
// Future<void>? _peersFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// No initial loading needed here anymore
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// Release service resources
|
||||||
|
_zerotierService.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all data loading and action methods
|
||||||
|
/*
|
||||||
|
Future<void> _loadStatus() async { ... }
|
||||||
|
Future<void> _loadNetworkList() async { ... }
|
||||||
|
Future<void> _loadPeers() async { ... }
|
||||||
|
Future<void> _startZeroTier() async { ... }
|
||||||
|
Future<void> _restartZeroTier() async { ... }
|
||||||
|
Future<void> _stopZeroTier() async { ... }
|
||||||
|
Future<void> _joinNetwork(String id) async { ... }
|
||||||
|
Future<void> _leaveNetwork(String id) async { ... }
|
||||||
|
*/
|
||||||
|
|
||||||
|
String _getTitle(AppLocalizations l10n) {
|
||||||
|
switch (currentPageIndex) {
|
||||||
|
case 0:
|
||||||
|
return l10n.appBarTitleStatus;
|
||||||
|
case 1:
|
||||||
|
return l10n.appBarTitleNetworks;
|
||||||
|
case 2:
|
||||||
|
return l10n.appBarTitlePeers;
|
||||||
|
default:
|
||||||
|
return 'ZeroTier';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getCurrentPage() {
|
||||||
|
switch (currentPageIndex) {
|
||||||
|
case 0:
|
||||||
|
// Pass only the service
|
||||||
|
return StatusPage(
|
||||||
|
zerotierService: _zerotierService,
|
||||||
|
// Remove callbacks
|
||||||
|
// onRefreshStatus: _loadStatus,
|
||||||
|
// startFunction: _startZeroTier,
|
||||||
|
// restartFunction: _restartZeroTier,
|
||||||
|
// stopFunction: _stopZeroTier,
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
// Pass only the service
|
||||||
|
return NetworkPage(
|
||||||
|
zerotierService: _zerotierService,
|
||||||
|
// Remove callbacks
|
||||||
|
// onRefreshNetworkList: _loadNetworkList,
|
||||||
|
// onJoinNetwork: _joinNetwork,
|
||||||
|
// onLeaveNetwork: _leaveNetwork,
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
// Pass only the service
|
||||||
|
return PeersPage(
|
||||||
|
zerotierService: _zerotierService,
|
||||||
|
// Remove callbacks
|
||||||
|
// onRefreshPeers: _loadPeers,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return const Center(child: Text('Page not found'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Get AppLocalizations instance
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(_getTitle(l10n))),
|
||||||
|
bottomNavigationBar: NavigationBar(
|
||||||
|
onDestinationSelected: (int index) {
|
||||||
|
setState(() {
|
||||||
|
currentPageIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selectedIndex: currentPageIndex,
|
||||||
|
destinations: <Widget>[
|
||||||
|
NavigationDestination(
|
||||||
|
selectedIcon: const Icon(Icons.home),
|
||||||
|
icon: const Icon(Icons.home_outlined),
|
||||||
|
label: l10n.navBarLabelStatus,
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
selectedIcon: const Icon(Icons.router),
|
||||||
|
icon: const Icon(Icons.router_outlined),
|
||||||
|
label: l10n.navBarLabelNetworks,
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
selectedIcon: const Icon(Icons.people_alt),
|
||||||
|
icon: const Icon(Icons.people_alt_outlined),
|
||||||
|
label: l10n.navBarLabelPeers,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: _getCurrentPage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
|
/// 带有ZeroTier认证的HTTP客户端
|
||||||
|
class AuthedDioClient {
|
||||||
|
final String _authToken;
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
/// 创建带有ZeroTier认证的HTTP客户端
|
||||||
|
///
|
||||||
|
/// [authToken] - ZeroTier认证令牌
|
||||||
|
/// [baseUrl] - API基础URL
|
||||||
|
AuthedDioClient(this._authToken, String baseUrl) : _dio = Dio(BaseOptions(
|
||||||
|
baseUrl: 'http://$baseUrl',
|
||||||
|
connectTimeout: const Duration(seconds: 5),
|
||||||
|
receiveTimeout: const Duration(seconds: 5),
|
||||||
|
)) {
|
||||||
|
|
||||||
|
// 添加拦截器以处理身份验证
|
||||||
|
_dio.interceptors.add(InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) {
|
||||||
|
// 添加ZeroTier特定认证头
|
||||||
|
options.headers['X-ZT1-Auth'] = _authToken;
|
||||||
|
return handler.next(options);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取底层的Dio实例
|
||||||
|
Dio get dio => _dio;
|
||||||
|
|
||||||
|
/// 发送GET请求
|
||||||
|
Future<Response> get(String path) {
|
||||||
|
return _dio.get(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送POST请求
|
||||||
|
Future<Response> post(String path, {dynamic data}) {
|
||||||
|
return _dio.post(path, data: data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送PUT请求
|
||||||
|
Future<Response> put(String path, {dynamic data}) {
|
||||||
|
return _dio.put(path, data: data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送DELETE请求
|
||||||
|
Future<Response> delete(String path) {
|
||||||
|
return _dio.delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 关闭客户端
|
||||||
|
void close() {
|
||||||
|
_dio.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ZeroTier认证服务,用于管理认证和客户端创建
|
||||||
|
class AuthService {
|
||||||
|
static const String API_HOST = 'localhost:9993';
|
||||||
|
|
||||||
|
/// 缓存的HTTP客户端实例
|
||||||
|
AuthedDioClient? _cachedClient;
|
||||||
|
|
||||||
|
/// 获取已认证的HTTP客户端
|
||||||
|
Future<AuthedDioClient> get client async {
|
||||||
|
// 如果我们已经有缓存的客户端,直接返回
|
||||||
|
if (_cachedClient != null) {
|
||||||
|
return _cachedClient!;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final authToken = await _loadAuthToken();
|
||||||
|
_cachedClient = AuthedDioClient(authToken, API_HOST);
|
||||||
|
return _cachedClient!;
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('加载认证令牌失败', error: e.toString());
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清理资源
|
||||||
|
void dispose() {
|
||||||
|
if (_cachedClient != null) {
|
||||||
|
_cachedClient!.close();
|
||||||
|
_cachedClient = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载认证令牌
|
||||||
|
Future<String> _loadAuthToken() async {
|
||||||
|
try {
|
||||||
|
final path = (await getApplicationDocumentsDirectory()).path;
|
||||||
|
final file = File('$path/run/authtoken');
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!await file.exists()) {
|
||||||
|
throw FileSystemException('认证令牌文件未找到', file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
final token = await file.readAsString();
|
||||||
|
return token.trim();
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('读取认证令牌失败', error: e.toString());
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 超时异常类
|
||||||
|
class TimeoutException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
TimeoutException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:developer' as developer;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'auth_service.dart';
|
||||||
|
import '../models/peer_info.dart';
|
||||||
|
|
||||||
|
/// ZeroTier服务状态详情
|
||||||
|
class ZerotierStatus {
|
||||||
|
final String address;
|
||||||
|
final String version;
|
||||||
|
final bool online;
|
||||||
|
final int primaryPort;
|
||||||
|
final List<String> listeningOn;
|
||||||
|
|
||||||
|
ZerotierStatus({
|
||||||
|
required this.address,
|
||||||
|
required this.version,
|
||||||
|
required this.online,
|
||||||
|
required this.primaryPort,
|
||||||
|
required this.listeningOn,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 从JSON创建状态对象
|
||||||
|
factory ZerotierStatus.fromJson(Map<String, dynamic> json) {
|
||||||
|
// 提取监听地址列表
|
||||||
|
List<String> listeningAddresses = [];
|
||||||
|
if (json['config'] != null &&
|
||||||
|
json['config']['settings'] != null &&
|
||||||
|
json['config']['settings']['listeningOn'] != null) {
|
||||||
|
listeningAddresses = List<String>.from(json['config']['settings']['listeningOn']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取主端口
|
||||||
|
int port = 0;
|
||||||
|
if (json['config'] != null &&
|
||||||
|
json['config']['settings'] != null &&
|
||||||
|
json['config']['settings']['primaryPort'] != null) {
|
||||||
|
port = json['config']['settings']['primaryPort'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ZerotierStatus(
|
||||||
|
address: json['address'] ?? '',
|
||||||
|
version: json['version'] ?? '',
|
||||||
|
online: json['online'] ?? false,
|
||||||
|
primaryPort: port,
|
||||||
|
listeningOn: listeningAddresses,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ZeroTier服务,提供所有ZeroTier相关功能
|
||||||
|
class ZerotierService {
|
||||||
|
final AuthService _authService = AuthService();
|
||||||
|
|
||||||
|
// 内部成员变量,存储最后一次加载的数据
|
||||||
|
List<String> _networkList = [];
|
||||||
|
List<PeerInfo> _peersList = [];
|
||||||
|
ZerotierStatus? _statusInfo;
|
||||||
|
|
||||||
|
// 表示ZeroTier服务是否正在运行
|
||||||
|
bool runningStatus = false;
|
||||||
|
|
||||||
|
// 允许外部访问最新数据的getter方法
|
||||||
|
List<String> get networkList => _networkList;
|
||||||
|
List<PeerInfo> get peersList => _peersList;
|
||||||
|
ZerotierStatus? get statusInfo => _statusInfo;
|
||||||
|
|
||||||
|
/// 向ZeroTier服务发送命令
|
||||||
|
Future<bool> zerotierCommand(String command) async {
|
||||||
|
try {
|
||||||
|
final path = (await getApplicationDocumentsDirectory()).path;
|
||||||
|
final file = File('$path/run/pipe');
|
||||||
|
|
||||||
|
if (!await file.exists()) {
|
||||||
|
developer.log('未找到ZeroTier命令管道: ${file.path}');
|
||||||
|
runningStatus = false;
|
||||||
|
// 特定错误,表示Magisk模块未运行
|
||||||
|
throw const FileSystemException('MODULE_NOT_RUNNING');
|
||||||
|
}
|
||||||
|
|
||||||
|
await file.writeAsString(command);
|
||||||
|
developer.log('已发送ZeroTier命令: $command');
|
||||||
|
return true;
|
||||||
|
} on FileSystemException catch (e) {
|
||||||
|
if (e.message == 'MODULE_NOT_RUNNING') {
|
||||||
|
developer.log('ZeroTier Magisk模块未运行');
|
||||||
|
} else {
|
||||||
|
developer.log('未找到ZeroTier命令管道,服务可能未运行');
|
||||||
|
}
|
||||||
|
runningStatus = false;
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('发送ZeroTier命令失败', error: e.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载网络列表
|
||||||
|
Future<List<String>?> loadNetwork() async {
|
||||||
|
developer.log('开始加载网络列表');
|
||||||
|
try {
|
||||||
|
final client = await _authService.client;
|
||||||
|
// 使用相对路径,基础URL已在客户端中配置
|
||||||
|
final resp = await client.get('/network');
|
||||||
|
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
throw HttpException('加载网络列表失败: ${resp.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = resp.data;
|
||||||
|
_networkList = List<String>.from(body.map((u) => u['id']));
|
||||||
|
developer.log('成功加载网络列表: ${_networkList.length} 个网络');
|
||||||
|
return _networkList;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.connectionError) {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
} else {
|
||||||
|
developer.log('加载网络列表失败', error: e.toString());
|
||||||
|
}
|
||||||
|
_networkList = [];
|
||||||
|
return null;
|
||||||
|
} on SocketException {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
_networkList = [];
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('加载网络列表失败', error: e.toString());
|
||||||
|
_networkList = [];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 离开指定网络
|
||||||
|
Future<bool> leaveNetwork(String id) async {
|
||||||
|
try {
|
||||||
|
final client = await _authService.client;
|
||||||
|
// 使用相对路径
|
||||||
|
final resp = await client.delete('/network/$id');
|
||||||
|
final success = resp.statusCode == 200;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
developer.log('成功离开网络: $id');
|
||||||
|
} else {
|
||||||
|
developer.log('离开网络失败: $id, 状态码: ${resp.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.connectionError) {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
// 更新运行状态
|
||||||
|
runningStatus = false;
|
||||||
|
} else {
|
||||||
|
developer.log('离开网络错误', error: e.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} on SocketException {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
// 更新运行状态
|
||||||
|
runningStatus = false;
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('离开网络错误', error: e.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加入指定网络
|
||||||
|
Future<bool> joinNetwork(String id) async {
|
||||||
|
try {
|
||||||
|
if (id.isEmpty) {
|
||||||
|
developer.log('无法加入空网络ID');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final client = await _authService.client;
|
||||||
|
// 使用相对路径
|
||||||
|
final resp = await client.put('/network/$id');
|
||||||
|
final success = resp.statusCode == 200;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
developer.log('成功加入网络: $id');
|
||||||
|
} else {
|
||||||
|
developer.log('加入网络失败: $id, 状态码: ${resp.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.connectionError) {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
// 更新运行状态
|
||||||
|
runningStatus = false;
|
||||||
|
} else {
|
||||||
|
developer.log('加入网络错误', error: e.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} on SocketException {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
// 更新运行状态
|
||||||
|
runningStatus = false;
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('加入网络错误', error: e.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载 Peer 列表
|
||||||
|
Future<List<PeerInfo>?> loadPeers() async {
|
||||||
|
developer.log('开始加载 Peer 列表');
|
||||||
|
try {
|
||||||
|
final client = await _authService.client;
|
||||||
|
final resp = await client.get('/peer');
|
||||||
|
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
throw HttpException('加载 Peer 列表失败: ${resp.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<dynamic> rawPeers = resp.data;
|
||||||
|
_peersList = rawPeers
|
||||||
|
.map((p) => PeerInfo.fromJson(p as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Sort the list (PLANETs first, then LEAFs by address)
|
||||||
|
_peersList.sort();
|
||||||
|
|
||||||
|
developer.log('成功加载 Peer 列表: ${_peersList.length} 个 Peers');
|
||||||
|
return _peersList;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.connectionError) {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
} else {
|
||||||
|
developer.log('加载 Peer 列表 Dio 错误', error: e.toString(), stackTrace: e.stackTrace);
|
||||||
|
}
|
||||||
|
_peersList = [];
|
||||||
|
return null;
|
||||||
|
} on SocketException {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
_peersList = [];
|
||||||
|
return null;
|
||||||
|
} catch (e, s) {
|
||||||
|
developer.log('加载 Peer 列表失败', error: e.toString(), stackTrace: s);
|
||||||
|
_peersList = [];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载ZeroTier服务状态
|
||||||
|
Future<ZerotierStatus?> loadStatus() async {
|
||||||
|
developer.log('检查ZeroTier服务状态');
|
||||||
|
Map<String, dynamic>? statusData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试获取状态信息
|
||||||
|
final client = await _authService.client;
|
||||||
|
final response = await client.get('/status');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
statusData = response.data;
|
||||||
|
developer.log('已获取ZeroTier状态信息');
|
||||||
|
} else {
|
||||||
|
developer.log('获取ZeroTier状态信息失败: ${response.statusCode}');
|
||||||
|
statusData = null;
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.connectionError) {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
} else {
|
||||||
|
developer.log('获取ZeroTier状态信息错误', error: e.toString());
|
||||||
|
}
|
||||||
|
statusData = null;
|
||||||
|
} on SocketException {
|
||||||
|
developer.log('无法连接到ZeroTier服务:服务可能未运行');
|
||||||
|
statusData = null;
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('检查ZeroTier状态错误', error: e.toString());
|
||||||
|
statusData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据获取的状态信息更新运行状态和详情
|
||||||
|
if (statusData != null) {
|
||||||
|
runningStatus = true;
|
||||||
|
_statusInfo = ZerotierStatus.fromJson(statusData);
|
||||||
|
developer.log('ZeroTier服务状态: 运行中');
|
||||||
|
developer.log('已加载ZeroTier详细状态信息');
|
||||||
|
return _statusInfo;
|
||||||
|
} else {
|
||||||
|
runningStatus = false;
|
||||||
|
_statusInfo = null;
|
||||||
|
developer.log('ZeroTier服务状态: 未运行');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 释放资源
|
||||||
|
void dispose() {
|
||||||
|
_authService.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
name: zerotier_magisk_app
|
||||||
|
description: "Controller app for zerotier-magisk"
|
||||||
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.3.4 <4.0.0'
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
intl: any
|
||||||
|
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
cupertino_icons: ^1.0.6
|
||||||
|
dio: ^5.4.0
|
||||||
|
path_provider: ^2.1.3
|
||||||
|
quickalert: ^1.1.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
# package. See that file for information about deactivating specific lint
|
||||||
|
# rules and activating additional ones.
|
||||||
|
flutter_lints: ^5.0.0
|
||||||
|
flutter_launcher_icons: ^0.14.3
|
||||||
|
launcher_name: ^1.0.2
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
generate: true
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/custom-fonts/#from-packages
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: "launcher_icon"
|
||||||
|
ios: false
|
||||||
|
image_path: "assets/icon.png"
|
||||||
|
min_sdk_android: 21 # android min sdk min:16, default 21
|
||||||
|
launcher_name:
|
||||||
|
default: "ZeroTier for Magisk"
|
||||||
@@ -28,6 +28,25 @@ with open(config_toml_path, 'w') as f:
|
|||||||
|
|
||||||
# -------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Patch ZeroTierOne/osdep/LinuxEthernetTap.cpp
|
||||||
|
|
||||||
|
linux_tap_path = 'ZeroTierOne/osdep/LinuxEthernetTap.cpp'
|
||||||
|
|
||||||
|
linux_tap_match1 = 'int rc = pthread_setaffinity_np(self, sizeof(cpu_set_t), &cpuset);'
|
||||||
|
linux_tap_replace1 = 'int rc = sched_setaffinity(pthread_gettid_np(self), sizeof(cpu_set_t), &cpuset);'
|
||||||
|
|
||||||
|
linux_tap_match2 = '#include <sys/utsname.h>'
|
||||||
|
linux_tap_replace2 = '#include <sys/utsname.h>\n#include <sched.h>'
|
||||||
|
|
||||||
|
with open(linux_tap_path, 'r') as file:
|
||||||
|
data = file.read()
|
||||||
|
patch_NDK = data.replace(linux_tap_match1, linux_tap_replace1).replace(linux_tap_match2, linux_tap_replace2)
|
||||||
|
|
||||||
|
with open(linux_tap_path, 'w') as file:
|
||||||
|
file.write(patch_NDK)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
# Patch make-linux.mk
|
# Patch make-linux.mk
|
||||||
|
|
||||||
make_linux_path = 'ZeroTierOne/make-linux.mk'
|
make_linux_path = 'ZeroTierOne/make-linux.mk'
|
||||||
@@ -68,8 +87,6 @@ with open(make_linux_path, 'r') as file:
|
|||||||
|
|
||||||
osutil_path = 'ZeroTierOne/osdep/OSUtils.cpp'
|
osutil_path = 'ZeroTierOne/osdep/OSUtils.cpp'
|
||||||
with open(osutil_path, 'r') as file:
|
with open(osutil_path, 'r') as file:
|
||||||
data = file.read().replace(
|
data = file.read().replace('/var/lib/zerotier-one', '/data/adb/zerotier/home')
|
||||||
'/var/lib/zerotier-one', '/data/adb/zerotier/home'
|
|
||||||
)
|
|
||||||
with open(osutil_path, 'w') as file:
|
with open(osutil_path, 'w') as file:
|
||||||
file.write(data)
|
file.write(data)
|
||||||
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -10,6 +10,7 @@ ui_print "- unzip"
|
|||||||
unzip -o $ZIPFILE -x 'META-INF/*' -d $MODPATH >&2
|
unzip -o $ZIPFILE -x 'META-INF/*' -d $MODPATH >&2
|
||||||
|
|
||||||
ui_print "- create work dir"
|
ui_print "- create work dir"
|
||||||
|
rm -rf $ZTPATH/lib $ZTPATH/run
|
||||||
mkdir -p $ZTPATH/run $ZTPATH/home
|
mkdir -p $ZTPATH/run $ZTPATH/home
|
||||||
mv $MODPATH/zerotier/* $ZTPATH
|
mv $MODPATH/zerotier/* $ZTPATH
|
||||||
ln -sf $ZTPATH/zerotier-one $ZTPATH/zerotier-cli
|
ln -sf $ZTPATH/zerotier-one $ZTPATH/zerotier-cli
|
||||||
@@ -19,10 +20,12 @@ ui_print "- link to system"
|
|||||||
mkdir -p $MODPATH$SYSBIN
|
mkdir -p $MODPATH$SYSBIN
|
||||||
ln -sf $ZTPATH/zerotier.sh $MODPATH$SYSBIN/zerotier.sh
|
ln -sf $ZTPATH/zerotier.sh $MODPATH$SYSBIN/zerotier.sh
|
||||||
ln -sf $ZTPATH/zerotier-cli $MODPATH$SYSBIN/zerotier-cli
|
ln -sf $ZTPATH/zerotier-cli $MODPATH$SYSBIN/zerotier-cli
|
||||||
|
ln -sf $ZTPATH/zerotier-idtool $MODPATH$SYSBIN/zerotier-idtool
|
||||||
|
|
||||||
ui_print "- set file permission"
|
ui_print "- set file permission"
|
||||||
set_perm_recursive $MODPATH 0 0 0755 0644
|
set_perm_recursive $MODPATH 0 0 0755 0644
|
||||||
set_perm_recursive $ZTPATH 0 0 0755 0644
|
set_perm_recursive $ZTPATH 0 0 0755 0644
|
||||||
|
set_perm $MODPATH/handle.sh 0 0 0755
|
||||||
set_perm $ZTPATH/zerotier.sh 0 0 0755
|
set_perm $ZTPATH/zerotier.sh 0 0 0755
|
||||||
set_perm $ZTPATH/zerotier-one 0 0 0755
|
set_perm $ZTPATH/zerotier-one 0 0 0755
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
|
||||||
|
MODDIR=${0%/*}
|
||||||
|
|
||||||
|
# load variables
|
||||||
|
. $MODDIR/lib.sh
|
||||||
|
|
||||||
|
log_cli() {
|
||||||
|
echo -e "$1" >> $ZTROOT/run/cli.out
|
||||||
|
}
|
||||||
|
log() {
|
||||||
|
t=`date +"%m-%d %H:%M:%S.%3N"`
|
||||||
|
echo -e "[$t][$$][L] $@" >> $DAEMON_LOG
|
||||||
|
log_cli "$@"
|
||||||
|
}
|
||||||
|
_stop() {
|
||||||
|
pid=`pidof zerotier-one`
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log "zerotier-one not running"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
kill -9 $pid
|
||||||
|
|
||||||
|
# delete from ip rules
|
||||||
|
ip rule del from all lookup main pref 1
|
||||||
|
ip -6 rule del from all lookup main pref 1
|
||||||
|
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log "kill zerotier-one failed"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "stopped zerotier-one"
|
||||||
|
}
|
||||||
|
_start() {
|
||||||
|
if pidof zerotier-one > /dev/null; then
|
||||||
|
log "zerotier-one already running"
|
||||||
|
else
|
||||||
|
__start
|
||||||
|
log "started zerotier-one"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
_status() {
|
||||||
|
# fake systemd lol
|
||||||
|
read pid < $ZTROOT/home/zerotier-one.pid
|
||||||
|
|
||||||
|
if pidof zerotier-one > /dev/null; then
|
||||||
|
log_cli "\033[32m●\033[0m zerotier-one.service - ZeroTier One - Global Area Networking"
|
||||||
|
log_cli " Active: \033[32mactive (running)\033[0m"
|
||||||
|
log_cli " Main PID: $pid (zerotier-one)"
|
||||||
|
else
|
||||||
|
log_cli "○ zerotier-one.service - ZeroTier One - Global Area Networking"
|
||||||
|
log_cli " Active: inactive (dead)"
|
||||||
|
log_cli " Main PID: $pid (code=exited)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
_restart() {
|
||||||
|
_stop
|
||||||
|
__start
|
||||||
|
log "started zerotier-one"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------
|
||||||
|
# call from inotifyd
|
||||||
|
# ----------------------------------------------
|
||||||
|
|
||||||
|
if [[ $# == 2 && "$1" == "w" ]]; then
|
||||||
|
read cmd < $2
|
||||||
|
rm -f $ZTROOT/run/cli.out
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
"start") _start;;
|
||||||
|
"stop") _stop;;
|
||||||
|
"restart") _restart;;
|
||||||
|
"status") _status;;
|
||||||
|
*) log "unknown command $cmd";;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -f "$ZTROOT/run/cli.pid" ]]; then
|
||||||
|
read cpid < $ZTROOT/run/cli.pid
|
||||||
|
rm -f $ZTROOT/run/cli.pid
|
||||||
|
kill -SIGUSR1 $cpid
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# variables
|
||||||
|
|
||||||
|
ZTROOT=/data/adb/zerotier
|
||||||
|
APPROOT=/data/data/com.eventlowop.zerotier_magisk_app/app_flutter
|
||||||
|
|
||||||
|
ZT_LOG=$ZTROOT/run/zerotier.log
|
||||||
|
DAEMON_LOG=$ZTROOT/run/daemon.log
|
||||||
|
|
||||||
|
PIPE_CLI=$ZTROOT/run/pipe
|
||||||
|
PIPE_APP=$APPROOT/run/pipe
|
||||||
|
|
||||||
|
# LD_LIBRARY_PATH for NDK
|
||||||
|
export LD_LIBRARY_PATH=/system/lib64:/data/adb/zerotier/lib
|
||||||
|
|
||||||
|
__start() {
|
||||||
|
# add main route table to lookup rules
|
||||||
|
ip rule add from all lookup main pref 1
|
||||||
|
ip -6 rule add from all lookup main pref 1
|
||||||
|
|
||||||
|
# start zerotier daemon
|
||||||
|
nohup $ZTROOT/zerotier-one -d >> $ZT_LOG 2>&1 &
|
||||||
|
}
|
||||||
@@ -2,87 +2,58 @@
|
|||||||
|
|
||||||
MODDIR=${0%/*}
|
MODDIR=${0%/*}
|
||||||
|
|
||||||
ZTROOT=/data/adb/zerotier
|
# load variables
|
||||||
ZTRUNTIME=$ZTROOT/run
|
. $MODDIR/lib.sh
|
||||||
|
|
||||||
pipe=$ZTRUNTIME/pipe
|
# wait_until_login from yc9559/uperf at uperf/magisk/script/libcommon.sh
|
||||||
ZTLOG=$ZTRUNTIME/zerotier.log
|
wait_until_login() {
|
||||||
daemon_log=$ZTRUNTIME/daemon.log
|
# in case of /data encryption is disabled
|
||||||
|
while [ "$(getprop sys.boot_completed)" != "1" ]; do
|
||||||
cli_output=$ZTRUNTIME/cli.out
|
sleep 1
|
||||||
cli_pid=$ZTRUNTIME/cli.pid
|
|
||||||
|
|
||||||
log() {
|
|
||||||
t=`date +"%m-%d %H:%M:%S.%3N"`
|
|
||||||
echo -e "[$t][$$][L] $1" >> $daemon_log
|
|
||||||
echo -e "$1" >> $cli_output
|
|
||||||
}
|
|
||||||
log_cli() {
|
|
||||||
echo -e "$1" >> $cli_output
|
|
||||||
}
|
|
||||||
_stop() {
|
|
||||||
pid=`pidof zerotier-one`
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
log "zerotier-one not running"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
kill -9 $pid
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
log "kill zerotier-one failed"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
wait
|
|
||||||
log "stopped zerotier-one"
|
|
||||||
}
|
|
||||||
__start() {
|
|
||||||
$ZTROOT/zerotier-one -d >> $ZTLOG 2>&1 &
|
|
||||||
}
|
|
||||||
_start() {
|
|
||||||
if pid=`pidof zerotier-one`; then
|
|
||||||
log "zerotier-one already running pid $pid"
|
|
||||||
else
|
|
||||||
__start
|
|
||||||
pid=`pidof zerotier-one`
|
|
||||||
log "started zerotier-one pid $pid"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
_status() {
|
|
||||||
# fake systemd lol
|
|
||||||
if pid=`pidof zerotier-one`; then
|
|
||||||
log_cli "\033[32m●\033[0m zerotier-one.service - ZeroTier One - Global Area Networking"
|
|
||||||
log_cli " Active: \033[32mactive (running)\033[0m"
|
|
||||||
log_cli " Main PID: $pid (zerotier-one)"
|
|
||||||
else
|
|
||||||
pid_=`cat $zerotier_root/home/zerotier-one.pid`
|
|
||||||
log_cli "○ zerotier-one.service - ZeroTier One - Global Area Networking"
|
|
||||||
log_cli " Active: inactive (dead)"
|
|
||||||
log_cli " Main PID: $pid_ (code=exited)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
cd $ZTROOT
|
|
||||||
rm -f $ZTRUNTIME
|
|
||||||
mkfifo $pipe
|
|
||||||
|
|
||||||
ip rule add from all lookup main pref 1
|
|
||||||
export LD_LIBRARY_PATH=/data/adb/zerotier/lib
|
|
||||||
|
|
||||||
__start
|
|
||||||
|
|
||||||
while true
|
|
||||||
do
|
|
||||||
if read cmd < $pipe; then
|
|
||||||
case "$cmd" in
|
|
||||||
"start") _start;;
|
|
||||||
"stop") _stop;;
|
|
||||||
"restart") _stop; _start;;
|
|
||||||
"status") _status;;
|
|
||||||
*) log "unknown command $cmd";;
|
|
||||||
esac
|
|
||||||
|
|
||||||
cpid=`cat $cli_pid`
|
|
||||||
kill -SIGUSR1 $cpid;
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# we doesn't have the permission to rw "/sdcard" before the user unlocks the screen
|
||||||
|
local test_file="/sdcard/Android/.PERMISSION_TEST"
|
||||||
|
true >"$test_file"
|
||||||
|
while [ ! -f "$test_file" ]; do
|
||||||
|
true >"$test_file"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
rm "$test_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------
|
||||||
|
# clean before start
|
||||||
|
# ----------------------------------------------
|
||||||
|
|
||||||
|
rm -rf $ZTROOT/run
|
||||||
|
mkdir -p $ZTROOT/run
|
||||||
|
|
||||||
|
# ----------------------------------------------
|
||||||
|
# start zerotier
|
||||||
|
# ----------------------------------------------
|
||||||
|
|
||||||
|
__start
|
||||||
|
|
||||||
|
# ----------------------------------------------
|
||||||
|
# CLI before login
|
||||||
|
# ----------------------------------------------
|
||||||
|
|
||||||
|
touch $PIPE_CLI
|
||||||
|
inotifyd $MODDIR/handle.sh $PIPE_CLI:w &>/dev/null & # toybox inotifyd bug: needs an extra character after colon
|
||||||
|
|
||||||
|
# ----------------------------------------------
|
||||||
|
# APP after login
|
||||||
|
# ----------------------------------------------
|
||||||
|
|
||||||
|
wait_until_login
|
||||||
|
|
||||||
|
if [[ -d "$APPROOT" ]]; then
|
||||||
|
rm -rf $APPROOT/run
|
||||||
|
mkdir -p $APPROOT/run
|
||||||
|
|
||||||
|
cp $ZTROOT/home/authtoken.secret $APPROOT/run/authtoken
|
||||||
|
touch $PIPE_APP
|
||||||
|
chmod 666 $APPROOT/run/authtoken $PIPE_APP
|
||||||
|
inotifyd $MODDIR/handle.sh $PIPE_APP:w &>/dev/null &
|
||||||
|
fi
|
||||||
@@ -1,26 +1,24 @@
|
|||||||
#!/system/bin/sh
|
#!/system/bin/sh
|
||||||
pipe=/data/adb/zerotier/run/pipe
|
|
||||||
|
|
||||||
cli_output=/data/adb/zerotier/run/cli.out
|
ZTROOT=/data/adb/zerotier
|
||||||
cli_pid=/data/adb/zerotier/run/cli.pid
|
PIPE=$ZTROOT/run/pipe
|
||||||
|
|
||||||
help_text="Usage: zerotier.sh {start|stop|restart|status}"
|
HELP="Usage: zerotier.sh {start|stop|restart|status}"
|
||||||
|
|
||||||
if [[ ! -p $pipe ]]; then
|
if [[ ! -f $PIPE ]]; then
|
||||||
echo "daemon not running"
|
echo "daemon not running"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f $cli_output
|
echo $$ > $ZTROOT/run/cli.pid
|
||||||
echo $$ > $cli_pid
|
|
||||||
|
|
||||||
on_receive() {
|
on_receive() {
|
||||||
kill -9 %%
|
kill -9 %%
|
||||||
cat $cli_output
|
cat $ZTROOT/run/cli.out
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
run() {
|
run() {
|
||||||
echo $cmd > $pipe
|
echo $cmd > $PIPE
|
||||||
}
|
}
|
||||||
|
|
||||||
trap 'on_receive' SIGUSR1
|
trap 'on_receive' SIGUSR1
|
||||||
@@ -35,7 +33,7 @@ if [[ $# -eq 1 ]]; then
|
|||||||
*) echo "unknown command $cmd";;
|
*) echo "unknown command $cmd";;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
echo $help_text
|
echo $HELP
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||