From a0be63b2e74ed33a03ba57b9d1c6f2c569c0ad86 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 20 Jan 2026 21:42:54 -0700 Subject: [PATCH] feat: integrate link handling in chat screen with linkify support - Added flutter_linkify package to auto-detect and linkify URLs in chat messages. - Implemented LinkHandler class to manage link tap confirmations and URL launching. - Updated chat_screen.dart to use Linkify for displaying message text with links. - Registered url_launcher plugin for handling URL launches across platforms. - Updated pubspec.yaml and pubspec.lock to include new dependencies. - Cleaned up untranslated.json by removing unused translations. --- android/app/src/main/AndroidManifest.xml | 9 + ios/Runner/Info.plist | 5 + lib/helpers/link_handler.dart | 76 ++ lib/l10n/app_bg.arb | 53 +- lib/l10n/app_de.arb | 53 +- lib/l10n/app_en.arb | 10 + lib/l10n/app_es.arb | 53 +- lib/l10n/app_fr.arb | 53 +- lib/l10n/app_it.arb | 53 +- lib/l10n/app_localizations.dart | 30 + lib/l10n/app_localizations_bg.dart | 32 +- lib/l10n/app_localizations_de.dart | 32 +- lib/l10n/app_localizations_en.dart | 18 + lib/l10n/app_localizations_es.dart | 32 +- lib/l10n/app_localizations_fr.dart | 32 +- lib/l10n/app_localizations_it.dart | 32 +- lib/l10n/app_localizations_nl.dart | 32 +- lib/l10n/app_localizations_pl.dart | 32 +- lib/l10n/app_localizations_pt.dart | 32 +- lib/l10n/app_localizations_sk.dart | 32 +- lib/l10n/app_localizations_sl.dart | 32 +- lib/l10n/app_localizations_sv.dart | 32 +- lib/l10n/app_localizations_zh.dart | 31 +- lib/l10n/app_nl.arb | 53 +- lib/l10n/app_pl.arb | 53 +- lib/l10n/app_pt.arb | 53 +- lib/l10n/app_sk.arb | 53 +- lib/l10n/app_sl.arb | 53 +- lib/l10n/app_sv.arb | 53 +- lib/l10n/app_zh.arb | 53 +- lib/screens/channel_chat_screen.dart | 17 +- lib/screens/channels_screen.dart | 815 +++++++++--------- lib/screens/chat_screen.dart | 16 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 84 +- pubspec.yaml | 2 + untranslated.json | 122 +-- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 41 files changed, 1615 insertions(+), 619 deletions(-) create mode 100644 lib/helpers/link_handler.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 43cacc99..b8dd623d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -67,5 +67,14 @@ + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index b4e35edb..92ebc463 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -55,5 +55,10 @@ This app uses Bluetooth to communicate with MeshCore devices. NSCameraUsageDescription This app uses the camera to scan QR codes for joining communities. + LSApplicationQueriesSchemes + + http + https + diff --git a/lib/helpers/link_handler.dart b/lib/helpers/link_handler.dart new file mode 100644 index 00000000..fa8e5ffd --- /dev/null +++ b/lib/helpers/link_handler.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../l10n/l10n.dart'; + +class LinkHandler { + static Future handleLinkTap(BuildContext context, String url) async { + // Show confirmation dialog + final shouldOpen = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.chat_openLink), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.chat_openLinkConfirmation, + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: SelectableText( + url, + style: const TextStyle( + fontSize: 12, + fontFamily: 'monospace', + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(context.l10n.common_cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.chat_open), + ), + ], + ), + ); + + if (shouldOpen != true) return; + + // Launch URL + try { + final uri = Uri.parse(url); + if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.chat_couldNotOpenLink(url)), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.chat_invalidLink), + backgroundColor: Colors.red, + ), + ); + } + } + } +} diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 1b5e5de3..e5f40f38 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Отваряне на връзката?", + "chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?", + "chat_open": "Отвори", + "chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Невалиден формат на връзката", "map_title": "Карта на възлите", "map_noNodesWithLocation": "Няма възли с данни за местоположение.", "map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Остави общността \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)", "community_communityHashtag": "Общностен хаштаг", "community_communityHashtagDesc": "Само за членове на общността", - "community_forCommunity": "За {name}" + "community_forCommunity": "За {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.", + "community_secretRegenerated": "Секретно презареждане за \"{name}\"", + "community_regenerateSecret": "Регенерейрай секрет", + "community_regenerate": "Регенерация", + "community_updateSecret": "Актуализирай тайна", + "community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"", + "community_secretUpdated": "Секретно обновено за \"{name}\"" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 07f395a6..0bb17c84 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Link öffnen?", + "chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?", + "chat_open": "Öffnen", + "chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Ungültiges Link-Format", "map_title": "Karte", "map_noNodesWithLocation": "Keine Knoten mit Standortdaten", "map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Community \"{name}\" verlassen", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)", "community_communityHashtagDesc": "Nur für Mitglieder der Community", "community_forCommunity": "Für {name}", - "community_communityHashtag": "Community Hashtag" + "community_communityHashtag": "Community Hashtag", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerate": "Neu generieren", + "community_secretRegenerated": "Geheime Wiederherstellung für \"{name}\" erfolgreich", + "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.", + "community_regenerateSecret": "Neu generieren Sie das Geheimnis", + "community_secretUpdated": "Geheime für \"{name}\" aktualisiert", + "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.", + "community_updateSecret": "Aktualisieren Sie das Geheimnis" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1c1ee514..56cb1cc1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -550,6 +550,16 @@ "count": {"type": "int"} } }, + "chat_openLink": "Open Link?", + "chat_openLinkConfirmation": "Do you want to open this link in your browser?", + "chat_open": "Open", + "chat_couldNotOpenLink": "Could not open link: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": {"type": "String"} + } + }, + "chat_invalidLink": "Invalid link format", "map_title": "Node Map", "map_noNodesWithLocation": "No nodes with location data", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index b406e942..4b6b5262 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "¿Abrir enlace?", + "chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?", + "chat_open": "Abrir", + "chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Formato de enlace no válido", "map_title": "Mapa de Nodos", "map_noNodesWithLocation": "No hay nodos con datos de ubicación", "map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Has salido de la comunidad \"{name}\"", @@ -1484,5 +1498,40 @@ "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}" + "community_forCommunity": "Para {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "Regenerar Contraseña Secreta", + "community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.", + "community_secretRegenerated": "Código secreto regenerado para \"{name}\"", + "community_regenerate": "Regenerar", + "community_secretUpdated": "Confidencialidad actualizada para \"{name}\"", + "community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"", + "community_updateSecret": "Actualizar Contraseña" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 785ee377..1b8d35d1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Ouvrir le lien ?", + "chat_openLinkConfirmation": "Voulez-vous ouvrir ce lien dans votre navigateur ?", + "chat_open": "Ouvrir", + "chat_couldNotOpenLink": "Impossible d'ouvrir le lien : {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Format de lien invalide", "map_title": "Carte des nœuds", "map_noNodesWithLocation": "Aucun nœud avec des données de localisation", "map_nodesNeedGps": "Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Communauté \"{name}\" quittée", @@ -1484,5 +1498,40 @@ "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}" + "community_forCommunity": "Pour {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "Régénérer le secret", + "community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.", + "community_regenerate": "Régénérer", + "community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"", + "community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"", + "community_updateSecret": "Mettre à jour le secret", + "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b0c13a00..cd031fbf 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Aprire il link?", + "chat_openLinkConfirmation": "Vuoi aprire questo link nel tuo browser?", + "chat_open": "Apri", + "chat_couldNotOpenLink": "Impossibile aprire il link: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Formato di link non valido", "map_title": "Mappa Nodi", "map_noNodesWithLocation": "Nessun nodo con dati di posizione", "map_nodesNeedGps": "I nodi devono condividere le loro coordinate GPS\nper apparire sulla mappa", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Hai lasciato la comunità \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)", "community_communityHashtag": "Hashtag della Comunità", "community_communityHashtagDesc": "Visibile solo ai membri della comunità", - "community_forCommunity": "Per {name}" + "community_forCommunity": "Per {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecretConfirm": "Regenera la chiave segreta per \"{name}\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.", + "community_regenerateSecret": "Ri genera la chiave segreta", + "community_regenerate": "Rigenera", + "community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"", + "community_updateSecret": "Aggiorna Segreto", + "community_secretUpdated": "Segreto aggiornato per \"{name}\"", + "community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\"" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index fe4fc016..d52830ca 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2226,6 +2226,36 @@ abstract class AppLocalizations { /// **'Unread: {count}'** String chat_unread(int count); + /// No description provided for @chat_openLink. + /// + /// In en, this message translates to: + /// **'Open Link?'** + String get chat_openLink; + + /// No description provided for @chat_openLinkConfirmation. + /// + /// In en, this message translates to: + /// **'Do you want to open this link in your browser?'** + String get chat_openLinkConfirmation; + + /// No description provided for @chat_open. + /// + /// In en, this message translates to: + /// **'Open'** + String get chat_open; + + /// No description provided for @chat_couldNotOpenLink. + /// + /// In en, this message translates to: + /// **'Could not open link: {url}'** + String chat_couldNotOpenLink(String url); + + /// No description provided for @chat_invalidLink. + /// + /// In en, this message translates to: + /// **'Invalid link format'** + String get chat_invalidLink; + /// No description provided for @map_title. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 314e702f..9b70d9c4 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1207,6 +1207,24 @@ class AppLocalizationsBg extends AppLocalizations { return 'Непрочетени: $count'; } + @override + String get chat_openLink => 'Отваряне на връзката?'; + + @override + String get chat_openLinkConfirmation => + 'Искате ли да отворите тази връзка в браузъра си?'; + + @override + String get chat_open => 'Отвори'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Не можа да се отвори връзката: $url'; + } + + @override + String get chat_invalidLink => 'Невалиден формат на връзката'; + @override String get map_title => 'Карта на възлите'; @@ -2567,32 +2585,32 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Регенерейрай секрет'; @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.'; + return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Регенерация'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Секретно презареждане за \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Актуализирай тайна'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Секретно обновено за \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b884f3c1..9bab237e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1206,6 +1206,24 @@ class AppLocalizationsDe extends AppLocalizations { return 'Ungelesen: $count'; } + @override + String get chat_openLink => 'Link öffnen?'; + + @override + String get chat_openLinkConfirmation => + 'Möchten Sie diesen Link in Ihrem Browser öffnen?'; + + @override + String get chat_open => 'Öffnen'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Link konnte nicht geöffnet werden: $url'; + } + + @override + String get chat_invalidLink => 'Ungültiges Link-Format'; + @override String get map_title => 'Karte'; @@ -2570,32 +2588,32 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Neu generieren Sie das Geheimnis'; @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.'; + return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Neu generieren'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Geheime Wiederherstellung für \"$name\" erfolgreich'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Aktualisieren Sie das Geheimnis'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Geheime für \"$name\" aktualisiert'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.'; } @override diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 96ba1b97..86f18ba2 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1186,6 +1186,24 @@ class AppLocalizationsEn extends AppLocalizations { return 'Unread: $count'; } + @override + String get chat_openLink => 'Open Link?'; + + @override + String get chat_openLinkConfirmation => + 'Do you want to open this link in your browser?'; + + @override + String get chat_open => 'Open'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Could not open link: $url'; + } + + @override + String get chat_invalidLink => 'Invalid link format'; + @override String get map_title => 'Node Map'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 029ed11e..908c88c8 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1204,6 +1204,24 @@ class AppLocalizationsEs extends AppLocalizations { return 'Sin leer: $count'; } + @override + String get chat_openLink => '¿Abrir enlace?'; + + @override + String get chat_openLinkConfirmation => + '¿Quiere abrir este enlace en su navegador?'; + + @override + String get chat_open => 'Abrir'; + + @override + String chat_couldNotOpenLink(String url) { + return 'No se pudo abrir el enlace: $url'; + } + + @override + String get chat_invalidLink => 'Formato de enlace no válido'; + @override String get map_title => 'Mapa de Nodos'; @@ -2565,32 +2583,32 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Regenerar Contraseña Secreta'; @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.'; + return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Regenerar'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Código secreto regenerado para \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Actualizar Contraseña'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Confidencialidad actualizada para \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 1dce57f6..48a6ac48 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1209,6 +1209,24 @@ class AppLocalizationsFr extends AppLocalizations { return 'Non lu : $count'; } + @override + String get chat_openLink => 'Ouvrir le lien ?'; + + @override + String get chat_openLinkConfirmation => + 'Voulez-vous ouvrir ce lien dans votre navigateur ?'; + + @override + String get chat_open => 'Ouvrir'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Impossible d\'ouvrir le lien : $url'; + } + + @override + String get chat_invalidLink => 'Format de lien invalide'; + @override String get map_title => 'Carte des nœuds'; @@ -2581,32 +2599,32 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Régénérer le 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.'; + return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Régénérer'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Mot de passe secret régénéré pour \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Mettre à jour le secret'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Modification secrète mise à jour pour \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 20df619a..83010d8d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1203,6 +1203,24 @@ class AppLocalizationsIt extends AppLocalizations { return 'Non letti: $count'; } + @override + String get chat_openLink => 'Aprire il link?'; + + @override + String get chat_openLinkConfirmation => + 'Vuoi aprire questo link nel tuo browser?'; + + @override + String get chat_open => 'Apri'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Impossibile aprire il link: $url'; + } + + @override + String get chat_invalidLink => 'Formato di link non valido'; + @override String get map_title => 'Mappa Nodi'; @@ -2565,32 +2583,32 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Ri genera la chiave segreta'; @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.'; + return 'Regenera la chiave segreta per \"$name\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Rigenera'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Codice segreto rigenerato per \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Aggiorna Segreto'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Segreto aggiornato per \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scansiona il nuovo codice QR per aggiornare il segreto di \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 50f5744b..ce60a8f8 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1199,6 +1199,24 @@ class AppLocalizationsNl extends AppLocalizations { return 'Nieuw: $count'; } + @override + String get chat_openLink => 'Link openen?'; + + @override + String get chat_openLinkConfirmation => + 'Wilt u deze link in uw browser openen?'; + + @override + String get chat_open => 'Openen'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Kan link niet openen: $url'; + } + + @override + String get chat_invalidLink => 'Ongeldig linkformaat'; + @override String get map_title => 'Node Map'; @@ -2556,32 +2574,32 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Regeneer Geheimwoord'; @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.'; + return 'Regeneere de geheime sleutel voor \"$name\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Regeneer'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Geheim hersteld voor \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Bijwerken Geheime'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Geheim gewijzigd voor \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scan de nieuwe QR-code om het geheim voor \"$name\" bij te werken'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 378858a0..13fbeb0a 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1205,6 +1205,24 @@ class AppLocalizationsPl extends AppLocalizations { return 'Niezgłoszone: $count'; } + @override + String get chat_openLink => 'Otworzyć link?'; + + @override + String get chat_openLinkConfirmation => + 'Czy chcesz otworzyć ten link w przeglądarce?'; + + @override + String get chat_open => 'Otwórz'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Nie można otworzyć linku: $url'; + } + + @override + String get chat_invalidLink => 'Nieprawidłowy format linku'; + @override String get map_title => 'Mapa węzłów'; @@ -2564,32 +2582,32 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Zregeneruj sekret'; @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.'; + return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Zregeneruj'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Hasło ponownie wygenerowane dla \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Zaktualizuj tajny klucz'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Hasło zaktualizowane dla \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ae02aff3..3f54001f 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1204,6 +1204,24 @@ class AppLocalizationsPt extends AppLocalizations { return 'Não lido: $count'; } + @override + String get chat_openLink => 'Abrir link?'; + + @override + String get chat_openLinkConfirmation => + 'Deseja abrir este link no seu navegador?'; + + @override + String get chat_open => 'Abrir'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Não foi possível abrir o link: $url'; + } + + @override + String get chat_invalidLink => 'Formato de link inválido'; + @override String get map_title => 'Mapa de Nós'; @@ -2567,32 +2585,32 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Regenerar Senha Segura'; @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.'; + return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Regenerar'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Senha secreta regenerada para \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Atualizar Segredo'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Segredo atualizado para \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 81bf16aa..75d46549 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1200,6 +1200,24 @@ class AppLocalizationsSk extends AppLocalizations { return 'Nezriadené: $count'; } + @override + String get chat_openLink => 'Otvoriť odkaz?'; + + @override + String get chat_openLinkConfirmation => + 'Chcete otvoriť tento odkaz v prehliadači?'; + + @override + String get chat_open => 'Otvoriť'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Nepodarilo sa otvoriť odkaz: $url'; + } + + @override + String get chat_invalidLink => 'Neplatný formát odkazu'; + @override String get map_title => 'Mapa uzlov'; @@ -2553,32 +2571,32 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Zobraziť nový tajný kód'; @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.'; + return 'Znovu vygenerovať tajný kľúč pre \"$name\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Znovu vygenerovať'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Záznam pre \"$name\" bol regenerovaný tajne'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Aktualizovať tajné heslo'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Zmena tajnej slova pre \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index cdcd23c7..be9556f3 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1197,6 +1197,24 @@ class AppLocalizationsSl extends AppLocalizations { return 'Nerešeno: $count'; } + @override + String get chat_openLink => 'Odpreti povezavo?'; + + @override + String get chat_openLinkConfirmation => + 'Ali želite odpreti to povezavo v brskalniku?'; + + @override + String get chat_open => 'Odpri'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Povezave ni bilo mogoče odpreti: $url'; + } + + @override + String get chat_invalidLink => 'Neveljavna oblika povezave'; + @override String get map_title => 'Mapa omrežja'; @@ -2557,32 +2575,32 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Preberi nov tajni kôd'; @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.'; + return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Preberi znova'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Tajna za \"$name\" ponovno ustvarjena'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Ažurniraj tajno'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Skrivnostno spremembo za \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Skeniraj nov kôd QR za posodabljanje tajne za $name'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 8b36b976..34b54b46 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1192,6 +1192,24 @@ class AppLocalizationsSv extends AppLocalizations { return 'Olästa: $count'; } + @override + String get chat_openLink => 'Öppna länk?'; + + @override + String get chat_openLinkConfirmation => + 'Vill du öppna den här länken i din webbläsare?'; + + @override + String get chat_open => 'Öppna'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Kunde inte öppna länken: $url'; + } + + @override + String get chat_invalidLink => 'Ogiltigt länkformat'; + @override String get map_title => 'Nodkarta'; @@ -2541,32 +2559,32 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Regenerera hemlig kod'; @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.'; + return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Regenerera'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Lösenord återskapad för \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Uppdatera hemlighet'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Hemlighet uppdaterad för \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 3d7bd06e..cd9c3be8 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1148,6 +1148,23 @@ class AppLocalizationsZh extends AppLocalizations { return '未读:$count'; } + @override + String get chat_openLink => '打开链接?'; + + @override + String get chat_openLinkConfirmation => '您想在浏览器中打开此链接吗?'; + + @override + String get chat_open => '打开'; + + @override + String chat_couldNotOpenLink(String url) { + return '无法打开链接:$url'; + } + + @override + String get chat_invalidLink => '链接格式无效'; + @override String get map_title => '节点地图'; @@ -2425,32 +2442,32 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => '重新生成密钥'; @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.'; + return '重新生成“$name”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => '重新生成'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return '密码已重置为“$name”'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => '更新密钥'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return '密码已更新为“$name”'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return '扫描新的二维码更新\"$name\"的密码'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 75a6cc09..48ef3ddd 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Link openen?", + "chat_openLinkConfirmation": "Wilt u deze link in uw browser openen?", + "chat_open": "Openen", + "chat_couldNotOpenLink": "Kan link niet openen: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Ongeldig linkformaat", "map_title": "Node Map", "map_noNodesWithLocation": "Geen nodes met locatiegegevens", "map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Community \"{name}\" verlaten", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Open hashtag (iedereen kan deelnemen)", "community_communityHashtag": "Gemeenschappelijk Hashtag", "community_communityHashtagDesc": "Alleen zichtbaar voor leden van de community", - "community_forCommunity": "Voor {name}" + "community_forCommunity": "Voor {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_secretRegenerated": "Geheim hersteld voor \"{name}\"", + "community_regenerateSecret": "Regeneer Geheimwoord", + "community_regenerateSecretConfirm": "Regeneere de geheime sleutel voor \"{name}\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.", + "community_regenerate": "Regeneer", + "community_updateSecret": "Bijwerken Geheime", + "community_secretUpdated": "Geheim gewijzigd voor \"{name}\"", + "community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 50732d10..823bba1e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Otworzyć link?", + "chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglądarce?", + "chat_open": "Otwórz", + "chat_couldNotOpenLink": "Nie można otworzyć linku: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Nieprawidłowy format linku", "map_title": "Mapa węzłów", "map_noNodesWithLocation": "Brak węzłów z danymi lokalizacyjnymi", "map_nodesNeedGps": "Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Opuszczono społeczność \"{name}\"", @@ -1484,5 +1498,40 @@ "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}" + "community_forCommunity": "Dla {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerate": "Zregeneruj", + "community_secretRegenerated": "Hasło ponownie wygenerowane dla \"{name}\"", + "community_regenerateSecret": "Zregeneruj sekret", + "community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.", + "community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"", + "community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"", + "community_updateSecret": "Zaktualizuj tajny klucz" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 34797bea..b48db37b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Abrir link?", + "chat_openLinkConfirmation": "Deseja abrir este link no seu navegador?", + "chat_open": "Abrir", + "chat_couldNotOpenLink": "Não foi possível abrir o link: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Formato de link inválido", "map_title": "Mapa de Nós", "map_noNodesWithLocation": "Não existem nós com dados de localização.", "map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Saiu da comunidade \"{name}\"", @@ -1484,5 +1498,40 @@ "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}" + "community_forCommunity": "Para {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.", + "community_regenerateSecret": "Regenerar Senha Segura", + "community_secretRegenerated": "Senha secreta regenerada para \"{name}\"", + "community_regenerate": "Regenerar", + "community_secretUpdated": "Segredo atualizado para \"{name}\"", + "community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++", + "community_updateSecret": "Atualizar Segredo" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d6ea7d83..71871d16 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Otvoriť odkaz?", + "chat_openLinkConfirmation": "Chcete otvoriť tento odkaz v prehliadači?", + "chat_open": "Otvoriť", + "chat_couldNotOpenLink": "Nepodarilo sa otvoriť odkaz: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Neplatný formát odkazu", "map_title": "Mapa uzlov", "map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe", "map_nodesNeedGps": "Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Opustená komunita \"{name}\"", @@ -1484,5 +1498,40 @@ "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}" + "community_forCommunity": "Pre {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne", + "community_regenerateSecretConfirm": "Znovu vygenerovať tajný kľúč pre \"{name}\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.", + "community_regenerate": "Znovu vygenerovať", + "community_regenerateSecret": "Zobraziť nový tajný kód", + "community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"", + "community_updateSecret": "Aktualizovať tajné heslo", + "community_secretUpdated": "Zmena tajnej slova pre \"{name}\"" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 09ee0bcf..977f29cd 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Odpreti povezavo?", + "chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?", + "chat_open": "Odpri", + "chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Neveljavna oblika povezave", "map_title": "Mapa omrežja", "map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.", "map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Zapustil skupnost \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)", "community_communityHashtag": "Skupnostni hashtag", "community_communityHashtagDesc": "Izključeno za uporabnike skupnosti", - "community_forCommunity": "Za {name}" + "community_forCommunity": "Za {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_secretRegenerated": "Tajna za \"{name}\" ponovno ustvarjena", + "community_regenerateSecret": "Preberi nov tajni kôd", + "community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.", + "community_regenerate": "Preberi znova", + "community_scanToUpdateSecret": "Skeniraj nov kôd QR za posodabljanje tajne za {name}", + "community_updateSecret": "Ažurniraj tajno", + "community_secretUpdated": "Skrivnostno spremembo za \"{name}\"" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 4d302a56..f1da7c8b 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Öppna länk?", + "chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?", + "chat_open": "Öppna", + "chat_couldNotOpenLink": "Kunde inte öppna länken: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Ogiltigt länkformat", "map_title": "Nodkarta", "map_noNodesWithLocation": "Inga noder med platsinformation", "map_nodesNeedGps": "Noder måste dela sina GPS-koordinater\nför att visas på kartan", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Lämnade community \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)", "community_communityHashtagDesc": "Endast för medlemmar", "community_forCommunity": "För {name}", - "community_communityHashtag": "Community Hashtag" + "community_communityHashtag": "Community Hashtag", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerate": "Regenerera", + "community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.", + "community_secretRegenerated": "Lösenord återskapad för \"{name}\"", + "community_regenerateSecret": "Regenerera hemlig kod", + "community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"", + "community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"", + "community_updateSecret": "Uppdatera hemlighet" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c0704140..ae10f604 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "打开链接?", + "chat_openLinkConfirmation": "您想在浏览器中打开此链接吗?", + "chat_open": "打开", + "chat_couldNotOpenLink": "无法打开链接:{url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "链接格式无效", "map_title": "节点地图", "map_noNodesWithLocation": "没有具有位置数据的节点", "map_nodesNeedGps": "节点需要共享它们的 GPS 坐标\n才能在地图上显示", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "已退出社区 \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "公共话题(任何人都可以加入)", "community_communityHashtag": "社区标签", "community_communityHashtagDesc": "仅限社区成员使用", - "community_forCommunity": "对于 {name}" + "community_forCommunity": "对于 {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "重新生成密钥", + "community_secretRegenerated": "密码已重置为“{name}”", + "community_regenerate": "重新生成", + "community_regenerateSecretConfirm": "重新生成“{name}”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。", + "community_scanToUpdateSecret": "扫描新的二维码更新\"{name}\"的密码", + "community_updateSecret": "更新密钥", + "community_secretUpdated": "密码已更新为“{name}”" } diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 46648433..84aeb0a6 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -3,11 +3,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; @@ -280,9 +282,20 @@ class _ChannelChatScreenState extends State { : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ) else - Text( - message.text, + Linkify( + text: message.text, style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => LinkHandler.handleLinkTap(context, link.url), ), if (displayPath.isNotEmpty) ...[ const SizedBox(height: 4), diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 1cb66ab5..a120a0d8 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -18,7 +18,6 @@ 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'; @@ -27,20 +26,12 @@ import 'contacts_screen.dart'; import 'map_screen.dart'; import 'settings_screen.dart'; -enum ChannelSortOption { - manual, - name, - latestMessages, - unread, -} +enum ChannelSortOption { manual, name, latestMessages, unread } class ChannelsScreen extends StatefulWidget { final bool hideBackButton; - const ChannelsScreen({ - super.key, - this.hideBackButton = false, - }); + const ChannelsScreen({super.key, this.hideBackButton = false}); @override State createState() => _ChannelsScreenState(); @@ -54,7 +45,7 @@ class _ChannelsScreenState extends State Timer? _searchDebounce; ChannelSortOption _sortOption = ChannelSortOption.manual; List _communities = []; - + // Cache of PSK hex -> Community for quick lookup final Map _pskToCommunity = {}; @@ -66,7 +57,7 @@ class _ChannelsScreenState extends State _loadCommunities(); }); } - + Future _loadCommunities() async { final communities = await _communityStore.loadCommunities(); if (mounted) { @@ -76,14 +67,14 @@ class _ChannelsScreenState extends State }); } } - + 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); @@ -91,12 +82,12 @@ class _ChannelsScreenState extends State } } } - + /// 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(); @@ -181,7 +172,10 @@ class _ChannelsScreenState extends State ); } - final filteredChannels = _filterAndSortChannels(channels, connector); + final filteredChannels = _filterAndSortChannels( + channels, + connector, + ); return Column( children: [ @@ -211,17 +205,22 @@ class _ChannelsScreenState extends State border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), ), onChanged: (value) { _searchDebounce?.cancel(); - _searchDebounce = Timer(const Duration(milliseconds: 300), () { - if (!mounted) return; - setState(() { - _searchQuery = value.toLowerCase(); - }); - }); + _searchDebounce = Timer( + const Duration(milliseconds: 300), + () { + if (!mounted) return; + setState(() { + _searchQuery = value.toLowerCase(); + }); + }, + ); }, ), ), @@ -235,11 +234,18 @@ class _ChannelsScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.search_off, size: 64, color: Colors.grey[400]), + Icon( + Icons.search_off, + size: 64, + color: Colors.grey[400], + ), const SizedBox(height: 16), Text( context.l10n.channels_noChannelsFound, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), ), ], ), @@ -247,51 +253,58 @@ class _ChannelsScreenState extends State ), ], ) - : (_sortOption == ChannelSortOption.manual && _searchQuery.isEmpty) - ? ReorderableListView.builder( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 88, + : (_sortOption == ChannelSortOption.manual && + _searchQuery.isEmpty) + ? ReorderableListView.builder( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 88, + ), + buildDefaultDragHandles: false, + itemCount: filteredChannels.length, + onReorder: (oldIndex, newIndex) { + if (newIndex > oldIndex) newIndex -= 1; + final reordered = List.from( + filteredChannels, + ); + final item = reordered.removeAt(oldIndex); + reordered.insert(newIndex, item); + unawaited( + connector.setChannelOrder( + reordered.map((c) => c.index).toList(), ), - buildDefaultDragHandles: false, - itemCount: filteredChannels.length, - onReorder: (oldIndex, newIndex) { - if (newIndex > oldIndex) newIndex -= 1; - final reordered = List.from(filteredChannels); - final item = reordered.removeAt(oldIndex); - reordered.insert(newIndex, item); - unawaited( - connector.setChannelOrder( - reordered.map((c) => c.index).toList(), - ), - ); - }, - itemBuilder: (context, index) { - final channel = filteredChannels[index]; - return _buildChannelTile( - context, - connector, - channel, - showDragHandle: true, - dragIndex: index, - ); - }, - ) - : ListView.builder( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 88, - ), - itemCount: filteredChannels.length, - itemBuilder: (context, index) { - final channel = filteredChannels[index]; - return _buildChannelTile(context, connector, channel); - }, - ), + ); + }, + itemBuilder: (context, index) { + final channel = filteredChannels[index]; + return _buildChannelTile( + context, + connector, + channel, + showDragHandle: true, + dragIndex: index, + ); + }, + ) + : ListView.builder( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 88, + ), + itemCount: filteredChannels.length, + itemBuilder: (context, index) { + final channel = filteredChannels[index]; + return _buildChannelTile( + context, + connector, + channel, + ); + }, + ), ), ], ); @@ -305,7 +318,8 @@ class _ChannelsScreenState extends State top: false, child: QuickSwitchBar( selectedIndex: 1, - onDestinationSelected: (index) => _handleQuickSwitch(index, context), + onDestinationSelected: (index) => + _handleQuickSwitch(index, context), ), ), ), @@ -315,33 +329,34 @@ class _ChannelsScreenState extends State Widget _buildChannelTile( BuildContext context, MeshCoreConnector connector, - Channel channel, - { + Channel channel, { bool showDragHandle = false, int? dragIndex, - } - ) { + }) { final unreadCount = connector.getUnreadCountForChannel(channel); final community = _getCommunityForChannel(channel); final isCommunityChannel = community != null; - final isCommunityPublic = isCommunityChannel && _isCommunityPublicChannel(channel, community); - + 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}'; + subtitle = + '${context.l10n.community_publicChannel} • ${community.name}'; } else { icon = Icons.tag; - subtitle = '${context.l10n.community_hashtagChannel} • ${community.name}'; + subtitle = + '${context.l10n.community_hashtagChannel} • ${community.name}'; } } else if (channel.isPublicChannel) { icon = Icons.public; @@ -359,7 +374,7 @@ class _ChannelsScreenState extends State 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), @@ -389,24 +404,18 @@ class _ChannelsScreenState extends State width: 2, ), ), - child: const Icon( - Icons.people, - size: 8, - color: Colors.white, - ), + child: const Icon(Icons.people, size: 8, color: Colors.white), ), ), ], ), title: Text( - channel.name.isEmpty ? context.l10n.channels_channelIndex(channel.index) : channel.name, + channel.name.isEmpty + ? context.l10n.channels_channelIndex(channel.index) + : channel.name, style: const TextStyle(fontWeight: FontWeight.w500), ), - subtitle: Text( - subtitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -465,7 +474,10 @@ class _ChannelsScreenState extends State ), ListTile( leading: const Icon(Icons.delete_outline, color: Colors.red), - title: Text(context.l10n.channels_deleteChannel, style: const TextStyle(color: Colors.red)), + title: Text( + context.l10n.channels_deleteChannel, + style: const TextStyle(color: Colors.red), + ), onTap: () async { Navigator.pop(context); await Future.delayed(const Duration(milliseconds: 100)); @@ -486,17 +498,13 @@ class _ChannelsScreenState extends State case 0: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const ContactsScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)), ); break; case 2: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const MapScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const MapScreen(hideBackButton: true)), ); break; } @@ -587,8 +595,12 @@ class _ChannelsScreenState extends State filtered.sort((a, b) { final aMessages = connector.getChannelMessages(a); final bMessages = connector.getChannelMessages(b); - final aLast = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp; - final bLast = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp; + final aLast = aMessages.isEmpty + ? DateTime(1970) + : aMessages.last.timestamp; + final bLast = bMessages.isEmpty + ? DateTime(1970) + : bMessages.last.timestamp; final timeCompare = bLast.compareTo(aLast); if (timeCompare != 0) return timeCompare; return compareByName(a, b); @@ -612,7 +624,9 @@ class _ChannelsScreenState extends State } String _normalizeChannelName(Channel channel) { - if (channel.name.isEmpty) return 'Channel ${channel.index}'; // Fallback for sorting + if (channel.name.isEmpty) { + return 'Channel ${channel.index}'; // Fallback for sorting + } final trimmed = channel.name.trim(); if (trimmed.startsWith('#') && trimmed.length > 1) { return trimmed.substring(1); @@ -622,7 +636,10 @@ class _ChannelsScreenState extends State void _showAddChannelDialog(BuildContext context) { final connector = context.read(); - final nextIndex = _findNextAvailableIndex(connector.channels, connector.maxChannels); + final nextIndex = _findNextAvailableIndex( + connector.channels, + connector.maxChannels, + ); final hasPublicChannel = connector.channels.any((c) => c.isPublicChannel); int? selectedOption; final nameController = TextEditingController(); @@ -647,12 +664,16 @@ class _ChannelsScreenState extends State return ListTile( leading: CircleAvatar( backgroundColor: enabled - ? (isSelected ? Theme.of(dialogContext).colorScheme.primaryContainer : null) + ? (isSelected + ? Theme.of(dialogContext).colorScheme.primaryContainer + : null) : Colors.grey.withValues(alpha: 0.2), child: Icon( icon, color: enabled - ? (isSelected ? Theme.of(dialogContext).colorScheme.primary : null) + ? (isSelected + ? Theme.of(dialogContext).colorScheme.primary + : null) : Colors.grey, ), ), @@ -685,7 +706,10 @@ class _ChannelsScreenState extends State return Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: nameController, decoration: InputDecoration( @@ -704,8 +728,16 @@ class _ChannelsScreenState extends State onPressed: () { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .channels_enterChannelName, + ), + ), ); return; } @@ -718,7 +750,13 @@ class _ChannelsScreenState extends State connector.setChannel(nextIndex, name, psk); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelAdded(name))), + SnackBar( + content: Text( + context.l10n.channels_channelAdded( + name, + ), + ), + ), ); } }, @@ -735,7 +773,10 @@ class _ChannelsScreenState extends State return Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: nameController, decoration: InputDecoration( @@ -746,7 +787,10 @@ class _ChannelsScreenState extends State ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: pskController, decoration: InputDecoration( @@ -765,8 +809,16 @@ class _ChannelsScreenState extends State final name = nameController.text.trim(); final pskHex = pskController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .channels_enterChannelName, + ), + ), ); return; } @@ -774,8 +826,16 @@ class _ChannelsScreenState extends State try { psk = Channel.parsePskHex(pskHex); } on FormatException { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .channels_pskMustBe32Hex, + ), + ), ); return; } @@ -783,7 +843,13 @@ class _ChannelsScreenState extends State connector.setChannel(nextIndex, name, psk); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelAdded(name))), + SnackBar( + content: Text( + context.l10n.channels_channelAdded( + name, + ), + ), + ), ); } }, @@ -798,18 +864,27 @@ class _ChannelsScreenState extends State case 2: // Join Public Channel return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: Row( children: [ Expanded( child: FilledButton( onPressed: () { - final psk = Channel.parsePskHex(Channel.publicChannelPsk); + final psk = Channel.parsePskHex( + Channel.publicChannelPsk, + ); Navigator.pop(dialogContext); connector.setChannel(nextIndex, 'Public', psk); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_publicChannelAdded)), + SnackBar( + content: Text( + context.l10n.channels_publicChannelAdded, + ), + ), ); } }, @@ -825,55 +900,76 @@ class _ChannelsScreenState extends State children: [ // Only show type selection if user has communities if (_communities.isNotEmpty) ...[ - RadioGroup( + RadioListTile( + value: true, groupValue: isRegularHashtag, onChanged: (v) => setDialogState(() { - if (v != null) { - isRegularHashtag = v; - if (isRegularHashtag) { - selectedCommunity = null; - } else if (selectedCommunity == null && _communities.isNotEmpty) { - selectedCommunity = _communities.first; - } + isRegularHashtag = v!; + if (isRegularHashtag) { + selectedCommunity = null; } }), - 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, - ), - ], + title: Text( + dialogContext.l10n.community_regularHashtag, ), + subtitle: Text( + dialogContext.l10n.community_regularHashtagDesc, + ), + dense: true, + ), + RadioListTile( + value: false, + groupValue: isRegularHashtag, + onChanged: (v) => setDialogState(() { + isRegularHashtag = v!; + if (!isRegularHashtag && + selectedCommunity == null && + _communities.isNotEmpty) { + selectedCommunity = _communities.first; + } + }), + 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, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: DropdownButtonFormField( + initialValue: selectedCommunity, + items: _communities + .map( + (c) => DropdownMenuItem( + value: c, + child: Text(c.name), + ), + ) + .toList(), + onChanged: (c) => + setDialogState(() => selectedCommunity = c), + decoration: InputDecoration( + labelText: + dialogContext.l10n.community_selectCommunity, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.groups), + ), ), ), // Hashtag name input Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: hashtagController, decoration: InputDecoration( @@ -899,7 +995,10 @@ class _ChannelsScreenState extends State ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: Row( children: [ Expanded( @@ -907,18 +1006,26 @@ class _ChannelsScreenState extends State onPressed: () async { var hashtag = hashtagController.text.trim(); if (hashtag.isEmpty) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .channels_enterChannelName, + ), + ), ); return; } - + // 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 @@ -926,24 +1033,46 @@ class _ChannelsScreenState extends State } else { // Community hashtag - HMAC derivation from community secret if (selectedCommunity == null) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.community_selectCommunity)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .community_selectCommunity, + ), + ), ); return; } - psk = selectedCommunity!.deriveCommunityHashtagPsk(hashtag); + psk = selectedCommunity! + .deriveCommunityHashtagPsk(hashtag); // Track in community's hashtag list - await _communityStore.addHashtagChannel(selectedCommunity!.id, hashtag); + await _communityStore.addHashtagChannel( + selectedCommunity!.id, + hashtag, + ); _loadCommunities(); } - + if (dialogContext.mounted) { Navigator.pop(dialogContext); } - connector.setChannel(nextIndex, channelName, psk); + connector.setChannel( + nextIndex, + channelName, + psk, + ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelAdded(channelName))), + SnackBar( + content: Text( + context.l10n.channels_channelAdded( + channelName, + ), + ), + ), ); } }, @@ -958,7 +1087,10 @@ class _ChannelsScreenState extends State case 4: // Scan Community QR return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: Row( children: [ Expanded( @@ -966,15 +1098,16 @@ class _ChannelsScreenState extends State onPressed: () async { Navigator.pop(dialogContext); if (context.mounted) { - await Navigator.push( + final result = await Navigator.push( context, MaterialPageRoute( - builder: (context) => const CommunityQrScannerScreen(), + builder: (context) => + const CommunityQrScannerScreen(), ), ); - // Refresh communities list when returning from scanner - if (context.mounted) { - _loadCommunities(); + // Result handled by scanner screen + if (result != null && context.mounted) { + // Community was joined, refresh might be needed } } }, @@ -990,7 +1123,10 @@ class _ChannelsScreenState extends State return Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: nameController, decoration: InputDecoration( @@ -1009,10 +1145,16 @@ class _ChannelsScreenState extends State addPublicChannel = value ?? true; }); }, - title: Text(dialogContext.l10n.community_addPublicChannel), - subtitle: Text(dialogContext.l10n.community_addPublicChannelHint), + title: Text( + dialogContext.l10n.community_addPublicChannel, + ), + subtitle: Text( + dialogContext.l10n.community_addPublicChannelHint, + ), controlAffinity: ListTileControlAffinity.leading, - contentPadding: const EdgeInsets.symmetric(horizontal: 16), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -1023,47 +1165,68 @@ class _ChannelsScreenState extends State onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.community_enterName)), + 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); + 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))), + 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), + instructions: context.l10n + .community_qrInstructions(name), + embeddedImage: Image.asset( + 'assets/images/mesh-icon.png', + width: 40, + height: 40, + ), ); } }, @@ -1094,7 +1257,8 @@ class _ChannelsScreenState extends State optionIndex: 0, icon: Icons.add, title: dialogContext.l10n.channels_createPrivateChannel, - subtitle: dialogContext.l10n.channels_createPrivateChannelDesc, + subtitle: + dialogContext.l10n.channels_createPrivateChannelDesc, ), if (selectedOption == 0) buildExpandedContent()!, const Divider(height: 1), @@ -1102,7 +1266,8 @@ class _ChannelsScreenState extends State optionIndex: 1, icon: Icons.lock, title: dialogContext.l10n.channels_joinPrivateChannel, - subtitle: dialogContext.l10n.channels_joinPrivateChannelDesc, + subtitle: + dialogContext.l10n.channels_joinPrivateChannelDesc, ), if (selectedOption == 1) buildExpandedContent()!, if (!hasPublicChannel) ...[ @@ -1111,7 +1276,8 @@ class _ChannelsScreenState extends State optionIndex: 2, icon: Icons.public, title: dialogContext.l10n.channels_joinPublicChannel, - subtitle: dialogContext.l10n.channels_joinPublicChannelDesc, + subtitle: + dialogContext.l10n.channels_joinPublicChannelDesc, ), if (selectedOption == 2) buildExpandedContent()!, ], @@ -1120,7 +1286,8 @@ class _ChannelsScreenState extends State optionIndex: 3, icon: Icons.tag, title: dialogContext.l10n.channels_joinHashtagChannel, - subtitle: dialogContext.l10n.channels_joinHashtagChannelDesc, + subtitle: + dialogContext.l10n.channels_joinHashtagChannelDesc, ), if (selectedOption == 3) buildExpandedContent()!, const Divider(height: 1), @@ -1168,7 +1335,9 @@ class _ChannelsScreenState extends State context: context, builder: (dialogContext) => StatefulBuilder( builder: (dialogContext, setState) => AlertDialog( - title: Text(dialogContext.l10n.channels_editChannelTitle(channel.index)), + title: Text( + dialogContext.l10n.channels_editChannelTitle(channel.index), + ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -1226,7 +1395,9 @@ class _ChannelsScreenState extends State psk = Channel.parsePskHex(pskHex); } on FormatException { ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)), + SnackBar( + content: Text(dialogContext.l10n.channels_pskMustBe32Hex), + ), ); return; } @@ -1235,7 +1406,9 @@ class _ChannelsScreenState extends State connector.setChannel(channel.index, name, psk); connector.setChannelSmazEnabled(channel.index, smazEnabled); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelUpdated(name))), + SnackBar( + content: Text(context.l10n.channels_channelUpdated(name)), + ), ); }, child: Text(dialogContext.l10n.common_save), @@ -1255,7 +1428,9 @@ class _ChannelsScreenState extends State context: context, builder: (dialogContext) => AlertDialog( title: Text(dialogContext.l10n.channels_deleteChannel), - content: Text(dialogContext.l10n.channels_deleteChannelConfirm(channel.name)), + content: Text( + dialogContext.l10n.channels_deleteChannelConfirm(channel.name), + ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), @@ -1266,10 +1441,17 @@ class _ChannelsScreenState extends State Navigator.pop(dialogContext); connector.deleteChannel(channel.index); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelDeleted(channel.name))), + SnackBar( + content: Text( + context.l10n.channels_channelDeleted(channel.name), + ), + ), ); }, - child: Text(dialogContext.l10n.common_delete, style: const TextStyle(color: Colors.red)), + child: Text( + dialogContext.l10n.common_delete, + style: const TextStyle(color: Colors.red), + ), ), ], ), @@ -1323,16 +1505,26 @@ class _ChannelsScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.groups_outlined, size: 64, color: Colors.grey[400]), + 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]), + 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]), + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + ), textAlign: TextAlign.center, ), ], @@ -1345,8 +1537,13 @@ class _ChannelsScreenState extends State final community = _communities[index]; return ListTile( leading: CircleAvatar( - backgroundColor: Colors.purple.withValues(alpha: 0.2), - child: const Icon(Icons.groups, color: Colors.purple), + backgroundColor: Colors.purple.withValues( + alpha: 0.2, + ), + child: const Icon( + Icons.groups, + color: Colors.purple, + ), ), title: Text(community.name), subtitle: Text( @@ -1361,10 +1558,6 @@ class _ChannelsScreenState extends State 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); } @@ -1380,32 +1573,14 @@ class _ChannelsScreenState extends State ], ), ), - 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 Icon( + Icons.exit_to_app, + color: Colors.red, + ), const SizedBox(width: 12), Text( context.l10n.community_delete, @@ -1436,128 +1611,23 @@ class _ChannelsScreenState extends State 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), - ), - ], + embeddedImage: Image.asset( + 'assets/images/mesh-icon.png', + width: 40, + height: 40, ), ); } - /// 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()); - + final publicPskHex = Channel.formatPskHex( + community.deriveCommunityPublicPsk(), + ); + for (final channel in connector.channels) { // Check if it's the public channel if (channel.pskHex == publicPskHex) { @@ -1566,16 +1636,18 @@ class _ChannelsScreenState extends State } // Check if it's a hashtag channel for (final hashtag in community.hashtagChannels) { - final hashtagPskHex = Channel.formatPskHex(community.deriveCommunityHashtagPsk(hashtag)); + 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( @@ -1593,19 +1665,23 @@ class _ChannelsScreenState extends State 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))), + SnackBar( + content: Text( + context.l10n.community_deleted(community.name), + ), + ), ); } }, @@ -1619,32 +1695,3 @@ class _ChannelsScreenState extends State ); } } - -/// 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/chat_screen.dart b/lib/screens/chat_screen.dart index 5764bc88..a1707b95 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,11 +5,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:provider/provider.dart'; import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; @@ -988,11 +990,21 @@ class _MessageBubble extends StatelessWidget { fallbackTextColor: textColor.withValues(alpha: 0.7), ) else - Text( - messageText, + Linkify( + text: messageText, style: TextStyle( color: textColor, ), + linkStyle: const TextStyle( + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => LinkHandler.handleLinkTap(context, link.url), ), if (isOutgoing && message.retryCount > 0) ...[ const SizedBox(height: 4), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d2..f6f23bfe 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87a..f16b4c34 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index fdb93ad1..31428df1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,6 +12,7 @@ import package_info_plus import path_provider_foundation import shared_preferences_foundation import sqflite_darwin +import url_launcher_macos import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -22,5 +23,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index de12f546..2e2b7466 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -262,6 +262,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.1" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_lints: dependency: "direct dev" description: @@ -397,6 +405,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" + source: hosted + version: "5.0.0" lints: dependency: transitive description: @@ -818,6 +834,70 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: "direct main" description: @@ -907,5 +987,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.2 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index 490c83d6..c9e9120b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,8 @@ dependencies: package_info_plus: ^8.0.0 mobile_scanner: ^6.0.0 # QR/barcode scanning qr_flutter: ^4.1.0 # QR code generation + url_launcher: ^6.3.0 # Launch URLs in system browser + flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text dev_dependencies: flutter_test: diff --git a/untranslated.json b/untranslated.json index 2138a62f..9e26dfee 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,121 +1 @@ -{ - "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" - ] -} +{} \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c158b14b..eeb548fa 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterBluePlusPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterBluePlusPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 905321af..68825d8b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_blue_plus_winrt + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST