Match sensors bg and hide counts

This commit is contained in:
Janez T
2026-03-08 21:03:44 +01:00
parent 2ab8681ac4
commit d113f7a881
6 changed files with 128 additions and 112 deletions

View File

@@ -7,7 +7,6 @@ import '../models/contact.dart';
import '../providers/contacts_provider.dart'; import '../providers/contacts_provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../providers/connection_provider.dart'; import '../providers/connection_provider.dart';
import '../providers/messages_provider.dart';
import '../widgets/contacts/contact_tile.dart'; import '../widgets/contacts/contact_tile.dart';
import '../widgets/contacts/add_channel_dialog.dart'; import '../widgets/contacts/add_channel_dialog.dart';
@@ -207,8 +206,8 @@ class _ContactsTabState extends State<ContactsTab> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Scaffold( return Scaffold(
body: Consumer2<ContactsProvider, MessagesProvider>( body: Consumer<ContactsProvider>(
builder: (context, contactsProvider, messagesProvider, child) { builder: (context, contactsProvider, child) {
final chatContacts = _sortContactsByDistance( final chatContacts = _sortContactsByDistance(
contactsProvider.chatContacts, contactsProvider.chatContacts,
); );
@@ -292,10 +291,6 @@ class _ContactsTabState extends State<ContactsTab> {
formatDistance: _formatDistance, formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap, onNavigateToMap: widget.onNavigateToMap,
onNavigateToMessages: widget.onNavigateToMessages, onNavigateToMessages: widget.onNavigateToMessages,
messageCount: messagesProvider
.getMessageCountForDestination(contact),
unreadMessageCount: messagesProvider
.getUnreadCountForDestination(contact),
), ),
), ),
const Divider(height: 32), const Divider(height: 32),
@@ -316,10 +311,6 @@ class _ContactsTabState extends State<ContactsTab> {
formatDistance: _formatDistance, formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap, onNavigateToMap: widget.onNavigateToMap,
onNavigateToMessages: widget.onNavigateToMessages, onNavigateToMessages: widget.onNavigateToMessages,
messageCount: messagesProvider
.getMessageCountForDestination(contact),
unreadMessageCount: messagesProvider
.getUnreadCountForDestination(contact),
), ),
), ),
const Divider(height: 32), const Divider(height: 32),
@@ -340,10 +331,6 @@ class _ContactsTabState extends State<ContactsTab> {
formatDistance: _formatDistance, formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap, onNavigateToMap: widget.onNavigateToMap,
onNavigateToMessages: widget.onNavigateToMessages, onNavigateToMessages: widget.onNavigateToMessages,
messageCount: messagesProvider
.getMessageCountForDestination(contact),
unreadMessageCount: messagesProvider
.getUnreadCountForDestination(contact),
), ),
), ),
const Divider(height: 32), const Divider(height: 32),
@@ -364,10 +351,6 @@ class _ContactsTabState extends State<ContactsTab> {
formatDistance: _formatDistance, formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap, onNavigateToMap: widget.onNavigateToMap,
onNavigateToMessages: widget.onNavigateToMessages, onNavigateToMessages: widget.onNavigateToMessages,
messageCount: messagesProvider
.getMessageCountForDestination(contact),
unreadMessageCount: messagesProvider
.getUnreadCountForDestination(contact),
), ),
), ),
], ],

View File

@@ -371,6 +371,7 @@ class _SensorCard extends StatelessWidget {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final telemetry = contact?.telemetry; final telemetry = contact?.telemetry;
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final metrics = contact == null || telemetry == null final metrics = contact == null || telemetry == null
? const <_MetricCardData>[] ? const <_MetricCardData>[]
: _buildMetricCards(l10n, telemetry, contact!); : _buildMetricCards(l10n, telemetry, contact!);
@@ -378,16 +379,23 @@ class _SensorCard extends StatelessWidget {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surfaceContainerLow,
colorScheme.surfaceContainerHighest.withValues(alpha: 0.9),
],
),
border: Border.all( border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.14), color: colorScheme.outlineVariant.withValues(alpha: 0.35),
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.04), color: Colors.black.withValues(alpha: 0.045),
blurRadius: 18, blurRadius: 12,
offset: const Offset(0, 8), offset: const Offset(0, 4),
), ),
], ],
), ),

