fixed rendering bug on linux + ip configuration

This commit is contained in:
2025-10-15 14:20:31 +02:00
parent 86e89b5ed8
commit 74a918d62b
7 changed files with 146 additions and 24 deletions

View File

@ -7,12 +7,14 @@ part 'project.g.dart';
class Project { class Project {
final String id; final String id;
final String name; final String name;
final String ipAddress;
final int port; final int port;
final List<UdpPackage> packages; final List<UdpPackage> packages;
Project({ Project({
required this.id, required this.id,
required this.name, required this.name,
required this.ipAddress,
required this.port, required this.port,
required this.packages, required this.packages,
}); });
@ -25,12 +27,14 @@ class Project {
Project copyWith({ Project copyWith({
String? id, String? id,
String? name, String? name,
String? ipAddress,
int? port, int? port,
List<UdpPackage>? packages, List<UdpPackage>? packages,
}) { }) {
return Project( return Project(
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
ipAddress: ipAddress ?? this.ipAddress,
port: port ?? this.port, port: port ?? this.port,
packages: packages ?? this.packages, packages: packages ?? this.packages,
); );

View File

@ -9,6 +9,7 @@ part of 'project.dart';
Project _$ProjectFromJson(Map<String, dynamic> json) => Project( Project _$ProjectFromJson(Map<String, dynamic> json) => Project(
id: json['id'] as String, id: json['id'] as String,
name: json['name'] as String, name: json['name'] as String,
ipAddress: json['ipAddress'] as String,
port: (json['port'] as num).toInt(), port: (json['port'] as num).toInt(),
packages: (json['packages'] as List<dynamic>) packages: (json['packages'] as List<dynamic>)
.map((e) => UdpPackage.fromJson(e as Map<String, dynamic>)) .map((e) => UdpPackage.fromJson(e as Map<String, dynamic>))
@ -18,6 +19,7 @@ Project _$ProjectFromJson(Map<String, dynamic> json) => Project(
Map<String, dynamic> _$ProjectToJson(Project instance) => <String, dynamic>{ Map<String, dynamic> _$ProjectToJson(Project instance) => <String, dynamic>{
'id': instance.id, 'id': instance.id,
'name': instance.name, 'name': instance.name,
'ipAddress': instance.ipAddress,
'port': instance.port, 'port': instance.port,
'packages': instance.packages, 'packages': instance.packages,
}; };

View File

@ -279,7 +279,7 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Port: ${project.port}${project.packages.length} package(s)', '${project.ipAddress}:${project.port}${project.packages.length} package(s)',
style: style:
Theme.of(context).textTheme.bodySmall, Theme.of(context).textTheme.bodySmall,
), ),

View File

@ -20,17 +20,20 @@ class _ProjectScreenState extends State<ProjectScreen> {
final UdpService _udpService = UdpService(); final UdpService _udpService = UdpService();
String? _lastSentPackageId; String? _lastSentPackageId;
bool _isSending = false; bool _isSending = false;
late TextEditingController _ipController;
late TextEditingController _portController; late TextEditingController _portController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_project = widget.project; _project = widget.project;
_ipController = TextEditingController(text: _project.ipAddress);
_portController = TextEditingController(text: _project.port.toString()); _portController = TextEditingController(text: _project.port.toString());
} }
@override @override
void dispose() { void dispose() {
_ipController.dispose();
_portController.dispose(); _portController.dispose();
super.dispose(); super.dispose();
} }
@ -75,17 +78,58 @@ class _ProjectScreenState extends State<ProjectScreen> {
); );
} }
void _updatePort() { void _updateSettings() {
final ip = _ipController.text.trim();
final port = int.tryParse(_portController.text); 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(() { setState(() {
_project = _project.copyWith(port: port); _project = _project.copyWith(
ipAddress: ip,
port: port,
);
}); });
} else {
_portController.text = _project.port.toString();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( 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, backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
@ -128,7 +172,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
final success = await _udpService.sendPackage( final success = await _udpService.sendPackage(
data: package.data, data: package.data,
ipAddress: package.ipAddress, ipAddress: _project.ipAddress, // Use project IP to override package IP
port: _project.port, port: _project.port,
); );
@ -212,6 +256,18 @@ class _ProjectScreenState extends State<ProjectScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
children: [ 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( Expanded(
child: TextField( child: TextField(
controller: _portController, controller: _portController,
@ -224,12 +280,12 @@ class _ProjectScreenState extends State<ProjectScreen> {
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.digitsOnly FilteringTextInputFormatter.digitsOnly
], ],
onSubmitted: (_) => _updatePort(), onSubmitted: (_) => _updateSettings(),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
FilledButton.icon( FilledButton.icon(
onPressed: _updatePort, onPressed: _updateSettings,
icon: const Icon(Icons.check, size: 18), icon: const Icon(Icons.check, size: 18),
label: const Text('Apply'), label: const Text('Apply'),
), ),
@ -265,15 +321,26 @@ class _ProjectScreenState extends State<ProjectScreen> {
], ],
), ),
) )
: ListView.builder( : LayoutBuilder(
padding: const EdgeInsets.all(16), builder: (context, constraints) {
itemCount: _project.packages.length, // Use 2 columns when width is >= 800px, otherwise 1 column
itemBuilder: (context, index) { final crossAxisCount = constraints.maxWidth >= 800 ? 2 : 1;
final package = _project.packages[index]; final aspectRatio = constraints.maxWidth >= 800 ? 2.5 : 2.0;
final wasSent = _lastSentPackageId == package.id;
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( return Card(
margin: const EdgeInsets.only(bottom: 12),
color: wasSent color: wasSent
? Theme.of(context).colorScheme.primaryContainer ? Theme.of(context).colorScheme.primaryContainer
: null, : null,
@ -335,7 +402,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'To: ${package.ipAddress}', 'To: ${_project.ipAddress}:${_project.port}',
style: style:
Theme.of(context).textTheme.bodySmall, Theme.of(context).textTheme.bodySmall,
), ),
@ -455,6 +522,8 @@ class _ProjectScreenState extends State<ProjectScreen> {
), ),
); );
}, },
);
},
), ),
), ),
], ],

