initial upload

This commit is contained in:
tom.hempel
2025-10-15 10:53:36 +02:00
commit c15d1d1e49
140 changed files with 6730 additions and 0 deletions

58
lib/main.dart Normal file
View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'UnityUDP',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
useMaterial3: true,
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
useMaterial3: true,
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
),
),
themeMode: ThemeMode.system,
home: const HomeScreen(),
);
}
}

40
lib/models/project.dart Normal file
View File

@ -0,0 +1,40 @@
import 'package:json_annotation/json_annotation.dart';
import 'udp_package.dart';
part 'project.g.dart';
@JsonSerializable()
class Project {
final String id;
final String name;
final int port;
final List<UdpPackage> packages;
Project({
required this.id,
required this.name,
required this.port,
required this.packages,
});
factory Project.fromJson(Map<String, dynamic> json) =>
_$ProjectFromJson(json);
Map<String, dynamic> toJson() => _$ProjectToJson(this);
Project copyWith({
String? id,
String? name,
int? port,
List<UdpPackage>? packages,
}) {
return Project(
id: id ?? this.id,
name: name ?? this.name,
port: port ?? this.port,
packages: packages ?? this.packages,
);
}
}

23
lib/models/project.g.dart Normal file
View File

@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'project.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Project _$ProjectFromJson(Map<String, dynamic> json) => Project(
id: json['id'] as String,
name: json['name'] as String,
port: (json['port'] as num).toInt(),
packages: (json['packages'] as List<dynamic>)
.map((e) => UdpPackage.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ProjectToJson(Project instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'port': instance.port,
'packages': instance.packages,
};

View File

@ -0,0 +1,39 @@
import 'package:json_annotation/json_annotation.dart';
part 'udp_package.g.dart';
@JsonSerializable()
class UdpPackage {
final String id;
final String name;
final String data;
final String ipAddress;
UdpPackage({
required this.id,
required this.name,
required this.data,
required this.ipAddress,
});
factory UdpPackage.fromJson(Map<String, dynamic> json) =>
_$UdpPackageFromJson(json);
Map<String, dynamic> toJson() => _$UdpPackageToJson(this);
UdpPackage copyWith({
String? id,
String? name,
String? data,
String? ipAddress,
}) {
return UdpPackage(
id: id ?? this.id,
name: name ?? this.name,
data: data ?? this.data,
ipAddress: ipAddress ?? this.ipAddress,
);
}
}

View File

@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'udp_package.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UdpPackage _$UdpPackageFromJson(Map<String, dynamic> json) => UdpPackage(
id: json['id'] as String,
name: json['name'] as String,
data: json['data'] as String,
ipAddress: json['ipAddress'] as String,
);
Map<String, dynamic> _$UdpPackageToJson(UdpPackage instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'data': instance.data,
'ipAddress': instance.ipAddress,
};

View File