View File

@@ -24,8 +24,6 @@ class ContactTile extends StatelessWidget {
final String Function(double)? formatDistance; final String Function(double)? formatDistance;
final VoidCallback? onNavigateToMap; final VoidCallback? onNavigateToMap;
final VoidCallback? onNavigateToMessages; final VoidCallback? onNavigateToMessages;
final int messageCount;
final int unreadMessageCount;
const ContactTile({ const ContactTile({
super.key, super.key,
@@ -35,8 +33,6 @@ class ContactTile extends StatelessWidget {
this.formatDistance, this.formatDistance,
this.onNavigateToMap, this.onNavigateToMap,
this.onNavigateToMessages, this.onNavigateToMessages,
this.messageCount = 0,
this.unreadMessageCount = 0,
}); });
/// Get localized time since last seen /// Get localized time since last seen
@@ -240,13 +236,6 @@ class ContactTile extends StatelessWidget {
], ],
), ),
), ),
if (messageCount > 0) ...[
const SizedBox(width: 8),
_MessageCountBadge(
totalCount: messageCount,
unreadCount: unreadMessageCount,
),
],
const SizedBox(width: 8), const SizedBox(width: 8),
Text(timeAgoText, style: timeAgoStyle), Text(timeAgoText, style: timeAgoStyle),
if (isPingInProgress) ...[ if (isPingInProgress) ...[
@@ -321,21 +310,22 @@ class ContactTile extends StatelessWidget {
_showSetRouteDialog(context, contact); _showSetRouteDialog(context, contact);
}, },
), ),
ListTile( if (!contact.isPublicChannel)
leading: const Icon(Icons.delete, color: Colors.red), ListTile(
title: Text( leading: const Icon(Icons.delete, color: Colors.red),
contact.isChannel ? l10n.deleteChannel : l10n.deleteContact, title: Text(
style: const TextStyle(color: Colors.red), contact.isChannel ? l10n.deleteChannel : l10n.deleteContact,
style: const TextStyle(color: Colors.red),
),
onTap: () {
Navigator.pop(sheetContext);
if (contact.isChannel) {
_showDeleteChannelDialog(context, contact);
} else {
_showDeleteConfirmation(context, contact);
}
},
), ),
onTap: () {
Navigator.pop(sheetContext);
if (contact.isChannel) {
_showDeleteChannelDialog(context, contact);
} else {
_showDeleteConfirmation(context, contact);
}
},
),
], ],
), ),
), ),
@@ -373,7 +363,11 @@ class ContactTile extends StatelessWidget {
); );
} }
void _showDeleteConfirmation(BuildContext context, Contact contact) { void _showDeleteConfirmation(
BuildContext context,
Contact contact, {
bool closeDetailsSheetOnDelete = false,
}) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@@ -391,7 +385,9 @@ class ContactTile extends StatelessWidget {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
Navigator.pop(context); // Close confirmation dialog Navigator.pop(context); // Close confirmation dialog
Navigator.pop(context); // Close contact details sheet if (closeDetailsSheetOnDelete) {
Navigator.pop(context); // Close contact details sheet
}
await _deleteContact(context, contact); await _deleteContact(context, contact);
}, },
style: TextButton.styleFrom(foregroundColor: Colors.red), style: TextButton.styleFrom(foregroundColor: Colors.red),
@@ -895,7 +891,11 @@ class ContactTile extends StatelessWidget {
return; return;
} }
_showDeleteConfirmation(context, contact); _showDeleteConfirmation(
context,
contact,
closeDetailsSheetOnDelete: true,
);
}, },
icon: const Icon(Icons.delete_outline), icon: const Icon(Icons.delete_outline),
label: Text( label: Text(
@@ -1256,6 +1256,10 @@ class ContactTile extends StatelessWidget {
Contact contact, { Contact contact, {
bool closeDetailsSheetOnDelete = false, bool closeDetailsSheetOnDelete = false,
}) { }) {
if (contact.isPublicChannel) {
return;
}
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
showDialog( showDialog(
@@ -1390,59 +1394,3 @@ class ContactTile extends StatelessWidget {
); );
} }
} }
class _MessageCountBadge extends StatelessWidget {
final int totalCount;
final int unreadCount;
const _MessageCountBadge({
required this.totalCount,
required this.unreadCount,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final hasUnread = unreadCount > 0;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: hasUnread
? colorScheme.primaryContainer
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: hasUnread
? colorScheme.primary.withValues(alpha: 0.35)
: colorScheme.outlineVariant.withValues(alpha: 0.45),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (hasUnread) ...[
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
),
),
const SizedBox(width: 5),
],
Text(
hasUnread ? '$unreadCount/$totalCount' : '$totalCount',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: hasUnread
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w800,
),
),
],
),
);
}
}

