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
commit 8387304d2a
30 changed files with 467 additions and 236 deletions

View file

@ -1352,5 +1352,7 @@
"settings_locationIntervalSec": "Интервал за GPS (Секунди)",
"settings_locationGPSEnable": "Активиране на GPS",
"settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.",
"settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди."
"settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.",
"room_management": "Управление на сървъра за стая",
"contacts_manageRoom": "Управление на сървър за стая"
}

View file

@ -1348,9 +1348,5 @@
"channels_scanQrCode": "Scannen Sie einen QR-Code",
"channels_scanQrCodeComingSoon": "Bald verfügbar",
"channels_enterHashtag": "Gib Hashtag ein",
"channels_hashtagHint": "z.B. #team",
"settings_locationGPSEnable": "GPS aktivieren",
"settings_locationGPSEnableSubtitle": "Aktivieren Sie die automatische Aktualisierung der Standortdaten per GPS.",
"settings_locationIntervalInvalid": "Der Zeitraum muss mindestens 60 Sekunden betragen und weniger als 86400 Sekunden sein.",
"settings_locationIntervalSec": "Zeitintervall für GPS (Sekunden)"
"channels_hashtagHint": "z.B. #team"
}

View file

@ -258,7 +258,8 @@
}
},
"contacts_manageRepeater": "Manage Repeater",
"contacts_roomLogin": "Room Login",
"contacts_manageRoom": "Manage Room Server",
"contacts_roomLogin": "Room Server Login",
"contacts_openChat": "Open Chat",
"contacts_editGroup": "Edit Group",
"contacts_deleteGroup": "Delete Group",
@ -702,7 +703,7 @@
"dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?",
"login_repeaterLogin": "Repeater Login",
"login_roomLogin": "Room Login",
"login_roomLogin": "Room Server Login",
"login_password": "Password",
"login_enterPassword": "Enter password",
"login_savePassword": "Save password",
@ -765,6 +766,7 @@
"path_setPath": "Set Path",
"repeater_management": "Repeater Management",
"room_management": "Room Server Management",
"repeater_managementTools": "Management Tools",
"repeater_status": "Status",
"repeater_statusSubtitle": "View repeater status, stats, and neighbors",

View file

@ -1353,4 +1353,6 @@
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos."
"contacts_manageRoom": "Gestionar Servidor de Habitación",
"room_management": "Administración del Servidor de Habitación"
}

View file

@ -1349,8 +1349,10 @@
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #équipe",
"settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position grâce au GPS.",
"settings_locationIntervalInvalid": "L'intervalle doit être dau moins 60 secondes et inférieur à 86400 secondes.",
"settings_locationGPSEnable": "Activer le GPS",
"settings_locationIntervalSec": "Intervalle GPS (Secondes)"
"settings_locationGPSEnable": "Habilita GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo pour GPS (Segundos)",
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
"contacts_manageRoom": "Gestionar Servidor de Habitación",
"room_management": "Administración del Servidor de Habitación"
}

View file

@ -1352,5 +1352,7 @@
"settings_locationGPSEnable": "Abilita GPS",
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
"settings_locationIntervalSec": "Intervallo GPS (Secondi)",
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi."
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.",
"contacts_manageRoom": "Gestisci Server Camera",
"room_management": "Gestione del Server di Camera"
}

View file

