From 05fb5a13fabc9716f80698c273203522bfdc25f5 Mon Sep 17 00:00:00 2001 From: 446564 Date: Thu, 5 Feb 2026 08:33:07 -0800 Subject: [PATCH 01/15] remove direct msg notification prefix The prefix "New message from " takes up a lot of space and was not localized anyway. --- lib/services/notification_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index ea7f031d..d835d077 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -118,7 +118,7 @@ class NotificationService { await _notifications.show( contactId?.hashCode ?? 0, - 'New message from $contactName', + contactName, message, notificationDetails, payload: 'message:$contactId', From 6a3c59fa2c99218e8ae1f8bc142dc07851162430 Mon Sep 17 00:00:00 2001 From: 446564 Date: Thu, 5 Feb 2026 09:24:24 -0800 Subject: [PATCH 02/15] remove rotation in path map when zooming on the path map view window the rotation was too easy to trigger and provided little value to understanding the path --- lib/screens/channel_message_path_screen.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 970c152a..50dcb71e 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -349,6 +349,9 @@ class _ChannelMessagePathMapScreenState ), minZoom: 2.0, maxZoom: 18.0, + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate, + ), ), children: [ TileLayer( From ddee76ced26f41028f1b2dbc299ce07d09a20909 Mon Sep 17 00:00:00 2001 From: 446564 Date: Thu, 5 Feb 2026 09:36:43 -0800 Subject: [PATCH 03/15] add flutter test to actions --- .github/workflows/flutter_dart.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter_dart.yml b/.github/workflows/flutter_dart.yml index 5b68956b..117eb4f0 100644 --- a/.github/workflows/flutter_dart.yml +++ b/.github/workflows/flutter_dart.yml @@ -1,4 +1,4 @@ -name: Flutter and Dart Analysis +name: Flutter and Dart on: pull_request: @@ -7,7 +7,7 @@ on: - main jobs: - analyze_and_format: + analyze: runs-on: ubuntu-latest steps: - name: Checkout @@ -26,3 +26,6 @@ jobs: - name: Verify formatting run: dart format --output=none --set-exit-if-changed . + + - name: Run tests + run: flutter test -r github From 8b1228bf8d8d05f558f897769f9cee830e5d4995 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 5 Feb 2026 13:38:49 -0800 Subject: [PATCH 04/15] Add GPX export functionality and related UI components --- lib/l10n/app_en.arb | 13 ++- lib/screens/settings_screen.dart | 70 +++++++++++++ lib/utils/gpx_export.dart | 165 +++++++++++++++++++++++++++++++ pubspec.yaml | 3 + 4 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 lib/utils/gpx_export.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee5cf7d6..8f501e05 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1343,5 +1343,16 @@ "contacts_zeroHopContactAdvertSent": "Sent contact by advert.", "contacts_zeroHopContactAdvertFailed": "Failed to send contact.", "contacts_contactAdvertCopied": "Advert copied to Clipboard.", - "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed." + "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed.", + + "settings_gpxExportRepeaters": "Export repeaters to GPX", + "settings_gpxExportRepeatersSubtitle": "Exports repeaters with a location to GPX file.", + "settings_gpxExportContacts": "Export contacts to GPX", + "settings_gpxExportContactsSubtitle": "Exports chat contacts with a location to GPX file.", + "settings_gpxExportAll": "Export all to GPX", + "settings_gpxExportAllSubtitle": "Exports all contacts with a location to GPX file.", + "settings_gpxExportSuccess": "Successfully exported GPX file.", + "settings_gpxExportNoContacts": "No contacts to export.", + "settings_gpxExportNotAvailable": "Not supported on your device/OS", + "settings_gpxExportError": "There was an error when exporting." } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 415d5081..cbcbc0dd 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:meshcore_open/utils/gpx_export.dart'; import 'package:meshcore_open/widgets/elements_ui.dart'; import 'package:provider/provider.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -57,6 +58,8 @@ class _SettingsScreenState extends State { const SizedBox(height: 16), _buildDebugCard(context), const SizedBox(height: 16), + _buildExportCard(connector), + const SizedBox(height: 16), _buildAboutCard(context), ], ); @@ -684,6 +687,73 @@ class _SettingsScreenState extends State { ], ); } + + _gpxExport(GpxExport exporter) async { + final l10n = context.l10n; + final result = await exporter.exportGPX(); + // Implement GPX export functionality here + switch (result) { + case GpxExportSuccess: + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); + case GpxExportNoContacts: + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNoContacts))); + case GpxExportNotAvailable: + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNotAvailable))); + case GpxExportFailed: + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); + } + } + + _buildExportCard(MeshCoreConnector connector) { + final l10n = context.l10n; + return Card( + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.download_outlined), + title: Text(l10n.settings_gpxExportRepeaters), + subtitle: Text(l10n.settings_gpxExportRepeatersSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final exporter = GpxExport(connector); + exporter.addRepeaters(); + _gpxExport(exporter); + }, + ), + ListTile( + leading: const Icon(Icons.download_outlined), + title: Text(l10n.settings_gpxExportContacts), + subtitle: Text(l10n.settings_gpxExportContactsSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final exporter = GpxExport(connector); + exporter.addContacts(); + _gpxExport(exporter); + }, + ), + ListTile( + leading: const Icon(Icons.download_outlined), + title: Text(l10n.settings_gpxExportAll), + subtitle: Text(l10n.settings_gpxExportAllSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final exporter = GpxExport(connector); + exporter.addAll(); + _gpxExport(exporter); + }, + ), + ], + ), + ); + } } class _RadioSettingsDialog extends StatefulWidget { diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart new file mode 100644 index 00000000..ae1c1282 --- /dev/null +++ b/lib/utils/gpx_export.dart @@ -0,0 +1,165 @@ +import 'package:flutter/foundation.dart'; +import 'package:gpx/gpx.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; + +import 'package:share_plus/share_plus.dart'; + +class ContactExport { + final String name; + final double lat; + final double lon; + final String desc; + final double? ele; + + ContactExport({ + required this.name, + required this.lat, + required this.lon, + required this.desc, + this.ele, + }); +} + +const int GpxExportFailed = -1; +const int GpxExportSuccess = 1; +const int GpxExportNoContacts = 2; +const int GpxExportCancelled = 3; +const int GpxExportNotAvailable = 4; + +class GpxExport { + MeshCoreConnector _connector; + List _contacts = []; + + GpxExport(this._connector); + + void _addContact(String name, double lat, double lon, String desc, [double? ele]) { + _contacts.add(ContactExport( + name: name.trim(), + lat: lat, + lon: lon, + desc: desc.trim(), + ele: ele, + )); + } + + void addRepeaters() { + final contacts = _connector.contacts; + final repeaters = contacts.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom).toList(); + for (var repeater in repeaters) { + if (repeater.latitude == null || repeater.longitude == null) { + continue; + } + _addContact( + repeater.name, + repeater.latitude ?? 0.0, + repeater.longitude ?? 0.0, + "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + ); + } + } + + void addContacts() { + final contacts = _connector.contacts; + final repeaters = contacts.where((c) => c.type == advTypeChat).toList(); + for (var repeater in repeaters) { + if (repeater.latitude == null || repeater.longitude == null) { + continue; + } + _addContact( + repeater.name, + repeater.latitude ?? 0.0, + repeater.longitude ?? 0.0, + "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + ); + } + } + + void addAll() { + final contacts = _connector.contacts; + for (var repeater in contacts.toList()) { + if (repeater.latitude == null || repeater.longitude == null) { + continue; + } + _addContact( + repeater.name, + repeater.latitude ?? 0.0, + repeater.longitude ?? 0.0, + "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + ); + } + } + + Future exportGPX() async { + if (_contacts.isEmpty) { + debugPrint("No repeaters to export – nothing to share."); + return GpxExportNoContacts; + } + + try { + // 1. Build GPX content (your existing logic – unchanged here) + final gpx = Gpx() + ..version = '1.1' + ..creator = 'meshcore-open Repeater Exporter' + ..metadata = Metadata( + name: 'Meshcore Repeaters', + desc: 'Repeater & room locations exported from meshcore-open', + time: DateTime.now().toUtc(), + ); + + gpx.wpts = _contacts.map((c) => Wpt( + lat: c.lat, + lon: c.lon, + ele: c.ele, + name: c.name, + desc: c.desc, + )).toList(); + + final xml = GpxWriter().asString(gpx, pretty: true); + + // 2. Save to file + final dir = await getApplicationDocumentsDirectory(); + final timestamp = DateTime.now().toUtc().toIso8601String() + .replaceAll(':', '-') + .replaceAll('.', '-') + .split('T') + .join('_'); + final path = '${dir.path}/meshcore_repeaters_$timestamp.gpx'; + + final file = File(path); + await file.writeAsString(xml); + + // 3. Modern share call (2025+ style) + final result = await SharePlus.instance.share( + ShareParams( + text: 'Repeater locations exported from meshcore-open app as GPX file.', + subject: 'Meshcore Repeaters GPX Export', + files: [XFile(path)], + // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) + ), + ); + + // 4. Handle result + switch (result.status) { + case ShareResultStatus.success: + debugPrint('Share successful – user completed the action.'); + return GpxExportSuccess; + case ShareResultStatus.dismissed: + debugPrint('Share sheet was dismissed / cancelled by user.'); + return GpxExportCancelled; + case ShareResultStatus.unavailable: + debugPrint('Sharing is not available on this platform / context.'); + return GpxExportNotAvailable; + } + + // Optional cleanup (uncomment if you don't want to keep the file) + // await file.delete(); + } catch (e, stack) { + debugPrint('Export or share failed: $e\n$stack'); + // → here you could show a SnackBar / AlertDialog in real UI code + } + return GpxExportFailed; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 8b1415f6..6312ee37 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,9 @@ dependencies: 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 + gpx: ^2.3.0 + path_provider: ^2.1.5 + share_plus: ^12.0.1 dev_dependencies: flutter_test: From 978ea4790da8dccb323d9dd5b6a2677ee15ea197 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 5 Feb 2026 13:46:05 -0800 Subject: [PATCH 05/15] Refactor GPX export constants to use lowercase naming convention and improve export function error handling --- lib/screens/settings_screen.dart | 10 +++++----- lib/utils/gpx_export.dart | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index cbcbc0dd..c5a307f9 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -691,21 +691,21 @@ class _SettingsScreenState extends State { _gpxExport(GpxExport exporter) async { final l10n = context.l10n; final result = await exporter.exportGPX(); - // Implement GPX export functionality here + if(!mounted) return; switch (result) { - case GpxExportSuccess: + case gpxExportSuccess: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); - case GpxExportNoContacts: + case gpxExportNoContacts: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNoContacts))); - case GpxExportNotAvailable: + case gpxExportNotAvailable: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNotAvailable))); - case GpxExportFailed: + case gpxExportFailed: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index ae1c1282..12c6e273 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -23,15 +23,15 @@ class ContactExport { }); } -const int GpxExportFailed = -1; -const int GpxExportSuccess = 1; -const int GpxExportNoContacts = 2; -const int GpxExportCancelled = 3; -const int GpxExportNotAvailable = 4; +const int gpxExportFailed = -1; +const int gpxExportSuccess = 1; +const int gpxExportNoContacts = 2; +const int gpxExportCancelled = 3; +const int gpxExportNotAvailable = 4; class GpxExport { - MeshCoreConnector _connector; - List _contacts = []; + final MeshCoreConnector _connector; + final List _contacts = []; GpxExport(this._connector); @@ -95,7 +95,7 @@ class GpxExport { Future exportGPX() async { if (_contacts.isEmpty) { debugPrint("No repeaters to export – nothing to share."); - return GpxExportNoContacts; + return gpxExportNoContacts; } try { @@ -145,13 +145,13 @@ class GpxExport { switch (result.status) { case ShareResultStatus.success: debugPrint('Share successful – user completed the action.'); - return GpxExportSuccess; + return gpxExportSuccess; case ShareResultStatus.dismissed: debugPrint('Share sheet was dismissed / cancelled by user.'); - return GpxExportCancelled; + return gpxExportCancelled; case ShareResultStatus.unavailable: debugPrint('Sharing is not available on this platform / context.'); - return GpxExportNotAvailable; + return gpxExportNotAvailable; } // Optional cleanup (uncomment if you don't want to keep the file) @@ -160,6 +160,6 @@ class GpxExport { debugPrint('Export or share failed: $e\n$stack'); // → here you could show a SnackBar / AlertDialog in real UI code } - return GpxExportFailed; + return gpxExportFailed; } } \ No newline at end of file From d1009d3c20c79a500675511a38b4ed0725f8bbff Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 7 Feb 2026 11:07:57 -0800 Subject: [PATCH 06/15] ran formating --- lib/screens/settings_screen.dart | 18 ++++----- lib/utils/gpx_export.dart | 65 ++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c5a307f9..6dfbc0ac 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -687,24 +687,24 @@ class _SettingsScreenState extends State { ], ); } - + _gpxExport(GpxExport exporter) async { final l10n = context.l10n; final result = await exporter.exportGPX(); - if(!mounted) return; + if (!mounted) return; switch (result) { case gpxExportSuccess: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); case gpxExportNoContacts: - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNoContacts))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), + ); case gpxExportNotAvailable: - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNotAvailable))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), + ); case gpxExportFailed: ScaffoldMessenger.of( context, @@ -728,7 +728,7 @@ class _SettingsScreenState extends State { _gpxExport(exporter); }, ), - ListTile( + ListTile( leading: const Icon(Icons.download_outlined), title: Text(l10n.settings_gpxExportContacts), subtitle: Text(l10n.settings_gpxExportContactsSubtitle), diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 12c6e273..995f34c5 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -35,19 +35,29 @@ class GpxExport { GpxExport(this._connector); - void _addContact(String name, double lat, double lon, String desc, [double? ele]) { - _contacts.add(ContactExport( - name: name.trim(), - lat: lat, - lon: lon, - desc: desc.trim(), - ele: ele, - )); + void _addContact( + String name, + double lat, + double lon, + String desc, [ + double? ele, + ]) { + _contacts.add( + ContactExport( + name: name.trim(), + lat: lat, + lon: lon, + desc: desc.trim(), + ele: ele, + ), + ); } void addRepeaters() { final contacts = _connector.contacts; - final repeaters = contacts.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom).toList(); + final repeaters = contacts + .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) + .toList(); for (var repeater in repeaters) { if (repeater.latitude == null || repeater.longitude == null) { continue; @@ -79,7 +89,7 @@ class GpxExport { void addAll() { final contacts = _connector.contacts; - for (var repeater in contacts.toList()) { + for (var repeater in contacts.toList()) { if (repeater.latitude == null || repeater.longitude == null) { continue; } @@ -104,24 +114,30 @@ class GpxExport { ..version = '1.1' ..creator = 'meshcore-open Repeater Exporter' ..metadata = Metadata( - name: 'Meshcore Repeaters', - desc: 'Repeater & room locations exported from meshcore-open', - time: DateTime.now().toUtc(), - ); + name: 'Meshcore Repeaters', + desc: 'Repeater & room locations exported from meshcore-open', + time: DateTime.now().toUtc(), + ); - gpx.wpts = _contacts.map((c) => Wpt( - lat: c.lat, - lon: c.lon, - ele: c.ele, - name: c.name, - desc: c.desc, - )).toList(); + gpx.wpts = _contacts + .map( + (c) => Wpt( + lat: c.lat, + lon: c.lon, + ele: c.ele, + name: c.name, + desc: c.desc, + ), + ) + .toList(); final xml = GpxWriter().asString(gpx, pretty: true); // 2. Save to file final dir = await getApplicationDocumentsDirectory(); - final timestamp = DateTime.now().toUtc().toIso8601String() + final timestamp = DateTime.now() + .toUtc() + .toIso8601String() .replaceAll(':', '-') .replaceAll('.', '-') .split('T') @@ -134,7 +150,8 @@ class GpxExport { // 3. Modern share call (2025+ style) final result = await SharePlus.instance.share( ShareParams( - text: 'Repeater locations exported from meshcore-open app as GPX file.', + text: + 'Repeater locations exported from meshcore-open app as GPX file.', subject: 'Meshcore Repeaters GPX Export', files: [XFile(path)], // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) @@ -162,4 +179,4 @@ class GpxExport { } return gpxExportFailed; } -} \ No newline at end of file +} From 2a909e60813ee2427c8bd7e0892bcae1010ee157 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 7 Feb 2026 19:45:02 -0800 Subject: [PATCH 07/15] Enhance GPX export functionality with customizable parameters and improved metadata --- lib/l10n/app_en.arb | 17 ++++++++---- lib/screens/settings_screen.dart | 47 ++++++++++++++++++++++++++++---- lib/utils/gpx_export.dart | 23 ++++++++++------ 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8f501e05..18b5c6c1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1345,14 +1345,19 @@ "contacts_contactAdvertCopied": "Advert copied to Clipboard.", "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed.", - "settings_gpxExportRepeaters": "Export repeaters to GPX", - "settings_gpxExportRepeatersSubtitle": "Exports repeaters with a location to GPX file.", - "settings_gpxExportContacts": "Export contacts to GPX", - "settings_gpxExportContactsSubtitle": "Exports chat contacts with a location to GPX file.", - "settings_gpxExportAll": "Export all to GPX", + "settings_gpxExportRepeaters": "Export repeaters / room server to GPX", + "settings_gpxExportRepeatersSubtitle": "Exports repeaters / roomserver with a location to GPX file.", + "settings_gpxExportContacts": "Export companions to GPX", + "settings_gpxExportContactsSubtitle": "Exports companions with a location to GPX file.", + "settings_gpxExportAll": "Export all contacts to GPX", "settings_gpxExportAllSubtitle": "Exports all contacts with a location to GPX file.", "settings_gpxExportSuccess": "Successfully exported GPX file.", "settings_gpxExportNoContacts": "No contacts to export.", "settings_gpxExportNotAvailable": "Not supported on your device/OS", - "settings_gpxExportError": "There was an error when exporting." + "settings_gpxExportError": "There was an error when exporting.", + "settings_gpxExportRepeatersRoom": "Repeater & room server locations", + "settings_gpxExportChat": "Companion locations", + "settings_gpxExportAllContacts": "All contacts locations", + "settings_gpxExportShareText": "Map data exported from meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open GPX map data export" } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 6dfbc0ac..2212b8db 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -688,9 +688,22 @@ class _SettingsScreenState extends State { ); } - _gpxExport(GpxExport exporter) async { + _gpxExport( + GpxExport exporter, + String name, + String description, + String filename, + String shareText, + String subject, + ) async { final l10n = context.l10n; - final result = await exporter.exportGPX(); + final result = await exporter.exportGPX( + name, + description, + filename, + shareText, + subject, + ); if (!mounted) return; switch (result) { case gpxExportSuccess: @@ -701,14 +714,17 @@ class _SettingsScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), ); + break; case gpxExportNotAvailable: ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), ); + break; case gpxExportFailed: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); + break; } } @@ -725,7 +741,14 @@ class _SettingsScreenState extends State { onTap: () async { final exporter = GpxExport(connector); exporter.addRepeaters(); - _gpxExport(exporter); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportRepeatersRoom, + "meshcore_repeaters_", + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); }, ), ListTile( @@ -736,7 +759,14 @@ class _SettingsScreenState extends State { onTap: () async { final exporter = GpxExport(connector); exporter.addContacts(); - _gpxExport(exporter); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportChat, + "meshcore_contacts_", + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); }, ), ListTile( @@ -747,7 +777,14 @@ class _SettingsScreenState extends State { onTap: () async { final exporter = GpxExport(connector); exporter.addAll(); - _gpxExport(exporter); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportAllContacts, + "meshcore_all_", + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); }, ), ], diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 995f34c5..92494c93 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -102,7 +102,13 @@ class GpxExport { } } - Future exportGPX() async { + Future exportGPX( + String name, + String description, + String filename, + String shareText, + String subject, + ) async { if (_contacts.isEmpty) { debugPrint("No repeaters to export – nothing to share."); return gpxExportNoContacts; @@ -112,10 +118,10 @@ class GpxExport { // 1. Build GPX content (your existing logic – unchanged here) final gpx = Gpx() ..version = '1.1' - ..creator = 'meshcore-open Repeater Exporter' + ..creator = 'meshcore-open exporter' ..metadata = Metadata( - name: 'Meshcore Repeaters', - desc: 'Repeater & room locations exported from meshcore-open', + name: name, + desc: description, time: DateTime.now().toUtc(), ); @@ -142,7 +148,9 @@ class GpxExport { .replaceAll('.', '-') .split('T') .join('_'); - final path = '${dir.path}/meshcore_repeaters_$timestamp.gpx'; + + // ignore: unnecessary_string_escapes + final path = '${dir.path}/$filename$timestamp.gpx'; final file = File(path); await file.writeAsString(xml); @@ -150,9 +158,8 @@ class GpxExport { // 3. Modern share call (2025+ style) final result = await SharePlus.instance.share( ShareParams( - text: - 'Repeater locations exported from meshcore-open app as GPX file.', - subject: 'Meshcore Repeaters GPX Export', + text: shareText, + subject: subject, files: [XFile(path)], // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) ), From 98e0b05e73d0d4dc330d8fc245cd0b6f68b0e3ec Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 11:32:36 -0800 Subject: [PATCH 08/15] Implement PathTraceMapScreen and refactor path tracing functionality across screens --- lib/screens/channel_message_path_screen.dart | 79 ++- lib/screens/chat_screen.dart | 14 + lib/screens/contacts_screen.dart | 38 +- lib/screens/path_trace_map.dart | 535 +++++++++++++++++++ lib/widgets/path_trace_dialog.dart | 240 --------- 5 files changed, 641 insertions(+), 265 deletions(-) create mode 100644 lib/screens/path_trace_map.dart delete mode 100644 lib/widgets/path_trace_dialog.dart diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 970c152a..1646b73a 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -41,6 +42,21 @@ class ChannelMessagePathScreen extends StatelessWidget { appBar: AppBar( title: Text(l10n.channelPath_title), actions: [ + IconButton( + icon: const Icon(Icons.radar_outlined), + tooltip: l10n.channelPath_viewMap, + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: context.l10n.contacts_repeaterPathTrace, + path: Uint8List.fromList(primaryPath), + flipPathRound: true, + reversePathRound: true, + ), + ), + ), + ), IconButton( icon: const Icon(Icons.map_outlined), tooltip: l10n.channelPath_viewMap, @@ -263,6 +279,7 @@ class ChannelMessagePathMapScreen extends StatefulWidget { class _ChannelMessagePathMapScreenState extends State { Uint8List? _selectedPath; + double _pathDistance = 0.0; @override void initState() { @@ -282,6 +299,17 @@ class _ChannelMessagePathMapScreenState } } + double _getPathDistance(List points) { + double totalDistance = 0.0; + final distanceCalculator = Distance(); + + for (int i = 0; i < points.length - 1; i++) { + totalDistance += distanceCalculator(points[i], points[i + 1]); + } + + return totalDistance; + } + @override Widget build(BuildContext context) { return Consumer( @@ -306,10 +334,15 @@ class _ChannelMessagePathMapScreenState connector.contacts, context.l10n, ); - final points = hops - .where((hop) => hop.hasLocation) - .map((hop) => hop.position!) - .toList(); + + final points = []; + for (final hop in hops) { + if (hop.hasLocation) { + points.add(hop.position!); + } + } + points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + final polylines = points.length > 1 ? [ Polyline( @@ -327,7 +360,10 @@ class _ChannelMessagePathMapScreenState final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null; - final mapKey = ValueKey(_formatPathPrefixes(selectedPath)); + final mapKey = ValueKey( + '${_formatPathPrefixes(selectedPath)},${context.l10n.pathTrace_you}', + ); + _pathDistance = _getPathDistance(points); return Scaffold( appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)), @@ -487,6 +523,37 @@ class _ChannelMessagePathMapScreenState ), ), ), + Marker( + point: LatLng( + context.read().selfLatitude!, + context.read().selfLongitude!, + ), + width: 40, + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), ]; } @@ -509,7 +576,7 @@ class _ChannelMessagePathMapScreenState Padding( padding: const EdgeInsets.all(12), child: Text( - l10n.channelPath_repeaterHops, + '${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)', style: const TextStyle(fontWeight: FontWeight.w600), ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 3477361c..f00f242a 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import 'package:latlong2/latlong.dart'; @@ -701,6 +702,19 @@ class _ChatScreenState extends State { title: Text(context.l10n.chat_fullPath), content: SelectableText(formattedPath), actions: [ + TextButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: context.l10n.contacts_repeaterPathTrace, + path: Uint8List.fromList(pathBytes), + flipPathRound: true, + ), + ), + ), + child: Text(context.l10n.contacts_pathTrace), + ), TextButton( onPressed: () => Navigator.pop(context), child: Text(context.l10n.common_close), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index f04bc502..6799d695 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:meshcore_open/widgets/path_trace_dialog.dart'; import 'package:flutter/services.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -982,16 +982,16 @@ class _ContactsScreenState extends State ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), onTap: () { - showDialog( - context: context, - builder: (context) { - return PathTraceDialog( + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( title: contact.pathLength > 0 ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing, path: contact.traceRouteBytes ?? Uint8List(0), - ); - }, + ), + ), ); }, ), @@ -1010,16 +1010,16 @@ class _ContactsScreenState extends State ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), onTap: () { - showDialog( - context: context, - builder: (context) { - return PathTraceDialog( + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( title: contact.pathLength > 0 ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, path: contact.traceRouteBytes ?? Uint8List(0), - ); - }, + ), + ), ); }, ), @@ -1052,16 +1052,16 @@ class _ContactsScreenState extends State leading: const Icon(Icons.radar, color: Colors.green), title: Text(context.l10n.contacts_chatTraceRoute), onTap: () { - showDialog( - context: context, - builder: (context) { - return PathTraceDialog( + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_pathTraceTo( contact.name, ), path: contact.traceRouteBytes ?? Uint8List(0), - ); - }, + ), + ), ); }, ), diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart new file mode 100644 index 00000000..b4b280d6 --- /dev/null +++ b/lib/screens/path_trace_map.dart @@ -0,0 +1,535 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:meshcore_open/l10n/l10n.dart'; +import 'package:meshcore_open/models/contact.dart'; +import 'package:meshcore_open/services/map_tile_cache_service.dart'; +import 'package:meshcore_open/widgets/snr_indicator.dart'; +import 'package:provider/provider.dart'; + +class PathTraceData { + final Uint8List pathData; + final Uint8List snrData; + final Map pathContacts; + + PathTraceData({ + required this.pathData, + required this.snrData, + required this.pathContacts, + }); +} + +class PathTraceMapScreen extends StatefulWidget { + final String title; + final Uint8List path; + final bool flipPathRound; + final bool reversePathRound; + + const PathTraceMapScreen({ + super.key, + required this.title, + required this.path, + this.flipPathRound = false, + this.reversePathRound = false, + }); + + @override + State createState() => _PathTraceMapScreenState(); +} + +class _PathTraceMapScreenState extends State { + StreamSubscription? _frameSubscription; + Timer? _timeoutTimer; + + bool _isLoading = false; + bool _failed2Loaded = false; + bool _hasData = false; + PathTraceData? _traceData; + List _points = []; + List _polylines = []; + LatLng? _initialCenter = LatLng(0, 0); + double _initialZoom = 2.0; + LatLngBounds? _bounds; + ValueKey _mapKey = const ValueKey('initial'); + double _pathDistance = 0.0; + + String _formatPathPrefixes(Uint8List pathBytes) { + return pathBytes + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(','); + } + + @override + void initState() { + super.initState(); + _setupFrameListener(); + _doPathTrace(); + } + + @override + void dispose() { + _frameSubscription?.cancel(); + _timeoutTimer?.cancel(); + super.dispose(); + } + + Uint8List addReturnpath(Uint8List pathBytes) { + Uint8List? traceBytes; + final len = (pathBytes.length + pathBytes.length - 1); + traceBytes = Uint8List(len); + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length - 1) { + traceBytes[len - 1 - i] = pathBytes[i]; + } + } + return traceBytes; + } + + double getPathDistance() { + double totalDistance = 0.0; + final distanceCalculator = Distance(); + + for (int i = 0; i < _points.length - 1; i++) { + totalDistance += distanceCalculator(_points[i], _points[i + 1]); + } + + return totalDistance; + } + + Future _doPathTrace() async { + if (mounted) { + setState(() { + _isLoading = true; + _failed2Loaded = false; + }); + } + + final Uint8List path; + + Uint8List pathTmp = widget.reversePathRound + ? Uint8List.fromList(widget.path.reversed.toList()) + : widget.path; + + if (widget.flipPathRound) { + path = addReturnpath(pathTmp); + } else { + path = pathTmp; + } + + final connector = Provider.of(context, listen: false); + final frame = buildTraceReq( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + 0, //flags + 0, //auth + payload: path, + ); + connector.sendFrame(frame); + } + + void _setupFrameListener() { + final connector = Provider.of(context, listen: false); + Uint8List tagData = Uint8List(4); + // Listen for incoming text messages from the repeater + _frameSubscription = connector.receivedFrames.listen((frame) { + if (frame.isEmpty) return; + final frameBuffer = BufferReader(frame); + final code = frameBuffer.readUInt8(); + + if (code == respCodeSent) { + frameBuffer.skipBytes(1); //reserved + tagData = frameBuffer.readBytes(4); + final timeoutSeconds = frameBuffer.readUInt32LE(); + + // Start timeout timer for trace response + _timeoutTimer?.cancel(); + _timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () { + if (!mounted) return; + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + }); + } + + // Check if it's a binary response + if (code == pushCodeTraceData && + listEquals(frame.sublist(4, 8), tagData)) { + _timeoutTimer?.cancel(); + if (!mounted) return; + frameBuffer.skipBytes(3); //reserved + path length + flag + if (listEquals(frameBuffer.readBytes(4), tagData)) { + _handleTraceResponse(frame); + } + } + }); + } + + Future _handleTraceResponse(Uint8List frame) async { + final connector = Provider.of(context, listen: false); + + final buffer = BufferReader(frame); + buffer.skipBytes(2); // Skip push code and reserved byte + int pathLength = buffer.readUInt8(); + buffer.skipBytes(5); // Skip Flag byte and tag data + buffer.skipBytes(4); // Skip auth code + Uint8List pathData = buffer.readBytes(pathLength); + Uint8List snrData = buffer.readRemainingBytes(); + + Map pathContacts = {}; + + connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) { + for (var repeaterData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([repeaterData]), + )) { + pathContacts[repeaterData] = repeater; + } + } + }); + + setState(() { + _isLoading = false; + _hasData = true; + _traceData = PathTraceData( + pathData: pathData, + snrData: snrData, + pathContacts: pathContacts, + ); + _points = []; + _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + for (final hop in _traceData!.pathData) { + final contact = _traceData!.pathContacts[hop]; + if (contact != null && contact.hasLocation) { + _points.add(LatLng(contact.latitude!, contact.longitude!)); + } + } + _polylines = _points.length > 1 + ? [ + Polyline( + points: _points, + strokeWidth: 4, + color: Colors.blueAccent, + ), + ] + : []; + + _initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0); + _initialZoom = _points.isNotEmpty ? 13.0 : 2.0; + _bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null; + _mapKey = ValueKey( + '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', + ); + _pathDistance = getPathDistance(); + }); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, connector, _) { + final tileCache = context.read(); + + return Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + widget.title, + style: const TextStyle(fontSize: 24), + ), + ), + ], + ), + centerTitle: false, + actions: [ + IconButton( + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + onPressed: _isLoading ? null : _doPathTrace, + tooltip: context.l10n.pathTrace_refreshTooltip, + ), + ], + ), + body: SafeArea( + top: false, + child: Stack( + children: [ + if (!_hasData) + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isLoading) const CircularProgressIndicator(), + const SizedBox(height: 16), + if (!_isLoading && _failed2Loaded) + Text(context.l10n.pathTrace_notAvailable), + ], + ), + ), + if (_hasData) + FlutterMap( + key: _mapKey, + options: MapOptions( + initialCenter: _initialCenter!, + initialZoom: _initialZoom, + initialCameraFit: _bounds == null + ? null + : CameraFit.bounds( + bounds: _bounds!, + padding: const EdgeInsets.all(64), + maxZoom: 16, + ), + minZoom: 2.0, + maxZoom: 18.0, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: + MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ), + if (_polylines.isNotEmpty) + PolylineLayer(polylines: _polylines), + if (_traceData!.pathData.isNotEmpty) + MarkerLayer( + markers: _buildHopMarkers(_traceData!.pathData), + ), + ], + ), + if (_points.isEmpty && + !_hasData && + !_isLoading && + !_failed2Loaded) + Center( + child: Card( + color: Colors.white.withValues(alpha: 0.9), + child: Padding( + padding: EdgeInsets.all(12), + child: Text( + context.l10n.channelPath_noRepeaterLocations, + ), + ), + ), + ), + if (_hasData) _buildLegendCard(context, _traceData!), + ], + ), + ), + ); + }, + ); + } + + List _buildHopMarkers(List pathData) { + return [ + Marker( + point: LatLng( + context.read().selfLatitude!, + context.read().selfLongitude!, + ), + width: 40, + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + for (final hop in pathData) + if (_traceData!.pathContacts[hop]!.hasLocation) + Marker( + point: LatLng( + _traceData!.pathContacts[hop]!.latitude!, + _traceData!.pathContacts[hop]!.longitude!, + ), + width: 40, + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + _traceData!.pathContacts[hop]!.publicKey + .sublist(0, 1) + .map( + (b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(), + ) + .join(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + ]; + } + + String formatDirectionText(PathTraceData pathTraceData, int index) { + if (index == 0 || index == pathTraceData.snrData.length - 1) { + if (index == 0) { + return context.l10n.pathTrace_you; + } else { + final contactName = pathTraceData + .pathContacts[pathTraceData.pathData[pathTraceData.pathData.length - + 1]] + ?.name; + final hex = pathTraceData.pathData[pathTraceData.pathData.length - 1] + .toRadixString(16) + .padLeft(2, '0') + .toUpperCase(); + return contactName != null ? "$hex: $contactName" : hex; + } + } else { + final contactName = + pathTraceData.pathContacts[pathTraceData.pathData[index - 1]]?.name; + final hex = pathTraceData.pathData[index - 1] + .toRadixString(16) + .padLeft(2, '0') + .toUpperCase(); + return contactName != null ? "$hex: $contactName" : hex; + } + } + + String formatDirectionSubText(PathTraceData pathTraceData, int index) { + if (index == 0 || index == pathTraceData.snrData.length - 1) { + if (index == 0) { + final contactName = + pathTraceData.pathContacts[pathTraceData.pathData[0]]?.name; + final hex = pathTraceData.pathData[0] + .toRadixString(16) + .padLeft(2, '0') + .toUpperCase(); + return contactName != null ? "$hex: $contactName" : hex; + } else { + return context.l10n.pathTrace_you; + } + } else { + final contactName = + pathTraceData.pathContacts[pathTraceData.pathData[index]]?.name; + final hex = pathTraceData.pathData[index] + .toRadixString(16) + .padLeft(2, '0') + .toUpperCase(); + return contactName != null ? "$hex: $contactName" : hex; + } + } + + Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) { + final l10n = context.l10n; + final maxHeight = MediaQuery.of(context).size.height * 0.35; + final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0); + final cardHeight = max(96.0, min(maxHeight, estimatedHeight)); + + return Positioned( + left: 16, + right: 16, + bottom: 16, + child: SizedBox( + height: cardHeight, + child: Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Text( + '${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + const Divider(height: 1), + Expanded( + child: pathTraceData.pathData.isEmpty + ? Center( + child: Text(l10n.channelPath_noHopDetailsAvailable), + ) + : ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: pathTraceData.pathData.length + 1, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + return Column( + children: [ + ListTile( + leading: + index >= pathTraceData.snrData.length / 2 + ? Icon(Icons.call_received) + : Icon(Icons.call_made), + title: Text( + formatDirectionText(pathTraceData, index), + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + formatDirectionSubText(pathTraceData, index), + style: const TextStyle(fontSize: 14), + ), + trailing: SNRIcon( + snr: + pathTraceData.snrData[index].toSigned(8) / + 4.0, + ), + onTap: () { + // Handle item tap + }, + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/path_trace_dialog.dart b/lib/widgets/path_trace_dialog.dart deleted file mode 100644 index 7294c86d..00000000 --- a/lib/widgets/path_trace_dialog.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../connector/meshcore_connector.dart'; -import '../connector/meshcore_protocol.dart'; -import '../models/contact.dart'; -import '../widgets/snr_indicator.dart'; -import '../l10n/l10n.dart'; - -class PathTraceDialog extends StatefulWidget { - const PathTraceDialog({super.key, required this.title, required this.path}); - - final String title; - final Uint8List path; - - @override - State createState() => _PathTraceDialogState(); -} - -class _PathTraceDialogState extends State { - StreamSubscription? _frameSubscription; - Timer? _timeoutTimer; - - bool _isLoading = false; - bool _failed2Loaded = false; - bool _hasData = false; - Uint8List _pathData = Uint8List(0); - Uint8List _snrData = Uint8List(0); - Map _pathContacts = {}; - - @override - void initState() { - super.initState(); - _setupFrameListener(); - _doPathTrace(); - } - - @override - void dispose() { - _frameSubscription?.cancel(); - _timeoutTimer?.cancel(); - super.dispose(); - } - - Future _doPathTrace() async { - if (mounted) { - setState(() { - _isLoading = true; - _failed2Loaded = false; - }); - } - - final connector = Provider.of(context, listen: false); - final frame = buildTraceReq( - DateTime.now().millisecondsSinceEpoch ~/ 1000, - 0, //flags - 0, //auth - payload: widget.path, - ); - connector.sendFrame(frame); - } - - void _setupFrameListener() { - final connector = Provider.of(context, listen: false); - Uint8List tagData = Uint8List(4); - // Listen for incoming text messages from the repeater - _frameSubscription = connector.receivedFrames.listen((frame) { - if (frame.isEmpty) return; - final frameBuffer = BufferReader(frame); - final code = frameBuffer.readUInt8(); - - if (code == respCodeSent) { - frameBuffer.skipBytes(1); //reserved - tagData = frameBuffer.readBytes(4); - final timeoutSeconds = frameBuffer.readUInt32LE(); - - // Start timeout timer for trace response - _timeoutTimer?.cancel(); - _timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () { - if (!mounted) return; - setState(() { - _isLoading = false; - _failed2Loaded = true; - }); - }); - } - - // Check if it's a binary response - if (code == pushCodeTraceData && - listEquals(frame.sublist(4, 8), tagData)) { - _timeoutTimer?.cancel(); - if (!mounted) return; - frameBuffer.skipBytes(3); //reserved + path length + flag - if (listEquals(frameBuffer.readBytes(4), tagData)) { - _handleTraceResponse(frame); - } - } - }); - } - - Future _handleTraceResponse(Uint8List frame) async { - final connector = Provider.of(context, listen: false); - - final buffer = BufferReader(frame); - buffer.skipBytes(2); // Skip push code and reserved byte - int pathLength = buffer.readUInt8(); - buffer.skipBytes(5); // Skip Flag byte and tag data - buffer.skipBytes(4); // Skip auth code - Uint8List pathData = buffer.readBytes(pathLength); - Uint8List snrData = buffer.readRemainingBytes(); - - Map pathContacts = {}; - - connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) { - for (var neighbourData in pathData) { - if (listEquals( - repeater.publicKey.sublist(0, 1), - Uint8List.fromList([neighbourData]), - )) { - pathContacts[neighbourData] = repeater; - } - } - }); - - setState(() { - _isLoading = false; - _hasData = true; - _pathData = pathData; - _snrData = snrData; - _pathContacts = pathContacts; - }); - } - - String formatDirectionText(int index) { - if (index == 0 || index == _snrData.length - 1) { - if (index == 0) { - return context.l10n.pathTrace_you; - } else { - return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? - "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}"; - } - } else { - return _pathContacts[_pathData[index - 1]]?.name ?? - "0x${_pathData[index - 1].toRadixString(16).toUpperCase()}"; - } - } - - String formatDirectionSubText(int index) { - if (index == 0 || index == _snrData.length - 1) { - if (index == 0) { - return _pathContacts[_pathData[0]]?.name ?? - "0x${_pathData[0].toRadixString(16).toUpperCase()}"; - } else { - return context.l10n.pathTrace_you; - } - } else { - return _pathContacts[_pathData[index]]?.name ?? - "0x${_pathData[index].toRadixString(16).toUpperCase()}"; - } - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return AlertDialog( - title: Column( - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text(widget.title, style: const TextStyle(fontSize: 24)), - ), - if (_failed2Loaded) - Text( - l10n.pathTrace_failed, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.error, - ), - ), - ], - ), - content: SafeArea( - child: RefreshIndicator( - onRefresh: _doPathTrace, - child: !_hasData - ? Center(child: Text(l10n.pathTrace_notAvailable)) - : ListView.builder( - itemCount: _snrData.length, - itemBuilder: (context, index) { - return Column( - children: [ - ListTile( - leading: index >= _snrData.length / 2 - ? Icon(Icons.call_received) - : Icon(Icons.call_made), - title: Text( - formatDirectionText(index), - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - formatDirectionSubText(index), - style: const TextStyle(fontSize: 14), - ), - trailing: SNRIcon( - snr: _snrData[index].toSigned(8) / 4.0, - ), - onTap: () { - // Handle item tap - }, - ), - if (index < _snrData.length - 1) - const Divider(height: 0.0), - ], - ); - }, - ), - ), - ), - actions: [ - IconButton( - icon: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh), - onPressed: _isLoading ? null : _doPathTrace, - tooltip: l10n.pathTrace_refreshTooltip, - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(l10n.common_close), - ), - ], - ); - } -} From 2f4b230b31ca468a81278d131be92427152940fa Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 11:57:04 -0800 Subject: [PATCH 09/15] Add localization for missing location error in path tracing --- lib/l10n/app_bg.arb | 3 +- lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_it.arb | 3 +- lib/l10n/app_localizations.dart | 6 ++++ lib/l10n/app_localizations_bg.dart | 4 +++ lib/l10n/app_localizations_de.dart | 4 +++ lib/l10n/app_localizations_en.dart | 4 +++ lib/l10n/app_localizations_es.dart | 4 +++ lib/l10n/app_localizations_fr.dart | 4 +++ lib/l10n/app_localizations_it.dart | 4 +++ lib/l10n/app_localizations_nl.dart | 4 +++ lib/l10n/app_localizations_pl.dart | 4 +++ lib/l10n/app_localizations_pt.dart | 4 +++ lib/l10n/app_localizations_ru.dart | 4 +++ lib/l10n/app_localizations_sk.dart | 4 +++ lib/l10n/app_localizations_sl.dart | 4 +++ lib/l10n/app_localizations_sv.dart | 4 +++ lib/l10n/app_localizations_uk.dart | 4 +++ lib/l10n/app_localizations_zh.dart | 3 ++ lib/l10n/app_nl.arb | 3 +- lib/l10n/app_pl.arb | 3 +- lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_sk.arb | 3 +- lib/l10n/app_sl.arb | 3 +- lib/l10n/app_sv.arb | 3 +- lib/l10n/app_uk.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/screens/channel_message_path_screen.dart | 4 +-- lib/screens/path_trace_map.dart | 35 ++++++++++++++++---- 33 files changed, 125 insertions(+), 22 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 460bc9d1..f0998d51 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", - "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя." + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", + "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0a72559c..da3a2c53 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1568,5 +1568,6 @@ "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", - "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen." + "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", + "pathTrace_someHopsNoLocation": "Eine oder mehrere der Hopfen fehlen einen Standort!" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee5cf7d6..85e26476 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1316,6 +1316,7 @@ "pathTrace_failed": "Path trace failed.", "pathTrace_notAvailable": "Path trace not available.", "pathTrace_refreshTooltip": "Refresh Path Trace.", + "pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!", "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index c6dad1ff..ac2d926d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1568,5 +1568,6 @@ "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", - "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado." + "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", + "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c1157ed3..71f1b1f6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", - "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact." + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", + "pathTrace_someHopsNoLocation": "Une ou plusieurs des houblons manquent d'une localisation !" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index c32e8638..bf217f52 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", - "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti." + "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", + "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 055667fa..1a7a4085 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4724,6 +4724,12 @@ abstract class AppLocalizations { /// **'Refresh Path Trace.'** String get pathTrace_refreshTooltip; + /// No description provided for @pathTrace_someHopsNoLocation. + /// + /// In en, this message translates to: + /// **'One or more of the hops is missing a location!'** + String get pathTrace_someHopsNoLocation; + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 701429e6..523e7290 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2695,6 +2695,10 @@ class AppLocalizationsBg extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Обнови Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Един или повече от хмелите липсва местоположение!'; + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 514a7a19..7e51cfb8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2699,6 +2699,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Path Trace aktualisieren.'; + @override + String get pathTrace_someHopsNoLocation => + 'Eine oder mehrere der Hopfen fehlen einen Standort!'; + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 040d8090..9dc14270 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2655,6 +2655,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Refresh Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'One or more of the hops is missing a location!'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e65cbcd3..c81c9cb9 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2694,6 +2694,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Actualizar Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'Uno o más de los lúpulos carecen de una ubicación'; + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4496fc84..3e72ef0f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2711,6 +2711,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Actualiser Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'Une ou plusieurs des houblons manquent d\'une localisation !'; + @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 02345c4e..d465f303 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2695,6 +2695,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Aggiorna Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Uno o più dei luppoli mancano di una posizione!'; + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 292181fc..1aa06104 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2685,6 +2685,10 @@ class AppLocalizationsNl extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Path Trace vernieuwen.'; + @override + String get pathTrace_someHopsNoLocation => + 'Een of meer van de hops ontbreken een locatie!'; + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 08323295..07ed0a69 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2693,6 +2693,10 @@ class AppLocalizationsPl extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.'; + @override + String get pathTrace_someHopsNoLocation => + 'Jeden lub więcej z chmieli nie ma określonej lokalizacji!'; + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index eadea3bc..3ec98d0b 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2696,6 +2696,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Atualizar Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Um ou mais dos lúpulos estão sem localização!'; + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ec0f1ba3..8dc1be75 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2698,6 +2698,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Обновить Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'Одному или нескольким хмелям не указано местоположение!'; + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 346047b0..8dfc5ab9 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2681,6 +2681,10 @@ class AppLocalizationsSk extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Jedna alebo viac chmeľov chýba lokalita!'; + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ed711224..558423f0 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2684,6 +2684,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Osveži Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Ena ali več hmelju manjka lokacija!'; + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 97b849fa..24fe7649 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2669,6 +2669,10 @@ class AppLocalizationsSv extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Uppdatera Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'En eller flera av humlen saknar en plats!'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 899d540d..ec0630b9 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2705,6 +2705,10 @@ class AppLocalizationsUk extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Оновити Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'Одне або більше хмелів відсутнє місце розташування!'; + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 77467924..021ebf31 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2554,6 +2554,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pathTrace_refreshTooltip => '重新绘制路径。'; + @override + String get pathTrace_someHopsNoLocation => '其中一个或多个啤酒花缺少位置!'; + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index e94deb37..e12738ba 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", "contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContactZeroHop": "Contact delen via advertentie", - "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden" + "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", + "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 44552c3c..b7713579 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.", "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", "contacts_ShareContact": "Kopiuj kontakt do schowka", - "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu." + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", + "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 56a7f2b2..26010969 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1568,5 +1568,6 @@ "contacts_floodAdvert": "Anúncio de Inundação", "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", - "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato." + "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", + "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0bca5ef0..c62e6bbe 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -808,5 +808,6 @@ "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", - "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению." + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", + "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d61cca61..7e39e2f2 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", - "contacts_ShareContact": "Kopírovať kontakt do schránky" + "contacts_ShareContact": "Kopírovať kontakt do schránky", + "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index cbc4e3f6..c8067df2 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", - "contacts_ShareContact": "Kopiraj stik v Odložišče" + "contacts_ShareContact": "Kopiraj stik v Odložišče", + "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 05f77cb8..9e2f3bfc 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.", "contacts_ShareContact": "Kopiera kontakt till Urklipp", "contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.", - "contacts_ShareContactZeroHop": "Dela kontakt via annons" + "contacts_ShareContactZeroHop": "Dela kontakt via annons", + "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3362d409..429fcdbd 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням" + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c941461a..75de8791 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1568,5 +1568,6 @@ "contacts_zeroHopContactAdvertSent": "通过广告获取联系方式。", "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。" + "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", + "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!" } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 1646b73a..d8a31fcb 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -525,8 +525,8 @@ class _ChannelMessagePathMapScreenState ), Marker( point: LatLng( - context.read().selfLatitude!, - context.read().selfLongitude!, + context.read().selfLatitude ?? 0.0, + context.read().selfLongitude ?? 0.0, ), width: 40, height: 40, diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index b4b280d6..e67c8252 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -50,6 +50,7 @@ class _PathTraceMapScreenState extends State { bool _isLoading = false; bool _failed2Loaded = false; bool _hasData = false; + bool _noLocationErr = false; PathTraceData? _traceData; List _points = []; List _polylines = []; @@ -108,6 +109,7 @@ class _PathTraceMapScreenState extends State { setState(() { _isLoading = true; _failed2Loaded = false; + _noLocationErr = false; }); } @@ -159,7 +161,8 @@ class _PathTraceMapScreenState extends State { } // Check if it's a binary response - if (code == pushCodeTraceData && + if (frame.length > 8 && + code == pushCodeTraceData && listEquals(frame.sublist(4, 8), tagData)) { _timeoutTimer?.cancel(); if (!mounted) return; @@ -207,8 +210,13 @@ class _PathTraceMapScreenState extends State { _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); for (final hop in _traceData!.pathData) { final contact = _traceData!.pathContacts[hop]; - if (contact != null && contact.hasLocation) { + if (contact != null && + contact.hasLocation && + contact.latitude != null && + contact.longitude != null) { _points.add(LatLng(contact.latitude!, contact.longitude!)); + } else { + _noLocationErr = true; } } _polylines = _points.length > 1 @@ -271,7 +279,20 @@ class _PathTraceMapScreenState extends State { top: false, child: Stack( children: [ - if (!_hasData) + if (_noLocationErr) + Center( + child: Card( + color: Colors.red, + child: Padding( + padding: EdgeInsets.all(12), + child: Text( + context.l10n.pathTrace_someHopsNoLocation, + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + if (!_hasData && _noLocationErr) Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -283,7 +304,7 @@ class _PathTraceMapScreenState extends State { ], ), ), - if (_hasData) + if (_hasData && !_noLocationErr) FlutterMap( key: _mapKey, options: MapOptions( @@ -318,7 +339,8 @@ class _PathTraceMapScreenState extends State { if (_points.isEmpty && !_hasData && !_isLoading && - !_failed2Loaded) + !_failed2Loaded && + !_noLocationErr) Center( child: Card( color: Colors.white.withValues(alpha: 0.9), @@ -330,7 +352,8 @@ class _PathTraceMapScreenState extends State { ), ), ), - if (_hasData) _buildLegendCard(context, _traceData!), + if (_hasData && !_noLocationErr) + _buildLegendCard(context, _traceData!), ], ), ), From bcae6ac19f73da9447cbfc89311ed33ddcf148ba Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 12:14:03 -0800 Subject: [PATCH 10/15] Updated GPX export functionality for contacts and repeaters in multiple languages. --- lib/l10n/app_bg.arb | 17 +++++- lib/l10n/app_de.arb | 17 +++++- lib/l10n/app_es.arb | 17 +++++- lib/l10n/app_fr.arb | 17 +++++- lib/l10n/app_it.arb | 17 +++++- lib/l10n/app_localizations.dart | 90 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_bg.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_de.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_es.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_fr.dart | 57 +++++++++++++++++++ lib/l10n/app_localizations_it.dart | 55 ++++++++++++++++++ lib/l10n/app_localizations_nl.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_pl.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_pt.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_ru.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_sk.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_sl.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_sv.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_uk.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_zh.dart | 45 +++++++++++++++ lib/l10n/app_nl.arb | 17 +++++- lib/l10n/app_pl.arb | 17 +++++- lib/l10n/app_pt.arb | 17 +++++- lib/l10n/app_ru.arb | 17 +++++- lib/l10n/app_sk.arb | 17 +++++- lib/l10n/app_sl.arb | 17 +++++- lib/l10n/app_sv.arb | 17 +++++- lib/l10n/app_uk.arb | 17 +++++- lib/l10n/app_zh.arb | 17 +++++- 30 files changed, 1112 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 460bc9d1..7a9d5220 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", - "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя." + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", + "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", + "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", + "settings_gpxExportAll": "Експортирай всички контакти в GPX", + "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", + "settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX", + "settings_gpxExportContacts": "Експортирай спътници към GPX", + "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", + "settings_gpxExportNoContacts": "Няма контакти за изlexport.", + "settings_gpxExportChat": "Местоположения на спътници", + "settings_gpxExportError": "Възникна грешка при изнасяне.", + "settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята", + "settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС", + "settings_gpxExportAllContacts": "Местоположения на всички контакти", + "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0a72559c..add286c8 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1568,5 +1568,20 @@ "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", - "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen." + "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", + "settings_gpxExportAll": "Alle Kontakte nach GPX exportieren", + "settings_gpxExportAllSubtitle": "Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.", + "settings_gpxExportRepeaters": "Repeater und Raumserver nach GPX exportieren", + "settings_gpxExportContacts": "Begleiter nach GPX exportieren", + "settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.", + "settings_gpxExportContactsSubtitle": "Exportiert Begleiter mit einem Ort in eine GPX-Datei.", + "settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte", + "settings_gpxExportChat": "Begleiterstandorte", + "settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.", + "settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.", + "settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt", + "settings_gpxExportSuccess": "Erfolgreich GPX-Datei exportiert.", + "settings_gpxExportAllContacts": "Alle Kontaktstandorte", + "settings_gpxExportShareSubject": "meshcore-open GPX-Kartendaten exportieren", + "settings_gpxExportShareText": "Kartendaten aus meshcore-open exportiert" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index c6dad1ff..f808409b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1568,5 +1568,20 @@ "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", - "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado." + "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", + "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", + "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", + "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", + "settings_gpxExportNoContacts": "No hay contactos para exportar.", + "settings_gpxExportNotAvailable": "No compatible con tu dispositivo/SO", + "settings_gpxExportError": "Hubo un error al exportar.", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.", + "settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.", + "settings_gpxExportAll": "Exportar todos los contactos a GPX", + "settings_gpxExportContacts": "Exportar compañeros a GPX", + "settings_gpxExportChat": "Ubicaciones de compañero", + "settings_gpxExportRepeatersRoom": "Ubicaciones del servidor de repetidor y sala", + "settings_gpxExportAllContacts": "Todas las ubicaciones de contactos", + "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c1157ed3..532f791b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", - "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact." + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", + "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", + "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", + "settings_gpxExportNoContacts": "Aucun contact à exporter.", + "settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation", + "settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.", + "settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle", + "settings_gpxExportContacts": "Exporter les compagnons au format GPX", + "settings_gpxExportAll": "Exporter tous les contacts au format GPX", + "settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.", + "settings_gpxExportContactsSubtitle": "Exporte les compagnons avec un emplacement vers un fichier GPX.", + "settings_gpxExportChat": "Emplacements des compagnons", + "settings_gpxExportSuccess": "Fichier GPX exporté avec succès.", + "settings_gpxExportAllContacts": "Tous les emplacements des contacts", + "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index c32e8638..28aea4e7 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", - "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti." + "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", + "settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX", + "settings_gpxExportContacts": "Esporta compagni in GPX", + "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", + "settings_gpxExportNoContacts": "Nessun contatto da esportare.", + "settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo", + "settings_gpxExportError": "Si è verificato un errore durante l'esportazione.", + "settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.", + "settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.", + "settings_gpxExportAll": "Esporta tutti i contatti in GPX", + "settings_gpxExportAllSubtitle": "Esporta tutti i contatti con una posizione in un file GPX.", + "settings_gpxExportChat": "Posizioni dei compagni", + "settings_gpxExportRepeatersRoom": "Posizioni del server ripetitore e della stanza", + "settings_gpxExportAllContacts": "Tutte le posizioni dei contatti", + "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 055667fa..2ec2fa89 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4855,6 +4855,96 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Copying advert to Clipboard failed.'** String get contacts_contactAdvertCopyFailed; + + /// No description provided for @settings_gpxExportRepeaters. + /// + /// In en, this message translates to: + /// **'Export repeaters / room server to GPX'** + String get settings_gpxExportRepeaters; + + /// No description provided for @settings_gpxExportRepeatersSubtitle. + /// + /// In en, this message translates to: + /// **'Exports repeaters / roomserver with a location to GPX file.'** + String get settings_gpxExportRepeatersSubtitle; + + /// No description provided for @settings_gpxExportContacts. + /// + /// In en, this message translates to: + /// **'Export companions to GPX'** + String get settings_gpxExportContacts; + + /// No description provided for @settings_gpxExportContactsSubtitle. + /// + /// In en, this message translates to: + /// **'Exports companions with a location to GPX file.'** + String get settings_gpxExportContactsSubtitle; + + /// No description provided for @settings_gpxExportAll. + /// + /// In en, this message translates to: + /// **'Export all contacts to GPX'** + String get settings_gpxExportAll; + + /// No description provided for @settings_gpxExportAllSubtitle. + /// + /// In en, this message translates to: + /// **'Exports all contacts with a location to GPX file.'** + String get settings_gpxExportAllSubtitle; + + /// No description provided for @settings_gpxExportSuccess. + /// + /// In en, this message translates to: + /// **'Successfully exported GPX file.'** + String get settings_gpxExportSuccess; + + /// No description provided for @settings_gpxExportNoContacts. + /// + /// In en, this message translates to: + /// **'No contacts to export.'** + String get settings_gpxExportNoContacts; + + /// No description provided for @settings_gpxExportNotAvailable. + /// + /// In en, this message translates to: + /// **'Not supported on your device/OS'** + String get settings_gpxExportNotAvailable; + + /// No description provided for @settings_gpxExportError. + /// + /// In en, this message translates to: + /// **'There was an error when exporting.'** + String get settings_gpxExportError; + + /// No description provided for @settings_gpxExportRepeatersRoom. + /// + /// In en, this message translates to: + /// **'Repeater & room server locations'** + String get settings_gpxExportRepeatersRoom; + + /// No description provided for @settings_gpxExportChat. + /// + /// In en, this message translates to: + /// **'Companion locations'** + String get settings_gpxExportChat; + + /// No description provided for @settings_gpxExportAllContacts. + /// + /// In en, this message translates to: + /// **'All contacts locations'** + String get settings_gpxExportAllContacts; + + /// No description provided for @settings_gpxExportShareText. + /// + /// In en, this message translates to: + /// **'Map data exported from meshcore-open'** + String get settings_gpxExportShareText; + + /// No description provided for @settings_gpxExportShareSubject. + /// + /// In en, this message translates to: + /// **'meshcore-open GPX map data export'** + String get settings_gpxExportShareSubject; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 701429e6..45a02678 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2766,4 +2766,58 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Копирането на обявата в клипборда не успя.'; + + @override + String get settings_gpxExportRepeaters => + 'Експортиране на повтарящи се устройства / сървър на стаята до GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Изпраща повторители / roomserver с местоположение в GPX файл.'; + + @override + String get settings_gpxExportContacts => 'Експортирай спътници към GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Експортира спътници с местоположение в GPX файл.'; + + @override + String get settings_gpxExportAll => 'Експортирай всички контакти в GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Експортира всички контакти с местоположение в файл GPX.'; + + @override + String get settings_gpxExportSuccess => 'Успешно изlexport на файл GPX.'; + + @override + String get settings_gpxExportNoContacts => 'Няма контакти за изlexport.'; + + @override + String get settings_gpxExportNotAvailable => + 'Не е поддържан на вашето устройство/ОС'; + + @override + String get settings_gpxExportError => 'Възникна грешка при изнасяне.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Местоположения на повторител и сървър на стаята'; + + @override + String get settings_gpxExportChat => 'Местоположения на спътници'; + + @override + String get settings_gpxExportAllContacts => + 'Местоположения на всички контакти'; + + @override + String get settings_gpxExportShareText => + 'Картинни данни изнесени от meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open износ на данни за карта в формат GPX'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 514a7a19..8c5c50b6 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2773,4 +2773,58 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.'; + + @override + String get settings_gpxExportRepeaters => + 'Repeater und Raumserver nach GPX exportieren'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.'; + + @override + String get settings_gpxExportContacts => 'Begleiter nach GPX exportieren'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exportiert Begleiter mit einem Ort in eine GPX-Datei.'; + + @override + String get settings_gpxExportAll => 'Alle Kontakte nach GPX exportieren'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.'; + + @override + String get settings_gpxExportSuccess => 'Erfolgreich GPX-Datei exportiert.'; + + @override + String get settings_gpxExportNoContacts => 'Keine Kontakte zum Exportieren.'; + + @override + String get settings_gpxExportNotAvailable => + 'Nicht auf Ihrem Gerät/Betriebssystem unterstützt'; + + @override + String get settings_gpxExportError => + 'Beim Export ist ein Fehler aufgetreten.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Repeater- und Raumserver-Standorte'; + + @override + String get settings_gpxExportChat => 'Begleiterstandorte'; + + @override + String get settings_gpxExportAllContacts => 'Alle Kontaktstandorte'; + + @override + String get settings_gpxExportShareText => + 'Kartendaten aus meshcore-open exportiert'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open GPX-Kartendaten exportieren'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 040d8090..6667a36f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2723,4 +2723,57 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Copying advert to Clipboard failed.'; + + @override + String get settings_gpxExportRepeaters => + 'Export repeaters / room server to GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exports repeaters / roomserver with a location to GPX file.'; + + @override + String get settings_gpxExportContacts => 'Export companions to GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exports companions with a location to GPX file.'; + + @override + String get settings_gpxExportAll => 'Export all contacts to GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exports all contacts with a location to GPX file.'; + + @override + String get settings_gpxExportSuccess => 'Successfully exported GPX file.'; + + @override + String get settings_gpxExportNoContacts => 'No contacts to export.'; + + @override + String get settings_gpxExportNotAvailable => + 'Not supported on your device/OS'; + + @override + String get settings_gpxExportError => 'There was an error when exporting.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Repeater & room server locations'; + + @override + String get settings_gpxExportChat => 'Companion locations'; + + @override + String get settings_gpxExportAllContacts => 'All contacts locations'; + + @override + String get settings_gpxExportShareText => + 'Map data exported from meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open GPX map data export'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e65cbcd3..4661c25a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2766,4 +2766,58 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Copiar anuncio al Portapapeles ha fallado.'; + + @override + String get settings_gpxExportRepeaters => + 'Exportar repetidores / servidor de sala a GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporta repetidores o roomserver con una ubicación a un archivo GPX.'; + + @override + String get settings_gpxExportContacts => 'Exportar compañeros a GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporta compañeros con una ubicación a archivo GPX.'; + + @override + String get settings_gpxExportAll => 'Exportar todos los contactos a GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporta todos los contactos con una ubicación a un archivo GPX.'; + + @override + String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.'; + + @override + String get settings_gpxExportNoContacts => 'No hay contactos para exportar.'; + + @override + String get settings_gpxExportNotAvailable => + 'No compatible con tu dispositivo/SO'; + + @override + String get settings_gpxExportError => 'Hubo un error al exportar.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Ubicaciones del servidor de repetidor y sala'; + + @override + String get settings_gpxExportChat => 'Ubicaciones de compañero'; + + @override + String get settings_gpxExportAllContacts => + 'Todas las ubicaciones de contactos'; + + @override + String get settings_gpxExportShareText => + 'Datos del mapa exportados desde meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open exportación de datos de mapa GPX'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4496fc84..f0d81483 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2787,4 +2787,61 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'La copie de l\'annonce vers le presse-papiers a échoué.'; + + @override + String get settings_gpxExportRepeaters => + 'Exporter les répéteurs / serveur de salle au format GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.'; + + @override + String get settings_gpxExportContacts => + 'Exporter les compagnons au format GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporte les compagnons avec un emplacement vers un fichier GPX.'; + + @override + String get settings_gpxExportAll => + 'Exporter tous les contacts au format GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporte tous les contacts avec une localisation vers un fichier GPX.'; + + @override + String get settings_gpxExportSuccess => 'Fichier GPX exporté avec succès.'; + + @override + String get settings_gpxExportNoContacts => 'Aucun contact à exporter.'; + + @override + String get settings_gpxExportNotAvailable => + 'Non pris en charge sur votre appareil/Système d\'exploitation'; + + @override + String get settings_gpxExportError => + 'Une erreur s\'est produite lors de l\'exportation.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Emplacements des serveurs de répéteur et de salle'; + + @override + String get settings_gpxExportChat => 'Emplacements des compagnons'; + + @override + String get settings_gpxExportAllContacts => + 'Tous les emplacements des contacts'; + + @override + String get settings_gpxExportShareText => + 'Données de carte exportées à partir de meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open exporter les données de carte GPX'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 02345c4e..25c55af8 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2769,4 +2769,59 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Copia dell\'annuncio nella Clipboard non riuscita.'; + + @override + String get settings_gpxExportRepeaters => + 'Esporta ripetitori / server di stanza in GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Esporta ripetitori / roomserver con una posizione in un file GPX.'; + + @override + String get settings_gpxExportContacts => 'Esporta compagni in GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Esporta i compagni con una posizione in un file GPX.'; + + @override + String get settings_gpxExportAll => 'Esporta tutti i contatti in GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Esporta tutti i contatti con una posizione in un file GPX.'; + + @override + String get settings_gpxExportSuccess => + 'Esportazione del file GPX completata con successo.'; + + @override + String get settings_gpxExportNoContacts => 'Nessun contatto da esportare.'; + + @override + String get settings_gpxExportNotAvailable => + 'Non supportato sul tuo dispositivo/Sistema Operativo'; + + @override + String get settings_gpxExportError => + 'Si è verificato un errore durante l\'esportazione.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Posizioni del server ripetitore e della stanza'; + + @override + String get settings_gpxExportChat => 'Posizioni dei compagni'; + + @override + String get settings_gpxExportAllContacts => 'Tutte le posizioni dei contatti'; + + @override + String get settings_gpxExportShareText => + 'Dati mappa esportati da meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open esportazione dati mappa GPX'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 292181fc..5940333c 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2758,4 +2758,57 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopiëren van advertentie naar Clipboard is mislukt.'; + + @override + String get settings_gpxExportRepeaters => + 'Exporteer repeaters / roomserver naar GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporteert repeaters / roomserver met een locatie naar GPX-bestand.'; + + @override + String get settings_gpxExportContacts => 'Companions exporteren naar GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporteert metgezellen met een locatie naar een GPX-bestand.'; + + @override + String get settings_gpxExportAll => 'Alle contacten exporteren naar GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporteert alle contacten met een locatie naar een GPX-bestand.'; + + @override + String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.'; + + @override + String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.'; + + @override + String get settings_gpxExportNotAvailable => + 'Niet ondersteund op uw apparaat/besturingssysteem'; + + @override + String get settings_gpxExportError => 'Er was een fout bij het exporteren.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Repeater- en kamer servers locaties'; + + @override + String get settings_gpxExportChat => 'Locaties van metgezellen'; + + @override + String get settings_gpxExportAllContacts => 'Alle contactlocaties'; + + @override + String get settings_gpxExportShareText => + 'Kaartgegevens geëxporteerd uit meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open GPX kaartgegevens exporteren'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 08323295..00b5c1eb 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2766,4 +2766,58 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopiowanie ogłoszenia do schowka nie powiodło się.'; + + @override + String get settings_gpxExportRepeaters => + 'Eksportuj powtórki / serwer pokojowy do GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.'; + + @override + String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Eksportuje towarzyszy z lokalizacją do pliku GPX.'; + + @override + String get settings_gpxExportAll => 'Eksportuj wszystkie kontakty do GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.'; + + @override + String get settings_gpxExportSuccess => 'Pomyślnie wyeksportowano plik GPX.'; + + @override + String get settings_gpxExportNoContacts => + 'Brak kontaktów do wyeksportowania.'; + + @override + String get settings_gpxExportNotAvailable => + 'Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym'; + + @override + String get settings_gpxExportError => 'Wystąpił błąd podczas eksportowania.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Lokalizacje serwerów powtarzających i pomieszczeń'; + + @override + String get settings_gpxExportChat => 'Lokalizacje towarzyszy'; + + @override + String get settings_gpxExportAllContacts => 'Wszystkie lokalizacje kontaktów'; + + @override + String get settings_gpxExportShareText => + 'Dane mapy wyeksportowane z meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'Eksport danych mapy GPX meshcore-open'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index eadea3bc..e4877c87 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2768,4 +2768,57 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Cópia do anúncio para a Área de Transferência falhou.'; + + @override + String get settings_gpxExportRepeaters => + 'Exportar repetidores / servidor de sala para GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporta repetidores / roomserver com localização para arquivo GPX.'; + + @override + String get settings_gpxExportContacts => 'Exportar companheiros para GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporta companheiros com uma localização para um arquivo GPX.'; + + @override + String get settings_gpxExportAll => 'Exportar todos os contatos para GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporta todos os contatos com uma localização para um arquivo GPX.'; + + @override + String get settings_gpxExportSuccess => 'Arquivo GPX exportado com sucesso.'; + + @override + String get settings_gpxExportNoContacts => 'Nenhum contato para exportar.'; + + @override + String get settings_gpxExportNotAvailable => + 'Não suportado no seu dispositivo/SO'; + + @override + String get settings_gpxExportError => 'Ocorreu um erro ao exportar.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Localizações do servidor de repetidor e sala'; + + @override + String get settings_gpxExportChat => 'Localizações de companheiros'; + + @override + String get settings_gpxExportAllContacts => 'Todos os locais de contatos'; + + @override + String get settings_gpxExportShareText => + 'Dados do mapa exportados do meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open exportação de dados de mapa GPX'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ec0f1ba3..4facd82c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2773,4 +2773,57 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Копирование рекламы в буфер обмена не удалось.'; + + @override + String get settings_gpxExportRepeaters => + 'Экспортировать рипитеры / сервер комнаты в GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.'; + + @override + String get settings_gpxExportContacts => 'Экспортировать спутников в GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Экспортирует спутников с местоположением в файл GPX.'; + + @override + String get settings_gpxExportAll => 'Экспортировать все контакты в GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Экспортирует все контакты с местоположением в файл GPX.'; + + @override + String get settings_gpxExportSuccess => 'Успешно экспортирован файл GPX.'; + + @override + String get settings_gpxExportNoContacts => 'Нет контактов для экспорта.'; + + @override + String get settings_gpxExportNotAvailable => + 'Не поддерживается на вашем устройстве/ОС'; + + @override + String get settings_gpxExportError => 'Произошла ошибка при экспорте.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Местоположения повторителей и серверов комнат'; + + @override + String get settings_gpxExportChat => 'Местоположения спутников'; + + @override + String get settings_gpxExportAllContacts => 'Все местоположения контактов'; + + @override + String get settings_gpxExportShareText => + 'Данные карты экспортированы из meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open экспорт данных карты GPX'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 346047b0..c5551238 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2752,4 +2752,57 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopírovanie inzerátu do schránky zlyhalo.'; + + @override + String get settings_gpxExportRepeaters => + 'Exportovať repeater / server miestnosti do GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exportuje repeater / roomserver s lokalitou do súboru GPX.'; + + @override + String get settings_gpxExportContacts => 'Export sprievodcov do GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exportuje sprievodcov s umiestnením do súboru GPX.'; + + @override + String get settings_gpxExportAll => 'Exportovať všetky kontakty do GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exportuje všetky kontakty s lokalitou do súboru GPX.'; + + @override + String get settings_gpxExportSuccess => 'Úspešne exportovaný súbor GPX.'; + + @override + String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.'; + + @override + String get settings_gpxExportNotAvailable => + 'Nie je podporované na vašom zariadení/operáciomnom systéme'; + + @override + String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Umiestnenia opakovačov a serverov miestností'; + + @override + String get settings_gpxExportChat => 'Lokácie sprievodcov'; + + @override + String get settings_gpxExportAllContacts => 'Všetky kontaktné lokality'; + + @override + String get settings_gpxExportShareText => + 'Mapové údaje exportované z meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open export dát GPX mapových údajov'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ed711224..7190a9c4 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2754,4 +2754,57 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopiranje oglasa v odložišče je spodletelo.'; + + @override + String get settings_gpxExportRepeaters => + 'Izvoz ponoviteljev / strežnika sobe v GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.'; + + @override + String get settings_gpxExportContacts => 'Izvoz spremljevalcev v GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Izvozi spremljevalce z lokacijo v datoteko GPX.'; + + @override + String get settings_gpxExportAll => 'Izvozi vse kontakte v GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Izvozi vse kontakte z lokacijo v datoteko GPX.'; + + @override + String get settings_gpxExportSuccess => 'Uspešno izvoz GPX datoteke.'; + + @override + String get settings_gpxExportNoContacts => 'Ni stikov za izvoz.'; + + @override + String get settings_gpxExportNotAvailable => + 'Ni podprto na vašem napravi/operacijskem sistemu'; + + @override + String get settings_gpxExportError => 'Pri izvozu je prišlo do napake.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Lokacije ponovljivca in strežnika sobe'; + + @override + String get settings_gpxExportChat => 'Lokacije spremljevalcev'; + + @override + String get settings_gpxExportAllContacts => 'Lokacije vseh stikov'; + + @override + String get settings_gpxExportShareText => + 'Podatki kart izvoženi iz meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open izvoz podatkov GPX karte'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 97b849fa..63f3ea2c 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2739,4 +2739,58 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopiering av annons till Urklipp misslyckades.'; + + @override + String get settings_gpxExportRepeaters => + 'Exportera repeater / rumsservrar till GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporterar repeater / roomserver med plats till GPX-fil.'; + + @override + String get settings_gpxExportContacts => 'Exportera följeslagare till GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporterar följeslagare med en plats till GPX-fil.'; + + @override + String get settings_gpxExportAll => 'Exportera alla kontakter till GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporterar alla kontakter med en plats till GPX-fil.'; + + @override + String get settings_gpxExportSuccess => 'Har exporterat GPX-fil med framgång'; + + @override + String get settings_gpxExportNoContacts => 'Inga kontakter att exportera.'; + + @override + String get settings_gpxExportNotAvailable => + 'Stöds inte på din enhet/operativsystem'; + + @override + String get settings_gpxExportError => + 'Det uppstod ett fel när data exporterades.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Repeater- och rumsserverplatser'; + + @override + String get settings_gpxExportChat => 'Medhjälparplatser'; + + @override + String get settings_gpxExportAllContacts => 'Alla kontakters platser'; + + @override + String get settings_gpxExportShareText => + 'Kartdata exporterad från meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open export av GPX-kartdata'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 899d540d..838961a8 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2779,4 +2779,57 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Копіювання оголошення в буфер обміну завершилося невдало'; + + @override + String get settings_gpxExportRepeaters => + 'Експортувати ретранслятори / сервер кімнати до GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.'; + + @override + String get settings_gpxExportContacts => 'Експортувати супутників до GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Експортує супутників з місцезнаходженням у файл GPX.'; + + @override + String get settings_gpxExportAll => 'Експортувати всі контакти до GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Експортує всі контакти з місцем розташування у файл GPX.'; + + @override + String get settings_gpxExportSuccess => 'Успішно експортовано файл GPX.'; + + @override + String get settings_gpxExportNoContacts => 'Немає контактів для експорту.'; + + @override + String get settings_gpxExportNotAvailable => + 'Не підтримується на вашому пристрої/операційній системі'; + + @override + String get settings_gpxExportError => 'Сталася помилка під час експорту.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Місцезнаходження повторювача та сервера кімнати'; + + @override + String get settings_gpxExportChat => 'Місця супутників'; + + @override + String get settings_gpxExportAllContacts => 'Усі місця контактів'; + + @override + String get settings_gpxExportShareText => + 'Дані карти експортовані з meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'експорт даних карти meshcore-open у форматі GPX'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 77467924..020f795a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2621,4 +2621,49 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => '将广告复制到剪贴板操作失败。'; + + @override + String get settings_gpxExportRepeaters => '导出重复器/房间服务器到GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => '导出带有位置的重复器/房间服务器到GPX文件。'; + + @override + String get settings_gpxExportContacts => '导出伴侣到GPX'; + + @override + String get settings_gpxExportContactsSubtitle => '导出带有位置的伙伴到GPX文件。'; + + @override + String get settings_gpxExportAll => '导出所有联系人到GPX'; + + @override + String get settings_gpxExportAllSubtitle => '导出所有带有位置的联系人到GPX文件。'; + + @override + String get settings_gpxExportSuccess => '成功导出GPX文件'; + + @override + String get settings_gpxExportNoContacts => '没有联系人可导出'; + + @override + String get settings_gpxExportNotAvailable => '您的设备/操作系统不支持'; + + @override + String get settings_gpxExportError => '导出时发生错误'; + + @override + String get settings_gpxExportRepeatersRoom => '重复器和房间服务器位置'; + + @override + String get settings_gpxExportChat => '伴侣位置'; + + @override + String get settings_gpxExportAllContacts => '所有联系人位置'; + + @override + String get settings_gpxExportShareText => '来自meshcore-open的导出地图数据'; + + @override + String get settings_gpxExportShareSubject => 'meshcore-open GPX 地图数据导出'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index e94deb37..4dbd5061 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", "contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContactZeroHop": "Contact delen via advertentie", - "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden" + "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", + "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", + "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", + "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", + "settings_gpxExportNoContacts": "Geen contacten om te exporteren.", + "settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem", + "settings_gpxExportError": "Er was een fout bij het exporteren.", + "settings_gpxExportContacts": "Companions exporteren naar GPX", + "settings_gpxExportAll": "Alle contacten exporteren naar GPX", + "settings_gpxExportAllSubtitle": "Exporteert alle contacten met een locatie naar een GPX-bestand.", + "settings_gpxExportContactsSubtitle": "Exporteert metgezellen met een locatie naar een GPX-bestand.", + "settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties", + "settings_gpxExportChat": "Locaties van metgezellen", + "settings_gpxExportAllContacts": "Alle contactlocaties", + "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 44552c3c..53272155 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.", "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", "contacts_ShareContact": "Kopiuj kontakt do schowka", - "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu." + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", + "settings_gpxExportContacts": "Eksportuj towarzyszy do GPX", + "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", + "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.", + "settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.", + "settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym", + "settings_gpxExportError": "Wystąpił błąd podczas eksportowania.", + "settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzających i pomieszczeń", + "settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.", + "settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX", + "settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.", + "settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów", + "settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.", + "settings_gpxExportChat": "Lokalizacje towarzyszy", + "settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open", + "settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 56a7f2b2..7da9019f 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1568,5 +1568,20 @@ "contacts_floodAdvert": "Anúncio de Inundação", "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", - "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato." + "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", + "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", + "settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.", + "settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.", + "settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO", + "settings_gpxExportError": "Ocorreu um erro ao exportar.", + "settings_gpxExportAll": "Exportar todos os contatos para GPX", + "settings_gpxExportContacts": "Exportar companheiros para GPX", + "settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.", + "settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala", + "settings_gpxExportChat": "Localizações de companheiros", + "settings_gpxExportNoContacts": "Nenhum contato para exportar.", + "settings_gpxExportAllContacts": "Todos os locais de contatos", + "settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0bca5ef0..e0f44021 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -808,5 +808,20 @@ "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", - "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению." + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", + "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", + "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", + "settings_gpxExportContacts": "Экспортировать спутников в GPX", + "settings_gpxExportNotAvailable": "Не поддерживается на вашем устройстве/ОС", + "settings_gpxExportError": "Произошла ошибка при экспорте.", + "settings_gpxExportRepeatersRoom": "Местоположения повторителей и серверов комнат", + "settings_gpxExportChat": "Местоположения спутников", + "settings_gpxExportContactsSubtitle": "Экспортирует спутников с местоположением в файл GPX.", + "settings_gpxExportAll": "Экспортировать все контакты в GPX", + "settings_gpxExportAllSubtitle": "Экспортирует все контакты с местоположением в файл GPX.", + "settings_gpxExportAllContacts": "Все местоположения контактов", + "settings_gpxExportSuccess": "Успешно экспортирован файл GPX.", + "settings_gpxExportNoContacts": "Нет контактов для экспорта.", + "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d61cca61..e72888ec 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", - "contacts_ShareContact": "Kopírovať kontakt do schránky" + "contacts_ShareContact": "Kopírovať kontakt do schránky", + "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", + "settings_gpxExportContacts": "Export sprievodcov do GPX", + "settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.", + "settings_gpxExportNoContacts": "Žiadne kontakty na export.", + "settings_gpxExportNotAvailable": "Nie je podporované na vašom zariadení/operáciomnom systéme", + "settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností", + "settings_gpxExportError": "Vyskytol sa chyba počas exportu.", + "settings_gpxExportAllSubtitle": "Exportuje všetky kontakty s lokalitou do súboru GPX.", + "settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.", + "settings_gpxExportRepeaters": "Exportovať repeater / server miestnosti do GPX", + "settings_gpxExportAll": "Exportovať všetky kontakty do GPX", + "settings_gpxExportAllContacts": "Všetky kontaktné lokality", + "settings_gpxExportChat": "Lokácie sprievodcov", + "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index cbc4e3f6..97cd1f81 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", - "contacts_ShareContact": "Kopiraj stik v Odložišče" + "contacts_ShareContact": "Kopiraj stik v Odložišče", + "settings_gpxExportAll": "Izvozi vse kontakte v GPX", + "settings_gpxExportContacts": "Izvoz spremljevalcev v GPX", + "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", + "settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX", + "settings_gpxExportError": "Pri izvozu je prišlo do napake.", + "settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe", + "settings_gpxExportChat": "Lokacije spremljevalcev", + "settings_gpxExportAllContacts": "Lokacije vseh stikov", + "settings_gpxExportContactsSubtitle": "Izvozi spremljevalce z lokacijo v datoteko GPX.", + "settings_gpxExportAllSubtitle": "Izvozi vse kontakte z lokacijo v datoteko GPX.", + "settings_gpxExportSuccess": "Uspešno izvoz GPX datoteke.", + "settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open", + "settings_gpxExportNoContacts": "Ni stikov za izvoz.", + "settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu", + "settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 05f77cb8..03844f37 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.", "contacts_ShareContact": "Kopiera kontakt till Urklipp", "contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.", - "contacts_ShareContactZeroHop": "Dela kontakt via annons" + "contacts_ShareContactZeroHop": "Dela kontakt via annons", + "settings_gpxExportAll": "Exportera alla kontakter till GPX", + "settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.", + "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång", + "settings_gpxExportNoContacts": "Inga kontakter att exportera.", + "settings_gpxExportNotAvailable": "Stöds inte på din enhet/operativsystem", + "settings_gpxExportRepeatersRoom": "Repeater- och rumsserverplatser", + "settings_gpxExportRepeaters": "Exportera repeater / rumsservrar till GPX", + "settings_gpxExportAllSubtitle": "Exporterar alla kontakter med en plats till GPX-fil.", + "settings_gpxExportContacts": "Exportera följeslagare till GPX", + "settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.", + "settings_gpxExportChat": "Medhjälparplatser", + "settings_gpxExportError": "Det uppstod ett fel när data exporterades.", + "settings_gpxExportAllContacts": "Alla kontakters platser", + "settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata", + "settings_gpxExportShareText": "Kartdata exporterad från meshcore-open" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3362d409..796be1e2 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням" + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", + "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", + "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", + "settings_gpxExportNoContacts": "Немає контактів для експорту.", + "settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі", + "settings_gpxExportError": "Сталася помилка під час експорту.", + "settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.", + "settings_gpxExportAll": "Експортувати всі контакти до GPX", + "settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.", + "settings_gpxExportContacts": "Експортувати супутників до GPX", + "settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати", + "settings_gpxExportChat": "Місця супутників", + "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", + "settings_gpxExportAllContacts": "Усі місця контактів", + "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c941461a..a71541b6 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1568,5 +1568,20 @@ "contacts_zeroHopContactAdvertSent": "通过广告获取联系方式。", "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。" + "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", + "settings_gpxExportRepeaters": "导出重复器/房间服务器到GPX", + "settings_gpxExportRepeatersSubtitle": "导出带有位置的重复器/房间服务器到GPX文件。", + "settings_gpxExportContactsSubtitle": "导出带有位置的伙伴到GPX文件。", + "settings_gpxExportNotAvailable": "您的设备/操作系统不支持", + "settings_gpxExportSuccess": "成功导出GPX文件", + "settings_gpxExportError": "导出时发生错误", + "settings_gpxExportRepeatersRoom": "重复器和房间服务器位置", + "settings_gpxExportChat": "伴侣位置", + "settings_gpxExportAll": "导出所有联系人到GPX", + "settings_gpxExportContacts": "导出伴侣到GPX", + "settings_gpxExportAllSubtitle": "导出所有带有位置的联系人到GPX文件。", + "settings_gpxExportAllContacts": "所有联系人位置", + "settings_gpxExportNoContacts": "没有联系人可导出", + "settings_gpxExportShareText": "来自meshcore-open的导出地图数据", + "settings_gpxExportShareSubject": "meshcore-open GPX 地图数据导出" } From 0d8801fa7537d0540a67b13590adb51e9cdfb9c8 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 12:25:51 -0800 Subject: [PATCH 11/15] Add scrollbar to path trace details list for improved navigation --- lib/screens/path_trace_map.dart | 69 ++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index e67c8252..2e557a59 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -515,38 +515,45 @@ class _PathTraceMapScreenState extends State { ? Center( child: Text(l10n.channelPath_noHopDetailsAvailable), ) - : ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: pathTraceData.pathData.length + 1, - separatorBuilder: (_, __) => const Divider(height: 1), - itemBuilder: (context, index) { - return Column( - children: [ - ListTile( - leading: - index >= pathTraceData.snrData.length / 2 - ? Icon(Icons.call_received) - : Icon(Icons.call_made), - title: Text( - formatDirectionText(pathTraceData, index), - style: const TextStyle(fontSize: 14), + : Scrollbar( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: pathTraceData.pathData.length + 1, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + return Column( + children: [ + ListTile( + leading: + index >= pathTraceData.snrData.length / 2 + ? Icon(Icons.call_received) + : Icon(Icons.call_made), + title: Text( + formatDirectionText(pathTraceData, index), + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + formatDirectionSubText( + pathTraceData, + index, + ), + style: const TextStyle(fontSize: 14), + ), + trailing: SNRIcon( + snr: + pathTraceData.snrData[index].toSigned( + 8, + ) / + 4.0, + ), + onTap: () { + // Handle item tap + }, ), - subtitle: Text( - formatDirectionSubText(pathTraceData, index), - style: const TextStyle(fontSize: 14), - ), - trailing: SNRIcon( - snr: - pathTraceData.snrData[index].toSigned(8) / - 4.0, - ), - onTap: () { - // Handle item tap - }, - ), - ], - ); - }, + ], + ); + }, + ), ), ), ], From 2db30ace6a1e3f7937fd562697ca5961810dddcd Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 12:26:49 -0800 Subject: [PATCH 12/15] Integrate SharePlus plugin for enhanced sharing functionality across platforms --- lib/utils/gpx_export.dart | 60 ++++++++----------- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 50 +++++++++++++++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 5 files changed, 80 insertions(+), 36 deletions(-) diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 92494c93..595479c1 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -54,50 +54,50 @@ class GpxExport { } void addRepeaters() { - final contacts = _connector.contacts; - final repeaters = contacts + final contacts = _connector.contacts .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) .toList(); - for (var repeater in repeaters) { - if (repeater.latitude == null || repeater.longitude == null) { + for (var contact in contacts) { + if (contact.latitude == null || contact.longitude == null) { continue; } _addContact( - repeater.name, - repeater.latitude ?? 0.0, - repeater.longitude ?? 0.0, - "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + contact.name, + contact.latitude!, + contact.longitude!, + "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", ); } } void addContacts() { - final contacts = _connector.contacts; - final repeaters = contacts.where((c) => c.type == advTypeChat).toList(); - for (var repeater in repeaters) { - if (repeater.latitude == null || repeater.longitude == null) { + final contacts = _connector.contacts + .where((c) => c.type == advTypeChat) + .toList(); + for (var contact in contacts) { + if (contact.latitude == null || contact.longitude == null) { continue; } _addContact( - repeater.name, - repeater.latitude ?? 0.0, - repeater.longitude ?? 0.0, - "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + contact.name, + contact.latitude!, + contact.longitude!, + "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", ); } } void addAll() { final contacts = _connector.contacts; - for (var repeater in contacts.toList()) { - if (repeater.latitude == null || repeater.longitude == null) { + for (var contact in contacts.toList()) { + if (contact.latitude == null || contact.longitude == null) { continue; } _addContact( - repeater.name, - repeater.latitude ?? 0.0, - repeater.longitude ?? 0.0, - "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + contact.name, + contact.latitude ?? 0.0, + contact.longitude ?? 0.0, + "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", ); } } @@ -149,23 +149,17 @@ class GpxExport { .split('T') .join('_'); - // ignore: unnecessary_string_escapes final path = '${dir.path}/$filename$timestamp.gpx'; final file = File(path); await file.writeAsString(xml); - // 3. Modern share call (2025+ style) final result = await SharePlus.instance.share( - ShareParams( - text: shareText, - subject: subject, - files: [XFile(path)], - // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) - ), + ShareParams(text: shareText, subject: subject, files: [XFile(path)]), ); - // 4. Handle result + await file.delete(); + switch (result.status) { case ShareResultStatus.success: debugPrint('Share successful – user completed the action.'); @@ -177,12 +171,8 @@ class GpxExport { debugPrint('Sharing is not available on this platform / context.'); return gpxExportNotAvailable; } - - // Optional cleanup (uncomment if you don't want to keep the file) - // await file.delete(); } catch (e, stack) { debugPrint('Export or share failed: $e\n$stack'); - // → here you could show a SnackBar / AlertDialog in real UI code } return gpxExportFailed; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b4a41dd1..d2ea57e9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import share_plus import shared_preferences_foundation import sqflite_darwin import url_launcher_macos @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 1e275d4f..fc116566 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: "direct main" description: @@ -341,6 +349,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + gpx: + dependency: "direct main" + description: + name: gpx + sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e + url: "https://pub.dev" + source: hosted + version: "2.3.0" hooks: dependency: transitive description: @@ -501,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -566,7 +590,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -701,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" rxdart: dependency: transitive description: @@ -709,6 +741,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" shared_preferences: dependency: "direct main" description: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index eeb548fa..cd4fc19b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterBluePlusPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterBluePlusPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 68825d8b..571addb8 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 + share_plus url_launcher_windows ) From ea43cf17eba8d3cfd7ba4cfeb31ef526760cc15e Mon Sep 17 00:00:00 2001 From: Ded Date: Sun, 8 Feb 2026 18:40:58 -0800 Subject: [PATCH 13/15] reduce map marker size (#131) * reduce map marker size reduces map markers from 80 to 60 px to improve visibility with higher density areas * add flutter test to actions * Add GPX export functionality and related UI components * Refactor GPX export constants to use lowercase naming convention and improve export function error handling * ran formating * Enhance GPX export functionality with customizable parameters and improved metadata * Implement PathTraceMapScreen and refactor path tracing functionality across screens * Add localization for missing location error in path tracing * Updated GPX export functionality for contacts and repeaters in multiple languages. * Add scrollbar to path trace details list for improved navigation * Integrate SharePlus plugin for enhanced sharing functionality across platforms * reduce map marker size reduces map markers from 80 to 60 px to improve visibility with higher density areas * reduce marker size to improve map clarity and add path trace navigation to path management --------- Co-authored-by: Winston Lowe --- lib/screens/channel_message_path_screen.dart | 60 ++++++++-------- lib/screens/map_screen.dart | 45 ++++++++++-- lib/screens/path_trace_map.dart | 72 +++++++++++--------- lib/widgets/path_management_dialog.dart | 14 ++++ 4 files changed, 124 insertions(+), 67 deletions(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 65487d8a..8dea4758 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -500,8 +500,8 @@ class _ChannelMessagePathMapScreenState if (hop.hasLocation) Marker( point: hop.position!, - width: 40, - height: 40, + width: 35, + height: 35, child: Container( decoration: BoxDecoration( color: Colors.green, @@ -526,37 +526,39 @@ class _ChannelMessagePathMapScreenState ), ), ), - Marker( - point: LatLng( - context.read().selfLatitude ?? 0.0, - context.read().selfLongitude ?? 0.0, - ), - width: 40, - height: 40, - child: Container( - decoration: BoxDecoration( - color: Colors.blue, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + if (context.read().selfLatitude != null && + context.read().selfLongitude != null) + Marker( + point: LatLng( + context.read().selfLatitude!, + context.read().selfLongitude!, ), - alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + width: 35, + height: 35, + child: Container( + decoration: BoxDecoration( + color: Colors.teal, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), ), ), - ), ]; } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 0da99601..f522407b 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -349,6 +349,43 @@ class _MapScreenState extends State { ), ..._buildMarkers(contactsWithLocation, settings), ...sharedMarkers.map(_buildSharedMarker), + if (connector.selfLatitude != null && + connector.selfLongitude != null) + Marker( + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.teal, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), ], ), ], @@ -394,14 +431,14 @@ class _MapScreenState extends State { final marker = Marker( point: LatLng(contact.latitude!, contact.longitude!), - width: 80, - height: 80, + width: 35, + height: 35, child: GestureDetector( onTap: () => _showNodeInfo(context, contact), child: Column( children: [ Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: _getNodeColor(contact.type), shape: BoxShape.circle, @@ -417,7 +454,7 @@ class _MapScreenState extends State { child: Icon( _getNodeIcon(contact.type), color: Colors.white, - size: 24, + size: 20, ), ), ], diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 2e557a59..39de31e9 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -292,7 +292,7 @@ class _PathTraceMapScreenState extends State { ), ), ), - if (!_hasData && _noLocationErr) + if (!_hasData && !_noLocationErr) Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -364,37 +364,6 @@ class _PathTraceMapScreenState extends State { List _buildHopMarkers(List pathData) { return [ - Marker( - point: LatLng( - context.read().selfLatitude!, - context.read().selfLongitude!, - ), - width: 40, - height: 40, - child: Container( - decoration: BoxDecoration( - color: Colors.blue, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ), for (final hop in pathData) if (_traceData!.pathContacts[hop]!.hasLocation) Marker( @@ -402,9 +371,10 @@ class _PathTraceMapScreenState extends State { _traceData!.pathContacts[hop]!.latitude!, _traceData!.pathContacts[hop]!.longitude!, ), - width: 40, - height: 40, + width: 35, + height: 35, child: Container( + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.green, shape: BoxShape.circle, @@ -433,6 +403,40 @@ class _PathTraceMapScreenState extends State { ), ), ), + if (context.read().selfLatitude != null && + context.read().selfLongitude != null) + Marker( + point: LatLng( + context.read().selfLatitude!, + context.read().selfLongitude!, + ), + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), ]; } diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index f47b017d..483697f1 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -61,6 +62,19 @@ class _PathManagementDialog extends StatelessWidget { title: Text(l10n.chat_fullPath), content: SelectableText(formattedPath), actions: [ + TextButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: context.l10n.contacts_repeaterPathTrace, + path: Uint8List.fromList(pathBytes), + flipPathRound: true, + ), + ), + ), + child: Text(context.l10n.contacts_pathTrace), + ), TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.common_close), From daca42701c95a914676ddffe8b099a4cfa730896 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 8 Feb 2026 19:42:15 -0700 Subject: [PATCH 14/15] Notification rate limiting (#110) * Add notification rate limiting with privacy-safe debug logging - Add batching system to prevent notification storms (3s rate limit, 5s batch window) - Queue rapid notifications and show batch summaries - Debug logs show device names for adverts, sender/channel for messages (no content leaks) - Remove unused _maxBatchSize constant Context: Added after getting notification-flooded while evaluating RF flood management. The irony. * Update notification_service.dart I made a mistake and removed this * Add l10n support for notification strings Addresses PR #110 review feedback to use the translations system: - Add notification strings to app_en.arb (plurals for batch summary) - Update NotificationService to use lookupAppLocalizations() - Wire locale from MaterialApp to NotificationService - Regenerate localization files New strings added (English only, translations needed): - notification_activityTitle: "MeshCore Activity" - notification_messagesCount: "{count} message(s)" - notification_channelMessagesCount: "{count} channel message(s)" - notification_newNodesCount: "{count} new node(s)" - notification_newTypeDiscovered: "New {type} discovered" - notification_receivedNewMessage: "Received new message" * Add notification string translations for all supported languages Translated notification_activityTitle, notification_messagesCount, notification_channelMessagesCount, notification_newNodesCount, notification_newTypeDiscovered, and notification_receivedNewMessage to: bg, de, es, fr, it, nl, pl, pt, ru, sk, sl, sv, uk, zh Includes proper ICU plural forms for Slavic languages (few/many/other) and Slovenian dual form. * Apply dart format to notification_service.dart --------- Co-authored-by: Winston Lowe --- lib/l10n/app_bg.arb | 8 + lib/l10n/app_de.arb | 29 +++ lib/l10n/app_en.arb | 29 ++- lib/l10n/app_es.arb | 29 +++ lib/l10n/app_fr.arb | 8 + lib/l10n/app_it.arb | 8 + lib/l10n/app_localizations.dart | 36 ++++ lib/l10n/app_localizations_bg.dart | 44 ++++ lib/l10n/app_localizations_de.dart | 44 ++++ lib/l10n/app_localizations_en.dart | 44 ++++ lib/l10n/app_localizations_es.dart | 44 ++++ lib/l10n/app_localizations_fr.dart | 44 ++++ lib/l10n/app_localizations_it.dart | 44 ++++ lib/l10n/app_localizations_nl.dart | 44 ++++ lib/l10n/app_localizations_pl.dart | 50 +++++ lib/l10n/app_localizations_pt.dart | 44 ++++ lib/l10n/app_localizations_ru.dart | 50 +++++ lib/l10n/app_localizations_sk.dart | 47 +++++ lib/l10n/app_localizations_sl.dart | 50 +++++ lib/l10n/app_localizations_sv.dart | 44 ++++ lib/l10n/app_localizations_uk.dart | 50 +++++ lib/l10n/app_localizations_zh.dart | 26 +++ lib/l10n/app_nl.arb | 8 + lib/l10n/app_pl.arb | 8 + lib/l10n/app_pt.arb | 8 + lib/l10n/app_ru.arb | 8 + lib/l10n/app_sk.arb | 8 + lib/l10n/app_sl.arb | 8 + lib/l10n/app_sv.arb | 8 + lib/l10n/app_uk.arb | 8 + lib/l10n/app_zh.arb | 8 + lib/main.dart | 6 + lib/services/notification_service.dart | 267 ++++++++++++++++++++++++- untranslated.json | 128 +++++++++++- 34 files changed, 1282 insertions(+), 7 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 86ad4218..01afb0cb 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1569,6 +1569,13 @@ "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", + "notification_activityTitle": "Активност на MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}", + "notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}", + "notification_newTypeDiscovered": "Открит нов {contactType}", + "notification_receivedNewMessage": "Получено ново съобщение", + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", "settings_gpxExportAll": "Експортирай всички контакти в GPX", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX", "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!" + } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ccbdb2e9..3dcd0ca1 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1569,6 +1569,34 @@ "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", + + "notification_activityTitle": "MeshCore Aktivität", + "notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}", + "@notification_messagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}", + "@notification_channelMessagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}", + "@notification_newNodesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newTypeDiscovered": "Neuer {contactType} entdeckt", + "@notification_newTypeDiscovered": { + "placeholders": { + "contactType": {"type": "String"} + } + }, + "notification_receivedNewMessage": "Neue Nachricht empfangen", + "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", "settings_gpxExportAll": "Alle Kontakte nach GPX exportieren", "settings_gpxExportAllSubtitle": "Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.", "settings_gpxExportRepeaters": "Repeater und Raumserver nach GPX exportieren", @@ -1585,4 +1613,5 @@ "settings_gpxExportShareSubject": "meshcore-open GPX-Kartendaten exportieren", "settings_gpxExportShareText": "Kartendaten aus meshcore-open exportiert", "pathTrace_someHopsNoLocation": "Eine oder mehrere der Hopfen fehlen einen Standort!" + } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d939bea2..0c54be35 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1346,12 +1346,39 @@ "contacts_contactAdvertCopied": "Advert copied to Clipboard.", "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed.", + "notification_activityTitle": "MeshCore Activity", + "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}", + "@notification_messagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_channelMessagesCount": "{count} {count, plural, =1{channel message} other{channel messages}}", + "@notification_channelMessagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newNodesCount": "{count} {count, plural, =1{new node} other{new nodes}}", + "@notification_newNodesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newTypeDiscovered": "New {contactType} discovered", + "@notification_newTypeDiscovered": { + "placeholders": { + "contactType": {"type": "String"} + } + }, + "notification_receivedNewMessage": "Received new message", + "settings_gpxExportRepeaters": "Export repeaters / room server to GPX", "settings_gpxExportRepeatersSubtitle": "Exports repeaters / roomserver with a location to GPX file.", "settings_gpxExportContacts": "Export companions to GPX", "settings_gpxExportContactsSubtitle": "Exports companions with a location to GPX file.", "settings_gpxExportAll": "Export all contacts to GPX", - "settings_gpxExportAllSubtitle": "Exports all contacts with a location to GPX file.", + "settings_gpxExportAllSubtitle": "Exports all contacts with a location to GPX file.", "settings_gpxExportSuccess": "Successfully exported GPX file.", "settings_gpxExportNoContacts": "No contacts to export.", "settings_gpxExportNotAvailable": "Not supported on your device/OS", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 049d6a9d..3d5ab639 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1569,6 +1569,34 @@ "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", + + "notification_activityTitle": "Actividad de MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}", + "@notification_messagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}", + "@notification_channelMessagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}", + "@notification_newNodesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newTypeDiscovered": "Nuevo {contactType} descubierto", + "@notification_newTypeDiscovered": { + "placeholders": { + "contactType": {"type": "String"} + } + }, + "notification_receivedNewMessage": "Nuevo mensaje recibido", + "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", @@ -1585,4 +1613,5 @@ "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación" + } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 293a30ee..044b8060 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1569,6 +1569,13 @@ "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", + "notification_activityTitle": "Activité MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{message de canal} other{messages de canal}}", + "notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}", + "notification_newTypeDiscovered": "Nouveau {contactType} découvert", + "notification_receivedNewMessage": "Nouveau message reçu", + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", "settings_gpxExportNoContacts": "Aucun contact à exporter.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", "pathTrace_someHopsNoLocation": "Une ou plusieurs des houblons manquent d'une localisation !" + } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 74c92057..dd9c3730 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1569,6 +1569,13 @@ "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", + "notification_activityTitle": "Attività MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}", + "notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}", + "notification_newTypeDiscovered": "Nuovo {contactType} scoperto", + "notification_receivedNewMessage": "Nuovo messaggio ricevuto", + "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", "settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX", "settings_gpxExportContacts": "Esporta compagni in GPX", "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX", "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!" + } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b86882b3..8f4d693e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4862,6 +4862,42 @@ abstract class AppLocalizations { /// **'Copying advert to Clipboard failed.'** String get contacts_contactAdvertCopyFailed; + /// No description provided for @notification_activityTitle. + /// + /// In en, this message translates to: + /// **'MeshCore Activity'** + String get notification_activityTitle; + + /// No description provided for @notification_messagesCount. + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{message} other{messages}}'** + String notification_messagesCount(int count); + + /// No description provided for @notification_channelMessagesCount. + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{channel message} other{channel messages}}'** + String notification_channelMessagesCount(int count); + + /// No description provided for @notification_newNodesCount. + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{new node} other{new nodes}}'** + String notification_newNodesCount(int count); + + /// No description provided for @notification_newTypeDiscovered. + /// + /// In en, this message translates to: + /// **'New {contactType} discovered'** + String notification_newTypeDiscovered(String contactType); + + /// No description provided for @notification_receivedNewMessage. + /// + /// In en, this message translates to: + /// **'Received new message'** + String get notification_receivedNewMessage; + /// No description provided for @settings_gpxExportRepeaters. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 05279490..68e821e3 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2771,6 +2771,50 @@ class AppLocalizationsBg extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Копирането на обявата в клипборда не успя.'; + @override + String get notification_activityTitle => 'Активност на MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'съобщения', + one: 'съобщение', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'съобщения в канали', + one: 'съобщение в канал', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'нови възли', + one: 'нов възел', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Открит нов $contactType'; + } + + @override + String get notification_receivedNewMessage => 'Получено ново съобщение'; + @override String get settings_gpxExportRepeaters => 'Експортиране на повтарящи се устройства / сървър на стаята до GPX'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f66a37e1..8ad7f1eb 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2778,6 +2778,50 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.'; + @override + String get notification_activityTitle => 'MeshCore Aktivität'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Nachrichten', + one: 'Nachricht', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Kanalnachrichten', + one: 'Kanalnachricht', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'neue Knoten', + one: 'neuer Knoten', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Neuer $contactType entdeckt'; + } + + @override + String get notification_receivedNewMessage => 'Neue Nachricht empfangen'; + @override String get settings_gpxExportRepeaters => 'Repeater und Raumserver nach GPX exportieren'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b0315a88..8eb76e82 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2728,6 +2728,50 @@ class AppLocalizationsEn extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Copying advert to Clipboard failed.'; + @override + String get notification_activityTitle => 'MeshCore Activity'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messages', + one: 'message', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'channel messages', + one: 'channel message', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'new nodes', + one: 'new node', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'New $contactType discovered'; + } + + @override + String get notification_receivedNewMessage => 'Received new message'; + @override String get settings_gpxExportRepeaters => 'Export repeaters / room server to GPX'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c08c67a6..cc9bff79 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2771,6 +2771,50 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Copiar anuncio al Portapapeles ha fallado.'; + @override + String get notification_activityTitle => 'Actividad de MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'mensajes', + one: 'mensaje', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'mensajes de canal', + one: 'mensaje de canal', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nuevos nodos', + one: 'nuevo nodo', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nuevo $contactType descubierto'; + } + + @override + String get notification_receivedNewMessage => 'Nuevo mensaje recibido'; + @override String get settings_gpxExportRepeaters => 'Exportar repetidores / servidor de sala a GPX'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 0241b3b4..474d528d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2792,6 +2792,50 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'La copie de l\'annonce vers le presse-papiers a échoué.'; + @override + String get notification_activityTitle => 'Activité MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messages', + one: 'message', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messages de canal', + one: 'message de canal', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nouveaux nœuds', + one: 'nouveau nœud', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nouveau $contactType découvert'; + } + + @override + String get notification_receivedNewMessage => 'Nouveau message reçu'; + @override String get settings_gpxExportRepeaters => 'Exporter les répéteurs / serveur de salle au format GPX'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index e758ea43..68465ddf 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2774,6 +2774,50 @@ class AppLocalizationsIt extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Copia dell\'annuncio nella Clipboard non riuscita.'; + @override + String get notification_activityTitle => 'Attività MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messaggi', + one: 'messaggio', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messaggi del canale', + one: 'messaggio del canale', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nuovi nodi', + one: 'nuovo nodo', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nuovo $contactType scoperto'; + } + + @override + String get notification_receivedNewMessage => 'Nuovo messaggio ricevuto'; + @override String get settings_gpxExportRepeaters => 'Esporta ripetitori / server di stanza in GPX'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index f081a20d..a093a35b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2763,6 +2763,50 @@ class AppLocalizationsNl extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopiëren van advertentie naar Clipboard is mislukt.'; + @override + String get notification_activityTitle => 'MeshCore Activiteit'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'berichten', + one: 'bericht', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'kanaalberichten', + one: 'kanaalbericht', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nieuwe knooppunten', + one: 'nieuw knooppunt', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nieuw $contactType ontdekt'; + } + + @override + String get notification_receivedNewMessage => 'Nieuw bericht ontvangen'; + @override String get settings_gpxExportRepeaters => 'Exporteer repeaters / roomserver naar GPX'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 04b959cf..895e3c76 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2771,6 +2771,56 @@ class AppLocalizationsPl extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopiowanie ogłoszenia do schowka nie powiodło się.'; + @override + String get notification_activityTitle => 'Aktywność MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'wiadomości', + many: 'wiadomości', + few: 'wiadomości', + one: 'wiadomość', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'wiadomości kanału', + many: 'wiadomości kanału', + few: 'wiadomości kanału', + one: 'wiadomość kanału', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nowych węzłów', + many: 'nowych węzłów', + few: 'nowe węzły', + one: 'nowy węzeł', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nowy $contactType wykryty'; + } + + @override + String get notification_receivedNewMessage => 'Otrzymano nową wiadomość'; + @override String get settings_gpxExportRepeaters => 'Eksportuj powtórki / serwer pokojowy do GPX'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index de4adf58..ce8d07c4 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2773,6 +2773,50 @@ class AppLocalizationsPt extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Cópia do anúncio para a Área de Transferência falhou.'; + @override + String get notification_activityTitle => 'Atividade MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'mensagens', + one: 'mensagem', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'mensagens de canal', + one: 'mensagem de canal', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'novos nós', + one: 'novo nó', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Novo $contactType descoberto'; + } + + @override + String get notification_receivedNewMessage => 'Nova mensagem recebida'; + @override String get settings_gpxExportRepeaters => 'Exportar repetidores / servidor de sala para GPX'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index e3050afd..9e4cd958 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2778,6 +2778,56 @@ class AppLocalizationsRu extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Копирование рекламы в буфер обмена не удалось.'; + @override + String get notification_activityTitle => 'Активность MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'сообщений', + many: 'сообщений', + few: 'сообщения', + one: 'сообщение', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'сообщений канала', + many: 'сообщений канала', + few: 'сообщения канала', + one: 'сообщение канала', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'новых узлов', + many: 'новых узлов', + few: 'новых узла', + one: 'новый узел', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Обнаружен новый $contactType'; + } + + @override + String get notification_receivedNewMessage => 'Получено новое сообщение'; + @override String get settings_gpxExportRepeaters => 'Экспортировать рипитеры / сервер комнаты в GPX'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 745b3fd4..ed66f978 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2757,6 +2757,53 @@ class AppLocalizationsSk extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopírovanie inzerátu do schránky zlyhalo.'; + @override + String get notification_activityTitle => 'Aktivita MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'správ', + few: 'správy', + one: 'správa', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'správ kanálu', + few: 'správy kanálu', + one: 'správa kanálu', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nových uzlov', + few: 'nové uzly', + one: 'nový uzol', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nový $contactType objavený'; + } + + @override + String get notification_receivedNewMessage => 'Prijatá nová správa'; + @override String get settings_gpxExportRepeaters => 'Exportovať repeater / server miestnosti do GPX'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 70b28399..3307547c 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2759,6 +2759,56 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopiranje oglasa v odložišče je spodletelo.'; + @override + String get notification_activityTitle => 'Aktivnost MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'sporočil', + few: 'sporočila', + two: 'sporočili', + one: 'sporočilo', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'sporočil kanala', + few: 'sporočila kanala', + two: 'sporočili kanala', + one: 'sporočilo kanala', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'novih vozlišč', + few: 'nova vozlišča', + two: 'novi vozlišči', + one: 'novo vozlišče', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Odkrito novo $contactType'; + } + + @override + String get notification_receivedNewMessage => 'Prejeto novo sporočilo'; + @override String get settings_gpxExportRepeaters => 'Izvoz ponoviteljev / strežnika sobe v GPX'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index f930dcb1..5239b067 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2744,6 +2744,50 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopiering av annons till Urklipp misslyckades.'; + @override + String get notification_activityTitle => 'MeshCore Aktivitet'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'meddelanden', + one: 'meddelande', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'kanalmeddelanden', + one: 'kanalmeddelande', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nya noder', + one: 'ny nod', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Ny $contactType upptäckt'; + } + + @override + String get notification_receivedNewMessage => 'Nytt meddelande mottaget'; + @override String get settings_gpxExportRepeaters => 'Exportera repeater / rumsservrar till GPX'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index e48563d2..b6ff8ce5 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2784,6 +2784,56 @@ class AppLocalizationsUk extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Копіювання оголошення в буфер обміну завершилося невдало'; + @override + String get notification_activityTitle => 'Активність MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'повідомлень', + many: 'повідомлень', + few: 'повідомлення', + one: 'повідомлення', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'повідомлень каналу', + many: 'повідомлень каналу', + few: 'повідомлення каналу', + one: 'повідомлення каналу', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'нових вузлів', + many: 'нових вузлів', + few: 'нових вузли', + one: 'новий вузол', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Виявлено новий $contactType'; + } + + @override + String get notification_receivedNewMessage => 'Отримано нове повідомлення'; + @override String get settings_gpxExportRepeaters => 'Експортувати ретранслятори / сервер кімнати до GPX'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 51d3463e..a529a1bf 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2625,6 +2625,32 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => '将广告复制到剪贴板操作失败。'; + @override + String get notification_activityTitle => 'MeshCore 活动'; + + @override + String notification_messagesCount(int count) { + return '$count 条消息'; + } + + @override + String notification_channelMessagesCount(int count) { + return '$count 条频道消息'; + } + + @override + String notification_newNodesCount(int count) { + return '$count 个新节点'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return '发现新 $contactType'; + } + + @override + String get notification_receivedNewMessage => '收到新消息'; + @override String get settings_gpxExportRepeaters => '导出重复器/房间服务器到GPX'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index fece9686..91163ac0 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1569,6 +1569,13 @@ "contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContactZeroHop": "Contact delen via advertentie", "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", + "notification_activityTitle": "MeshCore Activiteit", + "notification_messagesCount": "{count} {count, plural, =1{bericht} other{berichten}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{kanaalbericht} other{kanaalberichten}}", + "notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}", + "notification_newTypeDiscovered": "Nieuw {contactType} ontdekt", + "notification_receivedNewMessage": "Nieuw bericht ontvangen", + "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren", "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!" + } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index a5b4925a..3c2a96fc 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1569,6 +1569,13 @@ "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", "contacts_ShareContact": "Kopiuj kontakt do schowka", "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", + "notification_activityTitle": "Aktywność MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomości} many{wiadomości} other{wiadomości}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanału} few{wiadomości kanału} many{wiadomości kanału} other{wiadomości kanału}}", + "notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}", + "notification_newTypeDiscovered": "Nowy {contactType} wykryty", + "notification_receivedNewMessage": "Otrzymano nową wiadomość", + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", "settings_gpxExportContacts": "Eksportuj towarzyszy do GPX", "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open", "settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open", "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!" + } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6c260b97..dc38c115 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1569,6 +1569,13 @@ "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", + "notification_activityTitle": "Atividade MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{mensagem} other{mensagens}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{mensagem de canal} other{mensagens de canal}}", + "notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}", + "notification_newTypeDiscovered": "Novo {contactType} descoberto", + "notification_receivedNewMessage": "Nova mensagem recebida", + "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX", "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", "settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX", "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!" + } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 8ab90e7e..ddbbe79d 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -809,6 +809,13 @@ "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", + "notification_activityTitle": "Активность MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{сообщение} few{сообщения} many{сообщений} other{сообщений}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{сообщение канала} few{сообщения канала} many{сообщений канала} other{сообщений канала}}", + "notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}", + "notification_newTypeDiscovered": "Обнаружен новый {contactType}", + "notification_receivedNewMessage": "Получено новое сообщение", + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", "settings_gpxExportContacts": "Экспортировать спутников в GPX", @@ -825,4 +832,5 @@ "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX", "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!" + } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 99881383..c09502a3 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1569,6 +1569,13 @@ "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", "contacts_ShareContact": "Kopírovať kontakt do schránky", + "notification_activityTitle": "Aktivita MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}", + "notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}", + "notification_newTypeDiscovered": "Nový {contactType} objavený", + "notification_receivedNewMessage": "Prijatá nová správa", + "contacts_ShareContact": "Kopírovať kontakt do schránky", "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", "settings_gpxExportContacts": "Export sprievodcov do GPX", "settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov", "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!" + } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 6392dff6..97a396a1 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1569,6 +1569,13 @@ "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", "contacts_ShareContact": "Kopiraj stik v Odložišče", + "notification_activityTitle": "Aktivnost MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}", + "notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}", + "notification_newTypeDiscovered": "Odkrito novo {contactType}", + "notification_receivedNewMessage": "Prejeto novo sporočilo", + "contacts_ShareContact": "Kopiraj stik v Odložišče", "settings_gpxExportAll": "Izvozi vse kontakte v GPX", "settings_gpxExportContacts": "Izvoz spremljevalcev v GPX", "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", @@ -1585,4 +1592,5 @@ "settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu", "settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte", "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!" + } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 08ed323e..6df28bdf 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1569,6 +1569,13 @@ "contacts_ShareContact": "Kopiera kontakt till Urklipp", "contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.", "contacts_ShareContactZeroHop": "Dela kontakt via annons", + "notification_activityTitle": "MeshCore Aktivitet", + "notification_messagesCount": "{count} {count, plural, =1{meddelande} other{meddelanden}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{kanalmeddelande} other{kanalmeddelanden}}", + "notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}", + "notification_newTypeDiscovered": "Ny {contactType} upptäckt", + "notification_receivedNewMessage": "Nytt meddelande mottaget", + "contacts_ShareContactZeroHop": "Dela kontakt via annons", "settings_gpxExportAll": "Exportera alla kontakter till GPX", "settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.", "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata", "settings_gpxExportShareText": "Kartdata exporterad från meshcore-open", "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!" + } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 2cea864b..9f5e64d2 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1569,6 +1569,13 @@ "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "notification_activityTitle": "Активність MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}", + "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", + "notification_newTypeDiscovered": "Виявлено новий {contactType}", + "notification_receivedNewMessage": "Отримано нове повідомлення", + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", @@ -1585,4 +1592,5 @@ "settings_gpxExportAllContacts": "Усі місця контактів", "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!" + } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c6b27f5a..8c65510a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1569,6 +1569,13 @@ "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", + "notification_activityTitle": "MeshCore 活动", + "notification_messagesCount": "{count} 条消息", + "notification_channelMessagesCount": "{count} 条频道消息", + "notification_newNodesCount": "{count} 个新节点", + "notification_newTypeDiscovered": "发现新 {contactType}", + "notification_receivedNewMessage": "收到新消息", + "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", "settings_gpxExportRepeaters": "导出重复器/房间服务器到GPX", "settings_gpxExportRepeatersSubtitle": "导出带有位置的重复器/房间服务器到GPX文件。", "settings_gpxExportContactsSubtitle": "导出带有位置的伙伴到GPX文件。", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "来自meshcore-open的导出地图数据", "settings_gpxExportShareSubject": "meshcore-open GPX 地图数据导出", "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!" + } diff --git a/lib/main.dart b/lib/main.dart index 96a853dd..8ee0ca47 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -150,6 +150,12 @@ class MeshCoreApp extends StatelessWidget { themeMode: _themeModeFromSetting( settingsService.settings.themeMode, ), + builder: (context, child) { + // Update notification service with resolved locale + final locale = Localizations.localeOf(context); + NotificationService().setLocale(locale); + return child ?? const SizedBox.shrink(); + }, home: const ScannerScreen(), ); }, diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index d835d077..57331aa2 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,6 +1,10 @@ +import 'dart:ui'; + import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; +import '../l10n/app_localizations.dart'; + class NotificationService { static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; @@ -10,6 +14,34 @@ class NotificationService { FlutterLocalNotificationsPlugin(); bool _isInitialized = false; + // Locale for localized notification strings + Locale _locale = const Locale('en'); + + /// Set the locale for notification strings (call when app locale changes) + void setLocale(Locale locale) { + _locale = locale; + } + + AppLocalizations get _l10n => lookupAppLocalizations(_locale); + + // Rate limiting to prevent notification storms + // (Added after getting notification-flooded while evaluating RF flood management. The irony.) + static const _minNotificationInterval = Duration(seconds: 3); + static const _batchWindow = Duration(seconds: 5); + + DateTime? _lastNotificationTime; + final List<_PendingNotification> _pendingNotifications = []; + bool _isBatchingActive = false; + bool _suppressNotifications = false; + + /// Temporarily suppress all notifications (e.g., during sync) + void suppressNotifications(bool suppress) { + _suppressNotifications = suppress; + if (suppress) { + _pendingNotifications.clear(); + } + } + Future initialize() async { if (_isInitialized) return; @@ -76,7 +108,7 @@ class NotificationService { return true; } - Future showMessageNotification({ + Future _showMessageNotificationImpl({ required String contactName, required String message, String? contactId, @@ -125,7 +157,7 @@ class NotificationService { ); } - Future showAdvertNotification({ + Future _showAdvertNotificationImpl({ required String contactName, required String contactType, String? contactId, @@ -163,14 +195,14 @@ class NotificationService { await _notifications.show( contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - 'New $contactType discovered', + _l10n.notification_newTypeDiscovered(contactType), contactName, notificationDetails, payload: 'advert:$contactId', ); } - Future showChannelMessageNotification({ + Future _showChannelMessageNotificationImpl({ required String channelName, required String message, int? channelIndex, @@ -211,7 +243,9 @@ class NotificationService { ); final preview = message.trim(); - final body = preview.isEmpty ? 'Received new message' : preview; + final body = preview.isEmpty + ? _l10n.notification_receivedNewMessage + : preview; await _notifications.show( channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, @@ -222,6 +256,21 @@ class NotificationService { ); } + /// Returns a privacy-safe identifier for debug logging. + /// - advert: shows device name (body contains contactName) + /// - message: shows "from: sender" (avoids logging message content) + /// - channelMessage: shows "in: channel" (avoids logging message content) + String _getNotificationIdentifier(_PendingNotification n) { + switch (n.type) { + case _NotificationType.advert: + return n.body; + case _NotificationType.message: + return 'from: ${n.title}'; + case _NotificationType.channelMessage: + return 'in: ${n.title}'; + } + } + void _onNotificationTapped(NotificationResponse response) { final payload = response.payload; if (payload != null) { @@ -238,4 +287,212 @@ class NotificationService { Future cancel(int id) async { await _notifications.cancel(id); } + + // ───────────────────────────────────────────────────────────────── + // Public notification methods (rate limiting is enforced automatically) + // ───────────────────────────────────────────────────────────────── + + Future showMessageNotification({ + required String contactName, + required String message, + String? contactId, + int? badgeCount, + }) async { + if (_suppressNotifications) return; + + _queueNotification( + _PendingNotification( + type: _NotificationType.message, + title: contactName, + body: message, + id: contactId, + badgeCount: badgeCount, + ), + ); + } + + Future showAdvertNotification({ + required String contactName, + required String contactType, + String? contactId, + }) async { + if (_suppressNotifications) return; + + _queueNotification( + _PendingNotification( + type: _NotificationType.advert, + title: contactType, + body: contactName, + id: contactId, + ), + ); + } + + Future showChannelMessageNotification({ + required String channelName, + required String message, + int? channelIndex, + int? badgeCount, + }) async { + if (_suppressNotifications) return; + + _queueNotification( + _PendingNotification( + type: _NotificationType.channelMessage, + title: channelName, + body: message, + id: channelIndex?.toString(), + badgeCount: badgeCount, + ), + ); + } + + void _queueNotification(_PendingNotification notification) { + final now = DateTime.now(); + + // If we recently showed a notification, start batching + if (_lastNotificationTime != null && + now.difference(_lastNotificationTime!) < _minNotificationInterval) { + _pendingNotifications.add(notification); + debugPrint( + '[Notification] queued: ${notification.type.name} (${_getNotificationIdentifier(notification)})', + ); + + // Start batch timer if not already running + if (!_isBatchingActive) { + _isBatchingActive = true; + Future.delayed(_batchWindow, _processBatch); + } + return; + } + + // Show immediately if enough time has passed + debugPrint( + '[Notification] sent immediately: ${notification.type.name} (${_getNotificationIdentifier(notification)})', + ); + _showNotificationImmediately(notification); + _lastNotificationTime = now; + } + + Future _processBatch() async { + _isBatchingActive = false; + + if (_pendingNotifications.isEmpty) return; + + final batch = List<_PendingNotification>.from(_pendingNotifications); + _pendingNotifications.clear(); + + if (batch.length == 1) { + // Single notification, show normally + _showNotificationImmediately(batch.first); + } else { + // Multiple notifications, show summary + await _showBatchSummary(batch); + } + + _lastNotificationTime = DateTime.now(); + } + + Future _showNotificationImmediately( + _PendingNotification notification, + ) async { + switch (notification.type) { + case _NotificationType.message: + await _showMessageNotificationImpl( + contactName: notification.title, + message: notification.body, + contactId: notification.id, + badgeCount: notification.badgeCount, + ); + break; + case _NotificationType.advert: + await _showAdvertNotificationImpl( + contactName: notification.body, + contactType: notification.title, + contactId: notification.id, + ); + break; + case _NotificationType.channelMessage: + await _showChannelMessageNotificationImpl( + channelName: notification.title, + message: notification.body, + channelIndex: int.tryParse(notification.id ?? ''), + badgeCount: notification.badgeCount, + ); + break; + } + } + + Future _showBatchSummary(List<_PendingNotification> batch) async { + if (!_isInitialized) await initialize(); + + // Group by type + final messages = batch + .where((n) => n.type == _NotificationType.message) + .toList(); + final adverts = batch + .where((n) => n.type == _NotificationType.advert) + .toList(); + final channelMsgs = batch + .where((n) => n.type == _NotificationType.channelMessage) + .toList(); + + // Build summary text using localized plurals + final parts = []; + if (messages.isNotEmpty) { + parts.add(_l10n.notification_messagesCount(messages.length)); + } + if (channelMsgs.isNotEmpty) { + parts.add(_l10n.notification_channelMessagesCount(channelMsgs.length)); + } + if (adverts.isNotEmpty) { + parts.add(_l10n.notification_newNodesCount(adverts.length)); + } + + if (parts.isEmpty) return; + + // Show first few device names in batch summary for debugging (only if adverts exist) + final deviceInfo = adverts.isNotEmpty + ? ' (${adverts.take(5).map((n) => n.body).join(', ')}${adverts.length > 5 ? ', ...' : ''})' + : ''; + debugPrint('[Notification] batch summary: ${parts.join(", ")}$deviceInfo'); + + const androidDetails = AndroidNotificationDetails( + 'batch_summary', + 'Activity Summary', + channelDescription: 'Batched notification summaries', + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + icon: '@mipmap/ic_launcher', + ); + + const notificationDetails = NotificationDetails(android: androidDetails); + + await _notifications.show( + 'batch_summary'.hashCode, + _l10n.notification_activityTitle, + parts.join(', '), + notificationDetails, + payload: 'batch', + ); + } +} + +// Helper class for pending notifications +enum _NotificationType { message, advert, channelMessage } + +class _PendingNotification { + final _NotificationType type; + final String title; + final String body; + final String? id; + final int? badgeCount; + + _PendingNotification({ + required this.type, + required this.title, + required this.body, + this.id, + this.badgeCount, + }); } diff --git a/untranslated.json b/untranslated.json index 9e26dfee..e0ad904b 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,127 @@ -{} \ No newline at end of file +{ + "bg": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "de": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "es": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "fr": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "it": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "nl": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "pl": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "pt": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "ru": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "sk": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "sl": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "sv": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "uk": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "zh": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ] +} From 87a2807f5b94af06ae8c2c0c56284a19a6ad91a5 Mon Sep 17 00:00:00 2001 From: 446564 Date: Sun, 8 Feb 2026 18:56:24 -0800 Subject: [PATCH 15/15] chore: update version to alpha 6 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6312ee37..6474c5f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 5.0.0+5 +version: 5.0.0+6 environment: sdk: ^3.9.2