From ed4dce94f3b356be2b926035cdeb6af168f77b4c Mon Sep 17 00:00:00 2001 From: Strycher Date: Tue, 30 Jun 2026 23:18:52 -0400 Subject: [PATCH] feat(#163): scan a channel QR to add it (unified with community scanner) The Add Channel "Scan QR Code" scanner now accepts channel share URIs (meshcore://channel/add) alongside community QR JSON: it parses name + secret, confirms, and adds the channel to the next free slot via setChannel. Completes the channel-QR round-trip in-app, and interops with the reference app's channel QRs. Bumps build to +52 for the b52 combined test binary (path-hash + trace + channel QR). Co-Authored-By: Claude Opus 4.8 --- lib/l10n/app_en.arb | 29 +++++++ lib/l10n/app_localizations.dart | 48 ++++++++++++ lib/l10n/app_localizations_bg.dart | 30 ++++++++ lib/l10n/app_localizations_de.dart | 30 ++++++++ lib/l10n/app_localizations_en.dart | 30 ++++++++ lib/l10n/app_localizations_es.dart | 30 ++++++++ lib/l10n/app_localizations_fr.dart | 30 ++++++++ lib/l10n/app_localizations_hu.dart | 30 ++++++++ lib/l10n/app_localizations_it.dart | 30 ++++++++ lib/l10n/app_localizations_ja.dart | 30 ++++++++ lib/l10n/app_localizations_ko.dart | 30 ++++++++ lib/l10n/app_localizations_nl.dart | 30 ++++++++ lib/l10n/app_localizations_pl.dart | 30 ++++++++ lib/l10n/app_localizations_pt.dart | 30 ++++++++ lib/l10n/app_localizations_ru.dart | 30 ++++++++ lib/l10n/app_localizations_sk.dart | 30 ++++++++ lib/l10n/app_localizations_sl.dart | 30 ++++++++ lib/l10n/app_localizations_sv.dart | 30 ++++++++ lib/l10n/app_localizations_uk.dart | 30 ++++++++ lib/l10n/app_localizations_zh.dart | 30 ++++++++ lib/screens/channels_screen.dart | 4 +- lib/screens/community_qr_scanner_screen.dart | 80 +++++++++++++++++++- 22 files changed, 696 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e09cf2f..3ded484 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1992,6 +1992,35 @@ } } }, + "channels_scanQr": "Scan QR Code", + "channels_scanQrDesc": "Add a community or channel from a QR code", + "channels_scanQrInstructions": "Point at a MeshCore QR code", + "channels_invalidQr": "Invalid QR code", + "channels_noFreeSlot": "No free channel slot", + "channels_qrAddConfirm": "Add channel \"{name}\"?", + "@channels_qrAddConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "channels_qrAdded": "Added \"{name}\".", + "@channels_qrAdded": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "channels_qrExists": "\"{name}\" is already added.", + "@channels_qrExists": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "community_hashtagPrivacyHint": "Community hashtag channels are only joinable by members of the community", "community_invalidQrCode": "Invalid community QR code", "community_alreadyMember": "Already a Member", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 490158a..620c693 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -6374,6 +6374,54 @@ abstract class AppLocalizations { /// **'Scan to add \"{name}\".'** String channels_shareQrInstructions(String name); + /// No description provided for @channels_scanQr. + /// + /// In en, this message translates to: + /// **'Scan QR Code'** + String get channels_scanQr; + + /// No description provided for @channels_scanQrDesc. + /// + /// In en, this message translates to: + /// **'Add a community or channel from a QR code'** + String get channels_scanQrDesc; + + /// No description provided for @channels_scanQrInstructions. + /// + /// In en, this message translates to: + /// **'Point at a MeshCore QR code'** + String get channels_scanQrInstructions; + + /// No description provided for @channels_invalidQr. + /// + /// In en, this message translates to: + /// **'Invalid QR code'** + String get channels_invalidQr; + + /// No description provided for @channels_noFreeSlot. + /// + /// In en, this message translates to: + /// **'No free channel slot'** + String get channels_noFreeSlot; + + /// No description provided for @channels_qrAddConfirm. + /// + /// In en, this message translates to: + /// **'Add channel \"{name}\"?'** + String channels_qrAddConfirm(String name); + + /// No description provided for @channels_qrAdded. + /// + /// In en, this message translates to: + /// **'Added \"{name}\".'** + String channels_qrAdded(String name); + + /// No description provided for @channels_qrExists. + /// + /// In en, this message translates to: + /// **'\"{name}\" is already added.'** + String channels_qrExists(String name); + /// No description provided for @community_hashtagPrivacyHint. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index b8f5799..88ca5bb 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -3688,6 +3688,36 @@ class AppLocalizationsBg extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Хаштаг каналите на общността са достъпни само за членове на общността'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 206ecdb..07238d0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3695,6 +3695,36 @@ class AppLocalizationsDe extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ed87392..44d2874 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3624,6 +3624,36 @@ class AppLocalizationsEn extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Community hashtag channels are only joinable by members of the community'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ed1c8b7..051baf5 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -3683,6 +3683,36 @@ class AppLocalizationsEs extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 9d64cd6..bb18839 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -3706,6 +3706,36 @@ class AppLocalizationsFr extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 5d94708..9a11a5d 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -3699,6 +3699,36 @@ class AppLocalizationsHu extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'A közösségi hashtagekhez tartozó csatornák csak a közösség tagjai számára érhetők el.'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index e6412e9..cdf047d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -3688,6 +3688,36 @@ class AppLocalizationsIt extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'I canali hashtag della community sono accessibili solo ai membri della community'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index b3e7171..410bf26 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -3501,6 +3501,36 @@ class AppLocalizationsJa extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'コミュニティハッシュタグのチャンネルは、コミュニティのメンバーのみが参加できます。'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 2d380f2..75d5fad 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -3503,6 +3503,36 @@ class AppLocalizationsKo extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => '커뮤니티 해시태그 채널은 커뮤니티 구성원만 가입할 수 있습니다.'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index b356576..67128b3 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -3669,6 +3669,36 @@ class AppLocalizationsNl extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Community hashtag-kanalen zijn alleen toegankelijk voor leden van de community'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2fdc687..7fb9be9 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -3701,6 +3701,36 @@ class AppLocalizationsPl extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index a251b0e..004bcd8 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -3682,6 +3682,36 @@ class AppLocalizationsPt extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 8c4a3d6..38e0b6c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -3690,6 +3690,36 @@ class AppLocalizationsRu extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Хэштег-каналы сообщества доступны только его участникам'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 0d1ec33..0559a95 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -3668,6 +3668,36 @@ class AppLocalizationsSk extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Hashtagové kanály komunity sú prístupné len členom komunity'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 9dd2e0c..9e88bc3 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -3663,6 +3663,36 @@ class AppLocalizationsSl extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Hashtag kanali skupnosti so dostopni samo članom skupnosti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index c02241f..d4ae65b 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -3643,6 +3643,36 @@ class AppLocalizationsSv extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Community-hashtagkanaler kan endast nås av medlemmar i communityn'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 22d3aec..8fd8300 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3686,6 +3686,36 @@ class AppLocalizationsUk extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => 'Хештег-канали спільноти доступні лише членам спільноти'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 94f4d62..39e150b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -3406,6 +3406,36 @@ class AppLocalizationsZh extends AppLocalizations { return 'Scan to add \"$name\".'; } + @override + String get channels_scanQr => 'Scan QR Code'; + + @override + String get channels_scanQrDesc => 'Add a community or channel from a QR code'; + + @override + String get channels_scanQrInstructions => 'Point at a MeshCore QR code'; + + @override + String get channels_invalidQr => 'Invalid QR code'; + + @override + String get channels_noFreeSlot => 'No free channel slot'; + + @override + String channels_qrAddConfirm(String name) { + return 'Add channel \"$name\"?'; + } + + @override + String channels_qrAdded(String name) { + return 'Added \"$name\".'; + } + + @override + String channels_qrExists(String name) { + return '\"$name\" is already added.'; + } + @override String get community_hashtagPrivacyHint => '仅社区成员可加入社区标签频道。'; diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 38c67b8..c1c7c00 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1355,8 +1355,8 @@ class _ChannelsScreenState extends State buildOptionTile( optionIndex: 4, icon: Icons.qr_code_scanner, - title: dialogContext.l10n.community_scanQr, - subtitle: dialogContext.l10n.community_join, + title: dialogContext.l10n.channels_scanQr, + subtitle: dialogContext.l10n.channels_scanQrDesc, ), if (selectedOption == 4) buildExpandedContent(_channelMessageStore)!, diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index 6b71715..289edce 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -4,6 +4,7 @@ import 'package:uuid/uuid.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../models/channel.dart'; import '../models/community.dart'; import '../storage/community_store.dart'; import '../widgets/adaptive_app_bar_title.dart'; @@ -31,16 +32,18 @@ class _CommunityQrScannerScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: AdaptiveAppBarTitle(context.l10n.community_scanQr), + title: AdaptiveAppBarTitle(context.l10n.channels_scanQr), centerTitle: true, ), body: _isProcessing ? const Center(child: CircularProgressIndicator()) : QrScannerWidget( onScanned: (data) => _handleScannedData(context, data), - validator: Community.isValidQrData, + validator: (data) => + Community.isValidQrData(data) || + Channel.isValidShareUri(data), onValidationFailed: (_) => _showInvalidQrError(context), - instructions: context.l10n.community_scanInstructions, + instructions: context.l10n.channels_scanQrInstructions, ), ); } @@ -56,6 +59,12 @@ class _CommunityQrScannerScreenState extends State { _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; try { + // A channel share URI (meshcore://channel/add) is handled separately + // from community QR JSON. (#161) + if (Channel.isValidShareUri(data)) { + await _handleChannelUri(context, data); + return; + } // Parse the community data final community = Community.fromQrData(const Uuid().v4(), data); @@ -239,6 +248,71 @@ class _CommunityQrScannerScreenState extends State { } } + Future _handleChannelUri(BuildContext context, String data) async { + final channel = Channel.fromShareUri(data); + if (channel == null) { + if (context.mounted) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_invalidQr), + backgroundColor: Colors.red, + ); + } + return; + } + final connector = context.read(); + if (connector.channels.any((c) => c.pskHex == channel.pskHex)) { + if (context.mounted) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_qrExists(channel.name)), + ); + Navigator.pop(context); + } + return; + } + final nextIndex = _findNextAvailableChannelIndex(connector); + if (nextIndex == null) { + if (context.mounted) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_noFreeSlot), + backgroundColor: Colors.red, + ); + } + return; + } + if (!context.mounted) return; + final confirmed = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.channels_scanQr), + content: Text(context.l10n.channels_qrAddConfirm(channel.name)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: Text(context.l10n.common_cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(dialogContext, true), + child: Text(context.l10n.common_add), + ), + ], + ), + ); + if (confirmed == true && context.mounted) { + connector.setChannel(nextIndex, channel.name, channel.psk); + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_qrAdded(channel.name)), + backgroundColor: Colors.green, + ); + Navigator.pop(context); + } else if (context.mounted) { + Navigator.pop(context); + } + } + int? _findNextAvailableChannelIndex(MeshCoreConnector connector) { final usedIndices = connector.channels.map((c) => c.index).toSet(); for (int i = 0; i < connector.maxChannels; i++) {