mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Implement command queue for BLE command handling
- Added BleCommandQueue to manage command serialization and responses in BleCommandSender. - Updated writeData, writeDataAndWaitForAck, and writeDataAndWaitForResponse methods to utilize the command queue. - Enhanced BleResponseHandler to complete commands based on responses received from the BLE device. - Introduced new commands for channel management, including getChannel and setChannel. - Created LocationTrailLayer and TrailControls widgets for displaying and managing location trails on the map. - Added PermissionRequestDialog to handle location permission requests on app startup. - Updated LocationTrackingService to allow GPS tracking without a BLE connection.
This commit is contained in:
@@ -92,8 +92,14 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
final chatContacts = contactsProvider.chatContacts;
|
||||
final repeaters = contactsProvider.repeaters;
|
||||
final rooms = contactsProvider.rooms;
|
||||
final channels = contactsProvider.channels;
|
||||
|
||||
if (contactsProvider.contacts.isEmpty) {
|
||||
// Check if there are any displayable contacts (excluding channels)
|
||||
final hasDisplayableContacts = chatContacts.isNotEmpty ||
|
||||
repeaters.isNotEmpty ||
|
||||
rooms.isNotEmpty;
|
||||
|
||||
if (!hasDisplayableContacts) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -160,7 +166,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
const Divider(height: 32),
|
||||
],
|
||||
|
||||
// Rooms/Channels
|
||||
// Rooms
|
||||
if (rooms.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: l10n.rooms,
|
||||
@@ -175,6 +181,24 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
formatDistance: _formatDistance,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
],
|
||||
|
||||
// Channels
|
||||
if (channels.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: l10n.channels,
|
||||
count: channels.length,
|
||||
icon: Icons.broadcast_on_personal,
|
||||
),
|
||||
...channels.map(
|
||||
(contact) => ContactTile(
|
||||
contact: contact,
|
||||
currentPosition: _currentPosition,
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -18,12 +18,14 @@ import 'device_config_screen.dart';
|
||||
import 'packet_log_screen.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../widgets/permission_request_dialog.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
final Function(AppThemeMode) onThemeChanged;
|
||||
final Function(Locale?) onLocaleChanged;
|
||||
final AppThemeMode currentTheme;
|
||||
final Locale? currentLocale;
|
||||
final bool shouldShowPermissionDialog;
|
||||
|
||||
const HomeScreen({
|
||||
super.key,
|
||||
@@ -31,6 +33,7 @@ class HomeScreen extends StatefulWidget {
|
||||
required this.onLocaleChanged,
|
||||
required this.currentTheme,
|
||||
required this.currentLocale,
|
||||
this.shouldShowPermissionDialog = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -58,6 +61,13 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
});
|
||||
});
|
||||
_loadRxTxPreference();
|
||||
|
||||
// Show permission dialog after the first frame if needed
|
||||
if (widget.shouldShowPermissionDialog) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_showPermissionDialog();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadRxTxPreference() async {
|
||||
@@ -75,6 +85,34 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showPermissionDialog() {
|
||||
if (!mounted) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => PermissionRequestDialog(
|
||||
onPermissionsGranted: () {
|
||||
debugPrint('✅ Location permissions granted');
|
||||
},
|
||||
onPermissionsDenied: () {
|
||||
debugPrint('⚠️ Location permissions denied');
|
||||
// Show a snackbar to inform the user
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.locationPermissionRequired,
|
||||
),
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _advertiseDevice(BuildContext context) async {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ import '../widgets/map/compass_widget.dart';
|
||||
import '../widgets/map/detailed_compass_dialog.dart';
|
||||
import '../widgets/map/drawing_layer.dart';
|
||||
import '../widgets/map/drawing_toolbar.dart';
|
||||
import '../widgets/map/location_trail_layer.dart';
|
||||
import '../widgets/map/trail_controls.dart';
|
||||
import '../widgets/messages/sar_update_sheet.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'map_management_screen.dart';
|
||||
@@ -135,6 +137,16 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
// Position updates trigger UI rebuild for markers
|
||||
});
|
||||
|
||||
// Add location point to trail when tracking is active
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
if (_locationService.isTracking) {
|
||||
mapProvider.addTrailPoint(
|
||||
LatLng(position.latitude, position.longitude),
|
||||
accuracy: position.accuracy,
|
||||
speed: position.speed,
|
||||
);
|
||||
}
|
||||
|
||||
// Rotate map if rotation mode is enabled and heading is available
|
||||
if (_isMapReady && _rotateMarkerWithHeading && position.heading >= 0) {
|
||||
try {
|
||||
@@ -1149,6 +1161,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
},
|
||||
),
|
||||
// Location trail layer (rendered after paths, before drawings)
|
||||
const LocationTrailLayer(),
|
||||
// Drawing layer (rendered after paths, before markers)
|
||||
DrawingLayer(
|
||||
drawings: drawingProvider.drawings,
|
||||
@@ -1339,6 +1353,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
objectCount: messagesProvider.objectMarkers.length,
|
||||
),
|
||||
),
|
||||
// Trail stats overlay (top-left, below legend if shown)
|
||||
if (!_isFullscreen)
|
||||
const TrailStatsOverlay(),
|
||||
// Map controls - right side (hidden in fullscreen mode)
|
||||
if (!_isFullscreen)
|
||||
Positioned(
|
||||
@@ -1383,6 +1400,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Trail controls button
|
||||
const TrailControls(),
|
||||
const SizedBox(height: 8),
|
||||
FloatingActionButton.small(
|
||||
heroTag: 'layer_selector',
|
||||
onPressed: () => _showLayerSelector(context),
|
||||
|
||||
@@ -249,6 +249,89 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleLocationPermissionTap() async {
|
||||
try {
|
||||
final permission = await Geolocator.checkPermission();
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
// Show dialog to open app settings
|
||||
if (!mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.settings, size: 24),
|
||||
SizedBox(width: 12),
|
||||
Text('Location Permission'),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
'Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await Geolocator.openAppSettings();
|
||||
},
|
||||
child: const Text('Open Settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (permission == LocationPermission.denied) {
|
||||
// Request permission
|
||||
final newPermission = await Geolocator.requestPermission();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (newPermission == LocationPermission.whileInUse ||
|
||||
newPermission == LocationPermission.always) {
|
||||
// Permission granted
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Location permission granted!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
setState(() {}); // Refresh UI to show new status
|
||||
} else {
|
||||
// Permission denied
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Location permission is required for GPS tracking and location sharing.'),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Already granted - show info
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Location permission is already granted.'),
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error handling location permission: $e');
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startBackgroundTracking() async {
|
||||
final success = await _locationService.startTracking(
|
||||
distanceThreshold: _locationService.gpsUpdateDistance,
|
||||
@@ -359,6 +442,54 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
// Permissions Section
|
||||
_buildSectionHeader('Permissions'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.location_on),
|
||||
title: const Text('Location Permission'),
|
||||
subtitle: FutureBuilder<LocationPermission>(
|
||||
future: Geolocator.checkPermission(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Text('Checking...');
|
||||
}
|
||||
final permission = snapshot.data!;
|
||||
String statusText;
|
||||
Color statusColor;
|
||||
|
||||
switch (permission) {
|
||||
case LocationPermission.always:
|
||||
statusText = 'Granted (Always)';
|
||||
statusColor = Colors.green;
|
||||
break;
|
||||
case LocationPermission.whileInUse:
|
||||
statusText = 'Granted (While In Use)';
|
||||
statusColor = Colors.green;
|
||||
break;
|
||||
case LocationPermission.denied:
|
||||
statusText = 'Denied - Tap to request';
|
||||
statusColor = Colors.orange;
|
||||
break;
|
||||
case LocationPermission.deniedForever:
|
||||
statusText = 'Permanently Denied - Open Settings';
|
||||
statusColor = Colors.red;
|
||||
break;
|
||||
default:
|
||||
statusText = 'Unknown';
|
||||
statusColor = Colors.grey;
|
||||
}
|
||||
|
||||
return Text(
|
||||
statusText,
|
||||
style: TextStyle(color: statusColor),
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _handleLocationPermissionTap(),
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
// Location Settings Section
|
||||
_buildSectionHeader(AppLocalizations.of(context)!.locationBroadcasting),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user