13 Commits
30 changed files with 3439 additions and 752 deletions
+7 -1
View File
@@ -15,6 +15,12 @@ jobs:
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
@@ -24,7 +30,7 @@ jobs:
- id: commit
uses: prompt/actions-commit-hash@v3
- name: Upload AArch64
- name: Upload App release
uses: actions/upload-artifact@v4
with:
name: app-${{ steps.commit.outputs.short }}
+1 -1
View File
@@ -5,7 +5,7 @@ on:
env:
api_version: 28 # min. 21
ndk: r26d # android-ndk-$ndk-linux.zip
ndk: r28 # android-ndk-$ndk-linux.zip
jobs:
build:
+12
View File
@@ -97,3 +97,15 @@ log files are placed in `run`, `daemon.log` for `service.sh` and `zerotier.log`
## Build binaries yourself
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.
+13
View File
@@ -97,3 +97,16 @@ ZeroTier 可执行文件和操作的 Shell 脚本放在 `/data/adb/zerotier/`
## 自行编译
参考 `.github/workflow/build-{gcc|ndk}.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
+2
View File
@@ -1,3 +1,5 @@
**/.cxx/
# Miscellaneous
*.class
*.log
File diff suppressed because one or more lines are too long
+3
View File
@@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
+3 -1
View File
@@ -1,5 +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
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
+2 -2
View File
@@ -19,8 +19,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
id "com.android.application" version '8.10.0-beta01' apply false
id "org.jetbrains.kotlin.android" version "2.1.20" apply false
}
include ":app"
+7
View File
@@ -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
-39
View File
@@ -1,39 +0,0 @@
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
class AuthedClientInner extends http.BaseClient {
final String _secret;
final http.Client _inner;
AuthedClientInner(this._secret, this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
request.headers['X-ZT1-Auth'] = _secret;
return _inner.send(request);
}
// @override
// Future<http.Response> post(Uri url,
// {Map<String, String>? headers, Object? body, Encoding? encoding}) {
// Map<String, String> hd = headers ?? <String, String>{};
// hd['content-type'] = 'application/json';
// return super.post(url, headers: hd, body: body, encoding: encoding);
// }
}
class AuthedClient {
late Future<AuthedClientInner> client;
Future<AuthedClientInner> loadSecret() async {
final path = (await getApplicationDocumentsDirectory()).path;
final file = File('$path/run/authtoken');
final token = await file.readAsString();
return AuthedClientInner(token.trim(), http.Client());
}
AuthedClient() {
client = loadSecret();
}
}
+357
View File
@@ -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)
),
])
)
);
}
),
)
),
],
));
}
}
+250
View File
@@ -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,
);
}
}
+390
View File
@@ -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),
),
),
),
],
),
);
}
}
+125
View File
@@ -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"
}
+429
View File
@@ -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, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects 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.'
);
}
+176
View File
@@ -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';
}
+176
View File
@@ -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 => '直连';
}
+125
View File
@@ -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": "直连"
}
+19 -262
View File
@@ -1,13 +1,16 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'dart:io';
import 'dart:developer' as developer;
import 'package:flutter/services.dart';
import './authed_client.dart';
import 'package:path_provider/path_provider.dart';
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());
}
@@ -18,6 +21,16 @@ class MyApp extends StatelessWidget {
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,
@@ -33,259 +46,3 @@ class MyApp extends StatelessWidget {
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
Future<void> zerotierCommand(String command) async {
final path = (await getApplicationDocumentsDirectory()).path;
final file = File('$path/run/pipe');
await file.writeAsString(command);
}
class _MyHomePageState extends State<MyHomePage> {
List<String> networkList = [];
bool runningStatus = false;
final AuthedClient authed = AuthedClient();
loadNetwork() async {
final client = await authed.client;
final resp = await client.get(
Uri.http('localhost:9993', 'network'),
);
final body = jsonDecode(resp.body);
networkList = List<String>.from(body.map((u) => u['id']));
}
void leaveNetwork(String id) async {
final client = await authed.client;
final resp = await client.delete(Uri.http('localhost:9993', 'network/$id'));
if (resp.statusCode != 200) {
// failed
return;
}
// success
setState(() {
networkList.remove(id);
});
}
Future<void> joinNetwork(String id) async {
final client = await authed.client;
final resp = await client.put(Uri.http('localhost:9993', 'network/$id'));
if (resp.statusCode != 200) {
// failed
return;
}
// success
if (!networkList.contains(id)) {
setState(() {
networkList.add(id);
});
}
}
Future<void> loadStatus() async {
final client = await authed.client;
try {
await client.get(Uri.http('localhost:9993'));
setState(() => runningStatus = true);
return;
} on SocketException {
setState(() => runningStatus = false);
} catch (e) {
developer.log('error', error: e.toString());
// error
}
}
late Future<void> loadingNetworkFuture = loadNetwork();
@override
void initState() {
super.initState();
loadStatus();
}
Function cmdButtonWrapper(func) {
bool doing = false;
return () => doing
? null
: () async {
setState(() => doing = true);
await func();
setState(() => doing = false);
};
}
late Function restartFn, startFn, stopFn, joinFn, leaveFn;
_MyHomePageState() : super() {
restartFn =
cmdButtonWrapper(() => zerotierCommand('restart'));
startFn = cmdButtonWrapper(() => zerotierCommand('start'));
stopFn = cmdButtonWrapper(() => zerotierCommand('stop'));
joinFn = cmdButtonWrapper(() => joinNetwork(_idInputController.text));
}
final _idInputController = TextEditingController();
homePage({bool title = false}) {
if (title) {
return 'Status';
}
return Column(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Builder(builder: (BuildContext context) {
final color = (runningStatus ? Colors.green : Colors.red)[200];
return Column(children: [
Icon(runningStatus ? Icons.play_arrow : Icons.stop, color: color, size: 72),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(runningStatus ? 'Running' : 'Stopped', style: TextStyle(color: color, fontSize: 18)),
IconButton(icon: const Icon(Icons.refresh), onPressed: loadStatus)
])
]);
}),
ButtonBar(
alignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
OutlinedButton.icon(
onPressed: startFn(),
label: const Text('Start'),
icon: const Icon(Icons.play_arrow),
),
OutlinedButton.icon(
onPressed: restartFn(),
label: const Text('Restart'),
icon: const Icon(Icons.restart_alt),
),
OutlinedButton.icon(
onPressed: stopFn(),
label: const Text('Stop'),
icon: const Icon(Icons.stop),
),
],
)
]);
}
networkPage({bool title = false}) {
if (title) {
return 'Network';
}
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
SizedBox(
width: 200,
child: TextField(
controller: _idInputController,
textAlign: TextAlign.center,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp('[0-9a-f]'))
],
maxLength: 16,
decoration: const InputDecoration(
labelText: "network id", icon: Icon(Icons.router)))),
OutlinedButton.icon(
onPressed: joinFn(),
label: const Text('Join'),
icon: const Icon(Icons.add)),
]),
const SizedBox(height: 30),
const Divider(height: 10),
ListTile(
title: const Text('Networks'),
titleTextStyle: const TextStyle(fontSize: 18),
trailing: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
setState(() {
loadingNetworkFuture = loadNetwork();
});
})),
const SizedBox(height: 10),
FutureBuilder<void>(
future: loadingNetworkFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return ListView.builder(
shrinkWrap: true,
itemCount: networkList.length,
itemBuilder: (BuildContext ctx, int i) {
return Card(
child: ListTile(
title: Text(networkList[i]),
trailing: SizedBox(
width: 96,
child: Row(children: [
IconButton(
onPressed: () => Clipboard.setData(
ClipboardData(
text: networkList[i]))
.then,
tooltip: '复制',
icon: const Icon(Icons.copy)),
IconButton(
onPressed: () =>
leaveNetwork(networkList[i]),
tooltip: '离开',
icon: const Icon(Icons.close)),
]))));
});
}
return const CircularProgressIndicator();
},
),
],
));
}
int currentPageIndex = 0;
genWidgetList({bool title = false}) {
return [
homePage(title: title),
networkPage(title: title)
][currentPageIndex];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(genWidgetList(title: true))),
bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) {
setState(() {
currentPageIndex = index;
});
},
selectedIndex: currentPageIndex,
destinations: const <Widget>[
NavigationDestination(
selectedIcon: Icon(Icons.home),
icon: Icon(Icons.home_outlined),
label: 'Status',
),
NavigationDestination(
selectedIcon: Icon(Icons.router),
icon: Icon(Icons.router_outlined),
label: 'Network',
),
],
),
body: Center(
child: genWidgetList(),
));
}
}
+82
View File
@@ -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);
}
}
+128
View File
@@ -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(),
);
}
}
+120
View File
@@ -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;
}
+305
View File
@@ -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();
}
}
-429
View File
@@ -1,429 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.5.1"
args:
dependency: transitive
description:
name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.5.0"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.3.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.18.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.0.3"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.2"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.13.1"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "4.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: "direct main"
description:
name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "4.0.2"
image:
dependency: transitive
description:
name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "4.1.7"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "4.9.0"
launcher_name:
dependency: "direct dev"
description:
name: launcher_name
sha256: "5fc9a8b8de9e255d5f21effc33632b5620771c9b2310d459a7710b725353b305"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.0.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "10.0.0"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.0.1"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "4.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.11.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.2.4"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.4.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.6.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.3.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "13.0.0"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
description:
name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "5.5.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.0.4"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.5.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.3.4 <4.0.0"
flutter: ">=3.16.6"
+8 -3
View File
@@ -30,13 +30,17 @@ environment:
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
http: ^1.2.1
dio: ^5.4.0
path_provider: ^2.1.3
quickalert: ^1.1.0
dev_dependencies:
flutter_test:
@@ -47,8 +51,8 @@ dev_dependencies:
# 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: ^4.0.0
flutter_launcher_icons: ^0.13.1
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
@@ -61,6 +65,7 @@ flutter:
# 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:
+26 -9
View File
@@ -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
make_linux_path = 'ZeroTierOne/make-linux.mk'
@@ -57,19 +76,17 @@ with open(make_linux_path, 'r') as file:
'override CFLAGS+=-march=armv7-a -marm -mfpu=vfp -fexceptions'
)
with open(make_linux_path + '.aarch64', 'w') as file:
file.write(patch_aarch64)
with open(make_linux_path + '.arm', 'w') as file:
file.write(patch_arm)
with open(make_linux_path + '.arm.ndk', 'w') as file:
file.write(patch_arm_ndk)
with open(make_linux_path + '.aarch64', 'w') as file:
file.write(patch_aarch64)
with open(make_linux_path + '.arm', 'w') as file:
file.write(patch_arm)
with open(make_linux_path + '.arm.ndk', 'w') as file:
file.write(patch_arm_ndk)
# Patch ZeroTierOne/osdep/OSUtils.cpp
osutil_path = 'ZeroTierOne/osdep/OSUtils.cpp'
with open(osutil_path, 'r') as file:
data = file.read().replace(
'/var/lib/zerotier-one', '/data/adb/zerotier/home'
)
data = file.read().replace('/var/lib/zerotier-one', '/data/adb/zerotier/home')
with open(osutil_path, 'w') as file:
file.write(data)
+5
View File
@@ -21,6 +21,11 @@ _stop() {
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
+5
View File
@@ -13,5 +13,10 @@ PIPE_APP=$APPROOT/run/pipe
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 &
}
-5
View File
@@ -33,11 +33,6 @@ mkdir -p $ZTROOT/run
# start zerotier
# ----------------------------------------------
# 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
__start
# ----------------------------------------------