View File

@@ -192,7 +192,8 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
? l10n.broadcastToAllNearby ? l10n.broadcastToAllNearby
: '${l10n.channel} ${channel.publicKey[1]}', // Show slot number : '${l10n.channel} ${channel.publicKey[1]}', // Show slot number
unreadCount: unreadCount:
widget.unreadCountsByPublicKey[channel.publicKeyHex] ?? widget.unreadCountsByPublicKey[channel
.publicKeyHex] ??
0, 0,
isSelected: _isSelected('channel', channel), isSelected: _isSelected('channel', channel),
onTap: () { onTap: () {
@@ -240,7 +241,8 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
title: contact.displayName, title: contact.displayName,
subtitle: contact.publicKeyShort, subtitle: contact.publicKeyShort,
unreadCount: unreadCount:
widget.unreadCountsByPublicKey[contact.publicKeyHex] ?? widget.unreadCountsByPublicKey[contact
.publicKeyHex] ??
0, 0,
isSelected: _isSelected('contact', contact), isSelected: _isSelected('contact', contact),
onTap: () { onTap: () {
@@ -288,7 +290,8 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
title: room.displayName, title: room.displayName,
subtitle: room.publicKeyShort, subtitle: room.publicKeyShort,
unreadCount: unreadCount:
widget.unreadCountsByPublicKey[room.publicKeyHex] ?? 0, widget.unreadCountsByPublicKey[room.publicKeyHex] ??
0,
isSelected: _isSelected('room', room), isSelected: _isSelected('room', room),
onTap: () { onTap: () {
widget.onSelect('room', room); widget.onSelect('room', room);

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2026.0308.1+21 version: 2026.0308.3+23
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2

View File

@@ -0,0 +1,74 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
import 'package:meshcore_sar_app/models/contact.dart';
import 'package:meshcore_sar_app/widgets/messages/recipient_selector_sheet.dart';
void main() {
Contact buildContact({
required String name,
required ContactType type,
int secondByte = 0,
}) {
final publicKey = Uint8List(32);
publicKey[1] = secondByte;
return Contact(
publicKey: publicKey,
type: type,
flags: 0,
outPathLen: 0,
outPath: Uint8List(0),
advName: name,
lastAdvert: 0,
advLat: 0,
advLon: 0,
lastMod: 0,
);
}
Future<void> pumpSheet(WidgetTester tester) async {
final channel = buildContact(
name: 'Ops',
type: ContactType.channel,
secondByte: 3,
);
final contact = buildContact(name: 'John Smith', type: ContactType.chat);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: RecipientSelectorSheet(
contacts: [contact],
rooms: const [],
channels: [channel],
unreadCount: 11,
unreadCountsByPublicKey: {
channel.publicKeyHex: 7,
contact.publicKeyHex: 3,
},
currentDestinationType: 'all',
onSelect: (_, __) {},
),
),
),
);
}
testWidgets('renders unread badges for all and destination filters', (
tester,
) async {
await pumpSheet(tester);
expect(find.byKey(const Key('unread-badge-11')), findsOneWidget);
expect(find.byKey(const Key('unread-badge-7')), findsOneWidget);
expect(find.byKey(const Key('unread-badge-3')), findsOneWidget);
expect(find.text('11'), findsOneWidget);
expect(find.text('7'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
});
}