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" + ] +}