feat: Favourites support using firmware flags bit 0

- Contact.isFavourite reads bit 0 of firmware flags field
- Toggle favourite via contact action sheet (writes to device)
- Favourites section shown first in contacts tab
- Path size options corrected to [1, 2, 3] bytes
This commit is contained in:
Janez T
2026-03-17 09:44:07 +01:00
parent 6187067017
commit 1e97ba9831
6 changed files with 72 additions and 3 deletions

View File

@@ -342,6 +342,8 @@ class ContactsProvider with ChangeNotifier {
}
List<Contact> get contacts => _contacts.values.toList();
List<Contact> get favouriteContacts =>
_contacts.values.where((c) => c.isFavourite).toList();
List<SavedContactGroup> get savedContactGroups =>
List<SavedContactGroup>.from(_savedContactGroups)
..sort((a, b) => b.createdAt.compareTo(a.createdAt));

View File

@@ -688,6 +688,19 @@ class _ContactsTabState extends State<ContactsTab> {
child: ListView(
padding: const EdgeInsets.all(8),
children: [
// Favourites (contacts with firmware favourite flag set)
if (contactsProvider.favouriteContacts.isNotEmpty) ...[
_SectionHeader(
title: 'Favourites',
count: contactsProvider.favouriteContacts.length,
icon: Icons.star,
),
..._buildContactSectionItems(
contactsProvider.favouriteContacts,
),
const Divider(height: 32),
],
// Team Members (Chat contacts)
if (showTeamMembersSection) ...[
_SectionHeader(

View File

@@ -210,6 +210,38 @@ class ContactStorageService {
}
}
// ── Favorites ───────────────────────────────────────────────────────────────
static const String _favoritesKey = 'contact_favorites';
Future<void> saveFavorites(
Set<String> publicKeyHexes, {
String? namespace,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
_key(_favoritesKey, namespace: namespace),
publicKeyHexes.toList(),
);
} catch (e) {
debugPrint('❌ [ContactStorage] Error saving favorites: $e');
}
}
Future<Set<String>> loadFavorites({String? namespace}) async {
try {
final prefs = await SharedPreferences.getInstance();
final list = prefs.getStringList(
_key(_favoritesKey, namespace: namespace),
);
return list?.toSet() ?? {};
} catch (e) {
debugPrint('❌ [ContactStorage] Error loading favorites: $e');
return {};
}
}
/// Get storage statistics
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
try {

View File

@@ -25,8 +25,7 @@ class RouteHashPreferences {
/// Valid hash sizes per the MeshCore protocol.
/// The path encoding uses 2 bits: 00=1B, 01=2B, 10=3B, 11=4B.
/// The official app offers 1, 2, 4 (skipping 3).
static const List<int> supportedSizes = [1, 2, 4];
static const List<int> supportedSizes = [1, 2, 3];
static int _normalize(int value) {
if (supportedSizes.contains(value)) return value;

View File

@@ -348,6 +348,29 @@ class ContactTile extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!contact.isChannel)
ListTile(
leading: Icon(
contact.isFavourite ? Icons.star : Icons.star_outline,
color: contact.isFavourite ? Colors.amber : null,
),
title: Text(
contact.isFavourite
? 'Remove from Favourites'
: 'Add to Favourites',
),
onTap: () async {
Navigator.pop(sheetContext);
final toggled = contact.toggleFavourite();
final connectionProvider =
context.read<ConnectionProvider>();
await connectionProvider.addOrUpdateContact(toggled);
if (context.mounted) {
// Refresh contact from device so the local cache is updated
await connectionProvider.getContact(contact.publicKey);
}
},
),
ListTile(
leading: const Icon(Icons.message_outlined),
title: Text(l10n.messages),

View File

@@ -44,7 +44,7 @@ dependencies:
meshcore_client:
git:
url: https://github.com/dz0ny/meshcore_client.git
ref: "77c1f81f24b4839cf70439a478ff077c026c120e"
ref: "b8d7549709c02a99dbde9e77a19fb65227d7e2c2"
# Codec2 ultra-low-bitrate speech codec (FFI plugin)
codec2_flutter: