Refactor contacts management and improve path handling

- Updated ContactsProvider to initialize with device public key for filtering out self contacts.
- Added methods to check if a contact has a learned routing path and to get path quality indicators.
- Enhanced AppProvider to initialize ContactsProvider with device public key.
- Modified ConnectionProvider to log path status when sending messages.
- Created ContactStorageService for persisting contacts to local storage.
- Removed import/export functionality from MapManagementScreen.
- Updated UI components in DirectMessageSheet and SarUpdateSheet to use theme colors.
- Removed file_picker dependency from pubspec.yaml and generated plugin registrant.
This commit is contained in:
Janez T
2025-10-15 19:20:46 +02:00
parent 3f03b2e789
commit 91d9326804
17 changed files with 501 additions and 255 deletions

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:file_picker/file_picker.dart';
import 'package:share_plus/share_plus.dart';
import '../services/tile_cache_service.dart';
import '../services/validation_service.dart';
import '../models/map_layer.dart';
@@ -235,77 +233,6 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
}
}
Future<void> _exportMaps() async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
final exportPath = await widget.tileCacheService.exportCache();
if (!mounted) return;
setState(() => _isLoading = false);
if (mounted) {
await Share.shareXFiles(
[XFile(exportPath)],
subject: 'MeshCore SAR Maps Export',
text: 'Offline maps export from MeshCore SAR',
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Maps exported to: $exportPath'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 5),
),
);
}
} catch (e) {
if (!mounted) return;
setState(() => _isLoading = false);
_showError('Export failed: $e');
}
}
Future<void> _importMaps() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['fmtc'],
allowMultiple: false,
);
if (result == null || result.files.isEmpty) {
return;
}
if (!mounted) return;
setState(() => _isLoading = true);
final filePath = result.files.first.path;
if (filePath == null) {
throw Exception('Invalid file path');
}
await widget.tileCacheService.importCache(filePath);
if (!mounted) return;
setState(() => _isLoading = false);
await _loadCacheStats();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Maps imported successfully!'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (!mounted) return;
setState(() => _isLoading = false);
_showError('Import failed: $e');
}
}
Future<void> _clearCache() async {
final confirmed = await showDialog<bool>(
context: context,
@@ -385,7 +312,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
_buildDownloadCard(),
const SizedBox(height: 16),
// Import/Export/Clear
// Clear Cache
_buildActionsCard(),
],
),
@@ -708,33 +635,11 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Map Actions',
'Cache Management',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
// Export Button
ElevatedButton.icon(
onPressed: _isDownloading ? null : _exportMaps,
icon: const Icon(Icons.upload),
label: const Text('Export Maps'),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
),
const SizedBox(height: 8),
// Import Button
ElevatedButton.icon(
onPressed: _isDownloading ? null : _importMaps,
icon: const Icon(Icons.download),
label: const Text('Import Maps'),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
),
const SizedBox(height: 8),
// Clear Cache Button
OutlinedButton.icon(
onPressed: _isDownloading ? null : _clearCache,

View File

@@ -26,6 +26,15 @@ class _MessagesTabState extends State<MessagesTab> {
int _characterCount = 0;
static const int _maxCharacters = 160;
/// Helper method to compare two public keys for equality
bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
if (key1.length != key2.length) return false;
for (int i = 0; i < key1.length; i++) {
if (key1[i] != key2[i]) return false;
}
return true;
}
@override
void initState() {
super.initState();
@@ -215,11 +224,19 @@ class _MessagesTabState extends State<MessagesTab> {
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Look up the room contact for path logging
final contactsProvider = context.read<ContactsProvider>();
final roomContact = contactsProvider.contacts.where((c) {
return c.publicKey.length >= roomPublicKey!.length &&
_publicKeysMatch(c.publicKey, roomPublicKey!);
}).firstOrNull;
// Send SAR message to selected room (persisted and immutable)
final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: roomPublicKey!,
text: fullMessage,
messageId: messageId, // Pass message ID so it can be tracked
contact: roomContact, // Include contact for path status logging
);
if (!sentSuccessfully) {
@@ -407,6 +424,15 @@ class _MessageBubble extends StatelessWidget {
this.onTap,
});
/// Helper method to compare two public keys for equality
bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
if (key1.length != key2.length) return false;
for (int i = 0; i < key1.length; i++) {
if (key1[i] != key2[i]) return false;
}
return true;
}
Future<void> _retryFailedMessage(BuildContext context, Message failedMessage) async {
final connectionProvider = context.read<ConnectionProvider>();
final messagesProvider = context.read<MessagesProvider>();
@@ -448,11 +474,19 @@ class _MessageBubble extends StatelessWidget {
return;
}
// Look up the room contact for path logging
final contactsProvider = context.read<ContactsProvider>();
final roomContact = contactsProvider.contacts.where((c) {
return c.publicKey.length >= failedMessage.recipientPublicKey!.length &&
_publicKeysMatch(c.publicKey, failedMessage.recipientPublicKey!);
}).firstOrNull;
// Resend to the same room
final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: failedMessage.recipientPublicKey!,
text: failedMessage.text,
messageId: retryMessageId,
contact: roomContact, // Include contact for path status logging
);
if (!sentSuccessfully) {