[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
+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),
),
),
),
],
),
);
}
}