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

View File

@@ -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) {

View File

@@ -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';

View File

@@ -32,6 +32,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
int _currentIndex = 0;
bool _isMapFullscreen = false;
@override
void initState() {
@@ -40,6 +41,10 @@ class _HomeScreenState extends State<HomeScreen> 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<HomeScreen> 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<HomeScreen> with SingleTickerProviderStateM
children: [
MessagesTab(onNavigateToMap: () => _tabController.animateTo(2)),
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) {
final unreadCount = messagesProvider.unreadCount;
final newContactsCount = contactsProvider.newContactsCount;
@@ -512,47 +526,58 @@ class _HomeScreenState extends State<HomeScreen> 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,
),
),
],
),
],
),

View File

@@ -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<MapTab> createState() => _MapTabState();
@@ -49,6 +54,7 @@ class _MapTabState extends State<MapTab> 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<CompassEvent>? _compassStreamSubscription;
@@ -180,6 +186,12 @@ class _MapTabState extends State<MapTab> 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<MapTab> 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<MapTab> 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<MapTab> 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<MapTab> 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<MapTab> 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<MapTab> 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,

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) {
// Show ALL messages regardless of recipient selection
@@ -243,33 +270,43 @@ class _MessagesTabState extends State<MessagesTab> {
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<MessagesTab> {
);
},
),
),
),
// Message input area

View File

@@ -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<MapDrawing> 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<Marker> 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),
),

View File

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