@ -1308,10 +1308,16 @@ abstract class AppLocalizations {
/// **'Manage Repeater'**
String get contacts_manageRepeater;
/// No description provided for @contacts_manageRoom.
///
/// In en, this message translates to:
/// **'Manage Room Server'**
String get contacts_manageRoom;
/// No description provided for @contacts_roomLogin.
///
/// In en, this message translates to:
/// **'Room Login'**
/// **'Room Server Login'**
String get contacts_roomLogin;
/// No description provided for @contacts_openChat.
@ -2696,7 +2702,7 @@ abstract class AppLocalizations {
/// No description provided for @login_roomLogin.
///
/// In en, this message translates to:
/// **'Room Login'**
/// **'Room Server Login'**
String get login_roomLogin;
/// No description provided for @login_password.
@ -2891,6 +2897,12 @@ abstract class AppLocalizations {
/// **'Repeater Management'**
String get repeater_management;
/// No description provided for @room_management.
///
/// In en, this message translates to:
/// **'Room Server Management'**
String get room_management;
/// No description provided for @repeater_managementTools.
///
/// In en, this message translates to:

View file

@ -664,6 +664,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент';
@override
String get contacts_manageRoom => 'Управление на сървър за стая';
@override
String get contacts_roomLogin => 'Вход в стаята';
@ -1601,6 +1604,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_management => 'Управление на повторители';
@override
String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_managementTools => 'Инструменти за управление';

View file

@ -661,6 +661,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Wiederholungen verwalten';
@override
String get contacts_manageRoom => 'Verwalten Sie den Raumserver';
@override
String get contacts_roomLogin => 'Raum-Login';
@ -1600,6 +1603,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_management => 'Repeater-Verwaltung';
@override
String get room_management => 'Raumserververwaltung';
@override
String get repeater_managementTools => 'Verwaltungs-Tools';

View file

@ -655,7 +655,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_manageRepeater => 'Manage Repeater';
@override
String get contacts_roomLogin => 'Room Login';
String get contacts_manageRoom => 'Manage Room Server';
@override
String get contacts_roomLogin => 'Room Server Login';
@override
String get contacts_openChat => 'Open Chat';
@ -1453,7 +1456,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get login_repeaterLogin => 'Repeater Login';
@override
String get login_roomLogin => 'Room Login';
String get login_roomLogin => 'Room Server Login';
@override
String get login_password => 'Password';
@ -1575,6 +1578,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_management => 'Repeater Management';
@override
String get room_management => 'Room Server Management';
@override
String get repeater_managementTools => 'Management Tools';

View file

@ -662,6 +662,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gestionar Repetidor';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
@override
String get contacts_roomLogin => 'Inicio de Sala';
@ -1599,6 +1602,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_management => 'Gestión de Repetidores';
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_managementTools => 'Herramientas de Gestión';

View file

@ -663,6 +663,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gérer le répétiteur';
@override
String get contacts_manageRoom => 'Gérer le serveur de salle';
@override
String get contacts_roomLogin => 'Connexion Salle';
@ -1605,6 +1608,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_management => 'Gestion des répétiteurs';
@override
String get room_management => 'Gestion du serveur de pièce';
@override
String get repeater_managementTools => 'Outils de Gestion';

View file

@ -660,6 +660,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gestisci Ripetitore';
@override
String get contacts_manageRoom => 'Gestisci Server Camera';
@override
String get contacts_roomLogin => 'Login Camera';
@ -1597,6 +1600,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_management => 'Gestione Ripetitori';
@override
String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_managementTools => 'Strumenti di Gestione';

View file

@ -658,6 +658,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Beheer Repeater';
@override
String get contacts_manageRoom => 'Beheer Ruimte Server';
@override
String get contacts_roomLogin => 'Ruimte Inloggen';
@ -1592,6 +1595,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_management => 'Beheer Repeaters';
@override
String get room_management => 'Beheer Server Kamer';
@override
String get repeater_managementTools => 'Beheerinstrumenten';

View file

@ -663,6 +663,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Zarządzaj Powtórzami';
@override
String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju';
@override
String get contacts_roomLogin => 'Logowanie do pokoju';
@ -1601,6 +1604,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_management => 'Zarządzanie Powtórzami';
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_managementTools => 'Narzędzia Zarządzania';

View file

@ -663,6 +663,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gerenciar Repetidor';
@override
String get contacts_manageRoom => 'Gerenciar Servidor de Sala';
@override
String get contacts_roomLogin => 'Login no Quarto';
@ -1599,6 +1602,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_management => 'Gerenciamento de Repetidor';
@override
String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_managementTools => 'Ferramentas de Gerenciamento';

View file

@ -656,6 +656,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Spravovať opakované zoznamy';
@override
String get contacts_manageRoom => 'Spravovať server miestnosti';
@override
String get contacts_roomLogin => 'Prihlásenie do miestnosti';
@ -1594,6 +1597,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_management => 'Správa opakérov';
@override
String get room_management => 'Správa servera miestnosti';
@override
String get repeater_managementTools => 'Nástroje na správu';

View file

@ -658,6 +658,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Upravljajte Ponovitve';
@override
String get contacts_manageRoom => 'Upravljajte strežnik sobe';
@override
String get contacts_roomLogin => 'Vnos v sobo';
@ -1594,6 +1597,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get repeater_management => 'Upravljanje ponovitve';
@override
String get room_management => 'Upravljanje stremlišča';
@override
String get repeater_managementTools => 'Upravne orodje';

View file

@ -652,6 +652,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Hantera Upprepare';
@override
String get contacts_manageRoom => 'Hantera Rumserver';
@override
String get contacts_roomLogin => 'Rum Inloggning';
@ -1583,6 +1586,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_management => 'Återuppspelarens Hantering';
@override
String get room_management => 'Rumserverhantering';
@override
String get repeater_managementTools => 'Administrationsverktyg';

View file

@ -623,6 +623,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contacts_manageRepeater => '管理重复项';
@override
String get contacts_manageRoom => '管理房间服务器';
@override
String get contacts_roomLogin => '房间登录';
@ -1525,6 +1528,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_management => '重复器管理';
@override
String get room_management => '房间服务器管理';
@override
String get repeater_managementTools => '管理工具';

View file

@ -1350,7 +1350,9 @@
"channels_enterHashtag": "Voer hashtag in",
"channels_hashtagHint": "bijv. #team",
"settings_locationGPSEnable": "GPS inschakelen",
"settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS.",
"settings_locationIntervalSec": "Interval voor GPS (Seconden)",
"settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.",
"settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS."
"contacts_manageRoom": "Beheer Ruimte Server",
"room_management": "Beheer Server Kamer"
}

View file

@ -1350,7 +1350,9 @@
"channels_enterHashtag": "Wprowadź hashtag",
"channels_hashtagHint": "np. #zespół",
"settings_locationGPSEnable": "Włącz GPS",
"settings_locationIntervalSec": "Interwał dla GPS (Sekundy)",
"settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.",
"settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund."
"settings_locationIntervalSec": "Interwał dla GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.",
"contacts_manageRoom": "Zarządzaj Serwerem Pokoju",
"room_management": "Zarządzanie Serwerem Pokoju"
}

View file

@ -1353,4 +1353,6 @@
"settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.",
"settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)"
"contacts_manageRoom": "Gerenciar Servidor de Sala",
"room_management": "Gerenciamento de Servidor de Sala"
}

