Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d0a3c2ea9 | ||
|
|
dfcd8922e5 | ||
|
|
f55e880f53 | ||
|
|
e969746889 | ||
|
|
eb00b1ae35 | ||
|
|
9db350f368 | ||
|
|
bacea863bf | ||
|
|
0405277231 | ||
|
|
4191d8cbc1 | ||
|
|
999ac75324 | ||
|
|
d783b6c413 | ||
|
|
4ee668dc13 | ||
|
|
b3210882dd |
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
**/.cxx/
|
||||
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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": "直连"
|
||||
}
|
||||
+19
-262
@@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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 &
|
||||
}
|
||||
@@ -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
|
||||
|
||||
# ----------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user