357 lines
14 KiB
Dart
357 lines
14 KiB
Dart
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)
|
|
),
|
|
])
|
|
)
|
|
);
|
|
}
|
|
),
|
|
)
|
|
),
|
|
],
|
|
));
|
|
}
|
|
} |