@ -0,0 +1,309 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import '../models/project.dart';
import '../services/storage_service.dart';
import 'project_screen.dart';
import '../widgets/project_dialog.dart';
import '../widgets/about_dialog.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final StorageService _storageService = StorageService();
List<Project> _projects = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadProjects();
// Show web warning after build
if (kIsWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showWebWarning();
});
}
}
Future<void> _loadProjects() async {
setState(() => _isLoading = true);
final projects = await _storageService.loadProjects();
setState(() {
_projects = projects;
_isLoading = false;
});
}
Future<void> _saveProjects() async {
await _storageService.saveProjects(_projects);
}
void _addProject(Project project) {
setState(() {
_projects.add(project);
});
_saveProjects();
}
void _updateProject(Project updatedProject) {
setState(() {
final index = _projects.indexWhere((p) => p.id == updatedProject.id);
if (index != -1) {
_projects[index] = updatedProject;
}
});
_saveProjects();
}
void _deleteProject(String projectId) {
setState(() {
_projects.removeWhere((p) => p.id == projectId);
});
_saveProjects();
}
void _showProjectDialog({Project? project}) {
showDialog(
context: context,
builder: (context) => ProjectDialog(
project: project,
onSave: (newProject) {
if (project == null) {
_addProject(newProject);
} else {
_updateProject(newProject);
}
},
),
);
}
void _showAboutDialog() {
showDialog(
context: context,
builder: (context) => const AppAboutDialog(),
);
}
void _showWebWarning() {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Text('Web Platform Limitation'),
],
),
content: const Text(
'You are running this app in a web browser.\n\n'
'⚠️ UDP networking is NOT supported in web browsers due to security restrictions.\n\n'
'✅ For full functionality, please run this app on:\n'
' • Windows (flutter run -d windows)\n'
' • macOS (flutter run -d macos)\n'
' • Linux (flutter run -d linux)\n'
' • Android or iOS devices\n\n'
'The app will work for creating and managing projects, but UDP packets cannot be sent from a browser.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('I Understand'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('UnityUDP'),
actions: [
if (kIsWeb)
Padding(
padding: const EdgeInsets.only(right: 8),
child: IconButton(
icon: const Icon(Icons.warning_amber, color: Colors.orange),
onPressed: _showWebWarning,
tooltip: 'Web Platform Limitations',
),
),
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: _showAboutDialog,
tooltip: 'About',
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _projects.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_open,
size: 64,
color: Theme.of(context).colorScheme.primary.withAlpha(128),
),
const SizedBox(height: 16),
Text(
'No projects yet',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Create your first project to get started',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _projects.length,
itemBuilder: (context, index) {
final project = _projects[index];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () async {
final updatedProject = await Navigator.push<Project>(
context,
MaterialPageRoute(
builder: (context) =>
ProjectScreen(project: project),
),
);
if (updatedProject != null) {
_updateProject(updatedProject);
}
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.folder,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
project.name,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Port: ${project.port}${project.packages.length} package(s)',
style:
Theme.of(context).textTheme.bodySmall,
),
],
),
),
PopupMenuButton(
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit),
SizedBox(width: 8),
Text('Edit'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete',
style:
TextStyle(color: Colors.red)),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
_showProjectDialog(project: project);
} else if (value == 'delete') {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Project'),
content: Text(
'Are you sure you want to delete "${project.name}"?'),
actions: [
TextButton(
onPressed: () =>
Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
_deleteProject(project.id);
Navigator.pop(context);
},
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Delete'),
),
],
),
);
}
},
),
],
),
),
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _showProjectDialog(),
icon: const Icon(Icons.add),
label: const Text('New Project'),
),
);
}
}

View File

