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,10 +526,14 @@ 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
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container( Container(
width: 8, width: 8,
height: 8, height: 8,
@@ -534,8 +552,13 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
color: Colors.grey, color: Colors.grey,
), ),
), ),
const SizedBox(width: 12), ],
),
const SizedBox(height: 4),
// TX indicator // TX indicator
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container( Container(
width: 8, width: 8,
height: 8, height: 8,
@@ -556,6 +579,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
), ),
], ],
), ),
],
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
// Settings button (tap for device settings, long press for packet logs) // Settings button (tap for device settings, long press for packet logs)

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,7 +1114,27 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
], ],
), ),
), ),
// Compass widget - top right (always visible) // 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),
),
),
// Compass widget - top right (hidden in fullscreen mode)
if (!_isFullscreen)
Positioned( Positioned(
top: 16, top: 16,
right: 16, right: 16,
@@ -1099,10 +1150,10 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
), ),
), ),
), ),
// Map legend overlay // Map legend overlay (hidden in fullscreen mode)
if (_showLegend) if (_showLegend && !_isFullscreen)
Positioned( Positioned(
top: 80, // Position below compass (which is always visible now) top: 80, // Position below compass
left: 16, left: 16,
child: MapLegend( child: MapLegend(
teamMemberCount: contactsWithLocation.length, teamMemberCount: contactsWithLocation.length,
@@ -1112,18 +1163,16 @@ 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,
left: 16,
child: const DrawingToolbar(),
),
// Map controls - right side
Positioned( Positioned(
bottom: 16, bottom: 16,
right: 16, right: 16,
child: Column( child: Column(
children: [ children: [
// Drawing toolbar
const DrawingToolbar(),
const SizedBox(height: 8),
FloatingActionButton.small( FloatingActionButton.small(
heroTag: 'center_map', heroTag: 'center_map',
onPressed: !_isMapReady ? null : () async { onPressed: !_isMapReady ? null : () async {
@@ -1170,8 +1219,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
], ],
), ),
), ),
// Map debug info - bottom left // Map debug info - bottom left (hidden in fullscreen mode)
if (_showMapDebugInfo && _isMapReady) if (_showMapDebugInfo && _isMapReady && !_isFullscreen)
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,10 +270,17 @@ class _MessagesTabState extends State<MessagesTab> {
return Column( return Column(
children: [ children: [
// Messages list // Messages list with pull-to-refresh
Expanded( Expanded(
child: RefreshIndicator(
onRefresh: _handleRefresh,
child: messages.isEmpty child: messages.isEmpty
? Center( ? LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -262,12 +296,15 @@ class _MessagesTabState extends State<MessagesTab> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Connect to a device to start receiving messages', 'Pull down to sync messages',
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
), ),
),
),
),
) )
: ListView.builder( : ListView.builder(
reverse: true, reverse: true,
@@ -299,6 +336,7 @@ class _MessagesTabState extends State<MessagesTab> {
}, },
), ),
), ),
),
// Message input area // Message input area
Container( Container(

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,
), ),