Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d0a3c2ea9 | ||
|
|
dfcd8922e5 | ||
|
|
f55e880f53 | ||
|
|
e969746889 | ||
|
|
eb00b1ae35 | ||
|
|
9db350f368 | ||
|
|
bacea863bf |
@@ -14,6 +14,12 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: stable
|
channel: stable
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jetbrains'
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
- name: build target apk
|
- name: build target apk
|
||||||
run: |
|
run: |
|
||||||
@@ -24,7 +30,7 @@ jobs:
|
|||||||
- id: commit
|
- id: commit
|
||||||
uses: prompt/actions-commit-hash@v3
|
uses: prompt/actions-commit-hash@v3
|
||||||
|
|
||||||
- name: Upload AArch64
|
- name: Upload App release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-${{ steps.commit.outputs.short }}
|
name: app-${{ steps.commit.outputs.short }}
|
||||||
|
|||||||
@@ -104,4 +104,8 @@ After 1.14.0, ZeroTierOne has introduce `multi-core concurrent packet processing
|
|||||||
|
|
||||||
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)
|
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>`.
|
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.
|
||||||
|
|||||||
+6
-1
@@ -104,4 +104,9 @@ ZeroTier 可执行文件和操作的 Shell 脚本放在 `/data/adb/zerotier/`
|
|||||||
|
|
||||||
但 `pthread_setaffinity_np` 在 NDK 的 API level 36, Android 16 才受支持。 (参考 https://android.googlesource.com/platform/bionic/+/master/libc/include/pthread.h)
|
但 `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` 的组合,来实现相同的功能
|
所以 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
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,6 @@
|
|||||||
org.gradle.jvmargs=-Xmx4G
|
org.gradle.jvmargs=-Xmx4G
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonTransitiveRClass=false
|
||||||
|
android.nonFinalResIds=false
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "7.3.0" apply false
|
id "com.android.application" version '8.10.0-beta01' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
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
-304
@@ -1,14 +1,16 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
import 'package:flutter/services.dart';
|
import './screens/home_page.dart';
|
||||||
import 'package:quickalert/quickalert.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import './l10n/app_localizations.dart';
|
||||||
import './authed_client.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// Add global error handling
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
|
FlutterError.presentError(details);
|
||||||
|
developer.log('Application error', error: details.toString());
|
||||||
|
};
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,6 +21,16 @@ class MyApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'ZeroTier for Magisk',
|
title: 'ZeroTier for Magisk',
|
||||||
|
localizationsDelegates: const [
|
||||||
|
AppLocalizations.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [
|
||||||
|
Locale('en'),
|
||||||
|
Locale('zh'),
|
||||||
|
],
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorSchemeSeed: Colors.lightBlue,
|
colorSchemeSeed: Colors.lightBlue,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
@@ -34,300 +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)[300];
|
|
||||||
|
|
||||||
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)
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}),
|
|
||||||
IntrinsicWidth(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
OutlinedButton.icon(
|
|
||||||
style: const ButtonStyle(alignment: Alignment.centerLeft),
|
|
||||||
onPressed: startFn(),
|
|
||||||
label: const Text('Start'),
|
|
||||||
icon: const Icon(Icons.play_arrow),
|
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
style: const ButtonStyle(alignment: Alignment.centerLeft),
|
|
||||||
onPressed: restartFn(),
|
|
||||||
label: const Text('Restart'),
|
|
||||||
icon: const Icon(Icons.restart_alt),
|
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
style: const ButtonStyle(alignment: Alignment.centerLeft),
|
|
||||||
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()();
|
|
||||||
QuickAlert.show(
|
|
||||||
context: context,
|
|
||||||
type: QuickAlertType.success,
|
|
||||||
text: 'Joined ${_idInputController.text}',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
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]));
|
|
||||||
QuickAlert.show(
|
|
||||||
context: context,
|
|
||||||
type: QuickAlertType.success,
|
|
||||||
text: 'Copied to clipboard!',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
tooltip: '复制',
|
|
||||||
icon: const Icon(Icons.copy)),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => QuickAlert.show(
|
|
||||||
context: context,
|
|
||||||
type: QuickAlertType.confirm,
|
|
||||||
text:
|
|
||||||
'Delete network ${networkList[i]} ?',
|
|
||||||
confirmBtnText: 'Yes',
|
|
||||||
cancelBtnText: 'No',
|
|
||||||
confirmBtnColor: Colors.green,
|
|
||||||
onConfirmBtnTap: () {
|
|
||||||
leaveNetwork(networkList[i]);
|
|
||||||
Navigator.pop(context);
|
|
||||||
}),
|
|
||||||
tooltip: '离开',
|
|
||||||
icon: const Icon(Icons.close)),
|
|
||||||
]))));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
peersPage({bool title = false}) {
|
|
||||||
if (title) {
|
|
||||||
return 'Peers';
|
|
||||||
}
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentPageIndex = 0;
|
|
||||||
genWidgetList({bool title = false}) {
|
|
||||||
return [
|
|
||||||
homePage(title: title),
|
|
||||||
networkPage(title: title),
|
|
||||||
peersPage(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',
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
|
||||||
selectedIcon: Icon(Icons.account_box),
|
|
||||||
icon: Icon(Icons.account_box_outlined),
|
|
||||||
label: 'Peers',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
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,437 +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: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
|
||||||
source: hosted
|
|
||||||
version: "10.0.4"
|
|
||||||
leak_tracker_flutter_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_flutter_testing
|
|
||||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.3"
|
|
||||||
leak_tracker_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_testing
|
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
|
||||||
source: hosted
|
|
||||||
version: "3.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: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
|
||||||
source: hosted
|
|
||||||
version: "1.12.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"
|
|
||||||
quickalert:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: quickalert
|
|
||||||
sha256: b5d62b1e20b08cc0ff5f40b6da519bdc7a5de6082f13d90572cf4e72eea56c5e
|
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.0"
|
|
||||||
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: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.0"
|
|
||||||
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: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
|
||||||
source: hosted
|
|
||||||
version: "14.2.1"
|
|
||||||
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"
|
|
||||||
+7
-3
@@ -30,12 +30,15 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
intl: any
|
||||||
|
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.6
|
cupertino_icons: ^1.0.6
|
||||||
http: ^1.2.1
|
dio: ^5.4.0
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
quickalert: ^1.1.0
|
quickalert: ^1.1.0
|
||||||
|
|
||||||
@@ -48,8 +51,8 @@ dev_dependencies:
|
|||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^4.0.0
|
flutter_lints: ^5.0.0
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.14.3
|
||||||
launcher_name: ^1.0.2
|
launcher_name: ^1.0.2
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
@@ -62,6 +65,7 @@ flutter:
|
|||||||
# included with your application, so that you can use the icons in
|
# included with your application, so that you can use the icons in
|
||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
generate: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
# assets:
|
||||||
|
|||||||
Reference in New Issue
Block a user