multicast/broadcast support

This commit is contained in:
2025-10-15 15:22:51 +02:00
parent eb154802b4
commit c5264186f3
12 changed files with 712 additions and 116 deletions

View File

@ -20,15 +20,18 @@ class _PackageDialogState extends State<PackageDialog> {
late TextEditingController _ipController;
late TextEditingController _dataController;
final _formKey = GlobalKey<FormState>();
late bool _isBroadcast;
late List<String> _ipAddresses;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.package?.name ?? '');
_ipController = TextEditingController(
text: widget.package?.ipAddress ?? '127.0.0.1',
);
_ipController = TextEditingController();
_dataController = TextEditingController(text: widget.package?.data ?? '');
_isBroadcast = widget.package?.isBroadcast ?? false;
_ipAddresses = widget.package?.ipAddresses.toList() ?? ['127.0.0.1'];
}
@override
@ -49,12 +52,62 @@ class _PackageDialogState extends State<PackageDialog> {
return true;
}
void _addIpAddress() {
final ip = _ipController.text.trim();
if (ip.isNotEmpty && _isValidIp(ip)) {
if (!_ipAddresses.contains(ip)) {
setState(() {
_ipAddresses.add(ip);
_ipController.clear();
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('IP address already added'),
behavior: SnackBarBehavior.floating,
),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid IP address'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
void _removeIpAddress(String ip) {
setState(() {
_ipAddresses.remove(ip);
// Ensure at least one IP if not in broadcast mode
if (_ipAddresses.isEmpty && !_isBroadcast) {
_ipAddresses.add('127.0.0.1');
}
});
}
void _save() {
if (_formKey.currentState!.validate()) {
// Validate we have IPs if not in broadcast mode
if (!_isBroadcast && _ipAddresses.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please add at least one IP address'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
return;
}
final package = UdpPackage(
id: widget.package?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
name: _nameController.text.trim(),
ipAddress: _ipController.text.trim(),
ipAddresses: _isBroadcast ? [] : _ipAddresses,
isBroadcast: _isBroadcast,
data: _dataController.text,
);
widget.onSave(package);
@ -71,7 +124,9 @@ class _PackageDialogState extends State<PackageDialog> {
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Package Name
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
@ -87,25 +142,103 @@ class _PackageDialogState extends State<PackageDialog> {
autofocus: true,
),
const SizedBox(height: 16),
TextFormField(
controller: _ipController,
decoration: const InputDecoration(
labelText: 'IP Address',
prefixIcon: Icon(Icons.computer),
hintText: '192.168.1.100',
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter an IP address';
}
if (!_isValidIp(value.trim())) {
return 'Invalid IP address';
}
return null;
// Broadcast Mode Toggle
SwitchListTile(
title: const Text('Broadcast Mode'),
subtitle: const Text('Send to all devices on network'),
value: _isBroadcast,
onChanged: (value) {
setState(() {
_isBroadcast = value;
});
},
secondary: const Icon(Icons.sensors),
),
const SizedBox(height: 16),
const SizedBox(height: 8),
// IP Addresses Section (only shown when not in broadcast mode)
if (!_isBroadcast) ...[
Text(
'Target IP Addresses',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
// IP Address Input
Row(
children: [
Expanded(
child: TextField(
controller: _ipController,
decoration: const InputDecoration(
labelText: 'IP Address',
prefixIcon: Icon(Icons.computer),
hintText: '192.168.1.100',
isDense: true,
),
keyboardType: TextInputType.number,
onSubmitted: (_) => _addIpAddress(),
),
),
const SizedBox(width: 8),
IconButton.filled(
onPressed: _addIpAddress,
icon: const Icon(Icons.add),
tooltip: 'Add IP',
),
],
),
const SizedBox(height: 12),
// IP Address Chips
if (_ipAddresses.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 8,
children: _ipAddresses.map((ip) {
return Chip(
label: Text(ip),
deleteIcon: const Icon(Icons.close, size: 18),
onDeleted: _ipAddresses.length > 1
? () => _removeIpAddress(ip)
: null,
);
}).toList(),
),
const SizedBox(height: 8),
] else ...[
// Broadcast Info
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.info_outline,
size: 20,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'This package will be broadcast to all devices on your local network',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
],
),
),
const SizedBox(height: 16),
],
// Data Field
TextFormField(
controller: _dataController,
decoration: const InputDecoration(

View File

@ -27,7 +27,7 @@ class _ProjectDialogState extends State<ProjectDialog> {
super.initState();
_nameController = TextEditingController(text: widget.project?.name ?? '');
_ipController = TextEditingController(
text: widget.project?.ipAddress ?? '127.0.0.1',
text: widget.project?.ipAddresses.firstOrNull ?? '127.0.0.1',
);
_portController = TextEditingController(
text: widget.project?.port.toString() ?? '8888',
@ -47,7 +47,8 @@ class _ProjectDialogState extends State<ProjectDialog> {
final project = Project(
id: widget.project?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
name: _nameController.text.trim(),
ipAddress: _ipController.text.trim(),
ipAddresses: [_ipController.text.trim()],
isBroadcast: false,
port: int.parse(_portController.text),
packages: widget.project?.packages ?? [],
);