[feat] add i18n localization: en/zh; add peers display; ui refresh

This commit is contained in:
eventlOwOp
2025-04-06 20:20:14 +08:00
parent 0405277231
commit bacea863bf
22 changed files with 3487 additions and 446 deletions
+2
View File
@@ -1,3 +1,5 @@
**/.cxx/
# Miscellaneous # Miscellaneous
*.class *.class
*.log *.log
File diff suppressed because one or more lines are too long
+3
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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"
+7
View File
@@ -0,0 +1,7 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-dir: lib/l10n
output-localization-file: app_localizations.dart
synthetic-package: false
nullable-getter: false
-39
View File
@@ -1,39 +0,0 @@
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
class AuthedClientInner extends http.BaseClient {
final String _secret;
final http.Client _inner;
AuthedClientInner(this._secret, this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
request.headers['X-ZT1-Auth'] = _secret;
return _inner.send(request);
}
// @override
// Future<http.Response> post(Uri url,
// {Map<String, String>? headers, Object? body, Encoding? encoding}) {
// Map<String, String> hd = headers ?? <String, String>{};
// hd['content-type'] = 'application/json';
// return super.post(url, headers: hd, body: body, encoding: encoding);
// }
}
class AuthedClient {
late Future<AuthedClientInner> client;
Future<AuthedClientInner> loadSecret() async {
final path = (await getApplicationDocumentsDirectory()).path;
final file = File('$path/run/authtoken');
final token = await file.readAsString();
return AuthedClientInner(token.trim(), http.Client());
}
AuthedClient() {
client = loadSecret();
}
}
+357
View File
@@ -0,0 +1,357 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:quickalert/quickalert.dart';
import '../services/zerotier_service.dart';
import '../l10n/app_localizations.dart';
class NetworkPage extends StatefulWidget {
final ZerotierService zerotierService;
const NetworkPage({
super.key,
required this.zerotierService,
});
@override
State<NetworkPage> createState() => _NetworkPageState();
}
class _NetworkPageState extends State<NetworkPage> {
final _idInputController = TextEditingController();
bool _isLoading = false;
List<String> _networks = [];
String? _error;
@override
void initState() {
super.initState();
_idInputController.addListener(_onTextChanged);
_fetchNetworkList();
}
void _onTextChanged() {
if (!mounted) return;
setState(() {});
}
void _setLoading(bool loading) {
if (!mounted) return;
setState(() {
_isLoading = loading;
if (loading && _error != null) {
_error = null;
}
});
}
Future<void> _fetchNetworkList() async {
_setLoading(true);
try {
final networks = await widget.zerotierService.loadNetwork();
if (mounted) {
final l10n = AppLocalizations.of(context)!;
setState(() {
if (networks == null) {
// ZeroTier服务不可用
_error = l10n.serviceNotRunning;
_networks = [];
} else {
_networks = List.from(networks);
_error = null;
}
});
}
} catch (e) {
if (mounted) {
// 存储原始错误信息
final rawError = e.toString();
setState(() {
_error = rawError; // 存储原始错误
_networks = [];
});
// 在界面上显示本地化错误信息
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.loadNetworksErrorText(rawError)),
backgroundColor: Colors.red,
),
);
}
} finally {
_setLoading(false);
}
}
Future<void> _performJoinNetwork(String networkId) async {
if (_isLoading) return;
_setLoading(true);
final l10n = AppLocalizations.of(context)!;
try {
final result = await widget.zerotierService.joinNetwork(networkId);
if (mounted) {
if (!widget.zerotierService.runningStatus) {
// ZeroTier服务未运行
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.serviceNotRunning),
backgroundColor: Colors.red,
),
);
} else if (result) {
QuickAlert.show(
context: context,
type: QuickAlertType.success,
text: l10n.joinNetworkSuccessText(networkId),
);
_idInputController.clear();
} else {
// 其他失败
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.joinNetworkErrorText("Unknown error")),
backgroundColor: Colors.red,
),
);
}
await _fetchNetworkList();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.joinNetworkErrorText(e.toString())), backgroundColor: Colors.red),
);
}
} finally {
_setLoading(false);
}
}
Future<void> _performLeaveNetwork(String networkId) async {
if (_isLoading) return;
_setLoading(true);
final l10n = AppLocalizations.of(context)!;
try {
final result = await widget.zerotierService.leaveNetwork(networkId);
if (mounted) {
if (!widget.zerotierService.runningStatus) {
// ZeroTier服务未运行
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.serviceNotRunning),
backgroundColor: Colors.red,
),
);
} else if (result) {
QuickAlert.show(
context: context,
type: QuickAlertType.success,
text: l10n.leaveNetworkSuccessText(networkId),
);
} else {
// 其他失败
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.leaveNetworkErrorText("Unknown error")),
backgroundColor: Colors.red,
),
);
}
await _fetchNetworkList();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.leaveNetworkErrorText(e.toString())), backgroundColor: Colors.red),
);
}
} finally {
_setLoading(false);
}
}
void _showLeaveConfirmation(String networkId) {
final l10n = AppLocalizations.of(context)!;
QuickAlert.show(
context: context,
type: QuickAlertType.confirm,
title: l10n.leaveNetworkConfirmationTitle,
text: l10n.leaveNetworkConfirmationText(networkId),
confirmBtnText: l10n.confirmButton,
cancelBtnText: l10n.cancelButton,
confirmBtnColor: Colors.red,
onConfirmBtnTap: () {
Navigator.pop(context);
_performLeaveNetwork(networkId);
},
onCancelBtnTap: () => Navigator.pop(context),
);
}
@override
void dispose() {
_idInputController.removeListener(_onTextChanged);
_idInputController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Expanded(
child: TextField(
controller: _idInputController,
textAlign: TextAlign.center,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp('[0-9a-fA-F]'))
],
enabled: !_isLoading,
maxLength: 16,
decoration: InputDecoration(
labelText: l10n.networkIdLabel,
hintText: l10n.networkIdHint,
prefixIcon: const Icon(Icons.lan_outlined),
suffixIcon: _idInputController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: !_isLoading ? () => _idInputController.clear() : null,
tooltip: l10n.clearInputTooltip,
)
: null,
counterText: "",
border: const OutlineInputBorder()
)
)
),
const SizedBox(width: 12),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
onPressed: _isLoading || _idInputController.text.isEmpty
? null
: () {
final networkId = _idInputController.text;
if (networkId.length != 16) {
QuickAlert.show(
context: context,
type: QuickAlertType.warning,
title: l10n.invalidNetworkIdTitle,
text: l10n.invalidNetworkIdText,
);
return;
}
_performJoinNetwork(networkId);
},
label: Text(l10n.joinButton),
icon: const Icon(Icons.add_link)),
]),
const SizedBox(height: 24),
const Divider(),
ListTile(
title: Text(l10n.networksTitle, style: Theme.of(context).textTheme.titleMedium),
trailing: IconButton(
icon: const Icon(Icons.refresh),
tooltip: l10n.refreshNetworksTooltip,
onPressed: _isLoading ? null : _fetchNetworkList
)
),
const SizedBox(height: 8),
Expanded(
child: _isLoading && _networks.isEmpty
? const Center(child: CircularProgressIndicator.adaptive())
: _error != null
? Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.loadNetworksErrorText(_error!),
style: TextStyle(color: Theme.of(context).colorScheme.error),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: Text(l10n.refreshButtonLabel ?? 'Retry'),
onPressed: _isLoading ? null : _fetchNetworkList,
)
],
),
)
)
: RefreshIndicator(
onRefresh: _fetchNetworkList,
child: _networks.isEmpty
? LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
l10n.noNetworksJoined,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor
),
textAlign: TextAlign.center,
),
),
),
),
)
: ListView.builder(
itemCount: _networks.length,
itemBuilder: (BuildContext ctx, int i) {
final networkId = _networks[i];
return Card(
elevation: 1.5,
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 0),
child: ListTile(
leading: CircleAvatar(
child: Icon(Icons.vpn_key, size: 18, color: Theme.of(context).colorScheme.secondary),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
),
title: Text(networkId, style: const TextStyle(fontFamily: 'monospace', letterSpacing: 0.8)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _isLoading ? null : () {
Clipboard.setData(ClipboardData(text: networkId));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.copiedToClipboard(networkId)))
);
},
tooltip: l10n.copyTooltip,
icon: const Icon(Icons.copy, size: 20)
),
IconButton(
onPressed: _isLoading ? null : () => _showLeaveConfirmation(networkId),
tooltip: l10n.leaveTooltip,
icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error, size: 20)
),
])
)
);
}
),
)
),
],
));
}
}
+250
View File
@@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import '../services/zerotier_service.dart';
import '../l10n/app_localizations.dart'; // Import localizations
import '../models/peer_info.dart'; // Import the model
// class PeersPage extends StatelessWidget { // Convert to StatefulWidget
class PeersPage extends StatefulWidget {
final ZerotierService zerotierService;
// Remove callback parameter
// final Future<void> Function()? onRefreshPeers;
const PeersPage({
super.key,
required this.zerotierService,
// Remove callback parameter from constructor
// this.onRefreshPeers,
});
@override
// State<PeersPage> createState() => _PeersPageState(); // Add createState
State<PeersPage> createState() => _PeersPageState();
}
// Add State class
class _PeersPageState extends State<PeersPage> {
bool _isLoading = false;
// Use the PeerInfo model for the list
List<PeerInfo> _peers = [];
String? _error;
@override
void initState() {
super.initState();
_fetchPeers(); // Call fetch method on init
}
// Helper to manage loading state
void _setLoading(bool loading) {
if (!mounted) return;
setState(() {
_isLoading = loading;
if (loading) {
_error = null; // Clear error when starting load
}
});
}
// Placeholder method to fetch peers
Future<void> _fetchPeers() async {
_setLoading(true);
try {
// 调用服务方法并获取返回值
final peers = await widget.zerotierService.loadPeers();
if (mounted) {
final l10n = AppLocalizations.of(context)!;
setState(() {
if (peers == null) {
// ZeroTier服务不可用
_error = l10n.serviceNotRunning;
_peers = [];
} else {
_peers = List.from(peers);
_error = null;
}
});
}
} catch (e) {
if (mounted) {
// 存储原始错误信息,不使用 l10n
final rawError = e.toString();
setState(() {
// 在 setState 内部构建错误显示文本
_error = rawError; // 存储原始错误,显示时再格式化
_peers = [];
});
// 在确认 mounted 后使用 l10n 和 context
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.loadPeersErrorText(rawError)),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
_setLoading(false);
}
}
}
// Helper to build list item display
Widget _buildPeerTile(PeerInfo peer, AppLocalizations l10n) {
IconData roleIcon = peer.isPlanet ? Icons.cloud_outlined : Icons.computer_outlined;
Color roleColor = peer.isPlanet ? Colors.blueGrey : Theme.of(context).colorScheme.secondary;
// 设置tunneled状态显示
final tunnelIcon = peer.tunneled ? Icons.settings_ethernet : Icons.wifi;
final tunnelColor = peer.tunneled ? Colors.orange.shade700 : Colors.green.shade600;
final tunnelText = peer.tunneled ? l10n.peerTunneled : l10n.peerDirect;
return Card(
elevation: 1.5,
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: roleColor.withOpacity(0.1),
child: Icon(roleIcon, size: 20, color: roleColor),
),
title: Row(
children: [
Expanded(
child: Text(
peer.address,
style: const TextStyle(fontFamily: 'monospace', fontSize: 14),
overflow: TextOverflow.ellipsis,
),
),
// 对于非PLANET节点显示版本
if (!peer.isPlanet && peer.version != null) ...[
const SizedBox(width: 8),
Text(
peer.version!,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
fontWeight: FontWeight.normal
),
),
],
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Row(
children: [
Chip(
avatar: Icon(roleIcon, size: 14, color: roleColor),
label: Text(peer.role, style: TextStyle(fontSize: 12, color: roleColor)),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
visualDensity: VisualDensity.compact,
backgroundColor: roleColor.withOpacity(0.1),
side: BorderSide.none,
),
const SizedBox(width: 8),
// 添加tunneled状态显示
Chip(
avatar: Icon(tunnelIcon, size: 14, color: tunnelColor),
label: Text(tunnelText, style: TextStyle(fontSize: 12, color: tunnelColor)),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
visualDensity: VisualDensity.compact,
backgroundColor: tunnelColor.withOpacity(0.1),
side: BorderSide.none,
),
const SizedBox(width: 8),
if (peer.latency != null) ...[
Icon(Icons.timer_outlined, size: 14, color: Colors.orange.shade700),
const SizedBox(width: 4),
Text('${peer.latency} ms', style: TextStyle(fontSize: 12, color: Colors.orange.shade700)),
],
],
),
if (peer.preferredPath != null) ...[
const SizedBox(height: 4),
Text(
peer.preferredPath!,
style: TextStyle(fontSize: 11, color: Theme.of(context).disabledColor, fontFamily: 'monospace'),
overflow: TextOverflow.ellipsis,
),
]
],
),
dense: true,
),
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
Widget bodyContent;
if (_isLoading && _peers.isEmpty) {
bodyContent = const Center(child: CircularProgressIndicator.adaptive());
} else if (_error != null) {
bodyContent = Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.loadPeersErrorText(_error!),
style: TextStyle(color: Theme.of(context).colorScheme.error),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: Text(l10n.refreshButtonLabel),
onPressed: _isLoading ? null : _fetchPeers,
)
],
),
)
);
} else if (!_isLoading && _peers.isEmpty) {
// Show "No peers found" centered, allowing pull-to-refresh
bodyContent = LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
l10n.noPeersFound, // Uses existing l10n key
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor
),
textAlign: TextAlign.center,
),
),
),
),
);
} else {
// Display the list using ListView.builder
bodyContent = ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), // Ensure scrollable even when few items
itemCount: _peers.length,
itemBuilder: (context, index) {
return _buildPeerTile(_peers[index], l10n);
},
);
}
// Wrap the main content with RefreshIndicator
return RefreshIndicator(
onRefresh: _fetchPeers,
child: bodyContent,
);
}
}
+390
View File
@@ -0,0 +1,390 @@
import 'package:flutter/material.dart';
import '../services/zerotier_service.dart';
import '../l10n/app_localizations.dart';
class StatusPage extends StatefulWidget {
final ZerotierService zerotierService;
const StatusPage({
super.key,
required this.zerotierService,
});
@override
State<StatusPage> createState() => _StatusPageState();
}
class _StatusPageState extends State<StatusPage> {
bool _isLoading = false;
ZerotierStatus? _statusInfo;
bool _isModuleInstalled = true;
@override
void initState() {
super.initState();
// 获取缓存的状态信息,如果有则直接使用
final cachedStatus = widget.zerotierService.statusInfo;
if (cachedStatus != null) {
_statusInfo = cachedStatus;
_isModuleInstalled = true;
}
// 然后异步更新状态
_fetchStatus();
}
Future<void> _fetchStatus() async {
if (!mounted) return;
// 检查是否有缓存的状态
final cachedStatus = widget.zerotierService.statusInfo;
final bool hasCache = cachedStatus != null;
// 如果没有缓存或者当前状态为空,才显示加载中
if (!hasCache || _statusInfo == null) {
setState(() => _isLoading = true);
}
try {
final status = await widget.zerotierService.loadStatus();
if (mounted) {
setState(() {
_statusInfo = status;
_isModuleInstalled = true;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load status: $e')),
);
setState(() => _isLoading = false);
}
}
}
Future<void> _performServiceAction(String command) async {
if (!mounted || _isLoading) return;
setState(() => _isLoading = true);
final l10n = AppLocalizations.of(context)!;
String actionVerb = command;
if (command == 'start') actionVerb = 'Starting';
if (command == 'stop') actionVerb = 'Stopping';
if (command == 'restart') actionVerb = 'Restarting';
try {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$actionVerb ZeroTier...'), duration: const Duration(seconds: 1)),
);
final success = await widget.zerotierService.zerotierCommand(command);
if (!success && mounted) {
setState(() {
_isModuleInstalled = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.moduleNotRunning),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$command failed: $e'), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
await _fetchStatus();
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cachedStatus = widget.zerotierService.statusInfo;
final bool runningStatus = (_statusInfo != null && _statusInfo!.online) ||
(cachedStatus != null && cachedStatus.online && _statusInfo == null);
final statusColor = runningStatus ? Colors.green.shade400 : Colors.red.shade400;
final l10n = AppLocalizations.of(context)!;
return RefreshIndicator(
onRefresh: _fetchStatus,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
child: Column(
children: [
_buildStatusIcon(runningStatus),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatusText(context, runningStatus, theme, l10n),
IconButton(
icon: Icon(Icons.refresh, color: theme.colorScheme.secondary),
onPressed: _isLoading ? null : _fetchStatus,
tooltip: l10n.refreshStatusTooltip,
),
],
),
],
),
),
const SizedBox(height: 20),
_ActionButtons(
l10n: l10n,
startFunction: () => _performServiceAction('start'),
restartFunction: () => _performServiceAction('restart'),
stopFunction: () => _performServiceAction('stop'),
isServiceRunning: runningStatus,
isModuleInstalled: _isModuleInstalled,
),
const SizedBox(height: 20),
if (_isLoading)
const Center(child: Padding(
padding: EdgeInsets.symmetric(vertical: 32.0),
child: CircularProgressIndicator(),
))
else if (_statusInfo != null && runningStatus)
_buildStatusInfo(context, _statusInfo!)
else if (cachedStatus != null && cachedStatus.online)
_buildStatusInfo(context, cachedStatus)
else
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildErrorText(context, theme, l10n),
],
),
)
)
],
),
),
);
}
Widget _buildStatusIcon(bool runningStatus) {
IconData iconData;
Color iconColor;
if (!_isModuleInstalled) {
iconData = Icons.error;
iconColor = Colors.orange.shade700;
} else if (runningStatus) {
iconData = Icons.play_circle_fill;
iconColor = Colors.green.shade400;
} else {
iconData = Icons.stop_circle;
iconColor = Colors.red.shade400;
}
return Icon(
iconData,
color: iconColor,
size: 64,
);
}
Widget _buildStatusText(BuildContext context, bool runningStatus, ThemeData theme, AppLocalizations l10n) {
String statusText;
Color textColor;
if (!_isModuleInstalled) {
statusText = l10n.moduleNotRunning;
textColor = Colors.orange.shade700;
} else if (runningStatus) {
statusText = l10n.statusRunning;
textColor = Colors.green.shade400;
} else {
statusText = l10n.statusStopped;
textColor = Colors.red.shade400;
}
if (_isLoading) {
textColor = textColor.withOpacity(0.5);
}
return Text(
statusText,
style: theme.textTheme.headlineSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.bold
),
);
}
Widget _buildErrorText(BuildContext context, ThemeData theme, AppLocalizations l10n) {
String errorText;
Color textColor;
if (!_isModuleInstalled) {
errorText = l10n.moduleNotRunning;
textColor = Colors.orange.shade700;
} else {
errorText = l10n.statusStopped;
textColor = theme.colorScheme.error;
}
return Text(
errorText,
style: theme.textTheme.bodyMedium?.copyWith(
color: textColor
),
textAlign: TextAlign.center,
);
}
Widget _buildStatusInfo(BuildContext context, ZerotierStatus status) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.statusDetailsTitle,
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)
),
const Divider(height: 20, thickness: 1),
_buildInfoTile(context, Icons.lan_outlined, l10n.nodeAddressLabel, status.address),
_buildInfoTile(context, Icons.info_outline, l10n.softwareVersionLabel, status.version),
_buildInfoTile(context, status.online ? Icons.cloud_done_outlined : Icons.cloud_off_outlined, l10n.onlineStatusLabel, status.online ? l10n.onlineStatusOnline : l10n.onlineStatusOffline),
_buildInfoTile(context, Icons.settings_ethernet, l10n.primaryPortLabel, status.primaryPort.toString()),
const SizedBox(height: 10),
ListTile(
leading: Icon(Icons.list_alt, color: theme.colorScheme.secondary),
title: Text(l10n.listeningAddressesLabel, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)),
),
Container(
height: 100,
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: theme.dividerColor),
),
child: status.listeningOn.isEmpty
? Center(child: Text(l10n.noListeningAddresses, style: theme.textTheme.bodySmall))
: ListView.builder(
shrinkWrap: true,
itemCount: status.listeningOn.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Text(
status.listeningOn[index],
style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'),
),
);
},
),
),
],
),
);
}
Widget _buildInfoTile(BuildContext context, IconData icon, String label, String value) {
final theme = Theme.of(context);
return ListTile(
leading: Icon(icon, color: theme.colorScheme.secondary),
title: Text(label, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)),
subtitle: Text(value, style: theme.textTheme.bodyMedium?.copyWith(fontFamily: 'monospace')),
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
);
}
}
class _ActionButtons extends StatelessWidget {
final AppLocalizations l10n;
final Function()? startFunction;
final Function()? restartFunction;
final Function()? stopFunction;
final bool isServiceRunning;
final bool isModuleInstalled;
const _ActionButtons({
required this.l10n,
required this.startFunction,
required this.restartFunction,
required this.stopFunction,
required this.isServiceRunning,
required this.isModuleInstalled,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: <Widget>[
Center(
child: SizedBox(
width: 180,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)),
onPressed: isServiceRunning || !isModuleInstalled ? null : startFunction,
label: Text(l10n.startButton),
icon: const Icon(Icons.play_arrow),
),
),
),
const SizedBox(height: 16),
Center(
child: SizedBox(
width: 180,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)),
onPressed: isServiceRunning && isModuleInstalled ? restartFunction : null,
label: Text(l10n.restartButton),
icon: const Icon(Icons.restart_alt),
),
),
),
const SizedBox(height: 16),
Center(
child: SizedBox(
width: 180,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: theme.colorScheme.error,
side: BorderSide(color: theme.colorScheme.error),
padding: const EdgeInsets.symmetric(vertical: 12)
),
onPressed: isServiceRunning && isModuleInstalled ? stopFunction : null,
label: Text(l10n.stopButton),
icon: const Icon(Icons.stop),
),
),
),
],
),
);
}
}
+125
View File
@@ -0,0 +1,125 @@
{
"@@locale": "en",
"statusRunning": "Running",
"statusStopped": "Stopped",
"refreshStatusTooltip": "Refresh Status",
"startButton": "Start",
"restartButton": "Restart",
"stopButton": "Stop",
"statusDetailsTitle": "ZeroTier Details",
"nodeAddressLabel": "Node Address",
"softwareVersionLabel": "Software Version",
"onlineStatusLabel": "Online Status",
"onlineStatusOnline": "Online",
"onlineStatusOffline": "Offline",
"primaryPortLabel": "Primary Port",
"listeningAddressesLabel": "Listening Addresses:",
"noListeningAddresses": "None",
"leaveNetworkConfirmationTitle": "Leave Network?",
"leaveNetworkConfirmationText": "Are you sure you want to leave network {networkId}?",
"@leaveNetworkConfirmationText": {
"placeholders": {
"networkId": {
"type": "String",
"example": "1234567890abcdef"
}
}
},
"confirmButton": "Confirm",
"cancelButton": "Cancel",
"networkIdLabel": "Network ID",
"networkIdHint": "Enter 16-character Network ID",
"invalidNetworkIdTitle": "Invalid Network ID",
"invalidNetworkIdText": "Please enter a 16-character network ID.",
"joinButton": "Join",
"networksTitle": "Networks",
"refreshNetworksTooltip": "Refresh Network List",
"noNetworksJoined": "No networks joined",
"copiedToClipboard": "Copied {networkId} to clipboard!",
"@copiedToClipboard": {
"placeholders": {
"networkId": {
"type": "String",
"example": "1234567890abcdef"
}
}
},
"copyTooltip": "Copy",
"leaveTooltip": "Leave",
"clearInputTooltip": "Clear input",
"peersFeatureComingSoon": "Peers feature coming soon",
"noPeersFound": "No peers found",
"navBarLabelStatus": "Status",
"navBarLabelNetworks": "Networks",
"navBarLabelPeers": "Peers",
"appBarTitleStatus": "Status",
"appBarTitleNetworks": "Networks",
"appBarTitlePeers": "Peers",
"joinNetworkSuccessText": "Successfully joined network {networkId}!",
"@joinNetworkSuccessText": {
"placeholders": {
"networkId": {
"type": "String",
"example": "1234567890abcdef"
}
}
},
"joinNetworkErrorText": "Failed to join network: {error}",
"@joinNetworkErrorText": {
"placeholders": {
"error": {
"type": "String",
"example": "Connection timed out"
}
}
},
"leaveNetworkSuccessText": "Successfully left network {networkId}.",
"@leaveNetworkSuccessText": {
"placeholders": {
"networkId": {
"type": "String",
"example": "1234567890abcdef"
}
}
},
"leaveNetworkErrorText": "Failed to leave network: {error}",
"@leaveNetworkErrorText": {
"placeholders": {
"error": {
"type": "String",
"example": "Permission denied"
}
}
},
"refreshButtonLabel": "Retry",
"loadPeersErrorText": "Failed to load peers: {error}",
"@loadPeersErrorText": {
"placeholders": {
"error": {
"type": "String",
"example": "Network error"
}
}
},
"loadNetworksErrorText": "Failed to load networks: {error}",
"@loadNetworksErrorText": {
"placeholders": {
"error": {
"type": "String",
"example": "Network error"
}
}
},
"serviceNotRunning": "ZeroTier service is not running",
"moduleNotRunning": "ZeroTier Magisk module is not running. Please check if you have installed the ZeroTier Magisk module.",
"peerTunneled": "Relayed",
"peerDirect": "Direct"
}
+429
View File
@@ -0,0 +1,429 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('zh')
];
/// No description provided for @statusRunning.
///
/// In en, this message translates to:
/// **'Running'**
String get statusRunning;
/// No description provided for @statusStopped.
///
/// In en, this message translates to:
/// **'Stopped'**
String get statusStopped;
/// No description provided for @refreshStatusTooltip.
///
/// In en, this message translates to:
/// **'Refresh Status'**
String get refreshStatusTooltip;
/// No description provided for @startButton.
///
/// In en, this message translates to:
/// **'Start'**
String get startButton;
/// No description provided for @restartButton.
///
/// In en, this message translates to:
/// **'Restart'**
String get restartButton;
/// No description provided for @stopButton.
///
/// In en, this message translates to:
/// **'Stop'**
String get stopButton;
/// No description provided for @statusDetailsTitle.
///
/// In en, this message translates to:
/// **'ZeroTier Details'**
String get statusDetailsTitle;
/// No description provided for @nodeAddressLabel.
///
/// In en, this message translates to:
/// **'Node Address'**
String get nodeAddressLabel;
/// No description provided for @softwareVersionLabel.
///
/// In en, this message translates to:
/// **'Software Version'**
String get softwareVersionLabel;
/// No description provided for @onlineStatusLabel.
///
/// In en, this message translates to:
/// **'Online Status'**
String get onlineStatusLabel;
/// No description provided for @onlineStatusOnline.
///
/// In en, this message translates to:
/// **'Online'**
String get onlineStatusOnline;
/// No description provided for @onlineStatusOffline.
///
/// In en, this message translates to:
/// **'Offline'**
String get onlineStatusOffline;
/// No description provided for @primaryPortLabel.
///
/// In en, this message translates to:
/// **'Primary Port'**
String get primaryPortLabel;
/// No description provided for @listeningAddressesLabel.
///
/// In en, this message translates to:
/// **'Listening Addresses:'**
String get listeningAddressesLabel;
/// No description provided for @noListeningAddresses.
///
/// In en, this message translates to:
/// **'None'**
String get noListeningAddresses;
/// No description provided for @leaveNetworkConfirmationTitle.
///
/// In en, this message translates to:
/// **'Leave Network?'**
String get leaveNetworkConfirmationTitle;
/// No description provided for @leaveNetworkConfirmationText.
///
/// In en, this message translates to:
/// **'Are you sure you want to leave network {networkId}?'**
String leaveNetworkConfirmationText(String networkId);
/// No description provided for @confirmButton.
///
/// In en, this message translates to:
/// **'Confirm'**
String get confirmButton;
/// No description provided for @cancelButton.
///
/// In en, this message translates to:
/// **'Cancel'**
String get cancelButton;
/// No description provided for @networkIdLabel.
///
/// In en, this message translates to:
/// **'Network ID'**
String get networkIdLabel;
/// No description provided for @networkIdHint.
///
/// In en, this message translates to:
/// **'Enter 16-character Network ID'**
String get networkIdHint;
/// No description provided for @invalidNetworkIdTitle.
///
/// In en, this message translates to:
/// **'Invalid Network ID'**
String get invalidNetworkIdTitle;
/// No description provided for @invalidNetworkIdText.
///
/// In en, this message translates to:
/// **'Please enter a 16-character network ID.'**
String get invalidNetworkIdText;
/// No description provided for @joinButton.
///
/// In en, this message translates to:
/// **'Join'**
String get joinButton;
/// No description provided for @networksTitle.
///
/// In en, this message translates to:
/// **'Networks'**
String get networksTitle;
/// No description provided for @refreshNetworksTooltip.
///
/// In en, this message translates to:
/// **'Refresh Network List'**
String get refreshNetworksTooltip;
/// No description provided for @noNetworksJoined.
///
/// In en, this message translates to:
/// **'No networks joined'**
String get noNetworksJoined;
/// No description provided for @copiedToClipboard.
///
/// In en, this message translates to:
/// **'Copied {networkId} to clipboard!'**
String copiedToClipboard(String networkId);
/// No description provided for @copyTooltip.
///
/// In en, this message translates to:
/// **'Copy'**
String get copyTooltip;
/// No description provided for @leaveTooltip.
///
/// In en, this message translates to:
/// **'Leave'**
String get leaveTooltip;
/// No description provided for @clearInputTooltip.
///
/// In en, this message translates to:
/// **'Clear input'**
String get clearInputTooltip;
/// No description provided for @peersFeatureComingSoon.
///
/// In en, this message translates to:
/// **'Peers feature coming soon'**
String get peersFeatureComingSoon;
/// No description provided for @noPeersFound.
///
/// In en, this message translates to:
/// **'No peers found'**
String get noPeersFound;
/// No description provided for @navBarLabelStatus.
///
/// In en, this message translates to:
/// **'Status'**
String get navBarLabelStatus;
/// No description provided for @navBarLabelNetworks.
///
/// In en, this message translates to:
/// **'Networks'**
String get navBarLabelNetworks;
/// No description provided for @navBarLabelPeers.
///
/// In en, this message translates to:
/// **'Peers'**
String get navBarLabelPeers;
/// No description provided for @appBarTitleStatus.
///
/// In en, this message translates to:
/// **'Status'**
String get appBarTitleStatus;
/// No description provided for @appBarTitleNetworks.
///
/// In en, this message translates to:
/// **'Networks'**
String get appBarTitleNetworks;
/// No description provided for @appBarTitlePeers.
///
/// In en, this message translates to:
/// **'Peers'**
String get appBarTitlePeers;
/// No description provided for @joinNetworkSuccessText.
///
/// In en, this message translates to:
/// **'Successfully joined network {networkId}!'**
String joinNetworkSuccessText(String networkId);
/// No description provided for @joinNetworkErrorText.
///
/// In en, this message translates to:
/// **'Failed to join network: {error}'**
String joinNetworkErrorText(String error);
/// No description provided for @leaveNetworkSuccessText.
///
/// In en, this message translates to:
/// **'Successfully left network {networkId}.'**
String leaveNetworkSuccessText(String networkId);
/// No description provided for @leaveNetworkErrorText.
///
/// In en, this message translates to:
/// **'Failed to leave network: {error}'**
String leaveNetworkErrorText(String error);
/// No description provided for @refreshButtonLabel.
///
/// In en, this message translates to:
/// **'Retry'**
String get refreshButtonLabel;
/// No description provided for @loadPeersErrorText.
///
/// In en, this message translates to:
/// **'Failed to load peers: {error}'**
String loadPeersErrorText(String error);
/// No description provided for @loadNetworksErrorText.
///
/// In en, this message translates to:
/// **'Failed to load networks: {error}'**
String loadNetworksErrorText(String error);
/// No description provided for @serviceNotRunning.
///
/// In en, this message translates to:
/// **'ZeroTier service is not running'**
String get serviceNotRunning;
/// No description provided for @moduleNotRunning.
///
/// In en, this message translates to:
/// **'ZeroTier Magisk module is not running. Please check if you have installed the ZeroTier Magisk module.'**
String get moduleNotRunning;
/// No description provided for @peerTunneled.
///
/// In en, this message translates to:
/// **'Relayed'**
String get peerTunneled;
/// No description provided for @peerDirect.
///
/// In en, this message translates to:
/// **'Direct'**
String get peerDirect;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en': return AppLocalizationsEn();
case 'zh': return AppLocalizationsZh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.'
);
}
+176
View File
@@ -0,0 +1,176 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get statusRunning => 'Running';
@override
String get statusStopped => 'Stopped';
@override
String get refreshStatusTooltip => 'Refresh Status';
@override
String get startButton => 'Start';
@override
String get restartButton => 'Restart';
@override
String get stopButton => 'Stop';
@override
String get statusDetailsTitle => 'ZeroTier Details';
@override
String get nodeAddressLabel => 'Node Address';
@override
String get softwareVersionLabel => 'Software Version';
@override
String get onlineStatusLabel => 'Online Status';
@override
String get onlineStatusOnline => 'Online';
@override
String get onlineStatusOffline => 'Offline';
@override
String get primaryPortLabel => 'Primary Port';
@override
String get listeningAddressesLabel => 'Listening Addresses:';
@override
String get noListeningAddresses => 'None';
@override
String get leaveNetworkConfirmationTitle => 'Leave Network?';
@override
String leaveNetworkConfirmationText(String networkId) {
return 'Are you sure you want to leave network $networkId?';
}
@override
String get confirmButton => 'Confirm';
@override
String get cancelButton => 'Cancel';
@override
String get networkIdLabel => 'Network ID';
@override
String get networkIdHint => 'Enter 16-character Network ID';
@override
String get invalidNetworkIdTitle => 'Invalid Network ID';
@override
String get invalidNetworkIdText => 'Please enter a 16-character network ID.';
@override
String get joinButton => 'Join';
@override
String get networksTitle => 'Networks';
@override
String get refreshNetworksTooltip => 'Refresh Network List';
@override
String get noNetworksJoined => 'No networks joined';
@override
String copiedToClipboard(String networkId) {
return 'Copied $networkId to clipboard!';
}
@override
String get copyTooltip => 'Copy';
@override
String get leaveTooltip => 'Leave';
@override
String get clearInputTooltip => 'Clear input';
@override
String get peersFeatureComingSoon => 'Peers feature coming soon';
@override
String get noPeersFound => 'No peers found';
@override
String get navBarLabelStatus => 'Status';
@override
String get navBarLabelNetworks => 'Networks';
@override
String get navBarLabelPeers => 'Peers';
@override
String get appBarTitleStatus => 'Status';
@override
String get appBarTitleNetworks => 'Networks';
@override
String get appBarTitlePeers => 'Peers';
@override
String joinNetworkSuccessText(String networkId) {
return 'Successfully joined network $networkId!';
}
@override
String joinNetworkErrorText(String error) {
return 'Failed to join network: $error';
}
@override
String leaveNetworkSuccessText(String networkId) {
return 'Successfully left network $networkId.';
}
@override
String leaveNetworkErrorText(String error) {
return 'Failed to leave network: $error';
}
@override
String get refreshButtonLabel => 'Retry';
@override
String loadPeersErrorText(String error) {
return 'Failed to load peers: $error';
}
@override
String loadNetworksErrorText(String error) {
return 'Failed to load networks: $error';
}
@override
String get serviceNotRunning => 'ZeroTier service is not running';
@override
String get moduleNotRunning => 'ZeroTier Magisk module is not running. Please check if you have installed the ZeroTier Magisk module.';
@override
String get peerTunneled => 'Relayed';
@override
String get peerDirect => 'Direct';
}
+176
View File
@@ -0,0 +1,176 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
@override
String get statusRunning => '运行中';
@override
String get statusStopped => '已停止';
@override
String get refreshStatusTooltip => '刷新状态';
@override
String get startButton => '启动';
@override
String get restartButton => '重启';
@override
String get stopButton => '停止';
@override
String get statusDetailsTitle => 'ZeroTier 详细信息';
@override
String get nodeAddressLabel => '节点地址';
@override
String get softwareVersionLabel => '软件版本';
@override
String get onlineStatusLabel => '在线状态';
@override
String get onlineStatusOnline => '在线';
@override
String get onlineStatusOffline => '离线';
@override
String get primaryPortLabel => '主端口';
@override
String get listeningAddressesLabel => '监听地址:';
@override
String get noListeningAddresses => '';
@override
String get leaveNetworkConfirmationTitle => '离开网络?';
@override
String leaveNetworkConfirmationText(String networkId) {
return '您确定要离开网络 $networkId 吗?';
}
@override
String get confirmButton => '确认';
@override
String get cancelButton => '取消';
@override
String get networkIdLabel => '网络 ID';
@override
String get networkIdHint => '输入 16 位网络 ID';
@override
String get invalidNetworkIdTitle => '无效的网络 ID';
@override
String get invalidNetworkIdText => '请输入一个 16 位的网络 ID。';
@override
String get joinButton => '加入';
@override
String get networksTitle => '网络';
@override
String get refreshNetworksTooltip => '刷新网络列表';
@override
String get noNetworksJoined => '未加入任何网络';
@override
String copiedToClipboard(String networkId) {
return '已将 $networkId 复制到剪贴板!';
}
@override
String get copyTooltip => '复制';
@override
String get leaveTooltip => '离开';
@override
String get clearInputTooltip => '清除输入';
@override
String get peersFeatureComingSoon => '节点功能即将推出';
@override
String get noPeersFound => '未找到节点';
@override
String get navBarLabelStatus => '状态';
@override
String get navBarLabelNetworks => '网络';
@override
String get navBarLabelPeers => '节点';
@override
String get appBarTitleStatus => '状态';
@override
String get appBarTitleNetworks => '网络';
@override
String get appBarTitlePeers => '节点';
@override
String joinNetworkSuccessText(String networkId) {
return '成功加入网络 $networkId!';
}
@override
String joinNetworkErrorText(String error) {
return '加入网络失败: $error';
}
@override
String leaveNetworkSuccessText(String networkId) {
return '成功离开网络 $networkId';
}
@override
String leaveNetworkErrorText(String error) {
return '离开网络失败: $error';
}
@override
String get refreshButtonLabel => '重试';
@override
String loadPeersErrorText(String error) {
return '加载节点列表失败: $error';
}
@override
String loadNetworksErrorText(String error) {
return '加载网络列表失败: $error';
}
@override
String get serviceNotRunning => 'ZeroTier服务未运行';
@override
String get moduleNotRunning => 'ZeroTier Magisk模块未运行,请检查您是否安装Zerotier Magisk模块。';
@override
String get peerTunneled => '中继';
@override
String get peerDirect => '直连';
}
+125
View File
@@ -0,0 +1,125 @@
{
"@@locale": "zh",
"statusRunning": "运行中",
"statusStopped": "已停止",
"refreshStatusTooltip": "刷新状态",
"startButton": "启动",
"restartButton": "重启",
"stopButton": "停止",
"statusDetailsTitle": "ZeroTier 详细信息",
"nodeAddressLabel": "节点地址",
"softwareVersionLabel": "软件版本",
"onlineStatusLabel": "在线状态",
"onlineStatusOnline": "在线",
"onlineStatusOffline": "离线",
"primaryPortLabel": "主端口",
"listeningAddressesLabel": "监听地址:",
"noListeningAddresses": "无",
"leaveNetworkConfirmationTitle": "离开网络?",
"leaveNetworkConfirmationText": "您确定要离开网络 {networkId} 吗?",
"@leaveNetworkConfirmationText": {
"placeholders": {
"networkId": {
"type": "String",
"example": "1234567890abcdef"
}
}
},
"confirmButton": "确认",
"cancelButton": "取消",
"networkIdLabel": "网络 ID",
"networkIdHint": "输入 16 位网络 ID",
"invalidNetworkIdTitle": "无效的网络 ID",
"invalidNetworkIdText": "请输入一个 16 位的网络 ID。",
"joinButton": "加入",
"networksTitle": "网络",
"refreshNetworksTooltip": "刷新网络列表",
"noNetworksJoined": "未加入任何网络",
"copiedToClipboard": "已将 {networkId} 复制到剪贴板!",
"@copiedToClipboard": {
"placeholders": {
"networkId": {
"type": "String",
"example": "1234567890abcdef"
}
}
},
"copyTooltip": "复制",
"leaveTooltip": "离开",
"clearInputTooltip": "清除输入",
"peersFeatureComingSoon": "节点功能即将推出",
"noPeersFound": "未找到节点",
"navBarLabelStatus": "状态",
"navBarLabelNetworks": "网络",
"navBarLabelPeers": "节点",
"appBarTitleStatus": "状态",
"appBarTitleNetworks": "网络",
"appBarTitlePeers": "节点",
"joinNetworkSuccessText": "成功加入网络 {networkId}!",
"@joinNetworkSuccessText": {
"placeholders": {
"networkId": {
"type": "String",
"example": "1234567890abcdef"
}
}
},
"joinNetworkErrorText": "加入网络失败: {error}",
"@joinNetworkErrorText": {
"placeholders": {
"error": {
"type": "String",
"example": "连接超时"
}
}
},
"leaveNetworkSuccessText": "成功离开网络 {networkId}。",
"@leaveNetworkSuccessText": {
"placeholders": {
"networkId": {
"type": "String",
"example": "1234567890abcdef"
}
}
},
"leaveNetworkErrorText": "离开网络失败: {error}",
"@leaveNetworkErrorText": {
"placeholders": {
"error": {
"type": "String",
"example": "权限不足"
}
}
},
"refreshButtonLabel": "重试",
"loadPeersErrorText": "加载节点列表失败: {error}",
"@loadPeersErrorText": {
"placeholders": {
"error": {
"type": "String",
"example": "网络错误"
}
}
},
"loadNetworksErrorText": "加载网络列表失败: {error}",
"@loadNetworksErrorText": {
"placeholders": {
"error": {
"type": "String",
"example": "网络错误"
}
}
},
"serviceNotRunning": "ZeroTier服务未运行",
"moduleNotRunning": "ZeroTier Magisk模块未运行,请检查您是否安装Zerotier Magisk模块。",
"peerTunneled": "中继",
"peerDirect": "直连"
}
+19 -304
View File
@@ -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(),
));
}
}
+82
View File
@@ -0,0 +1,82 @@
import 'dart:math'; // For min function
class PeerInfo implements Comparable<PeerInfo> {
final String address;
final int latency;
final String? version;
final String role;
final String? preferredPath;
final bool isPlanet;
final bool tunneled; // 是否通过中继
PeerInfo({
required this.address,
required this.latency,
this.version,
required this.role,
this.preferredPath,
required this.tunneled,
}) : isPlanet = (role == 'PLANET');
factory PeerInfo.fromJson(Map<String, dynamic> json) {
String? bestPath;
List<dynamic> paths = json['paths'] as List<dynamic>? ?? [];
// Find the preferred active path
var preferred = paths.firstWhere(
(p) =>
p is Map<String, dynamic> &&
p['active'] == true &&
p['expired'] == false &&
p['preferred'] == true,
orElse: () => null);
if (preferred != null) {
bestPath = preferred['address'] as String?;
} else {
// If no preferred, find the first active path
var firstActive = paths.firstWhere(
(p) =>
p is Map<String, dynamic> &&
p['active'] == true &&
p['expired'] == false,
orElse: () => null);
if (firstActive != null) {
bestPath = firstActive['address'] as String?;
}
}
return PeerInfo(
address: json['address'] as String? ?? 'Unknown',
latency: json['latency'] as int,
version: json['version'] as String?,
role: json['role'] as String? ?? 'UNKNOWN',
preferredPath: bestPath,
tunneled: json['tunneled'] as bool,
);
}
// Comparison logic for sorting: PLANETs first, then by tunneled (直连优先), then by latency
@override
int compareTo(PeerInfo other) {
if (isPlanet != other.isPlanet) {
return isPlanet ? 1 : -1;
}
if (tunneled != other.tunneled) {
return tunneled ? -1 : 1;
}
// 最后按延迟排序(如果有的话)
if (latency != null && other.latency != null) {
return latency!.compareTo(other.latency!);
} else if (latency != null) {
return -1; // 有延迟信息的优先
} else if (other.latency != null) {
return 1;
}
// 如果都没有延迟信息,按地址排序
return address.compareTo(other.address);
}
}
+128
View File
@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import '../services/zerotier_service.dart';
import '../components/status_page.dart';
import '../components/network_page.dart';
import '../components/peers_page.dart';
import '../l10n/app_localizations.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ZerotierService _zerotierService = ZerotierService();
int currentPageIndex = 0;
// Remove state related to specific page data/futures
// Future<void>? _networkFuture;
// Future<void>? _peersFuture;
@override
void initState() {
super.initState();
// No initial loading needed here anymore
}
@override
void dispose() {
// Release service resources
_zerotierService.dispose();
super.dispose();
}
// Remove all data loading and action methods
/*
Future<void> _loadStatus() async { ... }
Future<void> _loadNetworkList() async { ... }
Future<void> _loadPeers() async { ... }
Future<void> _startZeroTier() async { ... }
Future<void> _restartZeroTier() async { ... }
Future<void> _stopZeroTier() async { ... }
Future<void> _joinNetwork(String id) async { ... }
Future<void> _leaveNetwork(String id) async { ... }
*/
String _getTitle(AppLocalizations l10n) {
switch (currentPageIndex) {
case 0:
return l10n.appBarTitleStatus;
case 1:
return l10n.appBarTitleNetworks;
case 2:
return l10n.appBarTitlePeers;
default:
return 'ZeroTier';
}
}
Widget _getCurrentPage() {
switch (currentPageIndex) {
case 0:
// Pass only the service
return StatusPage(
zerotierService: _zerotierService,
// Remove callbacks
// onRefreshStatus: _loadStatus,
// startFunction: _startZeroTier,
// restartFunction: _restartZeroTier,
// stopFunction: _stopZeroTier,
);
case 1:
// Pass only the service
return NetworkPage(
zerotierService: _zerotierService,
// Remove callbacks
// onRefreshNetworkList: _loadNetworkList,
// onJoinNetwork: _joinNetwork,
// onLeaveNetwork: _leaveNetwork,
);
case 2:
// Pass only the service
return PeersPage(
zerotierService: _zerotierService,
// Remove callbacks
// onRefreshPeers: _loadPeers,
);
default:
return const Center(child: Text('Page not found'));
}
}
@override
Widget build(BuildContext context) {
// Get AppLocalizations instance
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(_getTitle(l10n))),
bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) {
setState(() {
currentPageIndex = index;
});
},
selectedIndex: currentPageIndex,
destinations: <Widget>[
NavigationDestination(
selectedIcon: const Icon(Icons.home),
icon: const Icon(Icons.home_outlined),
label: l10n.navBarLabelStatus,
),
NavigationDestination(
selectedIcon: const Icon(Icons.router),
icon: const Icon(Icons.router_outlined),
label: l10n.navBarLabelNetworks,
),
NavigationDestination(
selectedIcon: const Icon(Icons.people_alt),
icon: const Icon(Icons.people_alt_outlined),
label: l10n.navBarLabelPeers,
),
],
),
body: _getCurrentPage(),
);
}
}
+120
View File
@@ -0,0 +1,120 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:developer' as developer;
/// 带有ZeroTier认证的HTTP客户端
class AuthedDioClient {
final String _authToken;
final Dio _dio;
/// 创建带有ZeroTier认证的HTTP客户端
///
/// [authToken] - ZeroTier认证令牌
/// [baseUrl] - API基础URL
AuthedDioClient(this._authToken, String baseUrl) : _dio = Dio(BaseOptions(
baseUrl: 'http://$baseUrl',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
)) {
// 添加拦截器以处理身份验证
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// 添加ZeroTier特定认证头
options.headers['X-ZT1-Auth'] = _authToken;
return handler.next(options);
},
));
}
/// 获取底层的Dio实例
Dio get dio => _dio;
/// 发送GET请求
Future<Response> get(String path) {
return _dio.get(path);
}
/// 发送POST请求
Future<Response> post(String path, {dynamic data}) {
return _dio.post(path, data: data);
}
/// 发送PUT请求
Future<Response> put(String path, {dynamic data}) {
return _dio.put(path, data: data);
}
/// 发送DELETE请求
Future<Response> delete(String path) {
return _dio.delete(path);
}
/// 关闭客户端
void close() {
_dio.close();
}
}
/// ZeroTier认证服务,用于管理认证和客户端创建
class AuthService {
static const String API_HOST = 'localhost:9993';
/// 缓存的HTTP客户端实例
AuthedDioClient? _cachedClient;
/// 获取已认证的HTTP客户端
Future<AuthedDioClient> get client async {
// 如果我们已经有缓存的客户端,直接返回
if (_cachedClient != null) {
return _cachedClient!;
}
try {
final authToken = await _loadAuthToken();
_cachedClient = AuthedDioClient(authToken, API_HOST);
return _cachedClient!;
} catch (e) {
developer.log('加载认证令牌失败', error: e.toString());
rethrow;
}
}
/// 清理资源
void dispose() {
if (_cachedClient != null) {
_cachedClient!.close();
_cachedClient = null;
}
}
/// 加载认证令牌
Future<String> _loadAuthToken() async {
try {
final path = (await getApplicationDocumentsDirectory()).path;
final file = File('$path/run/authtoken');
// 检查文件是否存在
if (!await file.exists()) {
throw FileSystemException('认证令牌文件未找到', file.path);
}
final token = await file.readAsString();
return token.trim();
} catch (e) {
developer.log('读取认证令牌失败', error: e.toString());
rethrow;
}
}
}
/// 超时异常类
class TimeoutException implements Exception {
final String message;
TimeoutException(this.message);
@override
String toString() => message;
}
+305
View File
@@ -0,0 +1,305 @@
import 'dart:io';
import 'dart:developer' as developer;
import 'package:path_provider/path_provider.dart';
import 'package:dio/dio.dart';
import 'auth_service.dart';
import '../models/peer_info.dart';
/// ZeroTier服务状态详情
class ZerotierStatus {
final String address;
final String version;
final bool online;
final int primaryPort;
final List<String> listeningOn;
ZerotierStatus({
required this.address,
required this.version,
required this.online,
required this.primaryPort,
required this.listeningOn,
});
/// 从JSON创建状态对象
factory ZerotierStatus.fromJson(Map<String, dynamic> json) {
// 提取监听地址列表
List<String> listeningAddresses = [];
if (json['config'] != null &&
json['config']['settings'] != null &&
json['config']['settings']['listeningOn'] != null) {
listeningAddresses = List<String>.from(json['config']['settings']['listeningOn']);
}
// 提取主端口
int port = 0;
if (json['config'] != null &&
json['config']['settings'] != null &&
json['config']['settings']['primaryPort'] != null) {
port = json['config']['settings']['primaryPort'];
}
return ZerotierStatus(
address: json['address'] ?? '',
version: json['version'] ?? '',
online: json['online'] ?? false,
primaryPort: port,
listeningOn: listeningAddresses,
);
}
}
/// ZeroTier服务,提供所有ZeroTier相关功能
class ZerotierService {
final AuthService _authService = AuthService();
// 内部成员变量,存储最后一次加载的数据
List<String> _networkList = [];
List<PeerInfo> _peersList = [];
ZerotierStatus? _statusInfo;
// 表示ZeroTier服务是否正在运行
bool runningStatus = false;
// 允许外部访问最新数据的getter方法
List<String> get networkList => _networkList;
List<PeerInfo> get peersList => _peersList;
ZerotierStatus? get statusInfo => _statusInfo;
/// 向ZeroTier服务发送命令
Future<bool> zerotierCommand(String command) async {
try {
final path = (await getApplicationDocumentsDirectory()).path;
final file = File('$path/run/pipe');
if (!await file.exists()) {
developer.log('未找到ZeroTier命令管道: ${file.path}');
runningStatus = false;
// 特定错误,表示Magisk模块未运行
throw const FileSystemException('MODULE_NOT_RUNNING');
}
await file.writeAsString(command);
developer.log('已发送ZeroTier命令: $command');
return true;
} on FileSystemException catch (e) {
if (e.message == 'MODULE_NOT_RUNNING') {
developer.log('ZeroTier Magisk模块未运行');
} else {
developer.log('未找到ZeroTier命令管道,服务可能未运行');
}
runningStatus = false;
return false;
} catch (e) {
developer.log('发送ZeroTier命令失败', error: e.toString());
return false;
}
}
/// 加载网络列表
Future<List<String>?> loadNetwork() async {
developer.log('开始加载网络列表');
try {
final client = await _authService.client;
// 使用相对路径,基础URL已在客户端中配置
final resp = await client.get('/network');
if (resp.statusCode != 200) {
throw HttpException('加载网络列表失败: ${resp.statusCode}');
}
final body = resp.data;
_networkList = List<String>.from(body.map((u) => u['id']));
developer.log('成功加载网络列表: ${_networkList.length} 个网络');
return _networkList;
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.connectionError) {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
} else {
developer.log('加载网络列表失败', error: e.toString());
}
_networkList = [];
return null;
} on SocketException {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
_networkList = [];
return null;
} catch (e) {
developer.log('加载网络列表失败', error: e.toString());
_networkList = [];
return null;
}
}
/// 离开指定网络
Future<bool> leaveNetwork(String id) async {
try {
final client = await _authService.client;
// 使用相对路径
final resp = await client.delete('/network/$id');
final success = resp.statusCode == 200;
if (success) {
developer.log('成功离开网络: $id');
} else {
developer.log('离开网络失败: $id, 状态码: ${resp.statusCode}');
}
return success;
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.connectionError) {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
// 更新运行状态
runningStatus = false;
} else {
developer.log('离开网络错误', error: e.toString());
}
return false;
} on SocketException {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
// 更新运行状态
runningStatus = false;
return false;
} catch (e) {
developer.log('离开网络错误', error: e.toString());
return false;
}
}
/// 加入指定网络
Future<bool> joinNetwork(String id) async {
try {
if (id.isEmpty) {
developer.log('无法加入空网络ID');
return false;
}
final client = await _authService.client;
// 使用相对路径
final resp = await client.put('/network/$id');
final success = resp.statusCode == 200;
if (success) {
developer.log('成功加入网络: $id');
} else {
developer.log('加入网络失败: $id, 状态码: ${resp.statusCode}');
}
return success;
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.connectionError) {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
// 更新运行状态
runningStatus = false;
} else {
developer.log('加入网络错误', error: e.toString());
}
return false;
} on SocketException {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
// 更新运行状态
runningStatus = false;
return false;
} catch (e) {
developer.log('加入网络错误', error: e.toString());
return false;
}
}
/// 加载 Peer 列表
Future<List<PeerInfo>?> loadPeers() async {
developer.log('开始加载 Peer 列表');
try {
final client = await _authService.client;
final resp = await client.get('/peer');
if (resp.statusCode != 200) {
throw HttpException('加载 Peer 列表失败: ${resp.statusCode}');
}
final List<dynamic> rawPeers = resp.data;
_peersList = rawPeers
.map((p) => PeerInfo.fromJson(p as Map<String, dynamic>))
.toList();
// Sort the list (PLANETs first, then LEAFs by address)
_peersList.sort();
developer.log('成功加载 Peer 列表: ${_peersList.length} 个 Peers');
return _peersList;
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.connectionError) {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
} else {
developer.log('加载 Peer 列表 Dio 错误', error: e.toString(), stackTrace: e.stackTrace);
}
_peersList = [];
return null;
} on SocketException {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
_peersList = [];
return null;
} catch (e, s) {
developer.log('加载 Peer 列表失败', error: e.toString(), stackTrace: s);
_peersList = [];
return null;
}
}
/// 加载ZeroTier服务状态
Future<ZerotierStatus?> loadStatus() async {
developer.log('检查ZeroTier服务状态');
Map<String, dynamic>? statusData;
try {
// 尝试获取状态信息
final client = await _authService.client;
final response = await client.get('/status');
if (response.statusCode == 200) {
statusData = response.data;
developer.log('已获取ZeroTier状态信息');
} else {
developer.log('获取ZeroTier状态信息失败: ${response.statusCode}');
statusData = null;
}
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.connectionError) {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
} else {
developer.log('获取ZeroTier状态信息错误', error: e.toString());
}
statusData = null;
} on SocketException {
developer.log('无法连接到ZeroTier服务:服务可能未运行');
statusData = null;
} catch (e) {
developer.log('检查ZeroTier状态错误', error: e.toString());
statusData = null;
}
// 根据获取的状态信息更新运行状态和详情
if (statusData != null) {
runningStatus = true;
_statusInfo = ZerotierStatus.fromJson(statusData);
developer.log('ZeroTier服务状态: 运行中');
developer.log('已加载ZeroTier详细状态信息');
return _statusInfo;
} else {
runningStatus = false;
_statusInfo = null;
developer.log('ZeroTier服务状态: 未运行');
return null;
}
}
/// 释放资源
void dispose() {
_authService.dispose();
}
}
+118 -97
View File
@@ -5,42 +5,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "3.5.1" version: "4.0.5"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.5.0" version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.11.0" version: "2.13.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
name: boolean_selector name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
characters: characters:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -53,34 +53,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cli_util name: cli_util
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "0.4.1" version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
name: clock name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
collection: collection:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.18.0" version: "1.19.1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "3.0.3" version: "3.0.6"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -89,22 +89,38 @@ packages:
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dio:
dependency: "direct main"
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "5.8.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.3.1" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.1.2" version: "2.1.4"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -114,47 +130,52 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "0.13.1" version: "0.14.3"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "4.0.0" version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: http_parser:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "4.0.2" version: "4.1.2"
image: image:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "4.1.7" version: "4.5.4"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.19.0"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@@ -175,18 +196,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "10.0.4" version: "10.0.8"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "3.0.3" version: "3.0.9"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@@ -199,66 +220,66 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "4.0.0" version: "5.1.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "0.12.16+1" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "0.8.0" version: "0.11.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.12.0" version: "1.16.0"
path: path:
dependency: transitive dependency: transitive
description: description:
name: path name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.9.0" version: "1.9.1"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.1.3" version: "2.1.5"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.2.4" version: "2.2.16"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.4.0" version: "2.4.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -279,26 +300,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "6.0.2" version: "6.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "3.1.5" version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -307,6 +328,14 @@ packages:
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.0.1"
quickalert: quickalert:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -319,63 +348,63 @@ packages:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.0"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.10.0" version: "1.10.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.11.1" version: "1.12.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "2.1.2" version: "2.1.4"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.2.0" version: "1.4.1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "0.7.0" version: "0.7.4"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
name: typed_data name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.3.2" version: "1.4.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -388,34 +417,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "14.2.1" version: "14.3.1"
web: web:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "0.5.1" version: "1.1.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: xdg_directories:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "1.0.4" version: "1.1.0"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@@ -428,10 +449,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: yaml name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "3.1.2" version: "3.1.3"
sdks: sdks:
dart: ">=3.3.4 <4.0.0" dart: ">=3.7.0 <4.0.0"
flutter: ">=3.16.6" flutter: ">=3.27.0"
+7 -3
View File
@@ -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: