mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Match sensors bg and hide counts
This commit is contained in:
@@ -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),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
74
test/widgets/recipient_selector_sheet_test.dart
Normal file
74
test/widgets/recipient_selector_sheet_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user