feat: Update color handling in map drawings and enhance message sync functionality

This commit is contained in:
Janez T
2025-10-15 22:13:15 +02:00
parent 3651f53ef2
commit 6ee1c9ae79
8 changed files with 241 additions and 117 deletions

View File

@@ -100,7 +100,7 @@ class LineDrawing extends MapDrawing {
return LineDrawing( return LineDrawing(
id: json['id'] as String, 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), createdAt: DateTime.parse(json['createdAt'] as String),
points: points, points: points,
); );
@@ -157,7 +157,7 @@ class RectangleDrawing extends MapDrawing {
return RectangleDrawing( return RectangleDrawing(
id: json['id'] as String, 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), createdAt: DateTime.parse(json['createdAt'] as String),
topLeft: LatLng(topLeftJson['lat'] as double, topLeftJson['lon'] as double), topLeft: LatLng(topLeftJson['lat'] as double, topLeftJson['lon'] as double),
bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double), bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double),

View File

@@ -158,8 +158,13 @@ class AppProvider with ChangeNotifier {
// Automatically login to all saved rooms // Automatically login to all saved rooms
await _autoLoginToRooms(); await _autoLoginToRooms();
// Note: Messages are synced automatically via PUSH_CODE_MSG_WAITING events // FALLBACK: Sync messages once after connection to catch any missed push notifications
// No need to manually sync here - the BLE service handles this via callbacks // 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(); notifyListeners();
} catch (e) { } catch (e) {

View File

@@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';

View File

@@ -32,6 +32,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin { class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
late TabController _tabController; late TabController _tabController;
int _currentIndex = 0; int _currentIndex = 0;
bool _isMapFullscreen = false;
@override @override
void initState() { void initState() {
@@ -40,6 +41,10 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
_tabController.addListener(() { _tabController.addListener(() {
setState(() { setState(() {
_currentIndex = _tabController.index; _currentIndex = _tabController.index;
// Exit fullscreen when switching away from map tab
if (_currentIndex != 2) {
_isMapFullscreen = false;
}
}); });
}); });
} }
@@ -308,8 +313,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Determine if we should hide the UI (only in fullscreen on map tab)
final shouldHideUI = _isMapFullscreen && _currentIndex == 2;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: shouldHideUI ? null : AppBar(
title: _buildCompactStatusBar(), title: _buildCompactStatusBar(),
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
@@ -368,10 +376,16 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
children: [ children: [
MessagesTab(onNavigateToMap: () => _tabController.animateTo(2)), MessagesTab(onNavigateToMap: () => _tabController.animateTo(2)),
const ContactsTab(), const ContactsTab(),
const MapTab(), MapTab(
onFullscreenChanged: (isFullscreen) {
setState(() {
_isMapFullscreen = isFullscreen;
});
},
),
], ],
), ),
bottomNavigationBar: Consumer2<MessagesProvider, ContactsProvider>( bottomNavigationBar: shouldHideUI ? null : Consumer2<MessagesProvider, ContactsProvider>(
builder: (context, messagesProvider, contactsProvider, child) { builder: (context, messagesProvider, contactsProvider, child) {
final unreadCount = messagesProvider.unreadCount; final unreadCount = messagesProvider.unreadCount;
final newContactsCount = contactsProvider.newContactsCount; final newContactsCount = contactsProvider.newContactsCount;
@@ -512,47 +526,58 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
), ),
); );
}, },
child: Row( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// RX indicator // RX indicator
Container( Row(
width: 8, mainAxisSize: MainAxisSize.min,
height: 8, children: [
decoration: BoxDecoration( Container(
shape: BoxShape.circle, width: 8,
color: provider.rxActivity height: 8,
? Colors.green decoration: BoxDecoration(
: Colors.grey.withOpacity(0.3), 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), const SizedBox(height: 4),
Text(
'RX:${provider.rxPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
const SizedBox(width: 12),
// TX indicator // TX indicator
Container( Row(
width: 8, mainAxisSize: MainAxisSize.min,
height: 8, children: [
decoration: BoxDecoration( Container(
shape: BoxShape.circle, width: 8,
color: provider.txActivity height: 8,
? Colors.blue decoration: BoxDecoration(
: Colors.grey.withOpacity(0.3), shape: BoxShape.circle,
), color: provider.txActivity
), ? Colors.blue
const SizedBox(width: 4), : Colors.grey.withOpacity(0.3),
Text( ),
'TX:${provider.txPacketCount}', ),
style: const TextStyle( const SizedBox(width: 4),
fontSize: 11, Text(
color: Colors.grey, 'TX:${provider.txPacketCount}',
), style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
), ),
], ],
), ),

View File

@@ -31,7 +31,12 @@ import '../widgets/messages/sar_update_sheet.dart';
import 'map_management_screen.dart'; import 'map_management_screen.dart';
class MapTab extends StatefulWidget { class MapTab extends StatefulWidget {
const MapTab({super.key}); final Function(bool)? onFullscreenChanged;
const MapTab({
super.key,
this.onFullscreenChanged,
});
@override @override
State<MapTab> createState() => _MapTabState(); State<MapTab> createState() => _MapTabState();
@@ -49,6 +54,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
bool _rotateMarkerWithHeading = false; // Toggle for rotation bool _rotateMarkerWithHeading = false; // Toggle for rotation
bool _showLegend = false; bool _showLegend = false;
bool _showMapDebugInfo = false; // Toggle for debug info bool _showMapDebugInfo = false; // Toggle for debug info
bool _isFullscreen = false; // Toggle for fullscreen mode
double _gpsUpdateDistance = 3.0; // meters double _gpsUpdateDistance = 3.0; // meters
bool _backgroundTrackingEnabled = false; // Toggle for background tracking bool _backgroundTrackingEnabled = false; // Toggle for background tracking
StreamSubscription<CompassEvent>? _compassStreamSubscription; StreamSubscription<CompassEvent>? _compassStreamSubscription;
@@ -180,6 +186,12 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
_showLegend = prefs.getBool('map_show_legend') ?? false; _showLegend = prefs.getBool('map_show_legend') ?? false;
_rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false; _rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false;
_showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? 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; _gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 3.0;
_backgroundTrackingEnabled = prefs.getBool('background_tracking_enabled') ?? false; _backgroundTrackingEnabled = prefs.getBool('background_tracking_enabled') ?? false;
@@ -202,6 +214,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
await prefs.setBool('map_show_legend', _showLegend); await prefs.setBool('map_show_legend', _showLegend);
await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading); await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading);
await prefs.setBool('map_show_debug_info', _showMapDebugInfo); await prefs.setBool('map_show_debug_info', _showMapDebugInfo);
await prefs.setBool('map_fullscreen', _isFullscreen);
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance); await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled); await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled);
await prefs.setInt('map_last_layer', MapLayer.allLayers.indexOf(_currentLayer)); await prefs.setInt('map_last_layer', MapLayer.allLayers.indexOf(_currentLayer));
@@ -499,6 +512,23 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
_saveSettings(); _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<MapTab> with AutomaticKeepAliveClientMixin {
), ),
], ],
), ),
// Drawing markers layer (delete buttons on drawings) // Drawing markers layer (delete buttons on drawings, only shown when in drawing mode)
DrawingMarkersLayer( DrawingMarkersLayer(
drawings: drawingProvider.drawings, drawings: drawingProvider.drawings,
showDeleteButtons: drawingProvider.isDrawing,
onDeleteDrawing: (drawingId) { onDeleteDrawing: (drawingId) {
drawingProvider.removeDrawing(drawingId); drawingProvider.removeDrawing(drawingId);
}, },
@@ -1083,26 +1114,46 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
], ],
), ),
), ),
// Compass widget - top right (always visible) // Exit fullscreen button - top left (only shown in fullscreen mode)
Positioned( if (_isFullscreen)
top: 16, Positioned(
right: 16, top: 16,
child: GestureDetector( left: 16,
onTap: () => _showDetailedCompass( child: FloatingActionButton.small(
context, heroTag: 'exit_fullscreen',
contactsProvider.contactsWithLocation, onPressed: () {
messagesProvider.sarMarkers, setState(() {
), _isFullscreen = false;
child: CompassWidget( });
heading: _currentHeading ?? 0, _saveSettings();
hasHeading: _currentHeading != null, // 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),
), ),
), ),
), // Compass widget - top right (hidden in fullscreen mode)
// Map legend overlay if (!_isFullscreen)
if (_showLegend)
Positioned( 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, left: 16,
child: MapLegend( child: MapLegend(
teamMemberCount: contactsWithLocation.length, teamMemberCount: contactsWithLocation.length,
@@ -1112,19 +1163,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
objectCount: messagesProvider.objectMarkers.length, objectCount: messagesProvider.objectMarkers.length,
), ),
), ),
// Drawing toolbar - bottom left // Map controls - right side (hidden in fullscreen mode)
Positioned( if (!_isFullscreen)
bottom: 16, Positioned(
left: 16, bottom: 16,
child: const DrawingToolbar(), right: 16,
), child: Column(
// Map controls - right side children: [
Positioned( // Drawing toolbar
bottom: 16, const DrawingToolbar(),
right: 16, const SizedBox(height: 8),
child: Column( FloatingActionButton.small(
children: [
FloatingActionButton.small(
heroTag: 'center_map', heroTag: 'center_map',
onPressed: !_isMapReady ? null : () async { onPressed: !_isMapReady ? null : () async {
// Force update GPS location and jump to it // Force update GPS location and jump to it
@@ -1162,16 +1211,16 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
child: const Icon(Icons.layers), child: const Icon(Icons.layers),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
FloatingActionButton.small( FloatingActionButton.small(
heroTag: 'options_menu', heroTag: 'options_menu',
onPressed: () => _showOptionsMenu(context), onPressed: () => _showOptionsMenu(context),
child: const Icon(Icons.more_vert), child: const Icon(Icons.more_vert),
), ),
], ],
),
), ),
), // Map debug info - bottom left (hidden in fullscreen mode)
// Map debug info - bottom left if (_showMapDebugInfo && _isMapReady && !_isFullscreen)
if (_showMapDebugInfo && _isMapReady)
Positioned( Positioned(
bottom: 16, bottom: 16,
left: 16, left: 16,

View File

@@ -227,7 +227,34 @@ class _MessagesTabState extends State<MessagesTab> {
} }
// 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<void> _handleRefresh() async {
final connectionProvider = context.read<ConnectionProvider>();
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<Message> _getFilteredMessages(MessagesProvider messagesProvider) { List<Message> _getFilteredMessages(MessagesProvider messagesProvider) {
// Show ALL messages regardless of recipient selection // Show ALL messages regardless of recipient selection
@@ -243,33 +270,43 @@ class _MessagesTabState extends State<MessagesTab> {
return Column( return Column(
children: [ children: [
// Messages list // Messages list with pull-to-refresh
Expanded( Expanded(
child: messages.isEmpty child: RefreshIndicator(
? Center( onRefresh: _handleRefresh,
child: Column( child: messages.isEmpty
mainAxisAlignment: MainAxisAlignment.center, ? LayoutBuilder(
children: [ builder: (context, constraints) => SingleChildScrollView(
Icon( physics: const AlwaysScrollableScrollPhysics(),
Icons.message_outlined, child: ConstrainedBox(
size: 64, constraints: BoxConstraints(minHeight: constraints.maxHeight),
color: Theme.of(context).disabledColor, 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', : ListView.builder(
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(
reverse: true, reverse: true,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
itemCount: messages.length, itemCount: messages.length,
@@ -298,6 +335,7 @@ class _MessagesTabState extends State<MessagesTab> {
); );
}, },
), ),
),
), ),
// Message input area // Message input area

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../models/map_drawing.dart'; import '../../models/map_drawing.dart';
/// Widget that renders map drawings as polylines /// Widget that renders map drawings as polylines
@@ -37,9 +38,9 @@ class DrawingLayer extends StatelessWidget {
return Polyline( return Polyline(
points: points, points: points,
color: drawing.color.withOpacity(opacity), color: drawing.color.withValues(alpha: opacity),
strokeWidth: 4.0, strokeWidth: 4.0,
borderColor: Colors.white.withOpacity(opacity * 0.8), borderColor: Colors.white.withValues(alpha: opacity * 0.8),
borderStrokeWidth: 1.0, borderStrokeWidth: 1.0,
); );
} }
@@ -59,15 +60,22 @@ class DrawingLayer extends StatelessWidget {
class DrawingMarkersLayer extends StatelessWidget { class DrawingMarkersLayer extends StatelessWidget {
final List<MapDrawing> drawings; final List<MapDrawing> drawings;
final Function(String drawingId)? onDeleteDrawing; final Function(String drawingId)? onDeleteDrawing;
final bool showDeleteButtons;
const DrawingMarkersLayer({ const DrawingMarkersLayer({
super.key, super.key,
required this.drawings, required this.drawings,
this.onDeleteDrawing, this.onDeleteDrawing,
this.showDeleteButtons = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Only show delete buttons when showDeleteButtons is true
if (!showDeleteButtons) {
return const SizedBox.shrink();
}
final List<Marker> markers = []; final List<Marker> markers = [];
// Add delete markers for each drawing (at the center point) // Add delete markers for each drawing (at the center point)
@@ -87,12 +95,12 @@ class DrawingMarkersLayer extends StatelessWidget {
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: drawing.color.withOpacity(0.9), color: drawing.color.withValues(alpha: 0.9),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 2),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.3), color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),

View File

@@ -28,7 +28,7 @@ class DrawingToolbar extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.2), color: Colors.black.withValues(alpha: 0.2),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -83,7 +83,7 @@ class DrawingToolbar extends StatelessWidget {
boxShadow: [ boxShadow: [
if (isSelected) if (isSelected)
BoxShadow( BoxShadow(
color: color.withOpacity(0.5), color: color.withValues(alpha: 0.5),
blurRadius: 8, blurRadius: 8,
spreadRadius: 2, spreadRadius: 2,
), ),