fixed rendering bug on linux + ip configuration
This commit is contained in:
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user