diff --git a/lib/models/project.dart b/lib/models/project.dart index 8474df8..a7a1cc5 100644 --- a/lib/models/project.dart +++ b/lib/models/project.dart @@ -7,12 +7,14 @@ part 'project.g.dart'; class Project { final String id; final String name; + final String ipAddress; final int port; final List packages; Project({ required this.id, required this.name, + required this.ipAddress, required this.port, required this.packages, }); @@ -25,12 +27,14 @@ class Project { Project copyWith({ String? id, String? name, + String? ipAddress, int? port, List? packages, }) { return Project( id: id ?? this.id, name: name ?? this.name, + ipAddress: ipAddress ?? this.ipAddress, port: port ?? this.port, packages: packages ?? this.packages, ); diff --git a/lib/models/project.g.dart b/lib/models/project.g.dart index dcd89c7..801c9d8 100644 --- a/lib/models/project.g.dart +++ b/lib/models/project.g.dart @@ -9,6 +9,7 @@ part of 'project.dart'; Project _$ProjectFromJson(Map json) => Project( id: json['id'] as String, name: json['name'] as String, + ipAddress: json['ipAddress'] as String, port: (json['port'] as num).toInt(), packages: (json['packages'] as List) .map((e) => UdpPackage.fromJson(e as Map)) @@ -18,6 +19,7 @@ Project _$ProjectFromJson(Map json) => Project( Map _$ProjectToJson(Project instance) => { 'id': instance.id, 'name': instance.name, + 'ipAddress': instance.ipAddress, 'port': instance.port, 'packages': instance.packages, }; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 0346c02..16756eb 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -279,7 +279,7 @@ class _HomeScreenState extends State { ), const SizedBox(height: 4), Text( - 'Port: ${project.port} • ${project.packages.length} package(s)', + '${project.ipAddress}:${project.port} • ${project.packages.length} package(s)', style: Theme.of(context).textTheme.bodySmall, ), diff --git a/lib/screens/project_screen.dart b/lib/screens/project_screen.dart index 44ab71e..0183808 100644 --- a/lib/screens/project_screen.dart +++ b/lib/screens/project_screen.dart @@ -20,17 +20,20 @@ class _ProjectScreenState extends State { final UdpService _udpService = UdpService(); String? _lastSentPackageId; bool _isSending = false; + late TextEditingController _ipController; late TextEditingController _portController; @override void initState() { super.initState(); _project = widget.project; + _ipController = TextEditingController(text: _project.ipAddress); _portController = TextEditingController(text: _project.port.toString()); } @override void dispose() { + _ipController.dispose(); _portController.dispose(); super.dispose(); } @@ -75,17 +78,58 @@ class _ProjectScreenState extends State { ); } - void _updatePort() { + void _updateSettings() { + final ip = _ipController.text.trim(); final port = int.tryParse(_portController.text); - if (port != null && port >= 1 && port <= 65535) { + + // Validate IP address + 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; + } + } + } + + // Validate port + final isValidPort = port != null && port >= 1 && port <= 65535; + + if (isValidIp && isValidPort) { setState(() { - _project = _project.copyWith(port: port); + _project = _project.copyWith( + ipAddress: ip, + port: port, + ); }); - } else { - _portController.text = _project.port.toString(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Invalid port number (1-65535)'), + 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), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, ), @@ -128,7 +172,7 @@ class _ProjectScreenState extends State { final success = await _udpService.sendPackage( data: package.data, - ipAddress: package.ipAddress, + ipAddress: _project.ipAddress, // Use project IP to override package IP port: _project.port, ); @@ -212,6 +256,18 @@ class _ProjectScreenState extends State { const SizedBox(height: 16), 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, @@ -224,12 +280,12 @@ class _ProjectScreenState extends State { inputFormatters: [ FilteringTextInputFormatter.digitsOnly ], - onSubmitted: (_) => _updatePort(), + onSubmitted: (_) => _updateSettings(), ), ), const SizedBox(width: 8), FilledButton.icon( - onPressed: _updatePort, + onPressed: _updateSettings, icon: const Icon(Icons.check, size: 18), label: const Text('Apply'), ), @@ -265,15 +321,26 @@ class _ProjectScreenState extends State { ], ), ) - : ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: _project.packages.length, - itemBuilder: (context, index) { - final package = _project.packages[index]; - final wasSent = _lastSentPackageId == package.id; + : LayoutBuilder( + builder: (context, constraints) { + // Use 2 columns when width is >= 800px, otherwise 1 column + final crossAxisCount = constraints.maxWidth >= 800 ? 2 : 1; + final aspectRatio = constraints.maxWidth >= 800 ? 2.5 : 2.0; + + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + childAspectRatio: aspectRatio, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + 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), + return Card( color: wasSent ? Theme.of(context).colorScheme.primaryContainer : null, @@ -335,7 +402,7 @@ class _ProjectScreenState extends State { ), const SizedBox(height: 4), Text( - 'To: ${package.ipAddress}', + 'To: ${_project.ipAddress}:${_project.port}', style: Theme.of(context).textTheme.bodySmall, ), @@ -455,6 +522,8 @@ class _ProjectScreenState extends State { ), ); }, + ); + }, ), ), ], diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 638f1d9..667c051 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -43,7 +43,13 @@ class StorageService { } final List jsonList = json.decode(contents); - return jsonList.map((json) => Project.fromJson(json)).toList(); + return jsonList.map((json) { + // Migration: Add ipAddress field if it doesn't exist (for backwards compatibility) + if (!json.containsKey('ipAddress')) { + json['ipAddress'] = '127.0.0.1'; // Default IP address + } + return Project.fromJson(json); + }).toList(); } catch (e) { return []; } @@ -64,7 +70,13 @@ class StorageService { final contents = await importFile.readAsString(); final List jsonList = json.decode(contents); - final projects = jsonList.map((json) => Project.fromJson(json)).toList(); + final projects = jsonList.map((json) { + // Migration: Add ipAddress field if it doesn't exist (for backwards compatibility) + if (!json.containsKey('ipAddress')) { + json['ipAddress'] = '127.0.0.1'; // Default IP address + } + return Project.fromJson(json); + }).toList(); // Save the imported projects await saveProjects(projects); diff --git a/lib/widgets/project_dialog.dart b/lib/widgets/project_dialog.dart index 6995e33..6771815 100644 --- a/lib/widgets/project_dialog.dart +++ b/lib/widgets/project_dialog.dart @@ -18,6 +18,7 @@ class ProjectDialog extends StatefulWidget { class _ProjectDialogState extends State { late TextEditingController _nameController; + late TextEditingController _ipController; late TextEditingController _portController; final _formKey = GlobalKey(); @@ -25,6 +26,9 @@ class _ProjectDialogState extends State { void initState() { super.initState(); _nameController = TextEditingController(text: widget.project?.name ?? ''); + _ipController = TextEditingController( + text: widget.project?.ipAddress ?? '127.0.0.1', + ); _portController = TextEditingController( text: widget.project?.port.toString() ?? '8888', ); @@ -33,6 +37,7 @@ class _ProjectDialogState extends State { @override void dispose() { _nameController.dispose(); + _ipController.dispose(); _portController.dispose(); super.dispose(); } @@ -42,6 +47,7 @@ class _ProjectDialogState extends State { final project = Project( id: widget.project?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), name: _nameController.text.trim(), + ipAddress: _ipController.text.trim(), port: int.parse(_portController.text), packages: widget.project?.packages ?? [], ); @@ -74,6 +80,35 @@ class _ProjectDialogState extends State { autofocus: true, ), const SizedBox(height: 16), + TextFormField( + controller: _ipController, + decoration: const InputDecoration( + labelText: 'IP Address', + prefixIcon: Icon(Icons.computer), + hintText: '127.0.0.1', + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter an IP address'; + } + // Basic IP address validation + final ipPattern = RegExp( + r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$', + ); + if (!ipPattern.hasMatch(value.trim())) { + return 'Please enter a valid IP address'; + } + final parts = value.trim().split('.'); + for (final part in parts) { + final num = int.tryParse(part); + if (num == null || num < 0 || num > 255) { + return 'Each octet must be between 0 and 255'; + } + } + return null; + }, + ), + const SizedBox(height: 16), TextFormField( controller: _portController, decoration: const InputDecoration( diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 7bf753f..420d846 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -39,7 +39,6 @@ static void my_application_activate(GApplication* application) { #endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "flutterudp"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); @@ -48,18 +47,19 @@ static void my_application_activate(GApplication* application) { } gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); + + // Show the window and all its children together to avoid flashing + gtk_widget_show_all(GTK_WIDGET(window)); } // Implements GApplication::local_command_line.