Refactor SAR marker handling and add template management

- Updated CompassSarList and DetailedCompassDialog to use marker.displayName instead of marker.type.displayName.
- Enhanced DrawingLayer and DrawingMarkersLayer to support simple mode for drawing visibility and interaction.
- Added toggle switches in DrawingToolbar for showing/hiding received drawings and SAR markers.
- Modified MapMarkers to utilize custom emojis and display names for markers.
- Introduced RecipientSelectorSheet for selecting message recipients with search functionality.
- Refactored SarUpdateSheet to use SAR templates instead of marker types, allowing for emoji and name customization.
- Created SarTemplateEditDialog for adding and editing SAR templates with color selection and preview.
This commit is contained in:
Janez T
2025-10-21 23:44:49 +02:00
parent e9d516b749
commit 021ce21cbe
60 changed files with 8736 additions and 1413 deletions

View File

@@ -24,6 +24,9 @@ class AppProvider with ChangeNotifier {
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
bool _isSimpleMode = false;
bool get isSimpleMode => _isSimpleMode;
AppProvider({
required this.connectionProvider,
required this.contactsProvider,
@@ -35,9 +38,33 @@ class AppProvider with ChangeNotifier {
_setupCallbacks();
_initializeTileCache();
_initializeLocationTracking();
_loadSimpleMode();
_isInitialized = true;
}
/// Load simple mode setting from shared preferences
Future<void> _loadSimpleMode() async {
try {
final prefs = await SharedPreferences.getInstance();
_isSimpleMode = prefs.getBool('simple_mode') ?? false;
notifyListeners();
} catch (e) {
debugPrint('Error loading simple mode setting: $e');
}
}
/// Toggle simple mode on/off
Future<void> toggleSimpleMode(bool enabled) async {
try {
_isSimpleMode = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('simple_mode', enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving simple mode setting: $e');
}
}
/// Initialize tile cache service
Future<void> _initializeTileCache() async {
try {
@@ -116,19 +143,17 @@ class AppProvider with ChangeNotifier {
final drawing = DrawingMessageParser.parseDrawingMessage(
message.text,
senderName: senderName,
messageId: message.id, // Pass message ID for navigation linking
);
if (drawing != null) {
debugPrint('🎨 [AppProvider] Drawing parsed successfully: ${drawing.type.name} from ${drawing.senderName ?? "unknown"}');
debugPrint(' Drawing linked to message ID: ${message.id}');
drawingProvider.addReceivedDrawing(drawing);
// Add informational message to chat
final drawingTypeStr = drawing.type.name.substring(0, 1).toUpperCase() +
drawing.type.name.substring(1);
final infoMessage = message.copyWith(
text: '📍 Received map drawing ($drawingTypeStr) from ${drawing.senderName ?? "unknown"}',
);
// Add the original drawing message to chat (not a modified info message)
// This allows users to click on the drawing message to navigate to it
messagesProvider.addMessage(
infoMessage,
message,
contactLookup: (name) => '',
);
} else {
@@ -238,12 +263,11 @@ class AppProvider with ChangeNotifier {
try {
// Initialize contacts provider with device public key to exclude self
// If already initialized (from early load), this will just filter out self-contact
// This must happen before getContacts to ensure proper filtering
if (!contactsProvider.isInitialized) {
await contactsProvider.initialize(
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
}
await contactsProvider.initialize(
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
// Note: Device clock is automatically synced during connection in MeshCoreBleService
// No need to sync it again here
@@ -257,9 +281,12 @@ class AppProvider with ChangeNotifier {
// Small delay to ensure contacts are fully loaded
await Future.delayed(const Duration(milliseconds: 500));
// Sync all channels to get channel names
debugPrint('📻 [AppProvider] Syncing channels...');
await connectionProvider.syncChannels();
// Sync channels to get channel names
// In simple mode: only sync first 5 channels for faster startup
// In normal mode: sync all channels (up to device max)
final channelsToSync = _isSimpleMode ? 5 : null;
debugPrint('📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...');
await connectionProvider.syncChannels(maxChannels: channelsToSync);
debugPrint('✅ [AppProvider] Channel sync complete');
// Configure the default public channel (channel 0)

View File

@@ -17,10 +17,49 @@ class ContactsProvider with ChangeNotifier {
bool get isInitialized => _isInitialized;
/// Initialize and load persisted contacts at app startup
/// This loads contacts without filtering, allowing offline viewing
Future<void> initializeEarly() async {
if (_isInitialized) return;
try {
debugPrint('📦 [ContactsProvider] Early loading persisted contacts (no filtering)...');
final storedContacts = await _storageService.loadContacts();
// Add stored contacts (excluding any with all-zeros public key)
const publicChannelKey = '0000000000000000000000000000000000000000000000000000000000000000';
for (final contact in storedContacts) {
// Skip any contacts with all-zeros public key (shouldn't happen, but safety check)
if (contact.publicKeyHex == publicChannelKey) {
continue;
}
_contacts[contact.publicKeyHex] = contact;
}
_isInitialized = true;
debugPrint('✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts');
// Ensure public channel exists after loading
_ensurePublicChannelExists();
notifyListeners();
} catch (e) {
debugPrint('❌ [ContactsProvider] Error in early initialization: $e');
_isInitialized = true; // Mark as initialized even on error
_ensurePublicChannelExists();
}
}
/// Initialize and load persisted contacts
/// [devicePublicKey] - device's own public key to exclude from loaded contacts
Future<void> initialize({Uint8List? devicePublicKey}) async {
if (_isInitialized) return;
if (_isInitialized) {
// If already initialized (from early load), just filter out self-contact
if (devicePublicKey != null) {
_removeSelfContact(devicePublicKey);
}
return;
}
try {
debugPrint('📦 [ContactsProvider] Loading persisted contacts...');
@@ -52,6 +91,18 @@ class ContactsProvider with ChangeNotifier {
}
}
/// Remove self-contact from loaded contacts (called after BLE connection established)
void _removeSelfContact(Uint8List devicePublicKey) {
final selfKeyHex = devicePublicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
if (_contacts.containsKey(selfKeyHex)) {
final selfContact = _contacts[selfKeyHex]!;
debugPrint('🗑️ [ContactsProvider] Removing self-contact: ${selfContact.advName}');
_contacts.remove(selfKeyHex);
_persistContacts();
notifyListeners();
}
}
/// Ensure public channel always exists in the list
void _ensurePublicChannelExists() {
// Public channel has all-zeros public key (32 bytes = 64 hex chars)

View File

@@ -20,6 +20,8 @@ class DrawingProvider with ChangeNotifier {
// Drawing state
DrawingMode _drawingMode = DrawingMode.none;
Color _selectedColor = DrawingColors.palette[0];
bool _showReceivedDrawings = true;
bool _showSarMarkers = true;
// Completed drawings
final List<MapDrawing> _drawings = [];
@@ -32,7 +34,11 @@ class DrawingProvider with ChangeNotifier {
// Getters
DrawingMode get drawingMode => _drawingMode;
Color get selectedColor => _selectedColor;
List<MapDrawing> get drawings => List.unmodifiable(_drawings);
bool get showReceivedDrawings => _showReceivedDrawings;
bool get showSarMarkers => _showSarMarkers;
List<MapDrawing> get drawings => _showReceivedDrawings
? List.unmodifiable(_drawings)
: List.unmodifiable(_drawings.where((d) => !d.isReceived).toList());
MapDrawing? get currentDrawing => _currentDrawing;
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
LatLng? get rectangleStartPoint => _rectangleStartPoint;
@@ -59,6 +65,18 @@ class DrawingProvider with ChangeNotifier {
notifyListeners();
}
/// Toggle visibility of received drawings
void toggleReceivedDrawings() {
_showReceivedDrawings = !_showReceivedDrawings;
notifyListeners();
}
/// Toggle visibility of SAR markers
void toggleSarMarkers() {
_showSarMarkers = !_showSarMarkers;
notifyListeners();
}
/// Start drawing a line
void startLine(LatLng point) {
if (_drawingMode != DrawingMode.line) return;

View File

@@ -31,6 +31,9 @@ class MessagesProvider with ChangeNotifier {
// Track which contact each sent message was sent to (for retry logic)
final Map<String, Contact> _messageContactMap = {};
// Navigation state for message highlighting/scrolling
String? _targetMessageId;
// Callback to connection provider for sending messages (set by AppProvider)
Future<bool> Function({
required Uint8List contactPublicKey,
@@ -70,11 +73,24 @@ class MessagesProvider with ChangeNotifier {
bool get isInitialized => _isInitialized;
String? get targetMessageId => _targetMessageId;
/// Set localizations for notifications
void setLocalizations(AppLocalizations localizations) {
_localizations = localizations;
}
/// Navigate to a specific message (scroll and highlight)
void navigateToMessage(String messageId) {
_targetMessageId = messageId;
notifyListeners();
}
/// Clear message navigation state
void clearMessageNavigation() {
_targetMessageId = null;
}
/// Get count of unread messages (excluding sent messages and system messages)
int get unreadCount => _messages
.where((m) =>
@@ -178,6 +194,9 @@ class MessagesProvider with ChangeNotifier {
_triggerSarNotification(finalMessage, marker);
}
}
} else if (!finalMessage.isSentMessage && !finalMessage.isSystemMessage) {
// Trigger notification for regular messages (not SAR, not sent by user, not system)
_triggerMessageNotification(finalMessage);
}
// Persist to storage asynchronously
@@ -296,6 +315,40 @@ class MessagesProvider with ChangeNotifier {
}
}
/// Trigger notification for regular message
Future<void> _triggerMessageNotification(Message message) async {
try {
// Get sender name from message
final senderName = message.senderName ?? message.senderKeyShort ?? 'Unknown';
// Determine if it's a channel message
final isChannelMessage = message.isChannelMessage;
// Get channel name if available
String? channelName;
if (isChannelMessage) {
// You could map channelIdx to channel name here if needed
// For now, use "Public" for channel 0
channelName = message.channelIdx == 0 ? 'Public' : 'Channel ${message.channelIdx}';
}
debugPrint('🔔 [MessagesProvider] Triggering message notification');
debugPrint(' Sender: $senderName');
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
debugPrint(' Message: ${message.text.substring(0, message.text.length > 50 ? 50 : message.text.length)}...');
await _notificationService.showMessageNotification(
senderName: senderName,
messageText: message.text,
isChannelMessage: isChannelMessage,
channelName: channelName,
localizations: _localizations,
);
} catch (e) {
debugPrint('❌ [MessagesProvider] Error triggering message notification: $e');
}
}
/// Persist messages to storage (async, non-blocking)
Future<void> _persistMessages() async {
try {
@@ -529,6 +582,11 @@ class MessagesProvider with ChangeNotifier {
if (sendingMessage.isSarMarker) {
final marker = sendingMessage.toSarMarker();
if (marker != null) {
debugPrint(' ✅ SAR Marker created:');
debugPrint(' marker.id: ${marker.id}');
debugPrint(' marker.notes: "${marker.notes}"');
debugPrint(' marker.type: ${marker.type}');
debugPrint(' marker.displayName: ${marker.displayName}');
_sarMarkers[marker.id] = marker;
}
}