diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a4d90390..43cacc99 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -16,6 +16,9 @@
+
+
+
This app uses Bluetooth to communicate with MeshCore devices.
NSBluetoothPeripheralUsageDescription
This app uses Bluetooth to communicate with MeshCore devices.
+ NSCameraUsageDescription
+ This app uses the camera to scan QR codes for joining communities.
diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart
index 15614b5f..29f92af9 100644
--- a/lib/connector/meshcore_connector.dart
+++ b/lib/connector/meshcore_connector.dart
@@ -1615,6 +1615,10 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildSetChannelFrame(index, '', Uint8List(16)));
_channelLastReadMs.remove(index);
_unreadStore.saveChannelLastRead(Map.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();
}
diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb
index ca8e3388..1b5e5de3 100644
--- a/lib/l10n/app_bg.arb
+++ b/lib/l10n/app_bg.arb
@@ -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}"
}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index bd9faade..07f395a6 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -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"
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 48501696..1c1ee514 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -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",
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index eb8474d6..b406e942 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -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}"
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 769c189d..785ee377 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -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}"
}
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index 0c63a160..b0c13a00 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -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}"
}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 07b721be..fe4fc016 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -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:
diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart
index 4e6a1905..314e702f 100644
--- a/lib/l10n/app_localizations_bg.dart
+++ b/lib/l10n/app_localizations_bg.dart
@@ -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 => 'Филтрирайте и сортирайте';
diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart
index 0e516764..b884f3c1 100644
--- a/lib/l10n/app_localizations_de.dart
+++ b/lib/l10n/app_localizations_de.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index c87fbc66..96ba1b97 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart
index 404aeb0c..029ed11e 100644
--- a/lib/l10n/app_localizations_es.dart
+++ b/lib/l10n/app_localizations_es.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart
index 2e06d800..1dce57f6 100644
--- a/lib/l10n/app_localizations_fr.dart
+++ b/lib/l10n/app_localizations_fr.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart
index ebd3f577..20df619a 100644
--- a/lib/l10n/app_localizations_it.dart
+++ b/lib/l10n/app_localizations_it.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart
index 3a1afcbe..50f5744b 100644
--- a/lib/l10n/app_localizations_nl.dart
+++ b/lib/l10n/app_localizations_nl.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart
index bab42cd1..378858a0 100644
--- a/lib/l10n/app_localizations_pl.dart
+++ b/lib/l10n/app_localizations_pl.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index f0f4c213..ae02aff3 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart
index 4fa1f0b1..81bf16aa 100644
--- a/lib/l10n/app_localizations_sk.dart
+++ b/lib/l10n/app_localizations_sk.dart
@@ -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ť';
diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart
index e918f4f6..cdcd23c7 100644
--- a/lib/l10n/app_localizations_sl.dart
+++ b/lib/l10n/app_localizations_sl.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart
index ed11747e..8b36b976 100644
--- a/lib/l10n/app_localizations_sv.dart
+++ b/lib/l10n/app_localizations_sv.dart
@@ -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';
diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart
index aa40f264..3d7bd06e 100644
--- a/lib/l10n/app_localizations_zh.dart
+++ b/lib/l10n/app_localizations_zh.dart
@@ -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 => '筛选和排序';
diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb
index 9c39f666..75a6cc09 100644
--- a/lib/l10n/app_nl.arb
+++ b/lib/l10n/app_nl.arb
@@ -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}"
}
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index a3aebdcc..50732d10 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -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}"
}
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index e30495a0..34797bea 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -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}"
}
diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb
index c4d07f75..d6ea7d83 100644
--- a/lib/l10n/app_sk.arb
+++ b/lib/l10n/app_sk.arb
@@ -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}"
}
diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb
index 4667eac2..09ee0bcf 100644
--- a/lib/l10n/app_sl.arb
+++ b/lib/l10n/app_sl.arb
@@ -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}"
}
diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb
index d8a294d4..4d302a56 100644
--- a/lib/l10n/app_sv.arb
+++ b/lib/l10n/app_sv.arb
@@ -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"
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 8da91c8e..c0704140 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -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}"
}
diff --git a/lib/models/channel.dart b/lib/models/channel.dart
index 3325280b..e05a870e 100644
--- a/lib/models/channel.dart
+++ b/lib/models/channel.dart
@@ -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);
}
diff --git a/lib/models/community.dart b/lib/models/community.dart
new file mode 100644
index 00000000..3bacf887
--- /dev/null
+++ b/lib/models/community.dart
@@ -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 hashtagChannels;
+
+ Community({
+ required this.id,
+ required this.name,
+ required this.secret,
+ required this.createdAt,
+ List? 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;
+ 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 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?)
+ ?.map((e) => e as String)
+ .toList() ??
+ [],
+ );
+ }
+
+ /// Convert to JSON for storage
+ Map 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;
+ 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;
+ 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;
+}
diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart
index bd40e1f8..1cb66ab5 100644
--- a/lib/screens/channels_screen.dart
+++ b/lib/screens/channels_screen.dart
@@ -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,17 +49,59 @@ class ChannelsScreen extends StatefulWidget {
class _ChannelsScreenState extends State
with DisconnectNavigationMixin {
final TextEditingController _searchController = TextEditingController();
+ final CommunityStore _communityStore = CommunityStore();
String _searchQuery = '';
Timer? _searchDebounce;
ChannelSortOption _sortOption = ChannelSortOption.manual;
+ List _communities = [];
+
+ // Cache of PSK hex -> Community for quick lookup
+ final Map _pskToCommunity = {};
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read().getChannels();
+ _loadCommunities();
});
}
+
+ Future _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() {
@@ -82,6 +130,12 @@ class _ChannelsScreenState extends State
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
}
) {
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
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
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
case 3: // Join Hashtag Channel
return Column(
children: [
+ // Only show type selection if user has communities
+ if (_communities.isNotEmpty) ...[
+ RadioGroup(
+ 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(
+ value: true,
+ title: Text(dialogContext.l10n.community_regularHashtag),
+ subtitle: Text(dialogContext.l10n.community_regularHashtagDesc),
+ dense: true,
+ ),
+ RadioListTile(
+ 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(
+ 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
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),
+ 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
);
return;
}
- // Normalize hashtag name
- final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
- final psk = Channel.derivePskFromHashtag(hashtag);
- Navigator.pop(dialogContext);
- connector.setChannel(nextIndex, name, psk);
+
+ // 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, 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
],
);
+ 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(
+ 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
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
}
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(
+ 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();
+ 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(
+ 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();
+ 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 _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();
+
+ // Find all channels that belong to this community
+ List 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(),
+ ),
+ );
+ }
}
diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart
new file mode 100644
index 00000000..a2914a19
--- /dev/null
+++ b/lib/screens/community_qr_scanner_screen.dart
@@ -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 createState() =>
+ _CommunityQrScannerScreenState();
+}
+
+class _CommunityQrScannerScreenState extends State {
+ 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 _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 _showJoinConfirmationDialog(
+ BuildContext context,
+ Community community,
+ ) async {
+ bool addPublicChannel = true;
+
+ final result = await showDialog(
+ 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 _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();
+ 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;
+ }
+}
diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart
new file mode 100644
index 00000000..fe5c8310
--- /dev/null
+++ b/lib/storage/community_store.dart
@@ -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> loadCommunities() async {
+ final prefs = PrefsManager.instance;
+ final jsonString = prefs.getString(_communitiesKey);
+ if (jsonString == null || jsonString.isEmpty) {
+ return [];
+ }
+
+ try {
+ final jsonList = jsonDecode(jsonString) as List;
+ return jsonList
+ .map((json) => Community.fromJson(json as Map))
+ .toList();
+ } catch (e) {
+ // If JSON is corrupted, return empty list
+ return [];
+ }
+ }
+
+ /// Save all communities to storage
+ Future saveCommunities(List 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 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 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 removeCommunity(String communityId) async {
+ final communities = await loadCommunities();
+ communities.removeWhere((c) => c.id == communityId);
+ await saveCommunities(communities);
+ }
+
+ /// Get a community by ID
+ Future 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 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 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 removeHashtagChannel(
+ String communityId,
+ String hashtag,
+ ) async {
+ final community = await getCommunity(communityId);
+ if (community != null) {
+ final updated = community.removeHashtagChannel(hashtag);
+ await updateCommunity(updated);
+ }
+ }
+}
diff --git a/lib/widgets/qr_code_display.dart b/lib/widgets/qr_code_display.dart
new file mode 100644
index 00000000..4d96ebe7
--- /dev/null
+++ b/lib/widgets/qr_code_display.dart
@@ -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 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,
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/qr_scanner_widget.dart b/lib/widgets/qr_scanner_widget.dart
new file mode 100644
index 00000000..e328b6d7
--- /dev/null
+++ b/lib/widgets/qr_scanner_widget.dart
@@ -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 createState() => _QrScannerWidgetState();
+}
+
+class _QrScannerWidgetState extends State
+ 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 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;
+}
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 7deb1ef8..fdb93ad1 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -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"))
diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements
index 5a6782a8..f31e9afc 100644
--- a/macos/Runner/DebugProfile.entitlements
+++ b/macos/Runner/DebugProfile.entitlements
@@ -12,5 +12,7 @@
com.apple.security.device.bluetooth
+ com.apple.security.device.camera
+
diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist
index d054e5d9..c55e9d73 100644
--- a/macos/Runner/Info.plist
+++ b/macos/Runner/Info.plist
@@ -30,5 +30,7 @@
NSApplication
NSBluetoothAlwaysUsageDescription
MeshCore needs Bluetooth to communicate with LoRa mesh devices
+ NSCameraUsageDescription
+ This app uses the camera to scan QR codes for joining communities.
diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements
index 02a20326..29ef507e 100644
--- a/macos/Runner/Release.entitlements
+++ b/macos/Runner/Release.entitlements
@@ -8,5 +8,7 @@
com.apple.security.device.bluetooth
+ com.apple.security.device.camera
+
diff --git a/pubspec.lock b/pubspec.lock
index ef56ad05..de12f546 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -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:
diff --git a/pubspec.yaml b/pubspec.yaml
index 05d090d1..5a819106 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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
diff --git a/untranslated.json b/untranslated.json
index 9e26dfee..2138a62f 100644
--- a/untranslated.json
+++ b/untranslated.json
@@ -1 +1,121 @@
-{}
\ No newline at end of file
+{
+ "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"
+ ]
+}