View file

@ -559,7 +559,7 @@
"chat_setCustomPath": "Nastaviť vlastnú cestu",
"chat_setCustomPathSubtitle": "Ručne zadajte trasu.",
"chat_clearPath": "Vyčistiš cestu",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujacej pošlite",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujúcej pošlite",
"chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.",
"chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.",
"chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.",
@ -1352,5 +1352,7 @@
"settings_locationGPSEnable": "Aktivovať GPS",
"settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.",
"settings_locationIntervalSec": "Interval pre GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd."
"settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.",
"contacts_manageRoom": "Spravovať server miestnosti",
"room_management": "Správa servera miestnosti"
}

View file

@ -176,7 +176,7 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Obveščanja",
"appSettings_enableNotifications": "Omogoči obveščanje",
"appSettings_enableNotificationsSubtitle": "Prejmujte obvestila o sporočilih in oglasih",
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
"appSettings_notificationsEnabled": "Obvestila omogočena",
"appSettings_notificationsDisabled": "Obvestila so izklopljena",
@ -256,7 +256,7 @@
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
"contacts_searchContacts": "Iskanje kontaktov...",
"contacts_noUnreadContacts": "Nerešeno kontaktov.",
"contacts_noContactsFound": "Niti ena osebe ali skupine ni najdena.",
"contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.",
"contacts_deleteContact": "Izbrisati Kontakt",
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
"@contacts_removeConfirm": {
@ -291,7 +291,7 @@
}
},
"contacts_filterContacts": "Filtri kontakt\\,...",
"contacts_noContactsMatchFilter": "Niti ena osebe ne ustreza vašemu kriteriju.",
"contacts_noContactsMatchFilter": "Niti ena oseba ne ustreza vašemu kriteriju.",
"contacts_noMembers": "Nič članov.",
"contacts_lastSeenNow": "Datum zadnjega vpisa zdaj",
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
@ -606,7 +606,7 @@
},
"map_title": "Mapa omrežja",
"map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
"map_nodesCount": "Omize: {count}",
"@map_nodesCount": {
"placeholders": {
@ -1351,6 +1351,8 @@
"channels_hashtagHint": "npr. #ekipa",
"settings_locationGPSEnable": "Omogoči GPS",
"settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.",
"settings_locationIntervalSec": "Interval za GPS (Sekunde)",
"settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.",
"settings_locationIntervalSec": "Interval za GPS (Sekunde)"
"contacts_manageRoom": "Upravljajte strežnik sobe",
"room_management": "Upravljanje stremlišča"
}

View file

@ -1349,8 +1349,10 @@
"channels_scanQrCodeComingSoon": "Kommer snart",
"channels_enterHashtag": "Ange hashtag",
"channels_hashtagHint": "t.ex. #team",
"settings_locationGPSEnableSubtitle": "Aktiverar automatiska uppdateringar av platsen med hjälp av GPS.",
"settings_locationGPSEnable": "Aktivera GPS",
"settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.",
"settings_locationIntervalSec": "Interval för GPS (Sekunder)",
"settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder."
"settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.",
"contacts_manageRoom": "Hantera Rumserver",
"room_management": "Rumserverhantering"
}

View file

@ -1352,5 +1352,7 @@
"settings_locationGPSEnable": "启用GPS",
"settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。",
"settings_locationIntervalSec": "GPS 间隔(秒)",
"settings_locationIntervalInvalid": "时间间隔必须至少为60秒且小于86400秒。"
"settings_locationIntervalInvalid": "时间间隔必须至少为60秒且小于86400秒。",
"contacts_manageRoom": "管理房间服务器",
"room_management": "房间服务器管理"
}

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);
}
}

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(() {

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