From 6ee1c9ae79355e9fec5532abca5d41ee466c3984 Mon Sep 17 00:00:00 2001 From: Janez T Date: Wed, 15 Oct 2025 22:13:15 +0200 Subject: [PATCH] feat: Update color handling in map drawings and enhance message sync functionality --- lib/models/map_drawing.dart | 4 +- lib/providers/app_provider.dart | 9 +- lib/providers/drawing_provider.dart | 1 - lib/screens/home_screen.dart | 103 +++++++++++++-------- lib/screens/map_tab.dart | 131 ++++++++++++++++++--------- lib/screens/messages_tab.dart | 90 ++++++++++++------ lib/widgets/map/drawing_layer.dart | 16 +++- lib/widgets/map/drawing_toolbar.dart | 4 +- 8 files changed, 241 insertions(+), 117 deletions(-) diff --git a/lib/models/map_drawing.dart b/lib/models/map_drawing.dart index 211b1ba..7fff17e 100644 --- a/lib/models/map_drawing.dart +++ b/lib/models/map_drawing.dart @@ -100,7 +100,7 @@ class LineDrawing extends MapDrawing { return LineDrawing( id: json['id'] as String, - color: Color.fromARGB32(json['color'] as int), + color: Color(json['color'] as int), createdAt: DateTime.parse(json['createdAt'] as String), points: points, ); @@ -157,7 +157,7 @@ class RectangleDrawing extends MapDrawing { return RectangleDrawing( id: json['id'] as String, - color: Color.fromARGB32(json['color'] as int), + color: Color(json['color'] as int), createdAt: DateTime.parse(json['createdAt'] as String), topLeft: LatLng(topLeftJson['lat'] as double, topLeftJson['lon'] as double), bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double), diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 4fc2d91..e25327b 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -158,8 +158,13 @@ class AppProvider with ChangeNotifier { // Automatically login to all saved rooms await _autoLoginToRooms(); - // Note: Messages are synced automatically via PUSH_CODE_MSG_WAITING events - // No need to manually sync here - the BLE service handles this via callbacks + // FALLBACK: Sync messages once after connection to catch any missed push notifications + // This handles the case where messages arrived while the app was disconnected + debugPrint('🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)'); + final initialMessageCount = await connectionProvider.syncAllMessages(); + debugPrint('📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)'); + + // Note: Future messages are synced automatically via PUSH_CODE_MSG_WAITING events notifyListeners(); } catch (e) { diff --git a/lib/providers/drawing_provider.dart b/lib/providers/drawing_provider.dart index 897c6b8..b0ff331 100644 --- a/lib/providers/drawing_provider.dart +++ b/lib/providers/drawing_provider.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index e1b0e87..5a5ee13 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -32,6 +32,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; int _currentIndex = 0; + bool _isMapFullscreen = false; @override void initState() { @@ -40,6 +41,10 @@ class _HomeScreenState extends State with SingleTickerProviderStateM _tabController.addListener(() { setState(() { _currentIndex = _tabController.index; + // Exit fullscreen when switching away from map tab + if (_currentIndex != 2) { + _isMapFullscreen = false; + } }); }); } @@ -308,8 +313,11 @@ class _HomeScreenState extends State with SingleTickerProviderStateM @override Widget build(BuildContext context) { + // Determine if we should hide the UI (only in fullscreen on map tab) + final shouldHideUI = _isMapFullscreen && _currentIndex == 2; + return Scaffold( - appBar: AppBar( + appBar: shouldHideUI ? null : AppBar( title: _buildCompactStatusBar(), actions: [ PopupMenuButton( @@ -368,10 +376,16 @@ class _HomeScreenState extends State with SingleTickerProviderStateM children: [ MessagesTab(onNavigateToMap: () => _tabController.animateTo(2)), const ContactsTab(), - const MapTab(), + MapTab( + onFullscreenChanged: (isFullscreen) { + setState(() { + _isMapFullscreen = isFullscreen; + }); + }, + ), ], ), - bottomNavigationBar: Consumer2( + bottomNavigationBar: shouldHideUI ? null : Consumer2( builder: (context, messagesProvider, contactsProvider, child) { final unreadCount = messagesProvider.unreadCount; final newContactsCount = contactsProvider.newContactsCount; @@ -512,47 +526,58 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ), ); }, - child: Row( + child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ // RX indicator - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: provider.rxActivity - ? Colors.green - : Colors.grey.withOpacity(0.3), - ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: provider.rxActivity + ? Colors.green + : Colors.grey.withOpacity(0.3), + ), + ), + const SizedBox(width: 4), + Text( + 'RX:${provider.rxPacketCount}', + style: const TextStyle( + fontSize: 11, + color: Colors.grey, + ), + ), + ], ), - const SizedBox(width: 4), - Text( - 'RX:${provider.rxPacketCount}', - style: const TextStyle( - fontSize: 11, - color: Colors.grey, - ), - ), - const SizedBox(width: 12), + const SizedBox(height: 4), // TX indicator - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: provider.txActivity - ? Colors.blue - : Colors.grey.withOpacity(0.3), - ), - ), - const SizedBox(width: 4), - Text( - 'TX:${provider.txPacketCount}', - style: const TextStyle( - fontSize: 11, - color: Colors.grey, - ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: provider.txActivity + ? Colors.blue + : Colors.grey.withOpacity(0.3), + ), + ), + const SizedBox(width: 4), + Text( + 'TX:${provider.txPacketCount}', + style: const TextStyle( + fontSize: 11, + color: Colors.grey, + ), + ), + ], ), ], ), diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index 2a849b2..87a6b07 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -31,7 +31,12 @@ import '../widgets/messages/sar_update_sheet.dart'; import 'map_management_screen.dart'; class MapTab extends StatefulWidget { - const MapTab({super.key}); + final Function(bool)? onFullscreenChanged; + + const MapTab({ + super.key, + this.onFullscreenChanged, + }); @override State createState() => _MapTabState(); @@ -49,6 +54,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { bool _rotateMarkerWithHeading = false; // Toggle for rotation bool _showLegend = false; bool _showMapDebugInfo = false; // Toggle for debug info + bool _isFullscreen = false; // Toggle for fullscreen mode double _gpsUpdateDistance = 3.0; // meters bool _backgroundTrackingEnabled = false; // Toggle for background tracking StreamSubscription? _compassStreamSubscription; @@ -180,6 +186,12 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { _showLegend = prefs.getBool('map_show_legend') ?? false; _rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false; _showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false; + _isFullscreen = prefs.getBool('map_fullscreen') ?? false; + + // Notify parent about initial fullscreen state + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onFullscreenChanged?.call(_isFullscreen); + }); _gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 3.0; _backgroundTrackingEnabled = prefs.getBool('background_tracking_enabled') ?? false; @@ -202,6 +214,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { await prefs.setBool('map_show_legend', _showLegend); await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading); await prefs.setBool('map_show_debug_info', _showMapDebugInfo); + await prefs.setBool('map_fullscreen', _isFullscreen); await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance); await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled); await prefs.setInt('map_last_layer', MapLayer.allLayers.indexOf(_currentLayer)); @@ -499,6 +512,23 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { _saveSettings(); }, ), + const Divider(), + // Fullscreen mode toggle + SwitchListTile( + secondary: const Icon(Icons.fullscreen), + title: const Text('Fullscreen Mode'), + subtitle: const Text('Hide all UI controls for full map view'), + value: _isFullscreen, + onChanged: (value) { + setState(() { + _isFullscreen = value; + }); + setModalState(() {}); + _saveSettings(); + // Notify parent about fullscreen change + widget.onFullscreenChanged?.call(value); + }, + ), ], ), ), @@ -1060,9 +1090,10 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ), ], ), - // Drawing markers layer (delete buttons on drawings) + // Drawing markers layer (delete buttons on drawings, only shown when in drawing mode) DrawingMarkersLayer( drawings: drawingProvider.drawings, + showDeleteButtons: drawingProvider.isDrawing, onDeleteDrawing: (drawingId) { drawingProvider.removeDrawing(drawingId); }, @@ -1083,26 +1114,46 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ], ), ), - // Compass widget - top right (always visible) - Positioned( - top: 16, - right: 16, - child: GestureDetector( - onTap: () => _showDetailedCompass( - context, - contactsProvider.contactsWithLocation, - messagesProvider.sarMarkers, - ), - child: CompassWidget( - heading: _currentHeading ?? 0, - hasHeading: _currentHeading != null, + // Exit fullscreen button - top left (only shown in fullscreen mode) + if (_isFullscreen) + Positioned( + top: 16, + left: 16, + child: FloatingActionButton.small( + heroTag: 'exit_fullscreen', + onPressed: () { + setState(() { + _isFullscreen = false; + }); + _saveSettings(); + // Notify parent about fullscreen change + widget.onFullscreenChanged?.call(false); + }, + backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9), + child: const Icon(Icons.fullscreen_exit), ), ), - ), - // Map legend overlay - if (_showLegend) + // Compass widget - top right (hidden in fullscreen mode) + if (!_isFullscreen) Positioned( - top: 80, // Position below compass (which is always visible now) + top: 16, + right: 16, + child: GestureDetector( + onTap: () => _showDetailedCompass( + context, + contactsProvider.contactsWithLocation, + messagesProvider.sarMarkers, + ), + child: CompassWidget( + heading: _currentHeading ?? 0, + hasHeading: _currentHeading != null, + ), + ), + ), + // Map legend overlay (hidden in fullscreen mode) + if (_showLegend && !_isFullscreen) + Positioned( + top: 80, // Position below compass left: 16, child: MapLegend( teamMemberCount: contactsWithLocation.length, @@ -1112,19 +1163,17 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { objectCount: messagesProvider.objectMarkers.length, ), ), - // Drawing toolbar - bottom left - Positioned( - bottom: 16, - left: 16, - child: const DrawingToolbar(), - ), - // Map controls - right side - Positioned( - bottom: 16, - right: 16, - child: Column( - children: [ - FloatingActionButton.small( + // Map controls - right side (hidden in fullscreen mode) + if (!_isFullscreen) + Positioned( + bottom: 16, + right: 16, + child: Column( + children: [ + // Drawing toolbar + const DrawingToolbar(), + const SizedBox(height: 8), + FloatingActionButton.small( heroTag: 'center_map', onPressed: !_isMapReady ? null : () async { // Force update GPS location and jump to it @@ -1162,16 +1211,16 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { child: const Icon(Icons.layers), ), const SizedBox(height: 8), - FloatingActionButton.small( - heroTag: 'options_menu', - onPressed: () => _showOptionsMenu(context), - child: const Icon(Icons.more_vert), - ), - ], + FloatingActionButton.small( + heroTag: 'options_menu', + onPressed: () => _showOptionsMenu(context), + child: const Icon(Icons.more_vert), + ), + ], + ), ), - ), - // Map debug info - bottom left - if (_showMapDebugInfo && _isMapReady) + // Map debug info - bottom left (hidden in fullscreen mode) + if (_showMapDebugInfo && _isMapReady && !_isFullscreen) Positioned( bottom: 16, left: 16, diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index 7e91ccd..902b1c1 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -227,7 +227,34 @@ class _MessagesTabState extends State { } - // Removed _handleRefresh() - messages are synced automatically via PUSH_CODE_MSG_WAITING events + /// Handle pull-to-refresh for manual message sync + /// This is a FALLBACK mechanism - messages are normally synced automatically via PUSH_CODE_MSG_WAITING + Future _handleRefresh() async { + final connectionProvider = context.read(); + + if (!connectionProvider.deviceInfo.isConnected) { + if (!mounted) return; + ToastLogger.warning(context, 'Not connected - cannot sync messages'); + return; + } + + try { + print('🔄 [MessagesTab] Manual refresh triggered - syncing messages'); + final messageCount = await connectionProvider.syncAllMessages(); + print('✅ [MessagesTab] Synced $messageCount message(s)'); + + if (!mounted) return; + if (messageCount > 0) { + ToastLogger.success(context, 'Synced $messageCount message(s)'); + } else { + ToastLogger.info(context, 'No new messages'); + } + } catch (e) { + print('❌ [MessagesTab] Sync error: $e'); + if (!mounted) return; + ToastLogger.error(context, 'Sync failed: $e'); + } + } List _getFilteredMessages(MessagesProvider messagesProvider) { // Show ALL messages regardless of recipient selection @@ -243,33 +270,43 @@ class _MessagesTabState extends State { return Column( children: [ - // Messages list + // Messages list with pull-to-refresh Expanded( - child: messages.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.message_outlined, - size: 64, - color: Theme.of(context).disabledColor, + child: RefreshIndicator( + onRefresh: _handleRefresh, + child: messages.isEmpty + ? LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.message_outlined, + size: 64, + color: Theme.of(context).disabledColor, + ), + const SizedBox(height: 16), + Text( + 'No messages yet', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + 'Pull down to sync messages', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ], + ), + ), ), - const SizedBox(height: 16), - Text( - 'No messages yet', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text( - 'Connect to a device to start receiving messages', - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - ], - ), - ) - : ListView.builder( + ), + ) + : ListView.builder( reverse: true, padding: const EdgeInsets.all(8), itemCount: messages.length, @@ -298,6 +335,7 @@ class _MessagesTabState extends State { ); }, ), + ), ), // Message input area diff --git a/lib/widgets/map/drawing_layer.dart b/lib/widgets/map/drawing_layer.dart index 3bdccf5..67b8e45 100644 --- a/lib/widgets/map/drawing_layer.dart +++ b/lib/widgets/map/drawing_layer.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; import '../../models/map_drawing.dart'; /// Widget that renders map drawings as polylines @@ -37,9 +38,9 @@ class DrawingLayer extends StatelessWidget { return Polyline( points: points, - color: drawing.color.withOpacity(opacity), + color: drawing.color.withValues(alpha: opacity), strokeWidth: 4.0, - borderColor: Colors.white.withOpacity(opacity * 0.8), + borderColor: Colors.white.withValues(alpha: opacity * 0.8), borderStrokeWidth: 1.0, ); } @@ -59,15 +60,22 @@ class DrawingLayer extends StatelessWidget { class DrawingMarkersLayer extends StatelessWidget { final List drawings; final Function(String drawingId)? onDeleteDrawing; + final bool showDeleteButtons; const DrawingMarkersLayer({ super.key, required this.drawings, this.onDeleteDrawing, + this.showDeleteButtons = false, }); @override Widget build(BuildContext context) { + // Only show delete buttons when showDeleteButtons is true + if (!showDeleteButtons) { + return const SizedBox.shrink(); + } + final List markers = []; // Add delete markers for each drawing (at the center point) @@ -87,12 +95,12 @@ class DrawingMarkersLayer extends StatelessWidget { }, child: Container( decoration: BoxDecoration( - color: drawing.color.withOpacity(0.9), + color: drawing.color.withValues(alpha: 0.9), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 4, offset: const Offset(0, 2), ), diff --git a/lib/widgets/map/drawing_toolbar.dart b/lib/widgets/map/drawing_toolbar.dart index 89effe2..4e514c2 100644 --- a/lib/widgets/map/drawing_toolbar.dart +++ b/lib/widgets/map/drawing_toolbar.dart @@ -28,7 +28,7 @@ class DrawingToolbar extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 8, offset: const Offset(0, 2), ), @@ -83,7 +83,7 @@ class DrawingToolbar extends StatelessWidget { boxShadow: [ if (isSelected) BoxShadow( - color: color.withOpacity(0.5), + color: color.withValues(alpha: 0.5), blurRadius: 8, spreadRadius: 2, ),