Merge main into dev-gps

- Resolved localization conflicts by keeping both GPS settings and room management strings
- Merged room management features from main
- Merged map and contacts screen updates from main
This commit is contained in:
zjs81
2026-01-19 18:51:02 -07:00
30 changed files with 467 additions and 236 deletions
+181 -80
View File
@@ -27,13 +27,15 @@ import 'map_screen.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
enum RoomLoginDestination {
chat,
management,
}
class ContactsScreen extends StatefulWidget {
final bool hideBackButton;
const ContactsScreen({
super.key,
this.hideBackButton = false,
});
const ContactsScreen({super.key, this.hideBackButton = false});
@override
State<ContactsScreen> createState() => _ContactsScreenState();
@@ -114,7 +116,8 @@ class _ContactsScreenState extends State<ContactsScreen>
top: false,
child: QuickSwitchBar(
selectedIndex: 0,
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
),
),
),
@@ -168,8 +171,9 @@ class _ContactsScreenState extends State<ContactsScreen>
}
final filteredAndSorted = _filterAndSortContacts(contacts, connector);
final filteredGroups =
_showUnreadOnly ? const <ContactGroup>[] : _filterAndSortGroups(_groups, contacts);
final filteredGroups = _showUnreadOnly
? const <ContactGroup>[]
: _filterAndSortGroups(_groups, contacts);
return Column(
children: [
@@ -199,7 +203,10 @@ class _ContactsScreenState extends State<ContactsScreen>
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
_searchDebounce?.cancel();
@@ -238,14 +245,18 @@ class _ContactsScreenState extends State<ContactsScreen>
final group = filteredGroups[index];
return _buildGroupTile(context, group, contacts);
}
final contact = filteredAndSorted[index - filteredGroups.length];
final unreadCount = connector.getUnreadCountForContact(contact);
final contact =
filteredAndSorted[index - filteredGroups.length];
final unreadCount = connector.getUnreadCountForContact(
contact,
);
return _ContactTile(
contact: contact,
lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount,
onTap: () => _openChat(context, contact),
onLongPress: () => _showContactOptions(context, connector, contact),
onLongPress: () =>
_showContactOptions(context, connector, contact),
);
},
),
@@ -255,35 +266,48 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
List<ContactGroup> _filterAndSortGroups(List<ContactGroup> groups, List<Contact> contacts) {
List<ContactGroup> _filterAndSortGroups(
List<ContactGroup> groups,
List<Contact> contacts,
) {
final query = _searchQuery.trim().toLowerCase();
final contactsByKey = <String, Contact>{};
for (final contact in contacts) {
contactsByKey[contact.publicKeyHex] = contact;
}
final filtered = groups.where((group) {
if (query.isEmpty) return true;
if (group.name.toLowerCase().contains(query)) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && matchesContactQuery(contact, query)) return true;
}
return false;
}).where((group) {
if (_typeFilter == ContactTypeFilter.all) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
}
return false;
}).toList();
final filtered = groups
.where((group) {
if (query.isEmpty) return true;
if (group.name.toLowerCase().contains(query)) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && matchesContactQuery(contact, query)) {
return true;
}
}
return false;
})
.where((group) {
if (_typeFilter == ContactTypeFilter.all) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
}
return false;
})
.toList();
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return filtered;
}
List<Contact> _filterAndSortContacts(List<Contact> contacts, MeshCoreConnector connector) {
List<Contact> _filterAndSortContacts(
List<Contact> contacts,
MeshCoreConnector connector,
) {
var filtered = contacts.where((contact) {
if (_searchQuery.isEmpty) return true;
return matchesContactQuery(contact, _searchQuery);
@@ -301,19 +325,27 @@ class _ContactsScreenState extends State<ContactsScreen>
switch (_sortOption) {
case ContactSortOption.lastSeen:
filtered.sort((a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)));
filtered.sort(
(a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)),
);
break;
case ContactSortOption.recentMessages:
filtered.sort((a, b) {
final aMessages = connector.getMessages(a);
final bMessages = connector.getMessages(b);
final aLastMsg = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp;
final bLastMsg = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp;
final aLastMsg = aMessages.isEmpty
? DateTime(1970)
: aMessages.last.timestamp;
final bLastMsg = bMessages.isEmpty
? DateTime(1970)
: bMessages.last.timestamp;
return bLastMsg.compareTo(aLastMsg);
});
break;
case ContactSortOption.name:
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
break;
}
@@ -340,7 +372,11 @@ class _ContactsScreenState extends State<ContactsScreen>
: contact.lastSeen;
}
Widget _buildGroupTile(BuildContext context, ContactGroup group, List<Contact> contacts) {
Widget _buildGroupTile(
BuildContext context,
ContactGroup group,
List<Contact> contacts,
) {
final memberContacts = _resolveGroupContacts(group, contacts);
final subtitle = _formatGroupMembers(context, memberContacts);
return ListTile(
@@ -359,7 +395,10 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
List<Contact> _resolveGroupContacts(ContactGroup group, List<Contact> contacts) {
List<Contact> _resolveGroupContacts(
ContactGroup group,
List<Contact> contacts,
) {
final byKey = <String, Contact>{};
for (final contact in contacts) {
byKey[contact.publicKeyHex] = contact;
@@ -371,7 +410,9 @@ class _ContactsScreenState extends State<ContactsScreen>
resolved.add(contact);
}
}
resolved.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
resolved.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return resolved;
}
@@ -387,7 +428,7 @@ class _ContactsScreenState extends State<ContactsScreen>
if (contact.type == advTypeRepeater) {
_showRepeaterLogin(context, contact);
} else if (contact.type == advTypeRoom) {
_showRoomLogin(context, contact);
_showRoomLogin(context, contact, RoomLoginDestination.chat);
} else {
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
Navigator.push(
@@ -403,17 +444,13 @@ class _ContactsScreenState extends State<ContactsScreen>
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ChannelsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
case 2:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const MapScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
);
break;
}
@@ -429,10 +466,8 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
),
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
),
);
},
@@ -440,18 +475,23 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showRoomLogin(BuildContext context, Contact room) {
void _showRoomLogin(
BuildContext context,
Contact room,
RoomLoginDestination destination,
) {
showDialog(
context: context,
builder: (context) => RoomLoginDialog(
room: room,
onLogin: (password) {
// Navigate to chat screen after successful login
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(contact: room),
builder: (context) => destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password)
: ChatScreen(contact: room),
),
);
},
@@ -459,7 +499,11 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showGroupOptions(BuildContext context, ContactGroup group, List<Contact> contacts) {
void _showGroupOptions(
BuildContext context,
ContactGroup group,
List<Contact> contacts,
) {
final members = _resolveGroupContacts(group, contacts);
showModalBottomSheet(
context: context,
@@ -478,7 +522,10 @@ class _ContactsScreenState extends State<ContactsScreen>
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: Text(context.l10n.contacts_deleteGroup, style: const TextStyle(color: Colors.red)),
title: Text(
context.l10n.contacts_deleteGroup,
style: const TextStyle(color: Colors.red),
),
onTap: () {
Navigator.pop(sheetContext);
_confirmDeleteGroup(context, group);
@@ -522,7 +569,10 @@ class _ContactsScreenState extends State<ContactsScreen>
});
await _saveGroups();
},
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)),
child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
),
),
],
),
@@ -548,10 +598,16 @@ class _ContactsScreenState extends State<ContactsScreen>
final filteredContacts = filterQuery.isEmpty
? sortedContacts
: sortedContacts
.where((contact) => matchesContactQuery(contact, filterQuery))
.toList();
.where(
(contact) => matchesContactQuery(contact, filterQuery),
)
.toList();
return AlertDialog(
title: Text(isEditing ? context.l10n.contacts_editGroup : context.l10n.contacts_newGroup),
title: Text(
isEditing
? context.l10n.contacts_editGroup
: context.l10n.contacts_newGroup,
),
content: SizedBox(
width: double.maxFinite,
child: Column(
@@ -582,12 +638,18 @@ class _ContactsScreenState extends State<ContactsScreen>
SizedBox(
height: 240,
child: filteredContacts.isEmpty
? Center(child: Text(context.l10n.contacts_noContactsMatchFilter))
? Center(
child: Text(
context.l10n.contacts_noContactsMatchFilter,
),
)
: ListView.builder(
itemCount: filteredContacts.length,
itemBuilder: (context, index) {
final contact = filteredContacts[index];
final isSelected = selectedKeys.contains(contact.publicKeyHex);
final isSelected = selectedKeys.contains(
contact.publicKeyHex,
);
return CheckboxListTile(
value: isSelected,
title: Text(contact.name),
@@ -618,7 +680,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_groupNameRequired)),
SnackBar(
content: Text(context.l10n.contacts_groupNameRequired),
),
);
return;
}
@@ -628,13 +692,19 @@ class _ContactsScreenState extends State<ContactsScreen>
});
if (exists) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_groupAlreadyExists(name))),
SnackBar(
content: Text(
context.l10n.contacts_groupAlreadyExists(name),
),
),
);
return;
}
setState(() {
if (isEditing) {
final index = _groups.indexWhere((g) => g.name == group.name);
final index = _groups.indexWhere(
(g) => g.name == group.name,
);
if (index != -1) {
_groups[index] = ContactGroup(
name: name,
@@ -642,7 +712,12 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
} else {
_groups.add(ContactGroup(name: name, memberKeys: selectedKeys.toList()));
_groups.add(
ContactGroup(
name: name,
memberKeys: selectedKeys.toList(),
),
);
}
});
await _saveGroups();
@@ -650,7 +725,11 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(dialogContext);
}
},
child: Text(isEditing ? context.l10n.common_save : context.l10n.common_create),
child: Text(
isEditing
? context.l10n.common_save
: context.l10n.common_create,
),
),
],
);
@@ -682,16 +761,24 @@ class _ContactsScreenState extends State<ContactsScreen>
_showRepeaterLogin(context, contact);
},
)
else if (isRoom)
else if (isRoom) ...[
ListTile(
leading: const Icon(Icons.room, color: Colors.blue),
title: Text(context.l10n.contacts_roomLogin),
onTap: () {
Navigator.pop(sheetContext);
_showRoomLogin(context, contact);
_showRoomLogin(context, contact, RoomLoginDestination.chat);
},
)
else
),
ListTile(
leading: const Icon(Icons.room_preferences, color: Colors.orange),
title: Text(context.l10n.room_management),
onTap: () {
Navigator.pop(sheetContext);
_showRoomLogin(context, contact, RoomLoginDestination.management);
},
),
] else
ListTile(
leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat),
@@ -702,7 +789,10 @@ class _ContactsScreenState extends State<ContactsScreen>
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: Text(context.l10n.contacts_deleteContact, style: const TextStyle(color: Colors.red)),
title: Text(
context.l10n.contacts_deleteContact,
style: const TextStyle(color: Colors.red),
),
onTap: () {
Navigator.pop(sheetContext);
_confirmDelete(context, connector, contact);
@@ -734,7 +824,10 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(dialogContext);
connector.removeContact(contact);
},
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)),
child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
),
),
],
),
@@ -759,14 +852,17 @@ class _ContactTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final shotPublicKey = "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
final shotPublicKey =
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
return ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
child: _buildContactAvatar(contact),
),
title: Text(contact.name),
subtitle: Text('${contact.typeLabel}${contact.pathLabel} $shotPublicKey'),
subtitle: Text(
'${contact.typeLabel}${contact.pathLabel} $shotPublicKey',
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
@@ -791,10 +887,7 @@ class _ContactTile extends StatelessWidget {
Widget _buildContactAvatar(Contact contact) {
final emoji = firstEmoji(contact.name);
if (emoji != null) {
return Text(
emoji,
style: const TextStyle(fontSize: 18),
);
return Text(emoji, style: const TextStyle(fontSize: 18));
}
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
}
@@ -833,13 +926,21 @@ class _ContactTile extends StatelessWidget {
final now = DateTime.now();
final diff = now.difference(lastSeen);
if (diff.isNegative || diff.inMinutes < 5) return context.l10n.contacts_lastSeenNow;
if (diff.inMinutes < 60) return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
if (diff.isNegative || diff.inMinutes < 5) {
return context.l10n.contacts_lastSeenNow;
}
if (diff.inMinutes < 60) {
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
}
if (diff.inHours < 24) {
final hours = diff.inHours;
return hours == 1 ? context.l10n.contacts_lastSeenHourAgo : context.l10n.contacts_lastSeenHoursAgo(hours);
return hours == 1
? context.l10n.contacts_lastSeenHourAgo
: context.l10n.contacts_lastSeenHoursAgo(hours);
}
final days = diff.inDays;
return days == 1 ? context.l10n.contacts_lastSeenDayAgo : context.l10n.contacts_lastSeenDaysAgo(days);
return days == 1
? context.l10n.contacts_lastSeenDayAgo
: context.l10n.contacts_lastSeenDaysAgo(days);
}
}
+3
View File
@@ -273,6 +273,9 @@ class _MapScreenState extends State<MapScreen> {
initialZoom: initialZoom,
minZoom: 2.0,
maxZoom: 18.0,
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate
),
onTap: (_, latLng) {
if (_isSelectingPoi) {
setState(() {
+143 -126
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import 'repeater_status_screen.dart';
@@ -25,10 +26,17 @@ class RepeaterHubScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.repeater_management),
Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
Text(
repeater.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
@@ -39,135 +47,147 @@ class RepeaterHubScreen extends StatelessWidget {
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// Repeater info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(Icons.cell_tower, size: 40, color: Colors.white),
// Repeater info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(
Icons.cell_tower,
size: 40,
color: Colors.white,
),
const SizedBox(height: 16),
Text(
repeater.name,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
repeater.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 8),
Text(
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
repeater.pathLabel,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on, size: 14, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
repeater.pathLabel,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
],
),
],
),
],
),
],
),
],
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Status button
_buildManagementCard(
context,
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterStatusScreen(
repeater: repeater,
password: password,
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Status button
_buildManagementCard(
context,
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterStatusScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 16),
// Telemetry button
_buildManagementCard(
context,
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TelemetryScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 16),
// Telemetry button
_buildManagementCard(
context,
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
TelemetryScreen(repeater: repeater, password: password),
),
);
},
),
const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
),
);
},
),
),
);
},
),
],
),
),
@@ -214,10 +234,7 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),