@ -0,0 +1,472 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/project.dart';
import '../models/udp_package.dart';
import '../services/udp_service.dart';
import '../widgets/package_dialog.dart';
class ProjectScreen extends StatefulWidget {
final Project project;
const ProjectScreen({super.key, required this.project});
@override
State<ProjectScreen> createState() => _ProjectScreenState();
}
class _ProjectScreenState extends State<ProjectScreen> {
late Project _project;
final UdpService _udpService = UdpService();
String? _lastSentPackageId;
bool _isSending = false;
late TextEditingController _portController;
@override
void initState() {
super.initState();
_project = widget.project;
_portController = TextEditingController(text: _project.port.toString());
}
@override
void dispose() {
_portController.dispose();
super.dispose();
}
void _addPackage(UdpPackage package) {
setState(() {
_project = _project.copyWith(
packages: [..._project.packages, package],
);
});
}
void _updatePackage(UdpPackage updatedPackage) {
setState(() {
final packages = _project.packages.map((p) {
return p.id == updatedPackage.id ? updatedPackage : p;
}).toList();
_project = _project.copyWith(packages: packages);
});
}
void _deletePackage(String packageId) {
setState(() {
final packages = _project.packages.where((p) => p.id != packageId).toList();
_project = _project.copyWith(packages: packages);
});
}
void _copyPackage(UdpPackage package) {
final copiedPackage = UdpPackage(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: '${package.name} (Copy)',
data: package.data,
ipAddress: package.ipAddress,
);
_addPackage(copiedPackage);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Package copied successfully!'),
behavior: SnackBarBehavior.floating,
),
);
}
void _updatePort() {
final port = int.tryParse(_portController.text);
if (port != null && port >= 1 && port <= 65535) {
setState(() {
_project = _project.copyWith(port: port);
});
} else {
_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 _showPackageDialog({UdpPackage? package}) {
showDialog(
context: context,
builder: (context) => PackageDialog(
package: package,
onSave: (newPackage) {
if (package == null) {
_addPackage(newPackage);
} else {
_updatePackage(newPackage);
}
},
),
);
}
Future<void> _sendPackage(UdpPackage package) async {
if (kIsWeb) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('UDP is not supported in web browsers. Please run on desktop or mobile.'),
backgroundColor: Colors.orange,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 4),
),
);
return;
}
setState(() {
_isSending = true;
});
final success = await _udpService.sendPackage(
data: package.data,
ipAddress: package.ipAddress,
port: _project.port,
);
setState(() {
_isSending = false;
if (success) {
_lastSentPackageId = package.id;
// Reset the indicator after 2 seconds
Future.delayed(const Duration(seconds: 2), () {
if (mounted && _lastSentPackageId == package.id) {
setState(() {
_lastSentPackageId = null;
});
}
});
}
});
if (!mounted) return;
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Package "${package.name}" sent successfully!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to send package "${package.name}"'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
@override
Widget build(BuildContext context) {
return PopScope<Project>(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (!didPop) {
Navigator.of(context).pop(_project);
}
},
child: Scaffold(
appBar: AppBar(
title: Text(_project.name),
),
body: Column(
children: [
// Settings Card
Container(
width: double.infinity,
margin: const EdgeInsets.all(16),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.settings,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'Project Settings',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _portController,
decoration: const InputDecoration(
labelText: 'UDP Port',
prefixIcon: Icon(Icons.router),
isDense: true,
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
onSubmitted: (_) => _updatePort(),
),
),
const SizedBox(width: 8),
FilledButton.icon(
onPressed: _updatePort,
icon: const Icon(Icons.check, size: 18),
label: const Text('Apply'),
),
],
),
],
),
),
),
),
// Packages List
Expanded(
child: _project.packages.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: Theme.of(context).colorScheme.primary.withAlpha(128),
),
const SizedBox(height: 16),
Text(
'No packages yet',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Create your first UDP package',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _project.packages.length,
itemBuilder: (context, index) {
final package = _project.packages[index];
final wasSent = _lastSentPackageId == package.id;
return Card(
margin: const EdgeInsets.only(bottom: 12),
color: wasSent
? Theme.of(context).colorScheme.primaryContainer
: null,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
package.name,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
if (wasSent)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green,
borderRadius:
BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check,
size: 14,
color: Colors.white,
),
SizedBox(width: 4),
Text(
'Sent',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
),
),
],
),
const SizedBox(height: 4),
Text(
'To: ${package.ipAddress}',
style:
Theme.of(context).textTheme.bodySmall,
),
],
),
),
PopupMenuButton(
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit),
SizedBox(width: 8),
Text('Edit'),
],
),
),
const PopupMenuItem(
value: 'copy',
child: Row(
children: [
Icon(Icons.copy),
SizedBox(width: 8),
Text('Copy'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete',
style:
TextStyle(color: Colors.red)),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
_showPackageDialog(package: package);
} else if (value == 'copy') {
_copyPackage(package);
} else if (value == 'delete') {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Package'),
content: Text(
'Are you sure you want to delete "${package.name}"?'),
actions: [
TextButton(
onPressed: () =>
Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
_deletePackage(package.id);
Navigator.pop(context);
},
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Delete'),
),
],
),
);
}
},
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(
package.data,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _isSending
? null
: () => _sendPackage(package),
icon: _isSending
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.send),
label: Text(_isSending ? 'Sending...' : 'Send'),
),
),
],
),
),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _showPackageDialog(),
icon: const Icon(Icons.add),
label: const Text('New Package'),
),
),
);
}
}

View File

@ -0,0 +1,54 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/project.dart';
class StorageService {
static const String _projectsKey = 'projects';
static const String _currentPortKey = 'current_port';
Future<void> saveProjects(List<Project> projects) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonList = projects.map((p) => p.toJson()).toList();
final jsonString = json.encode(jsonList);
final success = await prefs.setString(_projectsKey, jsonString);
if (!success) {
throw Exception('Failed to save projects to storage');
}
// Force a commit on web
await prefs.reload();
} catch (e) {
rethrow;
}
}
Future<List<Project>> loadProjects() async {
try {
final prefs = await SharedPreferences.getInstance();
// Reload to ensure we get the latest data
await prefs.reload();
final jsonString = prefs.getString(_projectsKey);
if (jsonString == null || jsonString.isEmpty) {
return [];
}
final List<dynamic> jsonList = json.decode(jsonString);
return jsonList.map((json) => Project.fromJson(json)).toList();
} catch (e) {
return [];
}
}
Future<void> saveCurrentPort(int port) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_currentPortKey, port);
}
Future<int> loadCurrentPort() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt(_currentPortKey) ?? 8888;
}
}

View File

