mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Fixes
This commit is contained in:
@@ -49,13 +49,21 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
}
|
||||
|
||||
/// Calculate distance between two points in meters
|
||||
double _calculateDistanceInMeters(double lat1, double lon1, double lat2, double lon2) {
|
||||
double _calculateDistanceInMeters(
|
||||
double lat1,
|
||||
double lon1,
|
||||
double lat2,
|
||||
double lon2,
|
||||
) {
|
||||
const R = 6371000; // Earth's radius in meters
|
||||
final dLat = (lat2 - lat1) * pi / 180;
|
||||
final dLon = (lon2 - lon1) * pi / 180;
|
||||
final a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(lat1 * pi / 180) * cos(lat2 * pi / 180) *
|
||||
sin(dLon / 2) * sin(dLon / 2);
|
||||
final a =
|
||||
sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(lat1 * pi / 180) *
|
||||
cos(lat2 * pi / 180) *
|
||||
sin(dLon / 2) *
|
||||
sin(dLon / 2);
|
||||
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
@@ -110,52 +118,58 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
children: [
|
||||
// Team Members (Chat contacts)
|
||||
if (chatContacts.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: 'Team Members',
|
||||
count: chatContacts.length,
|
||||
icon: Icons.people,
|
||||
),
|
||||
...chatContacts.map((contact) => ContactTile(
|
||||
contact: contact,
|
||||
currentPosition: _currentPosition,
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
)),
|
||||
const Divider(height: 32),
|
||||
],
|
||||
// Team Members (Chat contacts)
|
||||
if (chatContacts.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: 'Team Members',
|
||||
count: chatContacts.length,
|
||||
icon: Icons.people,
|
||||
),
|
||||
...chatContacts.map(
|
||||
(contact) => ContactTile(
|
||||
contact: contact,
|
||||
currentPosition: _currentPosition,
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
],
|
||||
|
||||
// Repeaters
|
||||
if (repeaters.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: 'Repeaters',
|
||||
count: repeaters.length,
|
||||
icon: Icons.router,
|
||||
),
|
||||
...repeaters.map((contact) => ContactTile(
|
||||
contact: contact,
|
||||
currentPosition: _currentPosition,
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
)),
|
||||
const Divider(height: 32),
|
||||
],
|
||||
// Repeaters
|
||||
if (repeaters.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: 'Repeaters',
|
||||
count: repeaters.length,
|
||||
icon: Icons.router,
|
||||
),
|
||||
...repeaters.map(
|
||||
(contact) => ContactTile(
|
||||
contact: contact,
|
||||
currentPosition: _currentPosition,
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
],
|
||||
|
||||
// Rooms/Channels
|
||||
if (rooms.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: 'Rooms/Channels',
|
||||
count: rooms.length,
|
||||
icon: Icons.tag,
|
||||
),
|
||||
...rooms.map((contact) => ContactTile(
|
||||
contact: contact,
|
||||
currentPosition: _currentPosition,
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
)),
|
||||
],
|
||||
// Rooms/Channels
|
||||
if (rooms.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
title: 'Rooms',
|
||||
count: rooms.length,
|
||||
icon: Icons.tag,
|
||||
),
|
||||
...rooms.map(
|
||||
(contact) => ContactTile(
|
||||
contact: contact,
|
||||
currentPosition: _currentPosition,
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -185,9 +199,9 @@ class _SectionHeader extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'map_management_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'device_config_screen.dart';
|
||||
import 'packet_log_screen.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
final Function(AppThemeMode) onThemeChanged;
|
||||
@@ -52,9 +53,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Device not connected')),
|
||||
);
|
||||
ToastLogger.error(context, 'Device not connected');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -64,12 +63,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Location services are disabled. Please enable them in Settings.'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Location services are disabled. Please enable them in Settings.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -80,12 +74,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Location permission denied'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Location permission denied');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -93,12 +82,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Location permission permanently denied. Please enable in Settings.'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Location permission permanently denied. Please enable in Settings.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -115,9 +99,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
} catch (e) {
|
||||
print('❌ Failed to get GPS position: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to get GPS location')),
|
||||
);
|
||||
ToastLogger.error(context, 'Failed to get GPS location');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -135,21 +117,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
await connectionProvider.sendSelfAdvert(floodMode: true);
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Advertised at ${position.latitude.toStringAsFixed(6)}, ${position.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
ToastLogger.success(context, 'Advertised at ${position.latitude.toStringAsFixed(6)}, ${position.longitude.toStringAsFixed(6)}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Failed to advertise device: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to advertise: $e')),
|
||||
);
|
||||
ToastLogger.error(context, 'Failed to advertise: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -720,9 +693,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
final appProvider = context.read<AppProvider>();
|
||||
await appProvider.refresh();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Refreshed contacts')),
|
||||
);
|
||||
ToastLogger.success(context, 'Refreshed contacts');
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../providers/connection_provider.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import '../widgets/messages/sar_update_sheet.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
|
||||
class MessagesTab extends StatefulWidget {
|
||||
final VoidCallback onNavigateToMap;
|
||||
@@ -63,12 +64,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Not connected to device'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Not connected to device');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,21 +105,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
_focusNode.unfocus();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Message sent to public channel'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
ToastLogger.success(context, 'Message sent to public channel');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to send: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Failed to send: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,23 +138,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Not connected to device'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Not connected to device');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sendToChannel && roomPublicKey == null) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select a room to send SAR marker'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Please select a room to send SAR marker');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,13 +165,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${sarType.displayName} marker broadcast to public channel'),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
ToastLogger.warning(context, '${sarType.displayName} marker broadcast to public channel');
|
||||
} else {
|
||||
// Create message ID
|
||||
final messageId = '${DateTime.now().millisecondsSinceEpoch}_sent';
|
||||
@@ -245,22 +214,11 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${sarType.displayName} marker sent to room'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
ToastLogger.success(context, '${sarType.displayName} marker sent to room');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to send SAR marker: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Failed to send SAR marker: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +271,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
itemCount: messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messages[index];
|
||||
|
||||
// Display system messages with minimal styling
|
||||
if (message.isSystemMessage) {
|
||||
return _SystemMessageBubble(message: message);
|
||||
}
|
||||
|
||||
return _MessageBubble(
|
||||
message: message,
|
||||
onTap: message.isSarMarker &&
|
||||
@@ -438,12 +402,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Not connected to device'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Not connected to device');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -465,12 +424,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
// Direct message retry (for SAR markers sent to rooms)
|
||||
if (failedMessage.recipientPublicKey == null) {
|
||||
messagesProvider.markMessageFailed(retryMessageId);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cannot retry: recipient information missing'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Cannot retry: recipient information missing');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -491,20 +445,9 @@ class _MessageBubble extends StatelessWidget {
|
||||
|
||||
if (!sentSuccessfully) {
|
||||
messagesProvider.markMessageFailed(retryMessageId);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Failed to resend message'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Failed to resend message');
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Retrying message...'),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
ToastLogger.info(context, 'Retrying message...');
|
||||
}
|
||||
} else if (failedMessage.messageType == MessageType.channel) {
|
||||
// Channel message retry
|
||||
@@ -514,21 +457,10 @@ class _MessageBubble extends StatelessWidget {
|
||||
messageId: retryMessageId,
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Retrying message...'),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
ToastLogger.info(context, 'Retrying message...');
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Retry failed: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
ToastLogger.error(context, 'Retry failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,12 +483,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: message.text));
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Text copied to clipboard'),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
ToastLogger.success(context, 'Text copied to clipboard');
|
||||
},
|
||||
),
|
||||
// Delete message option
|
||||
@@ -590,12 +517,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
messagesProvider.deleteMessage(message.id);
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Message deleted'),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
ToastLogger.info(context, 'Message deleted');
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Delete'),
|
||||
@@ -953,3 +875,85 @@ class _MessageBubble extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// System message bubble - compact log-style display
|
||||
class _SystemMessageBubble extends StatelessWidget {
|
||||
final Message message;
|
||||
|
||||
const _SystemMessageBubble({required this.message});
|
||||
|
||||
Color _getLevelColor(String? level) {
|
||||
switch (level?.toLowerCase()) {
|
||||
case 'success':
|
||||
return Colors.green;
|
||||
case 'warning':
|
||||
return Colors.orange;
|
||||
case 'error':
|
||||
return Colors.red;
|
||||
case 'info':
|
||||
default:
|
||||
return Colors.blue.shade300;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getLevelIcon(String? level) {
|
||||
switch (level?.toLowerCase()) {
|
||||
case 'success':
|
||||
return Icons.check_circle_outline;
|
||||
case 'warning':
|
||||
return Icons.warning_amber_outlined;
|
||||
case 'error':
|
||||
return Icons.error_outline;
|
||||
case 'info':
|
||||
default:
|
||||
return Icons.info_outline;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
final level = message.senderName ?? 'info';
|
||||
final levelColor = _getLevelColor(level);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkMode
|
||||
? levelColor.withValues(alpha: 0.1)
|
||||
: levelColor.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getLevelIcon(level),
|
||||
size: 14,
|
||||
color: levelColor,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
message.timeAgo,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message.text,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.8),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user