This commit is contained in:
Janez T
2025-10-15 19:57:10 +02:00
parent 24234e7000
commit f5a6e1f9ab
13 changed files with 297 additions and 268 deletions

View File

@@ -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(

View File

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

View File

@@ -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,
),
),
],
),
);
}
}