@ -0,0 +1,42 @@
import 'dart:convert';
import 'dart:io';
import 'package:udp/udp.dart';
class UdpService {
Future<bool> sendPackage({
required String data,
required String ipAddress,
required int port,
}) async {
UDP? sender;
try {
// Create UDP instance
sender = await UDP.bind(Endpoint.any());
// Convert data to bytes
final dataBytes = utf8.encode(data);
// Parse IP address
final address = InternetAddress(ipAddress);
// Create endpoint
final destination = Endpoint.unicast(address, port: Port(port));
// Send the packet
final bytesSent = await sender.send(dataBytes, destination);
// Give a small delay to ensure packet is sent
await Future.delayed(const Duration(milliseconds: 100));
return bytesSent > 0;
} catch (e) {
// Error occurred while sending UDP package
return false;
} finally {
// Always close the socket
sender?.close();
}
}
}

View File

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
class AppAboutDialog extends StatelessWidget {
const AppAboutDialog({super.key});
@override
Widget build(BuildContext context) {
return AboutDialog(
applicationName: 'UnityUDP',
applicationVersion: '1.0.0',
applicationIcon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.send,
size: 32,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
children: [
const SizedBox(height: 16),
const Text(
'A simple and efficient app for sending UDP packages over custom ports.',
),
const SizedBox(height: 16),
const Text(
'Features:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('• Create and manage multiple projects'),
const Text('• Configure custom UDP ports'),
const Text('• Store pre-defined packages'),
const Text('• Quick send functionality'),
const Text('• Persistent local storage'),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.code,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
const Text(
'Developed by Tom Hempel',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
],
);
}
}

View File

@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import '../models/udp_package.dart';
class PackageDialog extends StatefulWidget {
final UdpPackage? package;
final Function(UdpPackage) onSave;
const PackageDialog({
super.key,
this.package,
required this.onSave,
});
@override
State<PackageDialog> createState() => _PackageDialogState();
}
class _PackageDialogState extends State<PackageDialog> {
late TextEditingController _nameController;
late TextEditingController _ipController;
late TextEditingController _dataController;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.package?.name ?? '');
_ipController = TextEditingController(
text: widget.package?.ipAddress ?? '127.0.0.1',
);
_dataController = TextEditingController(text: widget.package?.data ?? '');
}
@override
void dispose() {
_nameController.dispose();
_ipController.dispose();
_dataController.dispose();
super.dispose();
}
bool _isValidIp(String ip) {
final parts = ip.split('.');
if (parts.length != 4) return false;
for (final part in parts) {
final num = int.tryParse(part);
if (num == null || num < 0 || num > 255) return false;
}
return true;
}
void _save() {
if (_formKey.currentState!.validate()) {
final package = UdpPackage(
id: widget.package?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
name: _nameController.text.trim(),
ipAddress: _ipController.text.trim(),
data: _dataController.text,
);
widget.onSave(package);
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.package == null ? 'New Package' : 'Edit Package'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Package Name',
prefixIcon: Icon(Icons.label),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a package name';
}
return null;
},
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;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _dataController,
decoration: const InputDecoration(
labelText: 'Data',
prefixIcon: Icon(Icons.data_object),
hintText: 'Enter the data to send',
),
maxLines: 4,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter data to send';
}
return null;
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(
onPressed: _save,
child: const Text('Save'),
),
],
);
}
}

View File

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/project.dart';
class ProjectDialog extends StatefulWidget {
final Project? project;
final Function(Project) onSave;
const ProjectDialog({
super.key,
this.project,
required this.onSave,
});
@override
State<ProjectDialog> createState() => _ProjectDialogState();
}
class _ProjectDialogState extends State<ProjectDialog> {
late TextEditingController _nameController;
late TextEditingController _portController;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.project?.name ?? '');
_portController = TextEditingController(
text: widget.project?.port.toString() ?? '8888',
);
}
@override
void dispose() {
_nameController.dispose();
_portController.dispose();
super.dispose();
}
void _save() {
if (_formKey.currentState!.validate()) {
final project = Project(
id: widget.project?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
name: _nameController.text.trim(),
port: int.parse(_portController.text),
packages: widget.project?.packages ?? [],
);
widget.onSave(project);
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.project == null ? 'New Project' : 'Edit Project'),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Project Name',
prefixIcon: Icon(Icons.folder),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a project name';
}
return null;
},
autofocus: true,
),
const SizedBox(height: 16),
TextFormField(
controller: _portController,
decoration: const InputDecoration(
labelText: 'UDP Port',
prefixIcon: Icon(Icons.router),
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a port number';
}
final port = int.tryParse(value);
if (port == null || port < 1 || port > 65535) {
return 'Port must be between 1 and 65535';
}
return null;
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(
onPressed: _save,
child: const Text('Save'),
),
],
);
}
}