View File

@ -43,7 +43,13 @@ class StorageService {
} }
final List<dynamic> jsonList = json.decode(contents); final List<dynamic> 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) { } catch (e) {
return []; return [];
} }
@ -64,7 +70,13 @@ class StorageService {
final contents = await importFile.readAsString(); final contents = await importFile.readAsString();
final List<dynamic> jsonList = json.decode(contents); final List<dynamic> 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 // Save the imported projects
await saveProjects(projects); await saveProjects(projects);

View File

@ -18,6 +18,7 @@ class ProjectDialog extends StatefulWidget {
class _ProjectDialogState extends State<ProjectDialog> { class _ProjectDialogState extends State<ProjectDialog> {
late TextEditingController _nameController; late TextEditingController _nameController;
late TextEditingController _ipController;
late TextEditingController _portController; late TextEditingController _portController;
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
@ -25,6 +26,9 @@ class _ProjectDialogState extends State<ProjectDialog> {
void initState() { void initState() {
super.initState(); super.initState();
_nameController = TextEditingController(text: widget.project?.name ?? ''); _nameController = TextEditingController(text: widget.project?.name ?? '');
_ipController = TextEditingController(
text: widget.project?.ipAddress ?? '127.0.0.1',
);
_portController = TextEditingController( _portController = TextEditingController(
text: widget.project?.port.toString() ?? '8888', text: widget.project?.port.toString() ?? '8888',
); );
@ -33,6 +37,7 @@ class _ProjectDialogState extends State<ProjectDialog> {
@override @override
void dispose() { void dispose() {
_nameController.dispose(); _nameController.dispose();
_ipController.dispose();
_portController.dispose(); _portController.dispose();
super.dispose(); super.dispose();
} }
@ -42,6 +47,7 @@ class _ProjectDialogState extends State<ProjectDialog> {
final project = Project( final project = Project(
id: widget.project?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), id: widget.project?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
name: _nameController.text.trim(), name: _nameController.text.trim(),
ipAddress: _ipController.text.trim(),
port: int.parse(_portController.text), port: int.parse(_portController.text),
packages: widget.project?.packages ?? [], packages: widget.project?.packages ?? [],
); );
@ -74,6 +80,35 @@ class _ProjectDialogState extends State<ProjectDialog> {
autofocus: true, autofocus: true,
), ),
const SizedBox(height: 16), 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( TextFormField(
controller: _portController, controller: _portController,
decoration: const InputDecoration( decoration: const InputDecoration(

View File

@ -39,7 +39,6 @@ static void my_application_activate(GApplication* application) {
#endif #endif
if (use_header_bar) { if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 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_title(header_bar, "flutterudp");
gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 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_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new(); g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project); FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(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. // Implements GApplication::local_command_line.