mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Update color handling in map drawings and enhance message sync functionality
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user