mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-06 08:46:40 +10:00
upgraded flutter and other fixes
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -42,6 +41,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
context.read<MeshCoreConnector>().setActiveChannel(widget.channel.index);
|
||||
|
||||
// Scroll to bottom when opening channel chat
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -352,12 +356,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
|
||||
Widget contentPreview;
|
||||
if (gifId != null) {
|
||||
contentPreview = Row(
|
||||
children: [
|
||||
Icon(Icons.gif_box, size: 14, color: previewTextColor),
|
||||
const SizedBox(width: 4),
|
||||
Text('GIF', style: TextStyle(fontSize: 12, color: previewTextColor)),
|
||||
],
|
||||
contentPreview = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: GifMessage(
|
||||
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
fallbackTextColor: previewTextColor,
|
||||
width: 120,
|
||||
height: 80,
|
||||
),
|
||||
);
|
||||
} else if (poi != null) {
|
||||
contentPreview = Row(
|
||||
@@ -843,6 +850,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
|
||||
void _sendReaction(ChannelMessage message, String emoji) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
// Send reaction with full messageId to find target, but parser will extract
|
||||
// lightweight reactionKey (timestamp_senderPrefix) for deduplication
|
||||
final reactionText = 'r:${message.messageId}:$emoji';
|
||||
connector.sendChannelMessage(widget.channel, reactionText);
|
||||
}
|
||||
|
||||
@@ -78,48 +78,46 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
if (connector.isLoadingChannels) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
body: () {
|
||||
if (connector.isLoadingChannels) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final channels = connector.channels;
|
||||
final channels = connector.channels;
|
||||
|
||||
if (channels.isEmpty) {
|
||||
return EmptyState(
|
||||
icon: Icons.tag,
|
||||
title: 'No channels configured',
|
||||
action: FilledButton.icon(
|
||||
onPressed: () => _addPublicChannel(context, connector),
|
||||
icon: const Icon(Icons.public),
|
||||
label: const Text('Add Public Channel'),
|
||||
if (channels.isEmpty) {
|
||||
return EmptyState(
|
||||
icon: Icons.tag,
|
||||
title: 'No channels configured',
|
||||
action: FilledButton.icon(
|
||||
onPressed: () => _addPublicChannel(context, connector),
|
||||
icon: const Icon(Icons.public),
|
||||
label: const Text('Add Public Channel'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ReorderableListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 88),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: channels.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
final reordered = List<Channel>.from(channels);
|
||||
final item = reordered.removeAt(oldIndex);
|
||||
reordered.insert(newIndex, item);
|
||||
unawaited(
|
||||
connector.setChannelOrder(
|
||||
reordered.map((c) => c.index).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ReorderableListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 88),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: channels.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
final reordered = List<Channel>.from(channels);
|
||||
final item = reordered.removeAt(oldIndex);
|
||||
reordered.insert(newIndex, item);
|
||||
unawaited(
|
||||
connector.setChannelOrder(
|
||||
reordered.map((c) => c.index).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final channel = channels[index];
|
||||
return _buildChannelTile(context, connector, channel, index);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final channel = channels[index];
|
||||
return _buildChannelTile(context, connector, channel, index);
|
||||
},
|
||||
);
|
||||
}(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showAddChannelDialog(context),
|
||||
child: const Icon(Icons.add),
|
||||
@@ -144,7 +142,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
final unreadCount = connector.getUnreadCountForChannel(channel);
|
||||
return Card(
|
||||
key: ValueKey('channel_${channel.index}'),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
minVerticalPadding: 0,
|
||||
|
||||
@@ -32,7 +32,6 @@ class ChatScreen extends StatefulWidget {
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
final _textController = TextEditingController();
|
||||
final _scrollController = ScrollController();
|
||||
bool _clearPath = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -40,6 +39,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
context.read<MeshCoreConnector>().setActiveContact(widget.contact.publicKeyHex);
|
||||
|
||||
// Scroll to bottom when opening chat
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,75 +64,86 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final contact = _resolveContact(connector);
|
||||
final unreadCount = connector.getUnreadCountForContactKey(widget.contact.publicKeyHex);
|
||||
final unreadLabel = 'Unread: $unreadCount';
|
||||
final pathLabel = _clearPath ? 'Flood (forced)' : _currentPathLabel(contact);
|
||||
final canShowPathDetails = !_clearPath && contact.path.isNotEmpty;
|
||||
final pathLabel = _currentPathLabel(contact);
|
||||
|
||||
// Show path details if we have path data (from device or override)
|
||||
final hasPathData = contact.path.isNotEmpty || contact.pathOverrideBytes != null;
|
||||
final effectivePath = contact.pathOverrideBytes ?? contact.path;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(contact.name),
|
||||
if (canShowPathDetails)
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress: () => _showFullPathDialog(context, contact.path),
|
||||
child: Text(
|
||||
'$pathLabel • $unreadLabel',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.normal),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: hasPathData ? () => _showFullPathDialog(context, effectivePath) : null,
|
||||
child: Text(
|
||||
'$pathLabel • $unreadLabel',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.normal),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.normal,
|
||||
decoration: hasPathData ? TextDecoration.underline : null,
|
||||
decorationStyle: TextDecorationStyle.dotted,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(_clearPath ? Icons.waves : Icons.route),
|
||||
tooltip: 'Routing mode',
|
||||
onSelected: (mode) {
|
||||
setState(() {
|
||||
_clearPath = (mode == 'flood');
|
||||
});
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final contact = _resolveContact(connector);
|
||||
final isFloodMode = contact.pathOverride == -1;
|
||||
|
||||
return PopupMenuButton<String>(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
tooltip: 'Routing mode',
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(contact, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(contact, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Auto (use saved path)',
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Force Flood Mode',
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.auto_mode, size: 20, color: !_clearPath ? Theme.of(context).primaryColor : null),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Auto (use saved path)',
|
||||
style: TextStyle(
|
||||
fontWeight: !_clearPath ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.waves, size: 20, color: _clearPath ? Theme.of(context).primaryColor : null),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Force Flood Mode',
|
||||
style: TextStyle(
|
||||
fontWeight: _clearPath ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
@@ -304,7 +319,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
connector.sendMessage(
|
||||
widget.contact,
|
||||
text,
|
||||
clearPath: _clearPath,
|
||||
);
|
||||
_textController.clear();
|
||||
|
||||
@@ -416,23 +430,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final pathBytes = Uint8List.fromList(path.pathBytes);
|
||||
final pathLength = path.pathBytes.length;
|
||||
|
||||
await connector.setContactPath(
|
||||
// Set the path override to persist user's choice
|
||||
await connector.setPathOverride(
|
||||
widget.contact,
|
||||
pathBytes,
|
||||
pathLength,
|
||||
);
|
||||
|
||||
// Update contact in memory directly for immediate UI feedback
|
||||
connector.updateContactInMemory(
|
||||
widget.contact.publicKeyHex,
|
||||
pathLen: pathLength,
|
||||
pathBytes: pathBytes,
|
||||
pathLength: pathLength,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
setState(() {
|
||||
_clearPath = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Using ${path.hopCount} ${path.hopCount == 1 ? 'hop' : 'hops'} path'),
|
||||
@@ -499,10 +504,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
title: const Text('Force Flood Mode', style: TextStyle(fontSize: 14)),
|
||||
subtitle: const Text('Use routing toggle in app bar', style: TextStyle(fontSize: 11)),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_clearPath = true;
|
||||
});
|
||||
onTap: () async {
|
||||
await connector.setPathOverride(widget.contact, pathLen: -1);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Flood mode enabled. Toggle back via routing icon in app bar.'),
|
||||
@@ -573,9 +577,16 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
String _currentPathLabel(Contact contact) {
|
||||
// Check if user has set a path override
|
||||
if (contact.pathOverride != null) {
|
||||
if (contact.pathOverride! < 0) return 'Flood (forced)';
|
||||
if (contact.pathOverride == 0) return 'Direct (forced)';
|
||||
return '${contact.pathOverride} hops (forced)';
|
||||
}
|
||||
|
||||
// Use device's path
|
||||
if (contact.pathLength < 0) return 'Flood (auto)';
|
||||
if (contact.pathLength == 0) return 'Direct';
|
||||
if (contact.pathIdList.isNotEmpty) return contact.pathIdList;
|
||||
return '${contact.pathLength} hops';
|
||||
}
|
||||
|
||||
@@ -604,7 +615,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
'Location',
|
||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||
),
|
||||
_buildInfoRow('Public Key', contact.publicKeyHex.substring(0, 16) + '...'),
|
||||
_buildInfoRow('Public Key', '${contact.publicKeyHex.substring(0, 16)}...'),
|
||||
const Divider(),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -1125,12 +1136,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
void _retryMessage(Message message) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
// Retry with clearPath if the message has no path or pathLength is -1 (indicating flood was used)
|
||||
final shouldClearPath = message.pathLength != null && message.pathLength! < 0;
|
||||
// Retry using the contact's current path override setting
|
||||
connector.sendMessage(
|
||||
widget.contact,
|
||||
message.text,
|
||||
clearPath: shouldClearPath,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Retrying message')),
|
||||
@@ -1151,7 +1160,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
void _sendReaction(Message message, String emoji) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final reactionText = 'r:${message.messageId}:$emoji';
|
||||
// Send reaction with messageId if available, otherwise use lightweight format
|
||||
// Parser will extract reactionKey (timestamp_senderPrefix) for deduplication
|
||||
final messageId = message.messageId ??
|
||||
'${message.timestamp.millisecondsSinceEpoch}_${message.senderKeyHex.substring(0, 8)}';
|
||||
final reactionText = 'r:$messageId:$emoji';
|
||||
connector.sendMessage(widget.contact, reactionText);
|
||||
}
|
||||
}
|
||||
@@ -1176,7 +1189,6 @@ class _MessageBubble extends StatelessWidget {
|
||||
final gifId = _parseGifId(message.text);
|
||||
final poi = _parsePoiMessage(message.text);
|
||||
final isFailed = message.status == MessageStatus.failed;
|
||||
final attempts = message.retryCount + 1;
|
||||
final bubbleColor = isFailed
|
||||
? colorScheme.errorContainer
|
||||
: (isOutgoing ? colorScheme.primary : colorScheme.surfaceContainerHighest);
|
||||
@@ -1240,13 +1252,14 @@ class _MessageBubble extends StatelessWidget {
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
if (isOutgoing) ...[
|
||||
if (isOutgoing && message.retryCount > 0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Attempts: $attempts',
|
||||
'Retry ${message.retryCount}/4',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: metaColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -682,7 +682,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
return;
|
||||
}
|
||||
final exists = _groups.any((g) {
|
||||
if (isEditing && g.name == group!.name) return false;
|
||||
if (isEditing && g.name == group.name) return false;
|
||||
return g.name.toLowerCase() == name.toLowerCase();
|
||||
});
|
||||
if (exists) {
|
||||
@@ -693,7 +693,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
}
|
||||
setState(() {
|
||||
if (isEditing) {
|
||||
final index = _groups.indexWhere((g) => g.name == group!.name);
|
||||
final index = _groups.indexWhere((g) => g.name == group.name);
|
||||
if (index != -1) {
|
||||
_groups[index] = ContactGroup(
|
||||
name: name,
|
||||
|
||||
@@ -123,7 +123,7 @@ class _DeviceScreenState extends State<DeviceScreen>
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: colorScheme.surfaceVariant,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
|
||||
@@ -142,7 +142,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
|
||||
setState(() {
|
||||
_isDownloading = true;
|
||||
@@ -150,7 +152,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
_failedTiles = 0;
|
||||
});
|
||||
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
final result = await cacheService.downloadRegion(
|
||||
bounds: bounds,
|
||||
minZoom: _minZoom,
|
||||
@@ -198,7 +199,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
await cacheService.clearCache();
|
||||
|
||||
@@ -785,10 +785,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
final label = await _promptForLabel(context, defaultLabel);
|
||||
if (label == null) return;
|
||||
if (label == null || !mounted) return;
|
||||
|
||||
final markerText = _formatMarkerMessage(position, label, flags);
|
||||
if (!mounted) return;
|
||||
|
||||
await _showRecipientSheet(
|
||||
// ignore: use_build_context_synchronously
|
||||
context: context,
|
||||
connector: connector,
|
||||
markerText: markerText,
|
||||
|
||||
@@ -767,7 +767,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
color: colorScheme.surfaceVariant,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:provider/provider.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/radio_settings.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import 'app_settings_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
|
||||
@@ -63,6 +62,8 @@ class SettingsScreen extends StatelessWidget {
|
||||
_buildInfoRow('Node Name', connector.selfName!),
|
||||
if (connector.selfPublicKey != null)
|
||||
_buildInfoRow('Public Key', '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...'),
|
||||
_buildInfoRow('Contacts Count', '${connector.contacts.length}'),
|
||||
_buildInfoRow('Channel Count', '${connector.channels.length}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -619,7 +620,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<LoRaBandwidth>(
|
||||
value: _bandwidth,
|
||||
initialValue: _bandwidth,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Bandwidth',
|
||||
border: OutlineInputBorder(),
|
||||
@@ -636,7 +637,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<LoRaSpreadingFactor>(
|
||||
value: _spreadingFactor,
|
||||
initialValue: _spreadingFactor,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Spreading Factor',
|
||||
border: OutlineInputBorder(),
|
||||
@@ -653,7 +654,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<LoRaCodingRate>(
|
||||
value: _codingRate,
|
||||
initialValue: _codingRate,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Coding Rate',
|
||||
border: OutlineInputBorder(),
|
||||
|
||||
Reference in New Issue
Block a user