mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Keep contact GPS location saved
This commit is contained in:
@@ -5,22 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000209">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.001195">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.489082">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.410921">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="115.233655">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="97.264031">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="296.0895">
|
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="11727.562963">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,19 @@ class ContactRouteCodec {
|
|||||||
static const int maxPathBytes = 64;
|
static const int maxPathBytes = 64;
|
||||||
static const int _unknownDescriptor = 0xFF;
|
static const int _unknownDescriptor = 0xFF;
|
||||||
|
|
||||||
static ParsedContactRoute parse(String input) {
|
static ParsedContactRoute parse(String input, {int? expectedHashSize}) {
|
||||||
final normalized = input.trim().toUpperCase();
|
final normalized = input.trim().toUpperCase();
|
||||||
if (normalized.isEmpty) {
|
if (normalized.isEmpty) {
|
||||||
throw const ContactRouteFormatException('Route cannot be empty.');
|
throw const ContactRouteFormatException('Route cannot be empty.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expectedHashSize != null &&
|
||||||
|
(expectedHashSize < 1 || expectedHashSize > maxHashSize)) {
|
||||||
|
throw const ContactRouteFormatException(
|
||||||
|
'Hash size must be 1, 2, or 3 bytes.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final hopTokens = normalized
|
final hopTokens = normalized
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((token) => token.trim())
|
.map((token) => token.trim())
|
||||||
@@ -80,6 +87,13 @@ class ContactRouteCodec {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expectedHashSize != null && currentHashSize != expectedHashSize) {
|
||||||
|
throw ContactRouteFormatException(
|
||||||
|
'Hop "$token" must be $expectedHashSize '
|
||||||
|
'byte${expectedHashSize == 1 ? '' : 's'}.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
hashSize ??= currentHashSize;
|
hashSize ??= currentHashSize;
|
||||||
if (hashSize != currentHashSize) {
|
if (hashSize != currentHashSize) {
|
||||||
throw const ContactRouteFormatException(
|
throw const ContactRouteFormatException(
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import '../services/update_checker_service.dart';
|
|||||||
import '../services/voice_codec_service.dart';
|
import '../services/voice_codec_service.dart';
|
||||||
import '../services/voice_bitrate_preferences.dart';
|
import '../services/voice_bitrate_preferences.dart';
|
||||||
import '../services/image_preferences.dart';
|
import '../services/image_preferences.dart';
|
||||||
|
import '../services/route_hash_preferences.dart';
|
||||||
import '../services/image_codec_service.dart';
|
import '../services/image_codec_service.dart';
|
||||||
import '../utils/sample_data_generator.dart';
|
import '../utils/sample_data_generator.dart';
|
||||||
import '../utils/image_message_parser.dart';
|
import '../utils/image_message_parser.dart';
|
||||||
@@ -56,6 +57,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
bool _showRxTxIndicators = true;
|
bool _showRxTxIndicators = true;
|
||||||
bool _isCheckingForUpdates = false;
|
bool _isCheckingForUpdates = false;
|
||||||
int _voiceBitrate = VoiceBitratePreferences.defaultBitrate;
|
int _voiceBitrate = VoiceBitratePreferences.defaultBitrate;
|
||||||
|
int _routeHashSize = RouteHashPreferences.defaultHashSize;
|
||||||
int _imageMaxSize = ImagePreferences.defaultMaxSize;
|
int _imageMaxSize = ImagePreferences.defaultMaxSize;
|
||||||
int _imageCompression = ImagePreferences.defaultQuality;
|
int _imageCompression = ImagePreferences.defaultQuality;
|
||||||
bool _imageGrayscale = ImagePreferences.defaultGrayscale;
|
bool _imageGrayscale = ImagePreferences.defaultGrayscale;
|
||||||
@@ -77,6 +79,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_initializeLocationService();
|
_initializeLocationService();
|
||||||
_loadRxTxPreference();
|
_loadRxTxPreference();
|
||||||
_loadVoiceBitratePreference();
|
_loadVoiceBitratePreference();
|
||||||
|
_loadRouteHashSizePreference();
|
||||||
_loadImagePreferences();
|
_loadImagePreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +135,22 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
return '$bitrate bps';
|
return '$bitrate bps';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRouteHashSizePreference() async {
|
||||||
|
final value = await RouteHashPreferences.getHashSize();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_routeHashSize = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveRouteHashSizePreference(int value) async {
|
||||||
|
await RouteHashPreferences.setHashSize(value);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_routeHashSize = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadImagePreferences() async {
|
Future<void> _loadImagePreferences() async {
|
||||||
final size = await ImagePreferences.getMaxSize();
|
final size = await ImagePreferences.getMaxSize();
|
||||||
final compression = await ImagePreferences.getCompression();
|
final compression = await ImagePreferences.getCompression();
|
||||||
@@ -658,6 +677,46 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showRouteHashSizeDialog() async {
|
||||||
|
final selected = await showDialog<int>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SimpleDialog(
|
||||||
|
title: const Text('Route path byte size'),
|
||||||
|
children: [
|
||||||
|
for (final value in [1, 2, 3])
|
||||||
|
SimpleDialogOption(
|
||||||
|
onPressed: () => Navigator.of(context).pop(value),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('$value byte${value == 1 ? '' : 's'}'),
|
||||||
|
Text(
|
||||||
|
'Use ${value * 2} hex characters per hop',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_routeHashSize == value)
|
||||||
|
Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selected == null) return;
|
||||||
|
await _saveRouteHashSizePreference(selected);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -757,6 +816,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => _showLanguageDialog(),
|
onTap: () => _showLanguageDialog(),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.alt_route),
|
||||||
|
title: const Text('Route path byte size'),
|
||||||
|
subtitle: Text(
|
||||||
|
'$_routeHashSize byte${_routeHashSize == 1 ? '' : 's'} for manual contact routes',
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: _showRouteHashSizeDialog,
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep, color: Colors.red),
|
leading: const Icon(Icons.delete_sweep, color: Colors.red),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
|||||||
26
lib/services/route_hash_preferences.dart
Normal file
26
lib/services/route_hash_preferences.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class RouteHashPreferences {
|
||||||
|
static const String _hashSizeKey = 'route_hash_size';
|
||||||
|
static const int defaultHashSize = 1;
|
||||||
|
|
||||||
|
static Future<int> getHashSize() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final value = prefs.getInt(_hashSizeKey) ?? defaultHashSize;
|
||||||
|
return _normalize(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> setHashSize(int value) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setInt(_hashSizeKey, _normalize(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int normalizeSync(int value) => _normalize(value);
|
||||||
|
|
||||||
|
static int _normalize(int value) {
|
||||||
|
if (value < 1 || value > 3) {
|
||||||
|
return defaultHashSize;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../models/contact.dart';
|
import '../../models/contact.dart';
|
||||||
|
import '../../services/route_hash_preferences.dart';
|
||||||
|
|
||||||
class ContactRouteDialog extends StatefulWidget {
|
class ContactRouteDialog extends StatefulWidget {
|
||||||
final Contact contact;
|
final Contact contact;
|
||||||
@@ -17,11 +18,20 @@ class ContactRouteDialog extends StatefulWidget {
|
|||||||
required Contact contact,
|
required Contact contact,
|
||||||
required List<Contact> availableContacts,
|
required List<Contact> availableContacts,
|
||||||
}) {
|
}) {
|
||||||
return showDialog<ParsedContactRoute>(
|
return showModalBottomSheet<ParsedContactRoute>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ContactRouteDialog(
|
isScrollControlled: true,
|
||||||
contact: contact,
|
showDragHandle: true,
|
||||||
availableContacts: availableContacts,
|
builder: (context) => SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
),
|
||||||
|
child: ContactRouteDialog(
|
||||||
|
contact: contact,
|
||||||
|
availableContacts: availableContacts,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -32,20 +42,18 @@ class ContactRouteDialog extends StatefulWidget {
|
|||||||
|
|
||||||
class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
||||||
late final TextEditingController _controller;
|
late final TextEditingController _controller;
|
||||||
late int _selectedHashSize;
|
int _selectedHashSize = RouteHashPreferences.defaultHashSize;
|
||||||
ParsedContactRoute? _parsedRoute;
|
ParsedContactRoute? _parsedRoute;
|
||||||
String? _errorText;
|
String? _errorText;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedHashSize = widget.contact.routeHasPath
|
|
||||||
? widget.contact.routeHashSize
|
|
||||||
: 1;
|
|
||||||
_controller = TextEditingController(
|
_controller = TextEditingController(
|
||||||
text: widget.contact.routeCanonicalText,
|
text: widget.contact.routeCanonicalText,
|
||||||
);
|
);
|
||||||
_controller.addListener(_reparse);
|
_controller.addListener(_reparse);
|
||||||
|
_loadHashSizePreference();
|
||||||
_reparse();
|
_reparse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +76,12 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final parsed = ContactRouteCodec.parse(input);
|
final parsed = ContactRouteCodec.parse(
|
||||||
|
input,
|
||||||
|
expectedHashSize: _selectedHashSize,
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_parsedRoute = parsed;
|
_parsedRoute = parsed;
|
||||||
_selectedHashSize = parsed.hashSize;
|
|
||||||
_errorText = null;
|
_errorText = null;
|
||||||
});
|
});
|
||||||
} on ContactRouteFormatException catch (error) {
|
} on ContactRouteFormatException catch (error) {
|
||||||
@@ -82,6 +92,15 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadHashSizePreference() async {
|
||||||
|
final hashSize = await RouteHashPreferences.getHashSize();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_selectedHashSize = hashSize;
|
||||||
|
});
|
||||||
|
_reparse();
|
||||||
|
}
|
||||||
|
|
||||||
String _tokenFor(Contact contact, int hashSize) {
|
String _tokenFor(Contact contact, int hashSize) {
|
||||||
final hex = contact.publicKeyHex.toUpperCase();
|
final hex = contact.publicKeyHex.toUpperCase();
|
||||||
final length = hashSize * 2;
|
final length = hashSize * 2;
|
||||||
@@ -108,123 +127,103 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
|||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) => a.displayName.compareTo(b.displayName));
|
..sort((a, b) => a.displayName.compareTo(b.displayName));
|
||||||
|
|
||||||
return AlertDialog(
|
return FractionallySizedBox(
|
||||||
title: Text('Set Route for ${widget.contact.displayName}'),
|
heightFactor: 0.85,
|
||||||
content: SizedBox(
|
child: Padding(
|
||||||
width: 560,
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
child: SingleChildScrollView(
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text(
|
||||||
children: [
|
'Set Route for ${widget.contact.displayName}',
|
||||||
Text(
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
'Path hash size',
|
),
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Route',
|
||||||
|
hintText: _selectedHashSize == 1
|
||||||
|
? 'AA,BB,CC'
|
||||||
|
: _selectedHashSize == 2
|
||||||
|
? 'AABB,CCDD'
|
||||||
|
: 'AABBCC,DDEEFF',
|
||||||
|
helperText:
|
||||||
|
'Use comma-separated hops. Path byte size comes from global Settings. Colon form like AA:BB is also accepted.',
|
||||||
|
errorText: _errorText,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Wrap(
|
const SizedBox(height: 12),
|
||||||
spacing: 8,
|
Text(
|
||||||
children: [1, 2, 3]
|
_parsedRoute == null
|
||||||
.map(
|
? 'Preview: enter a route to validate it.'
|
||||||
(hashSize) => ChoiceChip(
|
: 'Preview: ${_parsedRoute!.summary} • ${_parsedRoute!.byteLength} bytes • descriptor 0x${_parsedRoute!.encodedPathLen.toRadixString(16).padLeft(2, '0').toUpperCase()}',
|
||||||
label: Text(
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
'$hashSize byte${hashSize == 1 ? '' : 's'}',
|
),
|
||||||
),
|
if (_parsedRoute != null) ...[
|
||||||
selected: _selectedHashSize == hashSize,
|
const SizedBox(height: 4),
|
||||||
onSelected: (_) {
|
SelectableText(
|
||||||
setState(() {
|
_parsedRoute!.canonicalText,
|
||||||
_selectedHashSize = hashSize;
|
style: Theme.of(
|
||||||
});
|
context,
|
||||||
},
|
).textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Pick hops from contacts',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (routeCandidates.isEmpty)
|
||||||
|
const Text(
|
||||||
|
'No repeater or room contacts are available for route building.',
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: routeCandidates.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final candidate = routeCandidates[index];
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(candidate.displayName),
|
||||||
|
subtitle: Text(
|
||||||
|
'1B ${_tokenFor(candidate, 1)} • 2B ${_tokenFor(candidate, 2)} • 3B ${_tokenFor(candidate, 3)}',
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
),
|
),
|
||||||
)
|
trailing: TextButton(
|
||||||
.toList(),
|
onPressed: () => _appendHop(candidate),
|
||||||
),
|
child: Text(
|
||||||
const SizedBox(height: 16),
|
'Use ${_tokenFor(candidate, _selectedHashSize)}',
|
||||||
TextField(
|
),
|
||||||
controller: _controller,
|
),
|
||||||
textCapitalization: TextCapitalization.characters,
|
);
|
||||||
decoration: InputDecoration(
|
},
|
||||||
labelText: 'Route',
|
|
||||||
hintText: _selectedHashSize == 1
|
|
||||||
? 'AA,BB,CC'
|
|
||||||
: _selectedHashSize == 2
|
|
||||||
? 'AABB,CCDD'
|
|
||||||
: 'AABBCC,DDEEFF',
|
|
||||||
helperText:
|
|
||||||
'Use comma-separated hops. Colon form like AA:BB is also accepted.',
|
|
||||||
errorText: _errorText,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Row(
|
||||||
_parsedRoute == null
|
children: [
|
||||||
? 'Preview: enter a route to validate it.'
|
TextButton(
|
||||||
: 'Preview: ${_parsedRoute!.summary} • ${_parsedRoute!.byteLength} bytes • descriptor 0x${_parsedRoute!.encodedPathLen.toRadixString(16).padLeft(2, '0').toUpperCase()}',
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
if (_parsedRoute != null) ...[
|
const Spacer(),
|
||||||
const SizedBox(height: 4),
|
FilledButton(
|
||||||
SelectableText(
|
onPressed: _parsedRoute == null
|
||||||
_parsedRoute!.canonicalText,
|
? null
|
||||||
style: Theme.of(
|
: () => Navigator.of(context).pop(_parsedRoute),
|
||||||
context,
|
child: const Text('Set Route'),
|
||||||
).textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
Text(
|
],
|
||||||
'Pick hops from contacts',
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
if (routeCandidates.isEmpty)
|
|
||||||
const Text(
|
|
||||||
'No repeater or room contacts are available for route building.',
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 240),
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: routeCandidates.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final candidate = routeCandidates[index];
|
|
||||||
return ListTile(
|
|
||||||
dense: true,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: Text(candidate.displayName),
|
|
||||||
subtitle: Text(
|
|
||||||
'1B ${_tokenFor(candidate, 1)} • 2B ${_tokenFor(candidate, 2)} • 3B ${_tokenFor(candidate, 3)}',
|
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
|
||||||
),
|
|
||||||
trailing: TextButton(
|
|
||||||
onPressed: () => _appendHop(candidate),
|
|
||||||
child: Text(
|
|
||||||
'Use ${_tokenFor(candidate, _selectedHashSize)}',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: _parsedRoute == null
|
|
||||||
? null
|
|
||||||
: () => Navigator.of(context).pop(_parsedRoute),
|
|
||||||
child: const Text('Set Route'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects routes that do not match the configured hash size', () {
|
||||||
|
expect(
|
||||||
|
() => ContactRouteCodec.parse('AABB,CCDD', expectedHashSize: 1),
|
||||||
|
throwsA(isA<ContactRouteFormatException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects invalid tokens', () {
|
test('rejects invalid tokens', () {
|
||||||
expect(
|
expect(
|
||||||
() => ContactRouteCodec.parse('AA,XYZ'),
|
() => ContactRouteCodec.parse('AA,XYZ'),
|
||||||
|
|||||||
Reference in New Issue
Block a user