feat: Add community management features with QR code scanning

- Implement Community model for managing community data, including secret handling and PSK derivation.
- Create CommunityQrScannerScreen for scanning and joining communities via QR codes.
- Develop CommunityStore for persisting community data using SharedPreferences.
- Introduce QrCodeDisplay widget for displaying QR codes with customizable options.
- Add QrScannerWidget for reusable QR code scanning functionality with validation and controls.
This commit is contained in:
zjs81
2026-01-19 20:56:07 -07:00
parent f790604d23
commit f4ec732de8
45 changed files with 5971 additions and 42 deletions
+3
View File
@@ -16,6 +16,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<application
android:label="meshcore_open"
Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

+2
View File
@@ -53,5 +53,7 @@
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string>
</dict>
</plist>
+4
View File
@@ -1615,6 +1615,10 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildSetChannelFrame(index, '', Uint8List(16)));
_channelLastReadMs.remove(index);
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
// Clear stored messages for this channel
await _channelMessageStore.clearChannelMessages(index);
// Clear in-memory messages for this channel
_channelMessages.remove(index);
// Refresh channels after deleting
await getChannels();
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.",
"settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.",
"room_management": "Управление на сървъра за стая",
"contacts_manageRoom": "Управление на сървър за стая"
"contacts_manageRoom": "Управление на сървър за стая",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Общност",
"common_ok": "Добре",
"community_createDesc": "Създайте нова общност и я споделете чрез QR код.",
"community_create": "Създай общност",
"community_joinTitle": "Присъедини се към общността",
"community_joinConfirmation": "Искате ли да се присъедините към общността \"{name}\"?",
"community_scanQr": "Сканирайте QR кода на общността",
"community_scanInstructions": "Насочете камерата към QR код на общността",
"community_showQr": "Покажи QR код",
"community_publicChannel": "Обществено общност",
"community_hashtagChannel": "Хаштаг на общността",
"community_name": "Име на общността",
"community_enterName": "Въведете име на общността",
"community_created": "Общността \"{name}\" е създадена",
"community_joined": "Присъединено общност \"{name}\"",
"community_qrTitle": "Споделяне в общността",
"community_join": "Присъедини се",
"community_qrInstructions": "Сканирайте този QR код, за да се присъедините към {name}.",
"community_hashtagPrivacyHint": "Хаштаг каналите на общността са достъпни само за членове на общността",
"community_invalidQrCode": "Невалиден QR код на общността",
"community_alreadyMember": "Вече съм член",
"community_alreadyMemberMessage": "Вие вече сте член на \"{name}\".",
"community_addPublicChannel": "Добави публичен общностен канал",
"community_addPublicChannelHint": "Автоматично добавете публичния канал за тази общност.",
"community_noCommunities": "Няма присъединени общности още.",
"community_scanOrCreate": "Сканирайте QR код или създайте общност, за да започнете.",
"community_manageCommunities": "Управление на общности",
"community_delete": "Напусни общността",
"community_deleteConfirm": "Напускате \"{name}\"?",
"community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Остави общността \"{name}\"",
"community_addHashtagChannel": "Добави общностен хаштаг",
"community_addHashtagChannelDesc": "Добавете хаштаг канал за тази общност",
"community_selectCommunity": "Изберете общност",
"community_regularHashtag": "Обикновен хаштаг",
"community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)",
"community_communityHashtag": "Общностен хаштаг",
"community_communityHashtagDesc": "Само за членове на общността",
"community_forCommunity": "За {name}"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
"settings_locationIntervalInvalid": "Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.",
"contacts_manageRoom": "Raum-Server verwalten",
"room_management": "Raum-Server-Verwaltung"
"room_management": "Raum-Server-Verwaltung",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_create": "Erstelle Community",
"community_createDesc": "Erstelle eine neue Community und teile sie über den QR-Code.",
"community_join": "Beitreten",
"community_joinTitle": "Tritt der Community bei",
"community_joinConfirmation": "Möchten Sie sich der Community \"{name}\" anschließen?",
"community_scanQr": "Scannen Sie die Community QR-Code",
"community_scanInstructions": "Richten Sie die Kamera auf einen Community-QR-Code.",
"community_showQr": "Zeige QR-Code",
"community_publicChannel": "Community Öffentlich",
"community_enterName": "Bitte Community-Name eingeben",
"community_title": "Community",
"community_created": "Community \"{name}\" wurde erstellt",
"community_joined": "Community \"{name}\" beigetreten",
"community_qrTitle": "Teile Community",
"community_qrInstructions": "Scannen Sie diesen QR-Code, um sich \"{name}\" anzuschließen.",
"community_hashtagPrivacyHint": "Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden",
"community_hashtagChannel": "Community Hashtag",
"community_name": "Community Name",
"community_invalidQrCode": "Ungültiger Community-QR-Code",
"community_alreadyMember": "Bereits registriert",
"community_alreadyMemberMessage": "Sie sind bereits Mitglied von \"{name}\".",
"community_addPublicChannel": "Füge einen öffentlichen Community-Kanal hinzu",
"community_addPublicChannelHint": "Automatisch den öffentlichen Kanal für diese Community hinzufügen",
"community_noCommunities": "Noch keiner Community beigetreten",
"community_scanOrCreate": "Scannen Sie einen QR-Code oder eine Community erstellen, um loszulegen.",
"community_manageCommunities": "Verwalten von Communities",
"community_delete": "Verlasse Community",
"community_deleteConfirm": "\"{name}\" verlassen?",
"community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Community \"{name}\" verlassen",
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
"community_selectCommunity": "Wählen Sie Community",
"community_regularHashtag": "Regulärer Hashtag",
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
"community_forCommunity": "Für {name}",
"community_communityHashtag": "Community Hashtag"
}
+113
View File
@@ -8,6 +8,7 @@
"nav_map": "Map",
"common_cancel": "Cancel",
"common_ok": "OK",
"common_connect": "Connect",
"common_unknownDevice": "Unknown Device",
"common_save": "Save",
@@ -1174,6 +1175,118 @@
},
"channelPath_noHopDetailsAvailable": "No hop details available for this packet.",
"channelPath_unknownRepeater": "Unknown Repeater",
"community_title": "Community",
"community_create": "Create Community",
"community_createDesc": "Create a new community and share via QR code.",
"community_join": "Join",
"community_joinTitle": "Join Community",
"community_joinConfirmation": "Do you want to join the community \"{name}\"?",
"@community_joinConfirmation": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_scanQr": "Scan Community QR",
"community_scanInstructions": "Point the camera at a community QR code",
"community_showQr": "Show QR Code",
"community_publicChannel": "Community Public",
"community_hashtagChannel": "Community Hashtag",
"community_name": "Community Name",
"community_enterName": "Enter community name",
"community_created": "Community \"{name}\" created",
"@community_created": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_joined": "Joined community \"{name}\"",
"@community_joined": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_qrTitle": "Share Community",
"community_qrInstructions": "Scan this QR code to join \"{name}\"",
"@community_qrInstructions": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_hashtagPrivacyHint": "Community hashtag channels are only joinable by members of the community",
"community_invalidQrCode": "Invalid community QR code",
"community_alreadyMember": "Already a Member",
"community_alreadyMemberMessage": "You are already a member of \"{name}\".",
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_addPublicChannel": "Add Community Public Channel",
"community_addPublicChannelHint": "Automatically add the public channel for this community",
"community_noCommunities": "No communities joined yet",
"community_scanOrCreate": "Scan a QR code or create a community to get started",
"community_manageCommunities": "Manage Communities",
"community_delete": "Leave Community",
"community_deleteConfirm": "Leave \"{name}\"?",
"@community_deleteConfirm": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_deleteChannelsWarning": "This will also delete {count} channel(s) and their messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Left community \"{name}\"",
"@community_deleted": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_regenerateSecret": "Regenerate Secret",
"community_regenerateSecretConfirm": "Regenerate the secret key for \"{name}\"? All members will need to scan the new QR code to continue communicating.",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_regenerate": "Regenerate",
"community_secretRegenerated": "Secret regenerated for \"{name}\"",
"@community_secretRegenerated": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_updateSecret": "Update Secret",
"community_secretUpdated": "Secret updated for \"{name}\"",
"@community_secretUpdated": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"",
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_addHashtagChannel": "Add Community Hashtag",
"community_addHashtagChannelDesc": "Add a hashtag channel for this community",
"community_selectCommunity": "Select Community",
"community_regularHashtag": "Regular Hashtag",
"community_regularHashtagDesc": "Public hashtag (anyone can join)",
"community_communityHashtag": "Community Hashtag",
"community_communityHashtagDesc": "Private to community members",
"community_forCommunity": "For {name}",
"@community_forCommunity": {
"placeholders": {
"name": {"type": "String"}
}
},
"listFilter_tooltip": "Filter and sort",
"listFilter_sortBy": "Sort by",
"listFilter_latestMessages": "Latest messages",
+101 -1
View File
@@ -1384,5 +1384,105 @@
"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"
"room_management": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Crear Comunidad",
"community_createDesc": "Crear una nueva comunidad y compartir a través de código QR.",
"community_title": "Comunidad",
"community_join": "Únete",
"community_joinTitle": "Únete a la comunidad",
"community_joinConfirmation": "¿Quieres unirte a la comunidad \"{name}\"?",
"community_scanQr": "Escanear Código QR de la Comunidad",
"community_scanInstructions": "Apunte la cámara a un código QR de la comunidad",
"community_showQr": "Mostrar Código QR",
"community_publicChannel": "Comunidad Pública",
"community_hashtagChannel": "Hashtag de la Comunidad",
"community_name": "Nombre de la comunidad",
"common_ok": "De acuerdo",
"community_enterName": "Introducir nombre de comunidad",
"community_created": "Comunidad \"{name}\" creada",
"community_joined": "Se unió a la comunidad \"{name}\"",
"community_qrTitle": "Compartir Comunidad",
"community_qrInstructions": "Escanear este código QR para unirte a {name}",
"community_hashtagPrivacyHint": "Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad",
"community_invalidQrCode": "Código QR de comunidad no válido",
"community_alreadyMember": "Ya eres Miembro",
"community_alreadyMemberMessage": "Ya eres miembro de \"{name}\".",
"community_addPublicChannel": "Añadir Canal Público de la Comunidad",
"community_addPublicChannelHint": "Añade automáticamente el canal público para esta comunidad.",
"community_noCommunities": "Aún no se han unido comunidades.",
"community_scanOrCreate": "Escanear un código QR o crear una comunidad para comenzar",
"community_manageCommunities": "Gestionar Comunidades",
"community_delete": "Salir de la Comunidad",
"community_deleteConfirm": "¿Salir de \"{name}\"?",
"community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Has salido de la comunidad \"{name}\"",
"community_addHashtagChannel": "Añadir Hashtag de la Comunidad",
"community_addHashtagChannelDesc": "Añadir un canal con hashtag para esta comunidad",
"community_selectCommunity": "Seleccionar Comunidad",
"community_regularHashtag": "Etiqueta de Hashtag Regular",
"community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)",
"community_communityHashtag": "Hashtag de la Comunidad",
"community_communityHashtagDesc": "Exclusivo para miembros de la comunidad",
"community_forCommunity": "Para {name}"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"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"
"room_management": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_title": "Communauté",
"community_create": "Créer une Communauté",
"community_createDesc": "Créer une nouvelle communauté et la partager via QR code.",
"community_join": "Rejoindre",
"community_joinTitle": "Rejoindre la communauté",
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
"community_scanQr": "Scanner la communauté QR",
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
"community_showQr": "Afficher le QR Code",
"community_publicChannel": "Communauté Publique",
"community_hashtagChannel": "Hashtag Communauté",
"community_name": "Nom de la communauté",
"community_enterName": "Entrez le nom de la communauté",
"community_created": "Communauté \"{name}\" créée",
"community_joined": "Rejoint la communauté \"{name}\"",
"community_qrTitle": "Partager Communauté",
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
"community_invalidQrCode": "Code QR de communauté non valide",
"community_alreadyMember": "Déjà membre",
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
"community_manageCommunities": "Gérer les Communautés",
"community_delete": "Quitter la communauté",
"community_deleteConfirm": "Quitter \"{name}\" ?",
"community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Communauté \"{name}\" quittée",
"community_addHashtagChannel": "Ajouter un Hashtag Communauté",
"community_addHashtagChannelDesc": "Ajouter un canal hachage pour cette communauté",
"community_selectCommunity": "Sélectionner Communauté",
"community_regularHashtag": "Hashtag régulier",
"community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)",
"community_communityHashtag": "Hashtag de la communauté",
"community_communityHashtagDesc": "Exclusif aux membres de la communauté",
"community_forCommunity": "Pour {name}"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"settings_locationIntervalSec": "Intervallo GPS (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"
"room_management": "Gestione del Server di Camera",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_title": "Comunità",
"community_create": "Crea Comunità",
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
"community_join": "Unisciti",
"community_joinTitle": "Unisciti alla Community",
"community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?",
"community_scanQr": "Scansiona il QR Code della Community",
"community_scanInstructions": "Punta la fotocamera su un codice QR della comunità",
"community_showQr": "Mostra il codice QR",
"community_publicChannel": "Comunità Pubblica",
"community_hashtagChannel": "Hashtag della Comunità",
"community_name": "Nome della Comunità",
"community_enterName": "Inserisci il nome della comunità",
"community_created": "Comunità \"{name}\" creata",
"community_joined": "Unito alla comunità \"{name}\"",
"community_qrTitle": "Condividi Comunità",
"community_qrInstructions": "Scansiona questo codice QR per unirti a {name}",
"community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community",
"community_invalidQrCode": "Codice QR della community non valido",
"community_alreadyMember": "Già membro",
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità",
"community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community",
"community_noCommunities": "Nessun gruppo aggiunto finora",
"community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.",
"community_manageCommunities": "Gestisci Comunità",
"community_delete": "Lascia la Comunità",
"community_deleteConfirm": "Uscire da \"{name}\"?",
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Hai lasciato la comunità \"{name}\"",
"community_addHashtagChannel": "Aggiungi Hashtag della Community",
"community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community",
"community_selectCommunity": "Seleziona Comunità",
"community_regularHashtag": "Hashtag regolare",
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
"community_communityHashtag": "Hashtag della Comunità",
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
"community_forCommunity": "Per {name}"
}
+276
View File
@@ -150,6 +150,12 @@ abstract class AppLocalizations {
/// **'Cancel'**
String get common_cancel;
/// No description provided for @common_ok.
///
/// In en, this message translates to:
/// **'OK'**
String get common_ok;
/// No description provided for @common_connect.
///
/// In en, this message translates to:
@@ -4306,6 +4312,276 @@ abstract class AppLocalizations {
/// **'Unknown Repeater'**
String get channelPath_unknownRepeater;
/// No description provided for @community_title.
///
/// In en, this message translates to:
/// **'Community'**
String get community_title;
/// No description provided for @community_create.
///
/// In en, this message translates to:
/// **'Create Community'**
String get community_create;
/// No description provided for @community_createDesc.
///
/// In en, this message translates to:
/// **'Create a new community and share via QR code.'**
String get community_createDesc;
/// No description provided for @community_join.
///
/// In en, this message translates to:
/// **'Join'**
String get community_join;
/// No description provided for @community_joinTitle.
///
/// In en, this message translates to:
/// **'Join Community'**
String get community_joinTitle;
/// No description provided for @community_joinConfirmation.
///
/// In en, this message translates to:
/// **'Do you want to join the community \"{name}\"?'**
String community_joinConfirmation(String name);
/// No description provided for @community_scanQr.
///
/// In en, this message translates to:
/// **'Scan Community QR'**
String get community_scanQr;
/// No description provided for @community_scanInstructions.
///
/// In en, this message translates to:
/// **'Point the camera at a community QR code'**
String get community_scanInstructions;
/// No description provided for @community_showQr.
///
/// In en, this message translates to:
/// **'Show QR Code'**
String get community_showQr;
/// No description provided for @community_publicChannel.
///
/// In en, this message translates to:
/// **'Community Public'**
String get community_publicChannel;
/// No description provided for @community_hashtagChannel.
///
/// In en, this message translates to:
/// **'Community Hashtag'**
String get community_hashtagChannel;
/// No description provided for @community_name.
///
/// In en, this message translates to:
/// **'Community Name'**
String get community_name;
/// No description provided for @community_enterName.
///
/// In en, this message translates to:
/// **'Enter community name'**
String get community_enterName;
/// No description provided for @community_created.
///
/// In en, this message translates to:
/// **'Community \"{name}\" created'**
String community_created(String name);
/// No description provided for @community_joined.
///
/// In en, this message translates to:
/// **'Joined community \"{name}\"'**
String community_joined(String name);
/// No description provided for @community_qrTitle.
///
/// In en, this message translates to:
/// **'Share Community'**
String get community_qrTitle;
/// No description provided for @community_qrInstructions.
///
/// In en, this message translates to:
/// **'Scan this QR code to join \"{name}\"'**
String community_qrInstructions(String name);
/// No description provided for @community_hashtagPrivacyHint.
///
/// In en, this message translates to:
/// **'Community hashtag channels are only joinable by members of the community'**
String get community_hashtagPrivacyHint;
/// No description provided for @community_invalidQrCode.
///
/// In en, this message translates to:
/// **'Invalid community QR code'**
String get community_invalidQrCode;
/// No description provided for @community_alreadyMember.
///
/// In en, this message translates to:
/// **'Already a Member'**
String get community_alreadyMember;
/// No description provided for @community_alreadyMemberMessage.
///
/// In en, this message translates to:
/// **'You are already a member of \"{name}\".'**
String community_alreadyMemberMessage(String name);
/// No description provided for @community_addPublicChannel.
///
/// In en, this message translates to:
/// **'Add Community Public Channel'**
String get community_addPublicChannel;
/// No description provided for @community_addPublicChannelHint.
///
/// In en, this message translates to:
/// **'Automatically add the public channel for this community'**
String get community_addPublicChannelHint;
/// No description provided for @community_noCommunities.
///
/// In en, this message translates to:
/// **'No communities joined yet'**
String get community_noCommunities;
/// No description provided for @community_scanOrCreate.
///
/// In en, this message translates to:
/// **'Scan a QR code or create a community to get started'**
String get community_scanOrCreate;
/// No description provided for @community_manageCommunities.
///
/// In en, this message translates to:
/// **'Manage Communities'**
String get community_manageCommunities;
/// No description provided for @community_delete.
///
/// In en, this message translates to:
/// **'Leave Community'**
String get community_delete;
/// No description provided for @community_deleteConfirm.
///
/// In en, this message translates to:
/// **'Leave \"{name}\"?'**
String community_deleteConfirm(String name);
/// No description provided for @community_deleteChannelsWarning.
///
/// In en, this message translates to:
/// **'This will also delete {count} channel(s) and their messages.'**
String community_deleteChannelsWarning(int count);
/// No description provided for @community_deleted.
///
/// In en, this message translates to:
/// **'Left community \"{name}\"'**
String community_deleted(String name);
/// No description provided for @community_regenerateSecret.
///
/// In en, this message translates to:
/// **'Regenerate Secret'**
String get community_regenerateSecret;
/// No description provided for @community_regenerateSecretConfirm.
///
/// In en, this message translates to:
/// **'Regenerate the secret key for \"{name}\"? All members will need to scan the new QR code to continue communicating.'**
String community_regenerateSecretConfirm(String name);
/// No description provided for @community_regenerate.
///
/// In en, this message translates to:
/// **'Regenerate'**
String get community_regenerate;
/// No description provided for @community_secretRegenerated.
///
/// In en, this message translates to:
/// **'Secret regenerated for \"{name}\"'**
String community_secretRegenerated(String name);
/// No description provided for @community_updateSecret.
///
/// In en, this message translates to:
/// **'Update Secret'**
String get community_updateSecret;
/// No description provided for @community_secretUpdated.
///
/// In en, this message translates to:
/// **'Secret updated for \"{name}\"'**
String community_secretUpdated(String name);
/// No description provided for @community_scanToUpdateSecret.
///
/// In en, this message translates to:
/// **'Scan the new QR code to update the secret for \"{name}\"'**
String community_scanToUpdateSecret(String name);
/// No description provided for @community_addHashtagChannel.
///
/// In en, this message translates to:
/// **'Add Community Hashtag'**
String get community_addHashtagChannel;
/// No description provided for @community_addHashtagChannelDesc.
///
/// In en, this message translates to:
/// **'Add a hashtag channel for this community'**
String get community_addHashtagChannelDesc;
/// No description provided for @community_selectCommunity.
///
/// In en, this message translates to:
/// **'Select Community'**
String get community_selectCommunity;
/// No description provided for @community_regularHashtag.
///
/// In en, this message translates to:
/// **'Regular Hashtag'**
String get community_regularHashtag;
/// No description provided for @community_regularHashtagDesc.
///
/// In en, this message translates to:
/// **'Public hashtag (anyone can join)'**
String get community_regularHashtagDesc;
/// No description provided for @community_communityHashtag.
///
/// In en, this message translates to:
/// **'Community Hashtag'**
String get community_communityHashtag;
/// No description provided for @community_communityHashtagDesc.
///
/// In en, this message translates to:
/// **'Private to community members'**
String get community_communityHashtagDesc;
/// No description provided for @community_forCommunity.
///
/// In en, this message translates to:
/// **'For {name}'**
String community_forCommunity(String name);
/// No description provided for @listFilter_tooltip.
///
/// In en, this message translates to:
+171
View File
@@ -23,6 +23,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_cancel => 'Отказ';
@override
String get common_ok => 'Добре';
@override
String get common_connect => 'Свържи се';
@@ -2452,6 +2455,174 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Неизвестен повторител';
@override
String get community_title => 'Общност';
@override
String get community_create => 'Създай общност';
@override
String get community_createDesc =>
'Създайте нова общност и я споделете чрез QR код.';
@override
String get community_join => 'Присъедини се';
@override
String get community_joinTitle => 'Присъедини се към общността';
@override
String community_joinConfirmation(String name) {
return 'Искате ли да се присъедините към общността \"$name\"?';
}
@override
String get community_scanQr => 'Сканирайте QR кода на общността';
@override
String get community_scanInstructions =>
'Насочете камерата към QR код на общността';
@override
String get community_showQr => 'Покажи QR код';
@override
String get community_publicChannel => 'Обществено общност';
@override
String get community_hashtagChannel => 'Хаштаг на общността';
@override
String get community_name => 'Име на общността';
@override
String get community_enterName => 'Въведете име на общността';
@override
String community_created(String name) {
return 'Общността \"$name\" е създадена';
}
@override
String community_joined(String name) {
return 'Присъединено общност \"$name\"';
}
@override
String get community_qrTitle => 'Споделяне в общността';
@override
String community_qrInstructions(String name) {
return 'Сканирайте този QR код, за да се присъедините към $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Хаштаг каналите на общността са достъпни само за членове на общността';
@override
String get community_invalidQrCode => 'Невалиден QR код на общността';
@override
String get community_alreadyMember => 'Вече съм член';
@override
String community_alreadyMemberMessage(String name) {
return 'Вие вече сте член на \"$name\".';
}
@override
String get community_addPublicChannel => 'Добави публичен общностен канал';
@override
String get community_addPublicChannelHint =>
'Автоматично добавете публичния канал за тази общност.';
@override
String get community_noCommunities => 'Няма присъединени общности още.';
@override
String get community_scanOrCreate =>
'Сканирайте QR код или създайте общност, за да започнете.';
@override
String get community_manageCommunities => 'Управление на общности';
@override
String get community_delete => 'Напусни общността';
@override
String community_deleteConfirm(String name) {
return 'Напускате \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Това ще изтрие също $count канал(а) и техните съобщения.';
}
@override
String community_deleted(String name) {
return 'Остави общността \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Добави общностен хаштаг';
@override
String get community_addHashtagChannelDesc =>
'Добавете хаштаг канал за тази общност';
@override
String get community_selectCommunity => 'Изберете общност';
@override
String get community_regularHashtag => 'Обикновен хаштаг';
@override
String get community_regularHashtagDesc =>
'Общ хаштаг (всеки може да се присъедини)';
@override
String get community_communityHashtag => 'Общностен хаштаг';
@override
String get community_communityHashtagDesc => 'Само за членове на общността';
@override
String community_forCommunity(String name) {
return 'За $name';
}
@override
String get listFilter_tooltip => 'Филтрирайте и сортирайте';
+174
View File
@@ -23,6 +23,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get common_cancel => 'Abbrechen';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Verbinden';
@@ -2454,6 +2457,177 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Unbekannter Repeater';
@override
String get community_title => 'Community';
@override
String get community_create => 'Erstelle Community';
@override
String get community_createDesc =>
'Erstelle eine neue Community und teile sie über den QR-Code.';
@override
String get community_join => 'Beitreten';
@override
String get community_joinTitle => 'Tritt der Community bei';
@override
String community_joinConfirmation(String name) {
return 'Möchten Sie sich der Community \"$name\" anschließen?';
}
@override
String get community_scanQr => 'Scannen Sie die Community QR-Code';
@override
String get community_scanInstructions =>
'Richten Sie die Kamera auf einen Community-QR-Code.';
@override
String get community_showQr => 'Zeige QR-Code';
@override
String get community_publicChannel => 'Community Öffentlich';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Community Name';
@override
String get community_enterName => 'Bitte Community-Name eingeben';
@override
String community_created(String name) {
return 'Community \"$name\" wurde erstellt';
}
@override
String community_joined(String name) {
return 'Community \"$name\" beigetreten';
}
@override
String get community_qrTitle => 'Teile Community';
@override
String community_qrInstructions(String name) {
return 'Scannen Sie diesen QR-Code, um sich \"$name\" anzuschließen.';
}
@override
String get community_hashtagPrivacyHint =>
'Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden';
@override
String get community_invalidQrCode => 'Ungültiger Community-QR-Code';
@override
String get community_alreadyMember => 'Bereits registriert';
@override
String community_alreadyMemberMessage(String name) {
return 'Sie sind bereits Mitglied von \"$name\".';
}
@override
String get community_addPublicChannel =>
'Füge einen öffentlichen Community-Kanal hinzu';
@override
String get community_addPublicChannelHint =>
'Automatisch den öffentlichen Kanal für diese Community hinzufügen';
@override
String get community_noCommunities => 'Noch keiner Community beigetreten';
@override
String get community_scanOrCreate =>
'Scannen Sie einen QR-Code oder eine Community erstellen, um loszulegen.';
@override
String get community_manageCommunities => 'Verwalten von Communities';
@override
String get community_delete => 'Verlasse Community';
@override
String community_deleteConfirm(String name) {
return '\"$name\" verlassen?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Dies löscht auch $count Kanal/Kanäle und deren Nachrichten.';
}
@override
String community_deleted(String name) {
return 'Community \"$name\" verlassen';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel =>
'Füge einen Community-Hashtag hinzu';
@override
String get community_addHashtagChannelDesc =>
'Füge einen Hashtag-Kanal für diese Community hinzu';
@override
String get community_selectCommunity => 'Wählen Sie Community';
@override
String get community_regularHashtag => 'Regulärer Hashtag';
@override
String get community_regularHashtagDesc =>
'Öffentliches Hashtag (jeder kann teilnehmen)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc =>
'Nur für Mitglieder der Community';
@override
String community_forCommunity(String name) {
return 'Für $name';
}
@override
String get listFilter_tooltip => 'Filteren und sortieren';
+170
View File
@@ -23,6 +23,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_cancel => 'Cancel';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connect';
@@ -2413,6 +2416,173 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Unknown Repeater';
@override
String get community_title => 'Community';
@override
String get community_create => 'Create Community';
@override
String get community_createDesc =>
'Create a new community and share via QR code.';
@override
String get community_join => 'Join';
@override
String get community_joinTitle => 'Join Community';
@override
String community_joinConfirmation(String name) {
return 'Do you want to join the community \"$name\"?';
}
@override
String get community_scanQr => 'Scan Community QR';
@override
String get community_scanInstructions =>
'Point the camera at a community QR code';
@override
String get community_showQr => 'Show QR Code';
@override
String get community_publicChannel => 'Community Public';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Community Name';
@override
String get community_enterName => 'Enter community name';
@override
String community_created(String name) {
return 'Community \"$name\" created';
}
@override
String community_joined(String name) {
return 'Joined community \"$name\"';
}
@override
String get community_qrTitle => 'Share Community';
@override
String community_qrInstructions(String name) {
return 'Scan this QR code to join \"$name\"';
}
@override
String get community_hashtagPrivacyHint =>
'Community hashtag channels are only joinable by members of the community';
@override
String get community_invalidQrCode => 'Invalid community QR code';
@override
String get community_alreadyMember => 'Already a Member';
@override
String community_alreadyMemberMessage(String name) {
return 'You are already a member of \"$name\".';
}
@override
String get community_addPublicChannel => 'Add Community Public Channel';
@override
String get community_addPublicChannelHint =>
'Automatically add the public channel for this community';
@override
String get community_noCommunities => 'No communities joined yet';
@override
String get community_scanOrCreate =>
'Scan a QR code or create a community to get started';
@override
String get community_manageCommunities => 'Manage Communities';
@override
String get community_delete => 'Leave Community';
@override
String community_deleteConfirm(String name) {
return 'Leave \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'This will also delete $count channel(s) and their messages.';
}
@override
String community_deleted(String name) {
return 'Left community \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Add Community Hashtag';
@override
String get community_addHashtagChannelDesc =>
'Add a hashtag channel for this community';
@override
String get community_selectCommunity => 'Select Community';
@override
String get community_regularHashtag => 'Regular Hashtag';
@override
String get community_regularHashtagDesc => 'Public hashtag (anyone can join)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc => 'Private to community members';
@override
String community_forCommunity(String name) {
return 'For $name';
}
@override
String get listFilter_tooltip => 'Filter and sort';
+173
View File
@@ -23,6 +23,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_cancel => 'Cancelar';
@override
String get common_ok => 'De acuerdo';
@override
String get common_connect => 'Conectar';
@@ -2449,6 +2452,176 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Repetidor Desconocido';
@override
String get community_title => 'Comunidad';
@override
String get community_create => 'Crear Comunidad';
@override
String get community_createDesc =>
'Crear una nueva comunidad y compartir a través de código QR.';
@override
String get community_join => 'Únete';
@override
String get community_joinTitle => 'Únete a la comunidad';
@override
String community_joinConfirmation(String name) {
return '¿Quieres unirte a la comunidad \"$name\"?';
}
@override
String get community_scanQr => 'Escanear Código QR de la Comunidad';
@override
String get community_scanInstructions =>
'Apunte la cámara a un código QR de la comunidad';
@override
String get community_showQr => 'Mostrar Código QR';
@override
String get community_publicChannel => 'Comunidad Pública';
@override
String get community_hashtagChannel => 'Hashtag de la Comunidad';
@override
String get community_name => 'Nombre de la comunidad';
@override
String get community_enterName => 'Introducir nombre de comunidad';
@override
String community_created(String name) {
return 'Comunidad \"$name\" creada';
}
@override
String community_joined(String name) {
return 'Se unió a la comunidad \"$name\"';
}
@override
String get community_qrTitle => 'Compartir Comunidad';
@override
String community_qrInstructions(String name) {
return 'Escanear este código QR para unirte a $name';
}
@override
String get community_hashtagPrivacyHint =>
'Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad';
@override
String get community_invalidQrCode => 'Código QR de comunidad no válido';
@override
String get community_alreadyMember => 'Ya eres Miembro';
@override
String community_alreadyMemberMessage(String name) {
return 'Ya eres miembro de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Añadir Canal Público de la Comunidad';
@override
String get community_addPublicChannelHint =>
'Añade automáticamente el canal público para esta comunidad.';
@override
String get community_noCommunities => 'Aún no se han unido comunidades.';
@override
String get community_scanOrCreate =>
'Escanear un código QR o crear una comunidad para comenzar';
@override
String get community_manageCommunities => 'Gestionar Comunidades';
@override
String get community_delete => 'Salir de la Comunidad';
@override
String community_deleteConfirm(String name) {
return '¿Salir de \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Esto también eliminará $count canal(es) y sus mensajes.';
}
@override
String community_deleted(String name) {
return 'Has salido de la comunidad \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Añadir Hashtag de la Comunidad';
@override
String get community_addHashtagChannelDesc =>
'Añadir un canal con hashtag para esta comunidad';
@override
String get community_selectCommunity => 'Seleccionar Comunidad';
@override
String get community_regularHashtag => 'Etiqueta de Hashtag Regular';
@override
String get community_regularHashtagDesc =>
'Hashtag público (cualquiera puede unirse)';
@override
String get community_communityHashtag => 'Hashtag de la Comunidad';
@override
String get community_communityHashtagDesc =>
'Exclusivo para miembros de la comunidad';
@override
String community_forCommunity(String name) {
return 'Para $name';
}
@override
String get listFilter_tooltip => 'Filtrar y ordenar';
+174
View File
@@ -23,6 +23,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get common_cancel => 'Annuler';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connecter';
@@ -2464,6 +2467,177 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Répéteur Inconnu';
@override
String get community_title => 'Communauté';
@override
String get community_create => 'Créer une Communauté';
@override
String get community_createDesc =>
'Créer une nouvelle communauté et la partager via QR code.';
@override
String get community_join => 'Rejoindre';
@override
String get community_joinTitle => 'Rejoindre la communauté';
@override
String community_joinConfirmation(String name) {
return 'Souhaitez-vous rejoindre la communauté \"$name\" ?';
}
@override
String get community_scanQr => 'Scanner la communauté QR';
@override
String get community_scanInstructions =>
'Pointez l\'appareil photo vers un code QR communautaire.';
@override
String get community_showQr => 'Afficher le QR Code';
@override
String get community_publicChannel => 'Communauté Publique';
@override
String get community_hashtagChannel => 'Hashtag Communauté';
@override
String get community_name => 'Nom de la communauté';
@override
String get community_enterName => 'Entrez le nom de la communauté';
@override
String community_created(String name) {
return 'Communauté \"$name\" créée';
}
@override
String community_joined(String name) {
return 'Rejoint la communauté \"$name\"';
}
@override
String get community_qrTitle => 'Partager Communauté';
@override
String community_qrInstructions(String name) {
return 'Scanner ce QR code pour rejoindre $name';
}
@override
String get community_hashtagPrivacyHint =>
'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté';
@override
String get community_invalidQrCode => 'Code QR de communauté non valide';
@override
String get community_alreadyMember => 'Déjà membre';
@override
String community_alreadyMemberMessage(String name) {
return 'Vous êtes déjà membre de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Ajouter un Canal Public de la Communauté';
@override
String get community_addPublicChannelHint =>
'Ajouter automatiquement le canal public pour cette communauté';
@override
String get community_noCommunities =>
'Aucun groupe n\'a été rejoint pour le moment.';
@override
String get community_scanOrCreate =>
'Scanner un code QR ou créer une communauté pour commencer';
@override
String get community_manageCommunities => 'Gérer les Communautés';
@override
String get community_delete => 'Quitter la communauté';
@override
String community_deleteConfirm(String name) {
return 'Quitter \"$name\" ?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Cela supprimera également $count canal/canaux et leurs messages.';
}
@override
String community_deleted(String name) {
return 'Communauté \"$name\" quittée';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Ajouter un Hashtag Communauté';
@override
String get community_addHashtagChannelDesc =>
'Ajouter un canal hachage pour cette communauté';
@override
String get community_selectCommunity => 'Sélectionner Communauté';
@override
String get community_regularHashtag => 'Hashtag régulier';
@override
String get community_regularHashtagDesc =>
'Hashtag public (tout le monde peut rejoindre)';
@override
String get community_communityHashtag => 'Hashtag de la communauté';
@override
String get community_communityHashtagDesc =>
'Exclusif aux membres de la communauté';
@override
String community_forCommunity(String name) {
return 'Pour $name';
}
@override
String get listFilter_tooltip => 'Filtrer et trier';
+173
View File
@@ -23,6 +23,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_cancel => 'Annulla';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connetti';
@@ -2449,6 +2452,176 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Ripetitore sconosciuto';
@override
String get community_title => 'Comunità';
@override
String get community_create => 'Crea Comunità';
@override
String get community_createDesc =>
'Crea una nuova comunità e condividila tramite codice QR.';
@override
String get community_join => 'Unisciti';
@override
String get community_joinTitle => 'Unisciti alla Community';
@override
String community_joinConfirmation(String name) {
return 'Vuoi unirti alla community \"$name\"?';
}
@override
String get community_scanQr => 'Scansiona il QR Code della Community';
@override
String get community_scanInstructions =>
'Punta la fotocamera su un codice QR della comunità';
@override
String get community_showQr => 'Mostra il codice QR';
@override
String get community_publicChannel => 'Comunità Pubblica';
@override
String get community_hashtagChannel => 'Hashtag della Comunità';
@override
String get community_name => 'Nome della Comunità';
@override
String get community_enterName => 'Inserisci il nome della comunità';
@override
String community_created(String name) {
return 'Comunità \"$name\" creata';
}
@override
String community_joined(String name) {
return 'Unito alla comunità \"$name\"';
}
@override
String get community_qrTitle => 'Condividi Comunità';
@override
String community_qrInstructions(String name) {
return 'Scansiona questo codice QR per unirti a $name';
}
@override
String get community_hashtagPrivacyHint =>
'I canali hashtag della community sono accessibili solo ai membri della community';
@override
String get community_invalidQrCode => 'Codice QR della community non valido';
@override
String get community_alreadyMember => 'Già membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Sei già un membro di \"$name\".';
}
@override
String get community_addPublicChannel =>
'Aggiungi Canale Pubblico della Comunità';
@override
String get community_addPublicChannelHint =>
'Aggiungi automaticamente il canale pubblico per questa community';
@override
String get community_noCommunities => 'Nessun gruppo aggiunto finora';
@override
String get community_scanOrCreate =>
'Scansiona un codice QR o crea una community per iniziare.';
@override
String get community_manageCommunities => 'Gestisci Comunità';
@override
String get community_delete => 'Lascia la Comunità';
@override
String community_deleteConfirm(String name) {
return 'Uscire da \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Questo eliminerà anche $count canale/i e i loro messaggi.';
}
@override
String community_deleted(String name) {
return 'Hai lasciato la comunità \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Aggiungi Hashtag della Community';
@override
String get community_addHashtagChannelDesc =>
'Aggiungi un canale con hashtag per questa community';
@override
String get community_selectCommunity => 'Seleziona Comunità';
@override
String get community_regularHashtag => 'Hashtag regolare';
@override
String get community_regularHashtagDesc =>
'Hashtag pubblico (chiunque può unirsi)';
@override
String get community_communityHashtag => 'Hashtag della Comunità';
@override
String get community_communityHashtagDesc =>
'Visibile solo ai membri della comunità';
@override
String community_forCommunity(String name) {
return 'Per $name';
}
@override
String get listFilter_tooltip => 'Filtra e ordina';
+174
View File
@@ -23,6 +23,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get common_cancel => 'Annuleren';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Verbinden';
@@ -2439,6 +2442,177 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Onbekend Repeater';
@override
String get community_title => 'Gemeenschap';
@override
String get community_create => 'Maak Gemeenschap';
@override
String get community_createDesc =>
'Maak een nieuwe community en deel deze via QR-code.';
@override
String get community_join => 'Sluit aan';
@override
String get community_joinTitle => 'Worden lid van de community';
@override
String community_joinConfirmation(String name) {
return 'Wil je je aansluiten bij de community \"$name\"?';
}
@override
String get community_scanQr => 'Scan Gemeenschap QR';
@override
String get community_scanInstructions =>
'Richt de camera op een gemeenschappelijke QR-code';
@override
String get community_showQr => 'Toon QR-code';
@override
String get community_publicChannel => 'Gemeenschap Openbaar';
@override
String get community_hashtagChannel => 'Gemeenschappelijk Hashtag';
@override
String get community_name => 'Gemeenschapnaam';
@override
String get community_enterName => 'Voer de gemeenschapsnaam in';
@override
String community_created(String name) {
return 'Gemeenschap \"$name\" is aangemaakt';
}
@override
String community_joined(String name) {
return 'Gevonden in de community \"$name\"';
}
@override
String get community_qrTitle => 'Deel Gemeenschap';
@override
String community_qrInstructions(String name) {
return 'Scan deze QR-code om je aan te sluiten bij $name';
}
@override
String get community_hashtagPrivacyHint =>
'Community hashtag-kanalen zijn alleen toegankelijk voor leden van de community';
@override
String get community_invalidQrCode => 'Ongeldige community QR-code';
@override
String get community_alreadyMember => 'Alleen al lid';
@override
String community_alreadyMemberMessage(String name) {
return 'U bent al lid van \"$name\".';
}
@override
String get community_addPublicChannel =>
'Voeg een Openbaar Gemeenschapskanaal toe';
@override
String get community_addPublicChannelHint =>
'Automatisch de publieke kanaal toevoegen voor deze community';
@override
String get community_noCommunities =>
'Nog geen gemeenschappen zijn bijgesloten.';
@override
String get community_scanOrCreate =>
'Scan een QR-code of een community aanmaken om te beginnen';
@override
String get community_manageCommunities => 'Beheer Gemeenschappen';
@override
String get community_delete => 'Laat Gemeenschap';
@override
String community_deleteConfirm(String name) {
return '\"$name\" verlaten?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Dit verwijdert ook $count kanaal/kanalen en hun berichten.';
}
@override
String community_deleted(String name) {
return 'Community \"$name\" verlaten';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Voeg Community Hashtag toe';
@override
String get community_addHashtagChannelDesc =>
'Voeg een hashtag-kanaal toe aan deze community';
@override
String get community_selectCommunity => 'Selecteer Gemeenschap';
@override
String get community_regularHashtag => 'Gewone Hashtag';
@override
String get community_regularHashtagDesc =>
'Open hashtag (iedereen kan deelnemen)';
@override
String get community_communityHashtag => 'Gemeenschappelijk Hashtag';
@override
String get community_communityHashtagDesc =>
'Alleen zichtbaar voor leden van de community';
@override
String community_forCommunity(String name) {
return 'Voor $name';
}
@override
String get listFilter_tooltip => 'Filteren en sorteren';
+173
View File
@@ -23,6 +23,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get common_cancel => 'Anuluj';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Połącz';
@@ -2448,6 +2451,176 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Nieznany Powtarzacz';
@override
String get community_title => 'Społeczność';
@override
String get community_create => 'Utwórz Społeczność';
@override
String get community_createDesc =>
'Utwórz nową społeczność i udostępnij za pomocą kodu QR.';
@override
String get community_join => 'Dołącz';
@override
String get community_joinTitle => 'Dołącz do społeczności';
@override
String community_joinConfirmation(String name) {
return 'Czy chcesz dołączyć do społeczności \"$name\"?';
}
@override
String get community_scanQr => 'Skanuj QR kod społeczności';
@override
String get community_scanInstructions =>
'Skieruj kamerę w kierunku kodu QR społeczności.';
@override
String get community_showQr => 'Pokaż kod QR';
@override
String get community_publicChannel => 'Społeczność Publiczna';
@override
String get community_hashtagChannel => 'Hashtag Społeczności';
@override
String get community_name => 'Nazwa Społeczności';
@override
String get community_enterName => 'Wprowadź nazwę społeczności';
@override
String community_created(String name) {
return 'Społeczność \"$name\" została utworzona';
}
@override
String community_joined(String name) {
return 'Dołączył do społeczności \"$name\"';
}
@override
String get community_qrTitle => 'Dziel się Społecznością';
@override
String community_qrInstructions(String name) {
return 'Skanuj ten kod QR, aby dołączyć $name';
}
@override
String get community_hashtagPrivacyHint =>
'Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności';
@override
String get community_invalidQrCode => 'Nieprawidłowy kod QR społeczności.';
@override
String get community_alreadyMember => 'Już jesteś członkiem.';
@override
String community_alreadyMemberMessage(String name) {
return 'Jesteś już członkiem \"$name\".';
}
@override
String get community_addPublicChannel => 'Dodaj Kanał Publiczny Społeczności';
@override
String get community_addPublicChannelHint =>
'Automatycznie dodaj kanał publiczny dla tej społeczności.';
@override
String get community_noCommunities =>
'Nie dołączono jeszcze żadnych społeczności.';
@override
String get community_scanOrCreate =>
'Skanuj kod QR lub utwórz społeczność, aby zacząć.';
@override
String get community_manageCommunities => 'Zarządzaj Grupami';
@override
String get community_delete => 'Opuszczenie Społeczności';
@override
String community_deleteConfirm(String name) {
return 'Opuścić \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Spowoduje to również usunięcie $count kanału/kanałów i ich wiadomości.';
}
@override
String community_deleted(String name) {
return 'Opuszczono społeczność \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Dodaj hashtag społeczności';
@override
String get community_addHashtagChannelDesc =>
'Dodaj kanał z hashtagiem dla tej społeczności';
@override
String get community_selectCommunity => 'Wybierz społeczność';
@override
String get community_regularHashtag => 'Hashtag regular';
@override
String get community_regularHashtagDesc =>
'Publiczny hashtag (każdy może dołączyć)';
@override
String get community_communityHashtag => 'Hashtag Społeczności';
@override
String get community_communityHashtagDesc =>
'Dostępne tylko dla członków społeczności';
@override
String community_forCommunity(String name) {
return 'Dla $name';
}
@override
String get listFilter_tooltip => 'Filtruj i sortuj';
+174
View File
@@ -23,6 +23,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_cancel => 'Cancelar';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Conectar';
@@ -2450,6 +2453,177 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Repetidor Desconhecido';
@override
String get community_title => 'Comunidade';
@override
String get community_create => 'Criar Comunidade';
@override
String get community_createDesc =>
'Crie uma nova comunidade e compartilhe via código QR.';
@override
String get community_join => 'Junte-se';
@override
String get community_joinTitle => 'Junte-se à Comunidade';
@override
String community_joinConfirmation(String name) {
return 'Você gostaria de se juntar à comunidade \"$name\"?';
}
@override
String get community_scanQr => 'Digitalizar a QR Code da Comunidade';
@override
String get community_scanInstructions =>
'Aponte a câmera para um código QR da comunidade';
@override
String get community_showQr => 'Mostrar Código QR';
@override
String get community_publicChannel => 'Comunidade Pública';
@override
String get community_hashtagChannel => 'Hashtag da Comunidade';
@override
String get community_name => 'Nome da Comunidade';
@override
String get community_enterName => 'Insira o nome da comunidade';
@override
String community_created(String name) {
return 'Comunidade \"$name\" criada';
}
@override
String community_joined(String name) {
return 'Juntou-se à comunidade \"$name\"';
}
@override
String get community_qrTitle => 'Partilhar Comunidade';
@override
String community_qrInstructions(String name) {
return 'Escanear este código QR para juntar-se a $name';
}
@override
String get community_hashtagPrivacyHint =>
'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade';
@override
String get community_invalidQrCode => 'Código QR da comunidade inválido';
@override
String get community_alreadyMember => 'Já é Membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Você já é membro de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Adicionar Canal Público da Comunidade';
@override
String get community_addPublicChannelHint =>
'Adicionar automaticamente o canal público para esta comunidade';
@override
String get community_noCommunities =>
'Ainda não foram adicionadas comunidades.';
@override
String get community_scanOrCreate =>
'Escaneie um código QR ou crie uma comunidade para começar.';
@override
String get community_manageCommunities => 'Gerenciar Comunidades';
@override
String get community_delete => 'Deixar Comunidade';
@override
String community_deleteConfirm(String name) {
return 'Sair de \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Isso também excluirá $count canal/canais e suas mensagens.';
}
@override
String community_deleted(String name) {
return 'Saiu da comunidade \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Adicionar Hashtag da Comunidade';
@override
String get community_addHashtagChannelDesc =>
'Adicionar um canal de hashtag para esta comunidade';
@override
String get community_selectCommunity => 'Selecione Comunidade';
@override
String get community_regularHashtag => 'Hashtag Regular';
@override
String get community_regularHashtagDesc =>
'Hashtag público (qualquer pessoa pode participar)';
@override
String get community_communityHashtag => 'Hashtag da Comunidade';
@override
String get community_communityHashtagDesc =>
'Apenas para membros da comunidade';
@override
String community_forCommunity(String name) {
return 'Para $name';
}
@override
String get listFilter_tooltip => 'Filtrar e ordenar';
+172
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_cancel => 'Zrušiť';
@override
String get common_ok => 'OK\nDobre';
@override
String get common_connect => 'Pripojiť';
@@ -2437,6 +2440,175 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Neznáme opakovače';
@override
String get community_title => 'Komunita';
@override
String get community_create => 'Vytvoriť komunitu';
@override
String get community_createDesc =>
'Vytvorte novú komunitu a zdieľajte cez QR kód.';
@override
String get community_join => 'Pripojiť';
@override
String get community_joinTitle => 'Pripojiť sa k spoločenstvu';
@override
String community_joinConfirmation(String name) {
return 'Chceš sa pridať do komunity \"$name\"?';
}
@override
String get community_scanQr => 'Skontrolujte komunitný QR kód';
@override
String get community_scanInstructions =>
'Zamerte kameru na komunitný QR kód.';
@override
String get community_showQr => 'Zobraziť QR kód';
@override
String get community_publicChannel => 'Komunita verejná';
@override
String get community_hashtagChannel => 'Komunitný Hashtag';
@override
String get community_name => 'Komunita';
@override
String get community_enterName => 'Zadajte názov komunity';
@override
String community_created(String name) {
return 'Komunita \"$name\" vytvorená';
}
@override
String community_joined(String name) {
return 'Pripojená komunita \"$name\"';
}
@override
String get community_qrTitle => 'Zdieľť komunitu';
@override
String community_qrInstructions(String name) {
return 'Skenejte tento QR kód, aby ste sa pripojili k $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Hashtagové kanály komunity sú prístupné len členom komunity';
@override
String get community_invalidQrCode => 'Neplatná QR kód komunity.';
@override
String get community_alreadyMember => 'Už ste členom.';
@override
String community_alreadyMemberMessage(String name) {
return 'Vy ste už členom \"$name\".';
}
@override
String get community_addPublicChannel => 'Pridať verejný komunikačný kanál';
@override
String get community_addPublicChannelHint =>
'Automaticky prida verejný kanál pre túto komunitu.';
@override
String get community_noCommunities =>
'Zatiaľ ste sa nepripojili k žiadnej komunite';
@override
String get community_scanOrCreate =>
'Skene QR kód alebo vytvor komunitu na začiatok.';
@override
String get community_manageCommunities => 'Spravovať komunity';
@override
String get community_delete => 'Nechajte komunitu';
@override
String community_deleteConfirm(String name) {
return 'Opustiť \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Tým sa tiež vymaže $count kanál/kanálov a ich správy.';
}
@override
String community_deleted(String name) {
return 'Opustená komunita \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Pridať komunitný hashtag';
@override
String get community_addHashtagChannelDesc =>
'Pridajte hashtagový kanál pre túto komunitu.';
@override
String get community_selectCommunity => 'Vyberte komunitu';
@override
String get community_regularHashtag => 'Zvyčajný hashtag';
@override
String get community_regularHashtagDesc =>
'Veľký hashtag (ktočokoľvek sa môže pridať)';
@override
String get community_communityHashtag => 'Komunitný Hashtag';
@override
String get community_communityHashtagDesc => 'Špecifické pre členov komunity';
@override
String community_forCommunity(String name) {
return 'Pre $name';
}
@override
String get listFilter_tooltip => 'Filtrovať a triediť';
+172
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_cancel => 'Prekliči';
@override
String get common_ok => 'V redu';
@override
String get common_connect => 'Poveži se';
@@ -2442,6 +2445,175 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Nepoznati ponovitelj';
@override
String get community_title => 'Skupnost';
@override
String get community_create => 'Ustvari skupnost';
@override
String get community_createDesc =>
'Ustvari novo skupnost in jo deli preko QR kode.';
@override
String get community_join => 'Pridružiti se';
@override
String get community_joinTitle => 'Pridružite se skupnosti';
@override
String community_joinConfirmation(String name) {
return 'Želiš se pridružiti skupnosti \"$name\"?';
}
@override
String get community_scanQr => 'Skeniraj QR kode skupnosti';
@override
String get community_scanInstructions =>
'Nasmerite kamero s skupnostnim QR kodom.';
@override
String get community_showQr => 'Pokaži QR kodo';
@override
String get community_publicChannel => 'Skupnostna javna';
@override
String get community_hashtagChannel => 'Skupnostni hashtag';
@override
String get community_name => 'Komunitarne ime';
@override
String get community_enterName => 'Vnesite ime skupnosti';
@override
String community_created(String name) {
return 'Skupnost \"$name\" je bila ustvarila.';
}
@override
String community_joined(String name) {
return 'Prilojen k skupnosti \"$name\"';
}
@override
String get community_qrTitle => 'Delite skupnost';
@override
String community_qrInstructions(String name) {
return 'Skenirajte to QR kodo za vključitev $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Hashtag kanali skupnosti so dostopni samo članom skupnosti';
@override
String get community_invalidQrCode => 'Neveljaven QR koden skupnosti';
@override
String get community_alreadyMember => 'Že član';
@override
String community_alreadyMemberMessage(String name) {
return 'Kljub temu ste že član/ka $name.';
}
@override
String get community_addPublicChannel => 'Dodaj Objavni Kanal Komunitarja';
@override
String get community_addPublicChannelHint =>
'Samodejno dodaj javni kanal za to skupnost.';
@override
String get community_noCommunities => 'Še nobena skupnost se ni pridružila.';
@override
String get community_scanOrCreate =>
'Skenirajte QR kodo ali ustvarite skupnost za začetek.';
@override
String get community_manageCommunities => 'Upravljajte skupnosti';
@override
String get community_delete => 'Opusti skupnost';
@override
String community_deleteConfirm(String name) {
return 'Zapustiti \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'To bo izbrisalo tudi $count kanal/kanalov in njihova sporočila.';
}
@override
String community_deleted(String name) {
return 'Zapustil skupnost \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Dodaj Oznako Obštnine';
@override
String get community_addHashtagChannelDesc =>
'Dodajte hashtag kanal za to skupnost.';
@override
String get community_selectCommunity => 'Izberi skupnost';
@override
String get community_regularHashtag => 'Oznaka s hashtagom';
@override
String get community_regularHashtagDesc =>
'javna oznaka (kateri koli lahko sodelujejo)';
@override
String get community_communityHashtag => 'Skupnostni hashtag';
@override
String get community_communityHashtagDesc =>
'Izključeno za uporabnike skupnosti';
@override
String community_forCommunity(String name) {
return 'Za $name';
}
@override
String get listFilter_tooltip => 'Filtri in vrstiči';
+172
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_cancel => 'Avbryt';
@override
String get common_ok => 'Okej';
@override
String get common_connect => 'Anslut';
@@ -2425,6 +2428,175 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Okänt Upprepare';
@override
String get community_title => 'Gemenskap';
@override
String get community_create => 'Skapa Gemenskap';
@override
String get community_createDesc =>
'Skapa en ny gemenskap och dela via QR-kod.';
@override
String get community_join => 'Gå med';
@override
String get community_joinTitle => 'Gå med i gemenskapen';
@override
String community_joinConfirmation(String name) {
return 'Vill du gå med i communityn \"$name\"?';
}
@override
String get community_scanQr => 'Skanna Gemenskapens QR';
@override
String get community_scanInstructions =>
'Rikta kameran mot en QR-kod i communityn';
@override
String get community_showQr => 'Visa QR-kod';
@override
String get community_publicChannel => 'Föreningens Offentliga';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Gemenskapens namn';
@override
String get community_enterName => 'Ange communities namn';
@override
String community_created(String name) {
return 'Community \"$name\" har skapats';
}
@override
String community_joined(String name) {
return 'Medlem i communityn \"$name\"';
}
@override
String get community_qrTitle => 'Dela Gemenskap';
@override
String community_qrInstructions(String name) {
return 'Skanna denna QR-kod för att gå med i \"$name\"';
}
@override
String get community_hashtagPrivacyHint =>
'Community-hashtagkanaler kan endast nås av medlemmar i communityn';
@override
String get community_invalidQrCode => 'Ogiltig community QR-kod';
@override
String get community_alreadyMember => 'Är redan medlem';
@override
String community_alreadyMemberMessage(String name) {
return 'Du är redan medlem av \"$name\".';
}
@override
String get community_addPublicChannel =>
'Lägg till Gemenskapskanal (Offentlig)';
@override
String get community_addPublicChannelHint =>
'Lägg automatiskt till den offentliga kanalen för denna community';
@override
String get community_noCommunities => 'Inga gemenskaper har anslutats ännu';
@override
String get community_scanOrCreate =>
'Skanna en QR-kod eller skapa en community för att komma igång';
@override
String get community_manageCommunities => 'Hantera Gemenskaper';
@override
String get community_delete => 'Lämna Gemenskap';
@override
String community_deleteConfirm(String name) {
return 'Lämna \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Detta kommer också att radera $count kanal/kanaler och deras meddelanden.';
}
@override
String community_deleted(String name) {
return 'Lämnade community \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Lägg till Gemenskapens Hashtag';
@override
String get community_addHashtagChannelDesc =>
'Lägg till en hashtag-kanal för denna community';
@override
String get community_selectCommunity => 'Välj Gemenskap';
@override
String get community_regularHashtag => 'Vanlig Hash Tag';
@override
String get community_regularHashtagDesc =>
'Offentlig hashtag (alla kan gå med)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc => 'Endast för medlemmar';
@override
String community_forCommunity(String name) {
return 'För $name';
}
@override
String get listFilter_tooltip => 'Filtrera och sortera';
+164
View File
@@ -23,6 +23,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get common_cancel => '取消';
@override
String get common_ok => '好的';
@override
String get common_connect => '连接';
@@ -2315,6 +2318,167 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get channelPath_unknownRepeater => '未知重复器';
@override
String get community_title => '社区';
@override
String get community_create => '创建社区';
@override
String get community_createDesc => '创建新的社区并可通过二维码分享。';
@override
String get community_join => '加入';
@override
String get community_joinTitle => '加入社区';
@override
String community_joinConfirmation(String name) {
return '您想加入社区 \"$name\" 吗?';
}
@override
String get community_scanQr => '扫描社区二维码';
@override
String get community_scanInstructions => '将相机对准社区二维码';
@override
String get community_showQr => '显示二维码';
@override
String get community_publicChannel => '社区公开';
@override
String get community_hashtagChannel => '社区标签';
@override
String get community_name => '社区名称';
@override
String get community_enterName => '请输入社区名称';
@override
String community_created(String name) {
return '社区“$name”已创建';
}
@override
String community_joined(String name) {
return '加入社区 \"$name\"';
}
@override
String get community_qrTitle => '分享社区';
@override
String community_qrInstructions(String name) {
return '扫描此二维码加入$name';
}
@override
String get community_hashtagPrivacyHint => '社区标签频道仅社区成员可加入';
@override
String get community_invalidQrCode => '无效的社区二维码';
@override
String get community_alreadyMember => '已经是会员了';
@override
String community_alreadyMemberMessage(String name) {
return '您已经是 \"$name\" 的会员。';
}
@override
String get community_addPublicChannel => '添加社区公共频道';
@override
String get community_addPublicChannelHint => '自动添加该社区的公共频道';
@override
String get community_noCommunities => '尚未加入任何社区';
@override
String get community_scanOrCreate => '扫描二维码或创建社区开始';
@override
String get community_manageCommunities => '管理社群';
@override
String get community_delete => '退出社区';
@override
String community_deleteConfirm(String name) {
return '退出 \"$name\"';
}
@override
String community_deleteChannelsWarning(int count) {
return '这也将删除 $count 个频道及其消息。';
}
@override
String community_deleted(String name) {
return '已退出社区 \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => '添加社区标签';
@override
String get community_addHashtagChannelDesc => '添加一个话题频道给此社区';
@override
String get community_selectCommunity => '选择社区';
@override
String get community_regularHashtag => '常规话题标签';
@override
String get community_regularHashtagDesc => '公共话题(任何人都可以加入)';
@override
String get community_communityHashtag => '社区标签';
@override
String get community_communityHashtagDesc => '仅限社区成员使用';
@override
String community_forCommunity(String name) {
return '对于 $name';
}
@override
String get listFilter_tooltip => '筛选和排序';
+101 -1
View File
@@ -1384,5 +1384,105 @@
"settings_locationIntervalSec": "Interval voor GPS (Seconden)",
"settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.",
"contacts_manageRoom": "Beheer Ruimte Server",
"room_management": "Beheer Server Kamer"
"room_management": "Beheer Server Kamer",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Gemeenschap",
"common_ok": "OK",
"community_createDesc": "Maak een nieuwe community en deel deze via QR-code.",
"community_create": "Maak Gemeenschap",
"community_join": "Sluit aan",
"community_joinTitle": "Worden lid van de community",
"community_joinConfirmation": "Wil je je aansluiten bij de community \"{name}\"?",
"community_scanQr": "Scan Gemeenschap QR",
"community_scanInstructions": "Richt de camera op een gemeenschappelijke QR-code",
"community_showQr": "Toon QR-code",
"community_publicChannel": "Gemeenschap Openbaar",
"community_hashtagChannel": "Gemeenschappelijk Hashtag",
"community_name": "Gemeenschapnaam",
"community_enterName": "Voer de gemeenschapsnaam in",
"community_created": "Gemeenschap \"{name}\" is aangemaakt",
"community_joined": "Gevonden in de community \"{name}\"",
"community_qrTitle": "Deel Gemeenschap",
"community_qrInstructions": "Scan deze QR-code om je aan te sluiten bij {name}",
"community_hashtagPrivacyHint": "Community hashtag-kanalen zijn alleen toegankelijk voor leden van de community",
"community_invalidQrCode": "Ongeldige community QR-code",
"community_alreadyMember": "Alleen al lid",
"community_alreadyMemberMessage": "U bent al lid van \"{name}\".",
"community_addPublicChannel": "Voeg een Openbaar Gemeenschapskanaal toe",
"community_addPublicChannelHint": "Automatisch de publieke kanaal toevoegen voor deze community",
"community_noCommunities": "Nog geen gemeenschappen zijn bijgesloten.",
"community_scanOrCreate": "Scan een QR-code of een community aanmaken om te beginnen",
"community_manageCommunities": "Beheer Gemeenschappen",
"community_delete": "Laat Gemeenschap",
"community_deleteConfirm": "\"{name}\" verlaten?",
"community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Community \"{name}\" verlaten",
"community_addHashtagChannel": "Voeg Community Hashtag toe",
"community_addHashtagChannelDesc": "Voeg een hashtag-kanaal toe aan deze community",
"community_selectCommunity": "Selecteer Gemeenschap",
"community_regularHashtag": "Gewone Hashtag",
"community_regularHashtagDesc": "Open hashtag (iedereen kan deelnemen)",
"community_communityHashtag": "Gemeenschappelijk Hashtag",
"community_communityHashtagDesc": "Alleen zichtbaar voor leden van de community",
"community_forCommunity": "Voor {name}"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"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"
"room_management": "Zarządzanie Serwerem Pokoju",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_createDesc": "Utwórz nową społeczność i udostępnij za pomocą kodu QR.",
"community_title": "Społeczność",
"community_create": "Utwórz Społeczność",
"common_ok": "OK",
"community_join": "Dołącz",
"community_joinTitle": "Dołącz do społeczności",
"community_joinConfirmation": "Czy chcesz dołączyć do społeczności \"{name}\"?",
"community_scanQr": "Skanuj QR kod społeczności",
"community_scanInstructions": "Skieruj kamerę w kierunku kodu QR społeczności.",
"community_showQr": "Pokaż kod QR",
"community_publicChannel": "Społeczność Publiczna",
"community_hashtagChannel": "Hashtag Społeczności",
"community_name": "Nazwa Społeczności",
"community_enterName": "Wprowadź nazwę społeczności",
"community_created": "Społeczność \"{name}\" została utworzona",
"community_joined": "Dołączył do społeczności \"{name}\"",
"community_qrTitle": "Dziel się Społecznością",
"community_qrInstructions": "Skanuj ten kod QR, aby dołączyć {name}",
"community_hashtagPrivacyHint": "Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności",
"community_invalidQrCode": "Nieprawidłowy kod QR społeczności.",
"community_alreadyMember": "Już jesteś członkiem.",
"community_alreadyMemberMessage": "Jesteś już członkiem \"{name}\".",
"community_addPublicChannel": "Dodaj Kanał Publiczny Społeczności",
"community_addPublicChannelHint": "Automatycznie dodaj kanał publiczny dla tej społeczności.",
"community_noCommunities": "Nie dołączono jeszcze żadnych społeczności.",
"community_scanOrCreate": "Skanuj kod QR lub utwórz społeczność, aby zacząć.",
"community_manageCommunities": "Zarządzaj Grupami",
"community_delete": "Opuszczenie Społeczności",
"community_deleteConfirm": "Opuścić \"{name}\"?",
"community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Opuszczono społeczność \"{name}\"",
"community_addHashtagChannel": "Dodaj hashtag społeczności",
"community_addHashtagChannelDesc": "Dodaj kanał z hashtagiem dla tej społeczności",
"community_selectCommunity": "Wybierz społeczność",
"community_regularHashtag": "Hashtag regular",
"community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)",
"community_communityHashtag": "Hashtag Społeczności",
"community_communityHashtagDesc": "Dostępne tylko dla członków społeczności",
"community_forCommunity": "Dla {name}"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"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"
"room_management": "Gerenciamento de Servidor de Sala",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Comunidade",
"community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.",
"common_ok": "OK",
"community_create": "Criar Comunidade",
"community_join": "Junte-se",
"community_joinTitle": "Junte-se à Comunidade",
"community_joinConfirmation": "Você gostaria de se juntar à comunidade \"{name}\"?",
"community_scanQr": "Digitalizar a QR Code da Comunidade",
"community_scanInstructions": "Aponte a câmera para um código QR da comunidade",
"community_showQr": "Mostrar Código QR",
"community_publicChannel": "Comunidade Pública",
"community_hashtagChannel": "Hashtag da Comunidade",
"community_name": "Nome da Comunidade",
"community_enterName": "Insira o nome da comunidade",
"community_created": "Comunidade \"{name}\" criada",
"community_joined": "Juntou-se à comunidade \"{name}\"",
"community_qrTitle": "Partilhar Comunidade",
"community_qrInstructions": "Escanear este código QR para juntar-se a {name}",
"community_hashtagPrivacyHint": "Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade",
"community_invalidQrCode": "Código QR da comunidade inválido",
"community_alreadyMember": "Já é Membro",
"community_alreadyMemberMessage": "Você já é membro de \"{name}\".",
"community_addPublicChannel": "Adicionar Canal Público da Comunidade",
"community_addPublicChannelHint": "Adicionar automaticamente o canal público para esta comunidade",
"community_noCommunities": "Ainda não foram adicionadas comunidades.",
"community_scanOrCreate": "Escaneie um código QR ou crie uma comunidade para começar.",
"community_manageCommunities": "Gerenciar Comunidades",
"community_delete": "Deixar Comunidade",
"community_deleteConfirm": "Sair de \"{name}\"?",
"community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Saiu da comunidade \"{name}\"",
"community_addHashtagChannel": "Adicionar Hashtag da Comunidade",
"community_addHashtagChannelDesc": "Adicionar um canal de hashtag para esta comunidade",
"community_selectCommunity": "Selecione Comunidade",
"community_regularHashtag": "Hashtag Regular",
"community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)",
"community_communityHashtag": "Hashtag da Comunidade",
"community_communityHashtagDesc": "Apenas para membros da comunidade",
"community_forCommunity": "Para {name}"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"settings_locationIntervalSec": "Interval pre GPS (Sekundy)",
"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"
"room_management": "Správa servera miestnosti",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Vytvoriť komunitu",
"community_title": "Komunita",
"community_createDesc": "Vytvorte novú komunitu a zdieľajte cez QR kód.",
"community_join": "Pripojiť",
"community_joinTitle": "Pripojiť sa k spoločenstvu",
"community_joinConfirmation": "Chceš sa pridať do komunity \"{name}\"?",
"community_scanQr": "Skontrolujte komunitný QR kód",
"community_scanInstructions": "Zamerte kameru na komunitný QR kód.",
"community_showQr": "Zobraziť QR kód",
"common_ok": "OK\nDobre",
"community_publicChannel": "Komunita verejná",
"community_hashtagChannel": "Komunitný Hashtag",
"community_name": "Komunita",
"community_enterName": "Zadajte názov komunity",
"community_created": "Komunita \"{name}\" vytvorená",
"community_joined": "Pripojená komunita \"{name}\"",
"community_qrTitle": "Zdieľť komunitu",
"community_qrInstructions": "Skenejte tento QR kód, aby ste sa pripojili k {name}.",
"community_hashtagPrivacyHint": "Hashtagové kanály komunity sú prístupné len členom komunity",
"community_invalidQrCode": "Neplatná QR kód komunity.",
"community_alreadyMember": "Už ste členom.",
"community_alreadyMemberMessage": "Vy ste už členom \"{name}\".",
"community_addPublicChannel": "Pridať verejný komunikačný kanál",
"community_addPublicChannelHint": "Automaticky prida verejný kanál pre túto komunitu.",
"community_noCommunities": "Zatiaľ ste sa nepripojili k žiadnej komunite",
"community_scanOrCreate": "Skene QR kód alebo vytvor komunitu na začiatok.",
"community_manageCommunities": "Spravovať komunity",
"community_delete": "Nechajte komunitu",
"community_deleteConfirm": "Opustiť \"{name}\"?",
"community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Opustená komunita \"{name}\"",
"community_addHashtagChannel": "Pridať komunitný hashtag",
"community_addHashtagChannelDesc": "Pridajte hashtagový kanál pre túto komunitu.",
"community_selectCommunity": "Vyberte komunitu",
"community_regularHashtag": "Zvyčajný hashtag",
"community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)",
"community_communityHashtag": "Komunitný Hashtag",
"community_communityHashtagDesc": "Špecifické pre členov komunity",
"community_forCommunity": "Pre {name}"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"settings_locationIntervalSec": "Interval za GPS (Sekunde)",
"settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.",
"contacts_manageRoom": "Upravljajte strežnik sobe",
"room_management": "Upravljanje stremlišča"
"room_management": "Upravljanje stremlišča",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_createDesc": "Ustvari novo skupnost in jo deli preko QR kode.",
"community_title": "Skupnost",
"common_ok": "V redu",
"community_create": "Ustvari skupnost",
"community_joinTitle": "Pridružite se skupnosti",
"community_joinConfirmation": "Želiš se pridružiti skupnosti \"{name}\"?",
"community_scanQr": "Skeniraj QR kode skupnosti",
"community_scanInstructions": "Nasmerite kamero s skupnostnim QR kodom.",
"community_showQr": "Pokaži QR kodo",
"community_publicChannel": "Skupnostna javna",
"community_hashtagChannel": "Skupnostni hashtag",
"community_name": "Komunitarne ime",
"community_enterName": "Vnesite ime skupnosti",
"community_join": "Pridružiti se",
"community_created": "Skupnost \"{name}\" je bila ustvarila.",
"community_joined": "Prilojen k skupnosti \"{name}\"",
"community_qrTitle": "Delite skupnost",
"community_qrInstructions": "Skenirajte to QR kodo za vključitev {name}.",
"community_hashtagPrivacyHint": "Hashtag kanali skupnosti so dostopni samo članom skupnosti",
"community_invalidQrCode": "Neveljaven QR koden skupnosti",
"community_alreadyMember": "Že član",
"community_alreadyMemberMessage": "Kljub temu ste že član/ka {name}.",
"community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja",
"community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.",
"community_noCommunities": "Še nobena skupnost se ni pridružila.",
"community_scanOrCreate": "Skenirajte QR kodo ali ustvarite skupnost za začetek.",
"community_manageCommunities": "Upravljajte skupnosti",
"community_delete": "Opusti skupnost",
"community_deleteConfirm": "Zapustiti \"{name}\"?",
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Zapustil skupnost \"{name}\"",
"community_addHashtagChannel": "Dodaj Oznako Obštnine",
"community_addHashtagChannelDesc": "Dodajte hashtag kanal za to skupnost.",
"community_selectCommunity": "Izberi skupnost",
"community_regularHashtag": "Oznaka s hashtagom",
"community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)",
"community_communityHashtag": "Skupnostni hashtag",
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
"community_forCommunity": "Za {name}"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"settings_locationIntervalSec": "Interval för GPS (Sekunder)",
"settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.",
"contacts_manageRoom": "Hantera Rumserver",
"room_management": "Rumserverhantering"
"room_management": "Rumserverhantering",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Skapa Gemenskap",
"community_createDesc": "Skapa en ny gemenskap och dela via QR-kod.",
"common_ok": "Okej",
"community_title": "Gemenskap",
"community_join": "Gå med",
"community_joinTitle": "Gå med i gemenskapen",
"community_joinConfirmation": "Vill du gå med i communityn \"{name}\"?",
"community_scanQr": "Skanna Gemenskapens QR",
"community_scanInstructions": "Rikta kameran mot en QR-kod i communityn",
"community_showQr": "Visa QR-kod",
"community_publicChannel": "Föreningens Offentliga",
"community_name": "Gemenskapens namn",
"community_enterName": "Ange communities namn",
"community_created": "Community \"{name}\" har skapats",
"community_joined": "Medlem i communityn \"{name}\"",
"community_qrTitle": "Dela Gemenskap",
"community_qrInstructions": "Skanna denna QR-kod för att gå med i \"{name}\"",
"community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nås av medlemmar i communityn",
"community_hashtagChannel": "Community Hashtag",
"community_invalidQrCode": "Ogiltig community QR-kod",
"community_alreadyMember": "Är redan medlem",
"community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".",
"community_addPublicChannel": "Lägg till Gemenskapskanal (Offentlig)",
"community_addPublicChannelHint": "Lägg automatiskt till den offentliga kanalen för denna community",
"community_noCommunities": "Inga gemenskaper har anslutats ännu",
"community_scanOrCreate": "Skanna en QR-kod eller skapa en community för att komma igång",
"community_manageCommunities": "Hantera Gemenskaper",
"community_delete": "Lämna Gemenskap",
"community_deleteConfirm": "Lämna \"{name}\"?",
"community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Lämnade community \"{name}\"",
"community_addHashtagChannel": "Lägg till Gemenskapens Hashtag",
"community_addHashtagChannelDesc": "Lägg till en hashtag-kanal för denna community",
"community_selectCommunity": "Välj Gemenskap",
"community_regularHashtag": "Vanlig Hash Tag",
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
"community_communityHashtagDesc": "Endast för medlemmar",
"community_forCommunity": "För {name}",
"community_communityHashtag": "Community Hashtag"
}
+101 -1
View File
@@ -1384,5 +1384,105 @@
"settings_locationIntervalSec": "GPS 间隔(秒)",
"settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。",
"contacts_manageRoom": "管理房间服务器",
"room_management": "房间服务器管理"
"room_management": "房间服务器管理",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "创建社区",
"community_title": "社区",
"community_createDesc": "创建新的社区并可通过二维码分享。",
"common_ok": "好的",
"community_join": "加入",
"community_joinTitle": "加入社区",
"community_joinConfirmation": "您想加入社区 \"{name}\" 吗?",
"community_scanQr": "扫描社区二维码",
"community_scanInstructions": "将相机对准社区二维码",
"community_showQr": "显示二维码",
"community_publicChannel": "社区公开",
"community_hashtagChannel": "社区标签",
"community_name": "社区名称",
"community_enterName": "请输入社区名称",
"community_created": "社区“{name}”已创建",
"community_joined": "加入社区 \"{name}\"",
"community_qrTitle": "分享社区",
"community_qrInstructions": "扫描此二维码加入{name}",
"community_hashtagPrivacyHint": "社区标签频道仅社区成员可加入",
"community_invalidQrCode": "无效的社区二维码",
"community_alreadyMember": "已经是会员了",
"community_alreadyMemberMessage": "您已经是 \"{name}\" 的会员。",
"community_addPublicChannel": "添加社区公共频道",
"community_addPublicChannelHint": "自动添加该社区的公共频道",
"community_noCommunities": "尚未加入任何社区",
"community_scanOrCreate": "扫描二维码或创建社区开始",
"community_manageCommunities": "管理社群",
"community_delete": "退出社区",
"community_deleteConfirm": "退出 \"{name}\"",
"community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "已退出社区 \"{name}\"",
"community_addHashtagChannel": "添加社区标签",
"community_addHashtagChannelDesc": "添加一个话题频道给此社区",
"community_selectCommunity": "选择社区",
"community_regularHashtag": "常规话题标签",
"community_regularHashtagDesc": "公共话题(任何人都可以加入)",
"community_communityHashtag": "社区标签",
"community_communityHashtagDesc": "仅限社区成员使用",
"community_forCommunity": "对于 {name}"
}
+29
View File
@@ -73,6 +73,35 @@ class Channel {
return Uint8List.fromList(hash.sublist(0, 16));
}
/// Derive PSK for community public channel using HMAC-SHA256.
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
///
/// This creates a channel that is "public" only to members who have
/// the community secret. Outsiders see only opaque IDs.
static Uint8List deriveCommunityPublicPsk(Uint8List secret) {
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Derive PSK for community hashtag channel using HMAC-SHA256.
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
///
/// Community hashtag channels are deterministic for all members
/// (same name => same id) but impossible to enumerate/guess without K.
static Uint8List deriveCommunityHashtagPsk(Uint8List secret, String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Normalize a hashtag name for consistent community PSK derivation.
/// Strips leading #, converts to lowercase, trims whitespace.
static String _normalizeCommunityHashtag(String hashtag) {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
static String formatPskHex(Uint8List psk) {
return _bytesToHex(psk);
}
+243
View File
@@ -0,0 +1,243 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
/// Represents a community with a shared secret for deriving channel PSKs.
///
/// A Community is a namespace with a shared secret K (32 random bytes),
/// distributed via QR code. Members can create Community Public Channels
/// and Community Hashtag Channels that are opaque to outsiders.
class Community {
/// Unique identifier for local storage
final String id;
/// Display name for the community
final String name;
/// The 32-byte shared secret (K)
final Uint8List secret;
/// Timestamp when the community was created/joined
final DateTime createdAt;
/// List of hashtag channel names (without #) that have been added
final List<String> hashtagChannels;
Community({
required this.id,
required this.name,
required this.secret,
required this.createdAt,
List<String>? hashtagChannels,
}) : hashtagChannels = hashtagChannels ?? [];
/// Generate a new community with a random 32-byte secret
factory Community.create({
required String id,
required String name,
}) {
final random = Random.secure();
final secret = Uint8List(32);
for (int i = 0; i < 32; i++) {
secret[i] = random.nextInt(256);
}
return Community(
id: id,
name: name,
secret: secret,
createdAt: DateTime.now(),
);
}
/// Parse a community from QR code JSON data
factory Community.fromQrData(String id, String qrData) {
final json = jsonDecode(qrData) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') {
throw const FormatException('Invalid QR code type');
}
if (json['v'] != 1) {
throw const FormatException('Unsupported QR code version');
}
final name = json['name'] as String;
final secretBase64 = json['k'] as String;
final secret = base64Url.decode(secretBase64);
if (secret.length != 32) {
throw const FormatException('Invalid secret length');
}
return Community(
id: id,
name: name,
secret: Uint8List.fromList(secret),
createdAt: DateTime.now(),
);
}
/// Parse a community from storage JSON
factory Community.fromJson(Map<String, dynamic> json) {
return Community(
id: json['id'] as String,
name: json['name'] as String,
secret: base64Decode(json['secret'] as String),
createdAt: DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int),
hashtagChannels: (json['hashtag_channels'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
/// Convert to JSON for storage
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'secret': base64Encode(secret),
'created_at': createdAt.millisecondsSinceEpoch,
'hashtag_channels': hashtagChannels,
};
}
/// Generate QR code JSON payload for sharing
String toQrJson() {
return jsonEncode({
'v': 1,
'type': 'meshcore_community',
'name': name,
'k': base64Url.encode(secret),
});
}
/// Derive the public Community ID from the secret.
/// This is safe to display/log since it's one-way derived.
/// CID = SHA256("community:v1" || K)
String get communityId {
final data = utf8.encode('community:v1') + secret;
final hash = crypto.sha256.convert(data).bytes;
return _bytesToHex(Uint8List.fromList(hash));
}
/// Short version of community ID for display (first 8 chars)
String get shortCommunityId => communityId.substring(0, 8);
/// Derive PSK for community public channel.
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
Uint8List deriveCommunityPublicPsk() {
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Derive PSK for community hashtag channel.
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
Uint8List deriveCommunityHashtagPsk(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Check if QR data is valid community data
static bool isValidQrData(String data) {
try {
final json = jsonDecode(data) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') return false;
if (json['v'] != 1) return false;
if (json['name'] == null || (json['name'] as String).isEmpty) {
return false;
}
if (json['k'] == null) return false;
final secret = base64Url.decode(json['k'] as String);
return secret.length == 32;
} catch (_) {
return false;
}
}
/// Normalize a hashtag name for consistent PSK derivation.
/// Strips leading #, converts to lowercase, trims whitespace.
static String _normalizeCommunityHashtag(String hashtag) {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
/// Add a hashtag channel to this community's list
Community addHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
if (hashtagChannels.contains(normalized)) {
return this;
}
return Community(
id: id,
name: name,
secret: secret,
createdAt: createdAt,
hashtagChannels: [...hashtagChannels, normalized],
);
}
/// Remove a hashtag channel from this community's list
Community removeHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
return Community(
id: id,
name: name,
secret: secret,
createdAt: createdAt,
hashtagChannels: hashtagChannels.where((h) => h != normalized).toList(),
);
}
/// Create a copy of this community with a new secret
Community withNewSecret(Uint8List newSecret) {
return Community(
id: id,
name: name,
secret: newSecret,
createdAt: createdAt,
hashtagChannels: hashtagChannels,
);
}
/// Create a copy of this community with a regenerated random secret
Community withRegeneratedSecret() {
final random = Random.secure();
final newSecret = Uint8List(32);
for (int i = 0; i < 32; i++) {
newSecret[i] = random.nextInt(256);
}
return withNewSecret(newSecret);
}
/// Extract secret from QR data (for updating existing community)
static Uint8List? extractSecretFromQrData(String qrData) {
try {
final json = jsonDecode(qrData) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') return null;
if (json['v'] != 1) return null;
final secretBase64 = json['k'] as String;
final secret = base64Url.decode(secretBase64);
if (secret.length != 32) return null;
return Uint8List.fromList(secret);
} catch (_) {
return null;
}
}
static String _bytesToHex(Uint8List bytes) {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Community &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}
+706 -26
View File
@@ -4,19 +4,25 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/route_transitions.dart';
import '../widgets/battery_indicator.dart';
import '../widgets/list_filter_widget.dart';
import '../widgets/empty_state.dart';
import '../widgets/qr_code_display.dart';
import '../widgets/qr_scanner_widget.dart';
import '../widgets/quick_switch_bar.dart';
import '../widgets/unread_badge.dart';
import 'channel_chat_screen.dart';
import 'community_qr_scanner_screen.dart';
import 'contacts_screen.dart';
import 'map_screen.dart';
import 'settings_screen.dart';
@@ -43,18 +49,60 @@ class ChannelsScreen extends StatefulWidget {
class _ChannelsScreenState extends State<ChannelsScreen>
with DisconnectNavigationMixin {
final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore();
String _searchQuery = '';
Timer? _searchDebounce;
ChannelSortOption _sortOption = ChannelSortOption.manual;
List<Community> _communities = [];
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MeshCoreConnector>().getChannels();
_loadCommunities();
});
}
Future<void> _loadCommunities() async {
final communities = await _communityStore.loadCommunities();
if (mounted) {
setState(() {
_communities = communities;
_buildPskCommunityMap();
});
}
}
void _buildPskCommunityMap() {
_pskToCommunity.clear();
for (final community in _communities) {
// Map the community public channel PSK
final publicPsk = community.deriveCommunityPublicPsk();
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
// Map all known hashtag channel PSKs
for (final hashtag in community.hashtagChannels) {
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
}
}
}
/// Returns the community this channel belongs to, or null if not a community channel
Community? _getCommunityForChannel(Channel channel) {
return _pskToCommunity[channel.pskHex];
}
/// Returns true if this is the community's public channel
bool _isCommunityPublicChannel(Channel channel, Community community) {
final publicPsk = community.deriveCommunityPublicPsk();
return channel.pskHex == Channel.formatPskHex(publicPsk);
}
@override
void dispose() {
_searchDebounce?.cancel();
@@ -82,6 +130,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
if (_communities.isNotEmpty)
IconButton(
icon: const Icon(Icons.groups),
tooltip: context.l10n.community_manageCommunities,
onPressed: () => _showManageCommunitiesDialog(context),
),
IconButton(
icon: const Icon(Icons.bluetooth_disabled),
tooltip: context.l10n.common_disconnect,
@@ -268,6 +322,44 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
) {
final unreadCount = connector.getUnreadCountForChannel(channel);
final community = _getCommunityForChannel(channel);
final isCommunityChannel = community != null;
final isCommunityPublic = isCommunityChannel && _isCommunityPublicChannel(channel, community);
// Determine icon and colors based on channel type
IconData icon;
Color iconColor;
Color bgColor;
String subtitle;
if (isCommunityChannel) {
// Community channel styling
iconColor = Colors.purple;
bgColor = Colors.purple.withValues(alpha: 0.2);
if (isCommunityPublic) {
icon = Icons.groups;
subtitle = '${context.l10n.community_publicChannel}${community.name}';
} else {
icon = Icons.tag;
subtitle = '${context.l10n.community_hashtagChannel}${community.name}';
}
} else if (channel.isPublicChannel) {
icon = Icons.public;
iconColor = Colors.green;
bgColor = Colors.green.withValues(alpha: 0.2);
subtitle = context.l10n.channels_publicChannel;
} else if (channel.name.startsWith('#')) {
icon = Icons.tag;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
subtitle = context.l10n.channels_hashtagChannel;
} else {
icon = Icons.lock;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
subtitle = context.l10n.channels_privateChannel;
}
return Card(
key: ValueKey('channel_${channel.index}'),
margin: const EdgeInsets.only(bottom: 12),
@@ -276,29 +368,44 @@ class _ChannelsScreenState extends State<ChannelsScreen>
minVerticalPadding: 0,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
visualDensity: const VisualDensity(vertical: -2),
leading: CircleAvatar(
backgroundColor: channel.isPublicChannel
? Colors.green.withValues(alpha: 0.2)
: Colors.blue.withValues(alpha: 0.2),
child: Icon(
channel.isPublicChannel
? Icons.public
: channel.name.startsWith('#')
? Icons.tag
: Icons.lock,
color: channel.isPublicChannel ? Colors.green : Colors.blue,
leading: Stack(
children: [
CircleAvatar(
backgroundColor: bgColor,
child: Icon(icon, color: iconColor),
),
if (isCommunityChannel)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).cardColor,
width: 2,
),
),
child: const Icon(
Icons.people,
size: 8,
color: Colors.white,
),
),
),
],
),
title: Text(
channel.name.isEmpty ? context.l10n.channels_channelIndex(channel.index) : channel.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
channel.name.startsWith('#')
? context.l10n.channels_hashtagChannel
: channel.isPublicChannel
? context.l10n.channels_publicChannel
: context.l10n.channels_privateChannel,
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
@@ -521,6 +628,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final nameController = TextEditingController();
final pskController = TextEditingController();
final hashtagController = TextEditingController();
bool addPublicChannel = true;
bool isRegularHashtag = true;
Community? selectedCommunity;
showDialog(
context: context,
@@ -713,6 +823,55 @@ class _ChannelsScreenState extends State<ChannelsScreen>
case 3: // Join Hashtag Channel
return Column(
children: [
// Only show type selection if user has communities
if (_communities.isNotEmpty) ...[
RadioGroup<bool>(
groupValue: isRegularHashtag,
onChanged: (v) => setDialogState(() {
if (v != null) {
isRegularHashtag = v;
if (isRegularHashtag) {
selectedCommunity = null;
} else if (selectedCommunity == null && _communities.isNotEmpty) {
selectedCommunity = _communities.first;
}
}
}),
child: Column(
children: [
RadioListTile<bool>(
value: true,
title: Text(dialogContext.l10n.community_regularHashtag),
subtitle: Text(dialogContext.l10n.community_regularHashtagDesc),
dense: true,
),
RadioListTile<bool>(
value: false,
title: Text(dialogContext.l10n.community_communityHashtag),
subtitle: Text(dialogContext.l10n.community_communityHashtagDesc),
dense: true,
),
],
),
),
],
// Community dropdown (only if community hashtag selected)
if (!isRegularHashtag && _communities.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: DropdownMenu<Community>(
initialSelection: selectedCommunity,
dropdownMenuEntries: _communities.map((c) => DropdownMenuEntry(
value: c,
label: c.name,
)).toList(),
onSelected: (c) => setDialogState(() => selectedCommunity = c),
label: Text(dialogContext.l10n.community_selectCommunity),
leadingIcon: const Icon(Icons.groups),
expandedInsets: EdgeInsets.zero,
),
),
// Hashtag name input
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
@@ -726,13 +885,26 @@ class _ChannelsScreenState extends State<ChannelsScreen>
maxLength: 31,
),
),
// Privacy hint for community hashtags
if (!isRegularHashtag)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
dialogContext.l10n.community_hashtagPrivacyHint,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: FilledButton(
onPressed: () {
onPressed: () async {
var hashtag = hashtagController.text.trim();
if (hashtag.isEmpty) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
@@ -740,14 +912,38 @@ class _ChannelsScreenState extends State<ChannelsScreen>
);
return;
}
// Normalize hashtag name
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
final psk = Channel.derivePskFromHashtag(hashtag);
// Normalize hashtag name (remove leading # if present)
if (hashtag.startsWith('#')) {
hashtag = hashtag.substring(1);
}
final channelName = '#$hashtag';
final Uint8List psk;
if (isRegularHashtag) {
// Regular hashtag - public derivation using SHA256
psk = Channel.derivePskFromHashtag(hashtag);
} else {
// Community hashtag - HMAC derivation from community secret
if (selectedCommunity == null) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(content: Text(dialogContext.l10n.community_selectCommunity)),
);
return;
}
psk = selectedCommunity!.deriveCommunityHashtagPsk(hashtag);
// Track in community's hashtag list
await _communityStore.addHashtagChannel(selectedCommunity!.id, hashtag);
_loadCommunities();
}
if (dialogContext.mounted) {
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk);
}
connector.setChannel(nextIndex, channelName, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
SnackBar(content: Text(context.l10n.channels_channelAdded(channelName))),
);
}
},
@@ -760,6 +956,126 @@ class _ChannelsScreenState extends State<ChannelsScreen>
],
);
case 4: // Scan Community QR
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: () async {
Navigator.pop(dialogContext);
if (context.mounted) {
await Navigator.push<Community>(
context,
MaterialPageRoute(
builder: (context) => const CommunityQrScannerScreen(),
),
);
// Refresh communities list when returning from scanner
if (context.mounted) {
_loadCommunities();
}
}
},
icon: const Icon(Icons.qr_code_scanner),
label: Text(dialogContext.l10n.community_scanQr),
),
),
],
),
);
case 5: // Create Community
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
controller: nameController,
decoration: InputDecoration(
labelText: dialogContext.l10n.community_name,
hintText: dialogContext.l10n.community_enterName,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.groups),
),
maxLength: 31,
),
),
CheckboxListTile(
value: addPublicChannel,
onChanged: (value) {
setDialogState(() {
addPublicChannel = value ?? true;
});
},
title: Text(dialogContext.l10n.community_addPublicChannel),
subtitle: Text(dialogContext.l10n.community_addPublicChannelHint),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: FilledButton(
onPressed: () async {
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(content: Text(dialogContext.l10n.community_enterName)),
);
return;
}
// Create community with random secret
final community = Community.create(
id: const Uuid().v4(),
name: name,
);
// Save to store
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
if (addPublicChannel) {
final psk = community.deriveCommunityPublicPsk();
final channelName = '${community.name} Public';
connector.setChannel(nextIndex, channelName, psk);
}
if (dialogContext.mounted) {
Navigator.pop(dialogContext);
}
// Refresh communities list
_loadCommunities();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.community_created(name))),
);
// Show QR code dialog
await QrCodeShareDialog.show(
context: context,
data: community.toQrJson(),
title: context.l10n.community_qrTitle,
instructions: context.l10n.community_qrInstructions(name),
embeddedImage: Image.asset('assets/images/mesh-icon.png', width: 40, height: 40),
);
}
},
child: Text(dialogContext.l10n.common_create),
),
),
],
),
),
],
);
default:
return null;
}
@@ -810,11 +1126,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
const Divider(height: 1),
buildOptionTile(
optionIndex: 4,
icon: Icons.qr_code,
title: dialogContext.l10n.channels_scanQrCode,
subtitle: dialogContext.l10n.channels_scanQrCodeComingSoon,
enabled: false,
icon: Icons.qr_code_scanner,
title: dialogContext.l10n.community_scanQr,
subtitle: dialogContext.l10n.community_join,
),
if (selectedOption == 4) buildExpandedContent()!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 5,
icon: Icons.groups,
title: dialogContext.l10n.community_create,
subtitle: dialogContext.l10n.community_createDesc,
),
if (selectedOption == 5) buildExpandedContent()!,
],
),
),
@@ -967,4 +1291,360 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
return 0;
}
void _showManageCommunitiesDialog(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (sheetContext) => DraggableScrollableSheet(
initialChildSize: 0.5,
minChildSize: 0.3,
maxChildSize: 0.9,
expand: false,
builder: (_, scrollController) => Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Icon(Icons.groups, size: 28),
const SizedBox(width: 12),
Text(
context.l10n.community_manageCommunities,
style: Theme.of(context).textTheme.titleLarge,
),
],
),
),
const Divider(height: 1),
Expanded(
child: _communities.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.groups_outlined, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
context.l10n.community_noCommunities,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
context.l10n.community_scanOrCreate,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
],
),
)
: ListView.builder(
controller: scrollController,
itemCount: _communities.length,
itemBuilder: (context, index) {
final community = _communities[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.purple.withValues(alpha: 0.2),
child: const Icon(Icons.groups, color: Colors.purple),
),
title: Text(community.name),
subtitle: Text(
'ID: ${community.shortCommunityId}...',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
trailing: PopupMenuButton<String>(
onSelected: (value) {
Navigator.pop(sheetContext);
if (value == 'share') {
_showCommunityQrDialog(context, community);
} else if (value == 'regenerate') {
_regenerateCommunitySecret(context, community);
} else if (value == 'update') {
_updateCommunitySecret(context, community);
} else if (value == 'leave') {
_confirmLeaveCommunity(context, community);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'share',
child: Row(
children: [
const Icon(Icons.qr_code),
const SizedBox(width: 12),
Text(context.l10n.community_showQr),
],
),
),
PopupMenuItem(
value: 'regenerate',
child: Row(
children: [
const Icon(Icons.refresh),
const SizedBox(width: 12),
Text(context.l10n.community_regenerateSecret),
],
),
),
PopupMenuItem(
value: 'update',
child: Row(
children: [
const Icon(Icons.qr_code_scanner),
const SizedBox(width: 12),
Text(context.l10n.community_updateSecret),
],
),
),
const PopupMenuDivider(),
PopupMenuItem(
value: 'leave',
child: Row(
children: [
const Icon(Icons.exit_to_app, color: Colors.red),
const SizedBox(width: 12),
Text(
context.l10n.community_delete,
style: const TextStyle(color: Colors.red),
),
],
),
),
],
),
onTap: () {
Navigator.pop(sheetContext);
_showCommunityQrDialog(context, community);
},
);
},
),
),
],
),
),
);
}
void _showCommunityQrDialog(BuildContext context, Community community) {
QrCodeShareDialog.show(
context: context,
data: community.toQrJson(),
title: context.l10n.community_qrTitle,
instructions: context.l10n.community_qrInstructions(community.name),
embeddedImage: Image.asset('assets/images/mesh-icon.png', width: 40, height: 40),
);
}
/// Regenerate the community secret and update all associated channels
void _regenerateCommunitySecret(BuildContext context, Community community) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(dialogContext.l10n.community_regenerateSecret),
content: Text(dialogContext.l10n.community_regenerateSecretConfirm(community.name)),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(dialogContext.l10n.common_cancel),
),
FilledButton(
onPressed: () async {
Navigator.pop(dialogContext);
final connector = context.read<MeshCoreConnector>();
final newCommunity = community.withRegeneratedSecret();
// Update channel PSKs
await _updateCommunityChannelPsks(connector, community, newCommunity);
// Save updated community
await _communityStore.updateCommunity(newCommunity);
_loadCommunities();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.community_secretRegenerated(community.name))),
);
// Show the new QR code
_showCommunityQrDialog(context, newCommunity);
}
},
child: Text(dialogContext.l10n.community_regenerate),
),
],
),
);
}
/// Update community secret from a scanned QR code
void _updateCommunitySecret(BuildContext context, Community community) async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => _CommunitySecretScannerScreen(
communityName: community.name,
),
),
);
if (result == null || !context.mounted) return;
final newSecret = Community.extractSecretFromQrData(result);
if (newSecret == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.community_invalidQrCode)),
);
return;
}
final connector = context.read<MeshCoreConnector>();
final newCommunity = community.withNewSecret(newSecret);
// Update channel PSKs
await _updateCommunityChannelPsks(connector, community, newCommunity);
// Save updated community
await _communityStore.updateCommunity(newCommunity);
_loadCommunities();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.community_secretUpdated(community.name))),
);
}
}
/// Update PSKs for all channels belonging to a community
Future<void> _updateCommunityChannelPsks(
MeshCoreConnector connector,
Community oldCommunity,
Community newCommunity,
) async {
// Find and update the public channel
final oldPublicPskHex = Channel.formatPskHex(oldCommunity.deriveCommunityPublicPsk());
final newPublicPsk = newCommunity.deriveCommunityPublicPsk();
for (final channel in connector.channels) {
if (channel.pskHex == oldPublicPskHex) {
await connector.setChannel(channel.index, channel.name, newPublicPsk);
break;
}
}
// Find and update hashtag channels
for (final hashtag in oldCommunity.hashtagChannels) {
final oldHashtagPskHex = Channel.formatPskHex(oldCommunity.deriveCommunityHashtagPsk(hashtag));
final newHashtagPsk = newCommunity.deriveCommunityHashtagPsk(hashtag);
for (final channel in connector.channels) {
if (channel.pskHex == oldHashtagPskHex) {
await connector.setChannel(channel.index, channel.name, newHashtagPsk);
break;
}
}
}
}
void _confirmLeaveCommunity(BuildContext context, Community community) {
final connector = context.read<MeshCoreConnector>();
// Find all channels that belong to this community
List<Channel> communityChannels = [];
final publicPskHex = Channel.formatPskHex(community.deriveCommunityPublicPsk());
for (final channel in connector.channels) {
// Check if it's the public channel
if (channel.pskHex == publicPskHex) {
communityChannels.add(channel);
continue;
}
// Check if it's a hashtag channel
for (final hashtag in community.hashtagChannels) {
final hashtagPskHex = Channel.formatPskHex(community.deriveCommunityHashtagPsk(hashtag));
if (channel.pskHex == hashtagPskHex) {
communityChannels.add(channel);
break;
}
}
}
final channelCount = communityChannels.length;
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(dialogContext.l10n.community_delete),
content: Text(
channelCount > 0
? '${dialogContext.l10n.community_deleteConfirm(community.name)}\n\n${dialogContext.l10n.community_deleteChannelsWarning(channelCount)}'
: dialogContext.l10n.community_deleteConfirm(community.name),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(dialogContext.l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(dialogContext);
// Delete all community channels from the device
for (final channel in communityChannels) {
await connector.deleteChannel(channel.index);
}
// Remove community from store
await _communityStore.removeCommunity(community.id);
_loadCommunities();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.community_deleted(community.name))),
);
}
},
child: Text(
dialogContext.l10n.community_delete,
style: const TextStyle(color: Colors.red),
),
),
],
),
);
}
}
/// Simple scanner screen for updating community secret
class _CommunitySecretScannerScreen extends StatelessWidget {
final String communityName;
const _CommunitySecretScannerScreen({required this.communityName});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.community_updateSecret),
),
body: QrScannerWidget(
onScanned: (data) {
Navigator.pop(context, data);
},
validator: (data) => Community.isValidQrData(data),
onValidationFailed: (data) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.community_invalidQrCode)),
);
},
instructions: context.l10n.community_scanToUpdateSecret(communityName),
overlay: const ScannerCornerOverlay(),
),
);
}
}
@@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../widgets/qr_scanner_widget.dart';
/// Screen for scanning community QR codes to join communities.
///
/// After successful scan, the user can:
/// 1. Join the community (saves to local storage)
/// 2. Optionally add the Community Public Channel to the device
class CommunityQrScannerScreen extends StatefulWidget {
const CommunityQrScannerScreen({super.key});
@override
State<CommunityQrScannerScreen> createState() =>
_CommunityQrScannerScreenState();
}
class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
final CommunityStore _communityStore = CommunityStore();
bool _isProcessing = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.community_scanQr),
centerTitle: true,
),
body: _isProcessing
? const Center(child: CircularProgressIndicator())
: QrScannerWidget(
onScanned: (data) => _handleScannedData(context, data),
validator: Community.isValidQrData,
onValidationFailed: (_) => _showInvalidQrError(context),
instructions: context.l10n.community_scanInstructions,
),
);
}
Future<void> _handleScannedData(BuildContext context, String data) async {
if (_isProcessing) return;
setState(() {
_isProcessing = true;
});
try {
// Parse the community data
final community = Community.fromQrData(const Uuid().v4(), data);
// Check if this community already exists
final existing = await _communityStore.findByCommunityId(
community.communityId,
);
if (existing != null) {
if (context.mounted) {
_showAlreadyMemberDialog(context, existing);
}
return;
}
// Show confirmation dialog
if (context.mounted) {
await _showJoinConfirmationDialog(context, community);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_isProcessing = false;
});
}
}
}
void _showInvalidQrError(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
),
);
}
void _showAlreadyMemberDialog(BuildContext context, Community community) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.community_alreadyMember),
content: Text(
context.l10n.community_alreadyMemberMessage(community.name),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
Navigator.pop(context);
},
child: Text(context.l10n.common_ok),
),
],
),
);
}
Future<void> _showJoinConfirmationDialog(
BuildContext context,
Community community,
) async {
bool addPublicChannel = true;
final result = await showDialog<bool>(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setDialogState) => AlertDialog(
title: Text(context.l10n.community_joinTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.community_joinConfirmation(community.name)),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.groups,
color: Theme.of(dialogContext).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
community.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'ID: ${community.shortCommunityId}...',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
CheckboxListTile(
value: addPublicChannel,
onChanged: (value) {
setDialogState(() {
addPublicChannel = value ?? true;
});
},
title: Text(context.l10n.community_addPublicChannel),
subtitle: Text(context.l10n.community_addPublicChannelHint),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: Text(context.l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: Text(context.l10n.community_join),
),
],
),
),
);
if (result == true && context.mounted) {
await _joinCommunity(context, community, addPublicChannel);
} else if (context.mounted) {
// User cancelled - go back
Navigator.pop(context);
}
}
Future<void> _joinCommunity(
BuildContext context,
Community community,
bool addPublicChannel,
) async {
// Save community to local storage
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
if (addPublicChannel && context.mounted) {
final connector = context.read<MeshCoreConnector>();
final nextIndex = _findNextAvailableChannelIndex(connector);
if (nextIndex != null) {
final psk = community.deriveCommunityPublicPsk();
final channelName = '${community.name} Public';
connector.setChannel(nextIndex, channelName, psk);
}
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_joined(community.name)),
backgroundColor: Colors.green,
),
);
// Return to previous screen
Navigator.pop(context, community);
}
}
int? _findNextAvailableChannelIndex(MeshCoreConnector connector) {
final usedIndices = connector.channels.map((c) => c.index).toSet();
for (int i = 0; i < connector.maxChannels; i++) {
if (!usedIndices.contains(i)) return i;
}
return null;
}
}
+117
View File
@@ -0,0 +1,117 @@
import 'dart:convert';
import '../models/community.dart';
import 'prefs_manager.dart';
/// Persists communities to local storage using SharedPreferences.
///
/// Communities are stored as a JSON array under a single key.
/// Each community contains its secret K, so this data should
/// be considered sensitive (though device encryption handles security).
class CommunityStore {
static const String _communitiesKey = 'communities_v1';
/// Load all communities from storage
Future<List<Community>> loadCommunities() async {
final prefs = PrefsManager.instance;
final jsonString = prefs.getString(_communitiesKey);
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((json) => Community.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
// If JSON is corrupted, return empty list
return [];
}
}
/// Save all communities to storage
Future<void> saveCommunities(List<Community> communities) async {
final prefs = PrefsManager.instance;
final jsonList = communities.map((c) => c.toJson()).toList();
await prefs.setString(_communitiesKey, jsonEncode(jsonList));
}
/// Add a new community
Future<void> addCommunity(Community community) async {
final communities = await loadCommunities();
// Check if community with same ID already exists
final existingIndex = communities.indexWhere((c) => c.id == community.id);
if (existingIndex >= 0) {
// Replace existing
communities[existingIndex] = community;
} else {
communities.add(community);
}
await saveCommunities(communities);
}
/// Update an existing community
Future<void> updateCommunity(Community community) async {
final communities = await loadCommunities();
final index = communities.indexWhere((c) => c.id == community.id);
if (index >= 0) {
communities[index] = community;
await saveCommunities(communities);
}
}
/// Remove a community by ID
Future<void> removeCommunity(String communityId) async {
final communities = await loadCommunities();
communities.removeWhere((c) => c.id == communityId);
await saveCommunities(communities);
}
/// Get a community by ID
Future<Community?> getCommunity(String communityId) async {
final communities = await loadCommunities();
try {
return communities.firstWhere((c) => c.id == communityId);
} catch (_) {
return null;
}
}
/// Check if a community with the same secret already exists
/// (to prevent duplicate imports from QR scanning)
Future<Community?> findByCommunityId(String cid) async {
final communities = await loadCommunities();
try {
return communities.firstWhere((c) => c.communityId == cid);
} catch (_) {
return null;
}
}
/// Add a hashtag channel to a community
Future<void> addHashtagChannel(
String communityId,
String hashtag,
) async {
final community = await getCommunity(communityId);
if (community != null) {
final updated = community.addHashtagChannel(hashtag);
await updateCommunity(updated);
}
}
/// Remove a hashtag channel from a community
Future<void> removeHashtagChannel(
String communityId,
String hashtag,
) async {
final community = await getCommunity(communityId);
if (community != null) {
final updated = community.removeHashtagChannel(hashtag);
await updateCommunity(updated);
}
}
}
+233
View File
@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
/// A reusable QR code display widget for sharing data.
///
/// Features:
/// - Configurable size and colors
/// - Optional logo/icon in center
/// - Automatic theming (light/dark mode aware)
/// - Title and instructions
class QrCodeDisplay extends StatelessWidget {
/// The data to encode in the QR code
final String data;
/// Size of the QR code (width and height)
final double size;
/// Optional widget to display in the center (e.g., app logo)
final Widget? embeddedImage;
/// Size of the embedded image (if provided)
final double embeddedImageSize;
/// Title displayed above the QR code
final String? title;
/// Instructions displayed below the QR code
final String? instructions;
/// Background color of the QR code (defaults to white)
final Color? backgroundColor;
/// Foreground color of the QR code modules (defaults to black)
final Color? foregroundColor;
/// Padding around the QR code
final EdgeInsets padding;
/// Error correction level
final int errorCorrectionLevel;
const QrCodeDisplay({
super.key,
required this.data,
this.size = 200,
this.embeddedImage,
this.embeddedImageSize = 50,
this.title,
this.instructions,
this.backgroundColor,
this.foregroundColor,
this.padding = const EdgeInsets.all(16),
this.errorCorrectionLevel = QrErrorCorrectLevel.M,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// Default colors based on theme
final bgColor = backgroundColor ?? Colors.white;
final fgColor = foregroundColor ?? Colors.black;
return Padding(
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (title != null) ...[
Text(
title!,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
],
// QR code container with rounded corners
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
boxShadow: isDark
? null
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: embeddedImage != null
? _buildQrWithEmbeddedImage(fgColor, bgColor)
: _buildSimpleQr(fgColor, bgColor),
),
if (instructions != null) ...[
const SizedBox(height: 16),
Text(
instructions!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
],
),
);
}
Widget _buildSimpleQr(Color fgColor, Color bgColor) {
return QrImageView(
data: data,
version: QrVersions.auto,
size: size,
backgroundColor: bgColor,
errorCorrectionLevel: errorCorrectionLevel,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: fgColor,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: fgColor,
),
);
}
Widget _buildQrWithEmbeddedImage(Color fgColor, Color bgColor) {
return Stack(
alignment: Alignment.center,
children: [
QrImageView(
data: data,
version: QrVersions.auto,
size: size,
backgroundColor: bgColor,
// Use higher error correction when embedding image
errorCorrectionLevel: QrErrorCorrectLevel.H,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: fgColor,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: fgColor,
),
),
Container(
width: embeddedImageSize,
height: embeddedImageSize,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(4),
child: embeddedImage,
),
],
);
}
}
/// Dialog to display a QR code for sharing
class QrCodeShareDialog extends StatelessWidget {
final String data;
final String? title;
final String? instructions;
final Widget? embeddedImage;
const QrCodeShareDialog({
super.key,
required this.data,
this.title,
this.instructions,
this.embeddedImage,
});
@override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
QrCodeDisplay(
data: data,
size: 250,
title: title,
instructions: instructions,
embeddedImage: embeddedImage,
padding: EdgeInsets.zero,
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Done'),
),
),
],
),
),
);
}
/// Show the dialog
static Future<void> show({
required BuildContext context,
required String data,
String? title,
String? instructions,
Widget? embeddedImage,
}) {
return showDialog(
context: context,
builder: (context) => QrCodeShareDialog(
data: data,
title: title,
instructions: instructions,
embeddedImage: embeddedImage,
),
);
}
}
+391
View File
@@ -0,0 +1,391 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
/// A reusable QR code scanner widget that can be embedded anywhere.
///
/// Features:
/// - Configurable scan window overlay
/// - Flash toggle button
/// - Camera switch button (front/back)
/// - Customizable callbacks for scan results
/// - Optional validation function for QR data
/// - Automatic pause when not visible
/// - Debouncing to prevent duplicate scans
class QrScannerWidget extends StatefulWidget {
/// Called when a valid QR code is scanned
final void Function(String data) onScanned;
/// Optional validator - return true if the QR data is valid
final bool Function(String data)? validator;
/// Optional error callback when validation fails
final void Function(String data)? onValidationFailed;
/// Whether to show the flash toggle button
final bool showFlashButton;
/// Whether to show the camera switch button
final bool showCameraSwitchButton;
/// Custom overlay widget (defaults to scan window frame)
final Widget? overlay;
/// Instructions text shown below the scan window
final String? instructions;
/// Whether to continue scanning after first successful scan
final bool continuousScanning;
/// Debounce duration to prevent duplicate scans
final Duration debounceDuration;
const QrScannerWidget({
super.key,
required this.onScanned,
this.validator,
this.onValidationFailed,
this.showFlashButton = true,
this.showCameraSwitchButton = true,
this.overlay,
this.instructions,
this.continuousScanning = false,
this.debounceDuration = const Duration(milliseconds: 500),
});
@override
State<QrScannerWidget> createState() => _QrScannerWidgetState();
}
class _QrScannerWidgetState extends State<QrScannerWidget>
with WidgetsBindingObserver {
late MobileScannerController _controller;
bool _hasScanned = false;
String? _lastScannedData;
DateTime? _lastScanTime;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_controller = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_controller.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Handle app lifecycle changes - pause/resume scanner
if (!_controller.value.hasCameraPermission) return;
switch (state) {
case AppLifecycleState.resumed:
_controller.start();
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
_controller.stop();
break;
}
}
void _handleDetection(BarcodeCapture capture) {
// Prevent duplicate scans
if (_hasScanned && !widget.continuousScanning) return;
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
final String? rawValue = barcode.rawValue;
if (rawValue == null || rawValue.isEmpty) continue;
// Debounce - ignore if same data scanned too quickly
final now = DateTime.now();
if (_lastScannedData == rawValue &&
_lastScanTime != null &&
now.difference(_lastScanTime!) < widget.debounceDuration) {
continue;
}
_lastScannedData = rawValue;
_lastScanTime = now;
// Validate if validator provided
if (widget.validator != null && !widget.validator!(rawValue)) {
widget.onValidationFailed?.call(rawValue);
continue;
}
// Mark as scanned to prevent duplicates
if (!widget.continuousScanning) {
setState(() {
_hasScanned = true;
});
_controller.stop();
}
// Notify callback
widget.onScanned(rawValue);
return;
}
}
/// Reset the scanner to allow scanning again
void resetScanner() {
setState(() {
_hasScanned = false;
_lastScannedData = null;
_lastScanTime = null;
});
_controller.start();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Scanner view
MobileScanner(
controller: _controller,
onDetect: _handleDetection,
errorBuilder: (context, error, child) {
return _buildErrorWidget(context, error);
},
),
// Overlay
widget.overlay ?? _buildDefaultOverlay(context),
// Control buttons
Positioned(
bottom: 16,
left: 0,
right: 0,
child: _buildControls(context),
),
],
);
}
Widget _buildDefaultOverlay(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withValues(alpha: 0.5),
BlendMode.srcOut,
),
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
color: Colors.black,
backgroundBlendMode: BlendMode.dstOut,
),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 250,
width: 250,
decoration: BoxDecoration(
color: Colors.red, // This color is used for cutout
borderRadius: BorderRadius.circular(16),
),
),
if (widget.instructions != null) ...[
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.instructions!,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
],
],
),
),
],
),
);
}
Widget _buildControls(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.showFlashButton)
ValueListenableBuilder(
valueListenable: _controller,
builder: (context, state, child) {
return IconButton.filled(
onPressed: () => _controller.toggleTorch(),
icon: Icon(
state.torchState == TorchState.on
? Icons.flash_on
: Icons.flash_off,
),
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
foregroundColor: Colors.white,
),
);
},
),
if (widget.showFlashButton && widget.showCameraSwitchButton)
const SizedBox(width: 24),
if (widget.showCameraSwitchButton)
IconButton.filled(
onPressed: () => _controller.switchCamera(),
icon: const Icon(Icons.cameraswitch),
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
foregroundColor: Colors.white,
),
),
],
);
}
Widget _buildErrorWidget(BuildContext context, MobileScannerException error) {
String message;
IconData icon;
switch (error.errorCode) {
case MobileScannerErrorCode.permissionDenied:
message = 'Camera permission denied.\nPlease enable camera access in settings.';
icon = Icons.no_photography;
break;
case MobileScannerErrorCode.unsupported:
message = 'Camera not supported on this device.';
icon = Icons.videocam_off;
break;
default:
message = 'Failed to start camera.\n${error.errorDetails?.message ?? ''}';
icon = Icons.error_outline;
}
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
),
);
}
}
/// A simpler scanner overlay with just corner brackets
class ScannerCornerOverlay extends StatelessWidget {
final double scanWindowSize;
final Color borderColor;
final double borderWidth;
final double cornerLength;
const ScannerCornerOverlay({
super.key,
this.scanWindowSize = 250,
this.borderColor = Colors.white,
this.borderWidth = 3,
this.cornerLength = 30,
});
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: scanWindowSize,
height: scanWindowSize,
child: CustomPaint(
painter: _CornerPainter(
color: borderColor,
strokeWidth: borderWidth,
cornerLength: cornerLength,
),
),
),
);
}
}
class _CornerPainter extends CustomPainter {
final Color color;
final double strokeWidth;
final double cornerLength;
_CornerPainter({
required this.color,
required this.strokeWidth,
required this.cornerLength,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final path = Path();
// Top-left corner
path.moveTo(0, cornerLength);
path.lineTo(0, 0);
path.lineTo(cornerLength, 0);
// Top-right corner
path.moveTo(size.width - cornerLength, 0);
path.lineTo(size.width, 0);
path.lineTo(size.width, cornerLength);
// Bottom-right corner
path.moveTo(size.width, size.height - cornerLength);
path.lineTo(size.width, size.height);
path.lineTo(size.width - cornerLength, size.height);
// Bottom-left corner
path.moveTo(cornerLength, size.height);
path.lineTo(0, size.height);
path.lineTo(0, size.height - cornerLength);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
@@ -7,6 +7,7 @@ import Foundation
import flutter_blue_plus_darwin
import flutter_local_notifications
import mobile_scanner
import package_info_plus
import path_provider_foundation
import shared_preferences_foundation
@@ -16,6 +17,7 @@ import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+2
View File
@@ -12,5 +12,7 @@
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
+2
View File
@@ -30,5 +30,7 @@
<string>NSApplication</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>MeshCore needs Bluetooth to communicate with LoRa mesh devices</string>
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string>
</dict>
</plist>
+2
View File
@@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
+24
View File
@@ -453,6 +453,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173"
url: "https://pub.dev"
source: hosted
version: "6.0.11"
nested:
dependency: transitive
description:
@@ -605,6 +613,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: transitive
description:
+5
View File
@@ -53,6 +53,8 @@ dependencies:
wakelock_plus: ^1.2.8
characters: ^1.4.0
package_info_plus: ^8.0.0
mobile_scanner: ^6.0.0 # QR/barcode scanning
qr_flutter: ^4.1.0 # QR code generation
dev_dependencies:
flutter_test:
@@ -78,6 +80,9 @@ flutter:
# the material Icons class.
uses-material-design: true
assets:
- assets/images/
flutter_launcher_icons:
android: true
ios: true
+121 -1
View File
@@ -1 +1,121 @@
{}
{
"bg": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"de": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"es": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"fr": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"it": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"nl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"pl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"pt": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"sk": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"sl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"sv": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"zh": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
]
}