multicast/broadcast support
This commit is contained in:
@ -22,13 +22,17 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
bool _isSending = false;
|
||||
late TextEditingController _ipController;
|
||||
late TextEditingController _portController;
|
||||
late List<String> _projectIpAddresses;
|
||||
late bool _projectIsBroadcast;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_project = widget.project;
|
||||
_ipController = TextEditingController(text: _project.ipAddress);
|
||||
_ipController = TextEditingController();
|
||||
_portController = TextEditingController(text: _project.port.toString());
|
||||
_projectIpAddresses = _project.ipAddresses.toList();
|
||||
_projectIsBroadcast = _project.isBroadcast;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -67,7 +71,8 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
name: '${package.name} (Copy)',
|
||||
data: package.data,
|
||||
ipAddress: package.ipAddress,
|
||||
ipAddresses: package.ipAddresses.toList(),
|
||||
isBroadcast: package.isBroadcast,
|
||||
);
|
||||
_addPackage(copiedPackage);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -78,58 +83,40 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _updateSettings() {
|
||||
final ip = _ipController.text.trim();
|
||||
final port = int.tryParse(_portController.text);
|
||||
|
||||
// Validate IP address
|
||||
bool _isValidIp(String ip) {
|
||||
final ipPattern = RegExp(r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$');
|
||||
bool isValidIp = ipPattern.hasMatch(ip);
|
||||
if (isValidIp) {
|
||||
final parts = ip.split('.');
|
||||
for (final part in parts) {
|
||||
final num = int.tryParse(part);
|
||||
if (num == null || num < 0 || num > 255) {
|
||||
isValidIp = false;
|
||||
break;
|
||||
}
|
||||
if (!ipPattern.hasMatch(ip)) return false;
|
||||
|
||||
final parts = ip.split('.');
|
||||
for (final part in parts) {
|
||||
final num = int.tryParse(part);
|
||||
if (num == null || num < 0 || num > 255) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate port
|
||||
final isValidPort = port != null && port >= 1 && port <= 65535;
|
||||
|
||||
if (isValidIp && isValidPort) {
|
||||
setState(() {
|
||||
_project = _project.copyWith(
|
||||
ipAddress: ip,
|
||||
port: port,
|
||||
return true;
|
||||
}
|
||||
|
||||
void _addProjectIp() {
|
||||
final ip = _ipController.text.trim();
|
||||
if (ip.isNotEmpty && _isValidIp(ip)) {
|
||||
if (!_projectIpAddresses.contains(ip)) {
|
||||
setState(() {
|
||||
_projectIpAddresses.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('Settings updated successfully!'),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Revert to current project values
|
||||
_ipController.text = _project.ipAddress;
|
||||
_portController.text = _project.port.toString();
|
||||
|
||||
String errorMessage = 'Invalid ';
|
||||
if (!isValidIp && !isValidPort) {
|
||||
errorMessage += 'IP address and port number';
|
||||
} else if (!isValidIp) {
|
||||
errorMessage += 'IP address';
|
||||
} else {
|
||||
errorMessage += 'port number (1-65535)';
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(errorMessage),
|
||||
content: Text('Invalid IP address'),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
@ -137,6 +124,117 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _removeProjectIp(String ip) {
|
||||
setState(() {
|
||||
_projectIpAddresses.remove(ip);
|
||||
// Ensure at least one IP if not in broadcast mode
|
||||
if (_projectIpAddresses.isEmpty && !_projectIsBroadcast) {
|
||||
_projectIpAddresses.add('127.0.0.1');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _updateSettings() {
|
||||
final port = int.tryParse(_portController.text);
|
||||
|
||||
// Validate port
|
||||
final isValidPort = port != null && port >= 1 && port <= 65535;
|
||||
|
||||
// Validate we have IPs if not in broadcast mode
|
||||
if (!_projectIsBroadcast && _projectIpAddresses.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please add at least one IP address'),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidPort) {
|
||||
// Show dialog asking if user wants to apply settings to all packages
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Apply Settings'),
|
||||
content: const Text(
|
||||
'Do you want to apply these settings to all packages?\n\n'
|
||||
'• Yes - All packages will use these settings\n'
|
||||
'• No - Only project defaults will be updated (packages keep their individual settings)',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_applySettings(port!, applyToPackages: false);
|
||||
},
|
||||
child: const Text('No'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_applySettings(port!, applyToPackages: true);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Revert to current project values
|
||||
_portController.text = _project.port.toString();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Invalid port number (1-65535)'),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _applySettings(int port, {required bool applyToPackages}) {
|
||||
setState(() {
|
||||
if (applyToPackages) {
|
||||
// Update all packages to use the new project settings
|
||||
final updatedPackages = _project.packages.map((package) {
|
||||
return package.copyWith(
|
||||
ipAddresses: _projectIsBroadcast ? [] : _projectIpAddresses,
|
||||
isBroadcast: _projectIsBroadcast,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
_project = _project.copyWith(
|
||||
ipAddresses: _projectIsBroadcast ? [] : _projectIpAddresses,
|
||||
isBroadcast: _projectIsBroadcast,
|
||||
port: port,
|
||||
packages: updatedPackages,
|
||||
);
|
||||
} else {
|
||||
// Only update project-level settings
|
||||
_project = _project.copyWith(
|
||||
ipAddresses: _projectIsBroadcast ? [] : _projectIpAddresses,
|
||||
isBroadcast: _projectIsBroadcast,
|
||||
port: port,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
applyToPackages
|
||||
? 'Settings applied to project and all packages!'
|
||||
: 'Project settings updated!',
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPackageDialog({UdpPackage? package}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@ -170,15 +268,28 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
_isSending = true;
|
||||
});
|
||||
|
||||
final success = await _udpService.sendPackage(
|
||||
// Determine if we should use project-level or package-level settings
|
||||
// Packages can override project settings, or use project defaults if empty
|
||||
final isBroadcast = package.isBroadcast || (_project.isBroadcast && package.ipAddresses.isEmpty);
|
||||
final ipAddresses = package.ipAddresses.isNotEmpty
|
||||
? package.ipAddresses
|
||||
: _project.ipAddresses;
|
||||
|
||||
// Use the new advanced send method that supports broadcast and multiple IPs
|
||||
final results = await _udpService.sendPackageAdvanced(
|
||||
data: package.data,
|
||||
ipAddress: _project.ipAddress, // Use project IP to override package IP
|
||||
port: _project.port,
|
||||
isBroadcast: isBroadcast,
|
||||
ipAddresses: isBroadcast ? null : ipAddresses,
|
||||
);
|
||||
|
||||
final successCount = results.values.where((v) => v).length;
|
||||
final totalCount = results.length;
|
||||
final allSuccess = successCount == totalCount;
|
||||
|
||||
setState(() {
|
||||
_isSending = false;
|
||||
if (success) {
|
||||
if (allSuccess) {
|
||||
_lastSentPackageId = package.id;
|
||||
// Reset the indicator after 2 seconds
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
@ -193,15 +304,35 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
// Show appropriate feedback based on results
|
||||
if (allSuccess) {
|
||||
String message;
|
||||
if (isBroadcast) {
|
||||
message = 'Package "${package.name}" broadcast successfully!';
|
||||
} else if (totalCount > 1) {
|
||||
message = 'Package "${package.name}" sent to $totalCount addresses!';
|
||||
} else {
|
||||
message = 'Package "${package.name}" sent successfully!';
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Package "${package.name}" sent successfully!'),
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} else if (successCount > 0) {
|
||||
// Partial success
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Package "${package.name}" sent to $successCount of $totalCount addresses'),
|
||||
backgroundColor: Colors.orange,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Complete failure
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to send package "${package.name}"'),
|
||||
@ -254,20 +385,106 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Broadcast Toggle
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Broadcast Mode'),
|
||||
subtitle: const Text('Send to all devices on network'),
|
||||
value: _projectIsBroadcast,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_projectIsBroadcast = value;
|
||||
});
|
||||
},
|
||||
secondary: const Icon(Icons.sensors),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// IP Addresses Section (only shown when not in broadcast mode)
|
||||
if (!_projectIsBroadcast) ...[
|
||||
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: (_) => _addProjectIp(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filled(
|
||||
onPressed: _addProjectIp,
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Add IP',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// IP Address Chips
|
||||
if (_projectIpAddresses.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _projectIpAddresses.map((ip) {
|
||||
return Chip(
|
||||
label: Text(ip),
|
||||
deleteIcon: const Icon(Icons.close, size: 18),
|
||||
onDeleted: _projectIpAddresses.length > 1
|
||||
? () => _removeProjectIp(ip)
|
||||
: null,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
] 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(
|
||||
'Packages will be broadcast to all devices on your local network',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Port Configuration
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _ipController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'IP Address',
|
||||
prefixIcon: Icon(Icons.computer),
|
||||
isDense: true,
|
||||
),
|
||||
onSubmitted: (_) => _updateSettings(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _portController,
|
||||
@ -401,11 +618,33 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'To: ${_project.ipAddress}:${_project.port}',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
if (package.isBroadcast)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.sensors,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Broadcast • Port: ${_project.port}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
package.ipAddresses.length > 1
|
||||
? 'To: ${package.ipAddresses.join(", ")} • Port: ${_project.port}'
|
||||
: 'To: ${package.ipAddresses.first}:${_project.port}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user