From 0047ee0c8c64435b156687360f25e87bc0a3e1c1 Mon Sep 17 00:00:00 2001 From: Strycher Date: Thu, 13 Aug 2026 03:13:53 -0400 Subject: [PATCH] feat(#574): stock config export screen Mirrors stock's Export Config screen: section list, Select All and Deselect All, everything checked by default. The asymmetry with import, which will start with nothing checked, is stock's own and is deliberate: opt out on the way out, opt in on the way in. A section the user ticked that could not be gathered is shown before the file is written, with its specific reason, and the user chooses whether to export anyway. Firmware-built-without-it reads differently from device-did-not-answer because only one of them is worth retrying. The screen also states plainly that this is the stock format and cannot carry Offband-only data, so importing the file back will not restore it. File naming follows stock, _meshcore_config_.json, and is sanitized for the filesystem without touching the name written inside the file, which has to round-trip exactly. Real device names in the reference corpus contain emoji and a trailing space, both covered by tests. Writing reuses LogExport for the per-platform mechanism (share sheet on mobile, Save As on desktop, download on web) rather than adding a second export path that could drift. The temp file can contain the node private key, so it is written under the app's own temp directory. The public key is abbreviated on screen; the private key is never rendered. 6 tests on the file name. --- lib/l10n/app_en.arb | 124 ++++++ lib/l10n/app_localizations.dart | 174 ++++++++ lib/l10n/app_localizations_bg.dart | 107 +++++ lib/l10n/app_localizations_de.dart | 107 +++++ lib/l10n/app_localizations_en.dart | 107 +++++ lib/l10n/app_localizations_es.dart | 107 +++++ lib/l10n/app_localizations_fr.dart | 107 +++++ lib/l10n/app_localizations_hu.dart | 107 +++++ lib/l10n/app_localizations_it.dart | 107 +++++ lib/l10n/app_localizations_ja.dart | 107 +++++ lib/l10n/app_localizations_ko.dart | 107 +++++ lib/l10n/app_localizations_nl.dart | 107 +++++ lib/l10n/app_localizations_pl.dart | 107 +++++ lib/l10n/app_localizations_pt.dart | 107 +++++ lib/l10n/app_localizations_ru.dart | 107 +++++ lib/l10n/app_localizations_sk.dart | 107 +++++ lib/l10n/app_localizations_sl.dart | 107 +++++ lib/l10n/app_localizations_sv.dart | 107 +++++ lib/l10n/app_localizations_uk.dart | 107 +++++ lib/l10n/app_localizations_zh.dart | 107 +++++ lib/screens/export_config_screen.dart | 310 +++++++++++++++ lib/utils/stock_config_file.dart | 70 ++++ test/utils/stock_config_file_test.dart | 56 +++ untranslated.json | 527 ++++++++++++++++++++++++- 24 files changed, 3170 insertions(+), 17 deletions(-) create mode 100644 lib/screens/export_config_screen.dart create mode 100644 lib/utils/stock_config_file.dart create mode 100644 test/utils/stock_config_file_test.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f261cfe..7d49eb2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2820,5 +2820,129 @@ "batteryOptimizationOpenFailed": "Couldn't open battery settings", "@batteryOptimizationOpenFailed": { "description": "Snackbar shown when the battery-optimization settings screen could not be opened (#443)." + }, + "stockConfig_selectAll": "Select All", + "@stockConfig_selectAll": { + "description": "Button selecting every section on the stock config export/import screens (#568)." + }, + "stockConfig_deselectAll": "Deselect All", + "@stockConfig_deselectAll": { + "description": "Button clearing every section on the stock config export/import screens (#568)." + }, + "stockConfig_sectionName": "Name", + "@stockConfig_sectionName": { + "description": "Config section holding the device's display name (#568)." + }, + "stockConfig_sectionIdentity": "Private Identity Key", + "@stockConfig_sectionIdentity": { + "description": "Config section holding the node's key pair (#568)." + }, + "stockConfig_sectionRadio": "Radio Settings", + "@stockConfig_sectionRadio": { + "description": "Config section holding LoRa radio parameters (#568)." + }, + "stockConfig_sectionPosition": "Position Settings", + "@stockConfig_sectionPosition": { + "description": "Config section holding the device's advertised position (#568)." + }, + "stockConfig_sectionOther": "Other Settings", + "@stockConfig_sectionOther": { + "description": "Config section holding miscellaneous device preferences (#568)." + }, + "stockConfig_sectionAutoAdd": "Auto Add Settings", + "@stockConfig_sectionAutoAdd": { + "description": "Config section holding automatic contact-add preferences (#568)." + }, + "stockConfig_sectionChannels": "Channels ({count})", + "@stockConfig_sectionChannels": { + "description": "Config section holding channels, with how many there are (#568).", + "placeholders": { "count": { "type": "int" } } + }, + "stockConfig_sectionContacts": "Contacts ({count})", + "@stockConfig_sectionContacts": { + "description": "Config section holding contacts, with how many there are (#568).", + "placeholders": { "count": { "type": "int" } } + }, + "stockConfig_identityWarning": "Your private key must be kept secret. It should only be exported as a backup.", + "@stockConfig_identityWarning": { + "description": "Warning shown beside the identity section when exporting (#568)." + }, + "stockConfig_publicKeyLabel": "Public Key: {key}", + "@stockConfig_publicKeyLabel": { + "description": "Abbreviated public key shown under the identity section (#568).", + "placeholders": { "key": { "type": "String" } } + }, + "stockConfig_privateKeyHidden": "Private Key: hidden", + "@stockConfig_privateKeyHidden": { + "description": "Placeholder shown instead of the private key, which is never displayed (#568)." + }, + "exportConfig_title": "Export Config", + "@exportConfig_title": { + "description": "Title of the stock config export screen (#574)." + }, + "exportConfig_instruction": "Please select data to export.", + "@exportConfig_instruction": { + "description": "Banner instructing the user to pick sections to export (#574)." + }, + "exportConfig_allChannels": "All channels will be exported.", + "@exportConfig_allChannels": { + "description": "Note that channels export as a whole, with no per-channel choice (#574)." + }, + "exportConfig_allContacts": "All contacts will be exported.", + "@exportConfig_allContacts": { + "description": "Note that contacts export as a whole, with no per-contact choice (#574)." + }, + "exportConfig_nothingSelected": "Select at least one section to export.", + "@exportConfig_nothingSelected": { + "description": "Shown when the user tries to export with every section unchecked (#574)." + }, + "exportConfig_lossyNotice": "This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.", + "@exportConfig_lossyNotice": { + "description": "Explains that an export drops Offband-only contact fields (#574)." + }, + "exportConfig_omittedTitle": "Some sections could not be exported", + "@exportConfig_omittedTitle": { + "description": "Title of the dialog listing sections that could not be gathered (#574)." + }, + "exportConfig_omittedUnsupported": "{section}: this radio's firmware was built without it.", + "@exportConfig_omittedUnsupported": { + "description": "Reason line when firmware lacks the feature entirely, so retrying cannot help (#574).", + "placeholders": { "section": { "type": "String" } } + }, + "exportConfig_omittedNoReply": "{section}: the device did not respond.", + "@exportConfig_omittedNoReply": { + "description": "Reason line when the device did not answer in time (#574).", + "placeholders": { "section": { "type": "String" } } + }, + "exportConfig_omittedRejected": "{section}: the device refused the request.", + "@exportConfig_omittedRejected": { + "description": "Reason line when the device refused the request (#574).", + "placeholders": { "section": { "type": "String" } } + }, + "exportConfig_omittedUnavailable": "{section}: the device has not reported this yet.", + "@exportConfig_omittedUnavailable": { + "description": "Reason line when the value is not known to the app yet (#574).", + "placeholders": { "section": { "type": "String" } } + }, + "exportConfig_exportAnyway": "Export anyway", + "@exportConfig_exportAnyway": { + "description": "Confirms writing the file even though some sections are missing (#574)." + }, + "exportConfig_cancel": "Cancel", + "@exportConfig_cancel": { + "description": "Dismisses the export without writing a file (#574)." + }, + "exportConfig_shareSubject": "MeshCore config", + "@exportConfig_shareSubject": { + "description": "Subject line used when sharing an exported config file (#574)." + }, + "exportConfig_failed": "Export failed: {message}", + "@exportConfig_failed": { + "description": "Snackbar shown when writing the export file failed (#574).", + "placeholders": { "message": { "type": "String" } } + }, + "exportConfig_notConnected": "Connect to a device before exporting.", + "@exportConfig_notConnected": { + "description": "Shown when the export screen has no connected radio to read from (#574)." } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e65dafa..7b3d36a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -8451,6 +8451,180 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Couldn\'t open battery settings'** String get batteryOptimizationOpenFailed; + + /// Button selecting every section on the stock config export/import screens (#568). + /// + /// In en, this message translates to: + /// **'Select All'** + String get stockConfig_selectAll; + + /// Button clearing every section on the stock config export/import screens (#568). + /// + /// In en, this message translates to: + /// **'Deselect All'** + String get stockConfig_deselectAll; + + /// Config section holding the device's display name (#568). + /// + /// In en, this message translates to: + /// **'Name'** + String get stockConfig_sectionName; + + /// Config section holding the node's key pair (#568). + /// + /// In en, this message translates to: + /// **'Private Identity Key'** + String get stockConfig_sectionIdentity; + + /// Config section holding LoRa radio parameters (#568). + /// + /// In en, this message translates to: + /// **'Radio Settings'** + String get stockConfig_sectionRadio; + + /// Config section holding the device's advertised position (#568). + /// + /// In en, this message translates to: + /// **'Position Settings'** + String get stockConfig_sectionPosition; + + /// Config section holding miscellaneous device preferences (#568). + /// + /// In en, this message translates to: + /// **'Other Settings'** + String get stockConfig_sectionOther; + + /// Config section holding automatic contact-add preferences (#568). + /// + /// In en, this message translates to: + /// **'Auto Add Settings'** + String get stockConfig_sectionAutoAdd; + + /// Config section holding channels, with how many there are (#568). + /// + /// In en, this message translates to: + /// **'Channels ({count})'** + String stockConfig_sectionChannels(int count); + + /// Config section holding contacts, with how many there are (#568). + /// + /// In en, this message translates to: + /// **'Contacts ({count})'** + String stockConfig_sectionContacts(int count); + + /// Warning shown beside the identity section when exporting (#568). + /// + /// In en, this message translates to: + /// **'Your private key must be kept secret. It should only be exported as a backup.'** + String get stockConfig_identityWarning; + + /// Abbreviated public key shown under the identity section (#568). + /// + /// In en, this message translates to: + /// **'Public Key: {key}'** + String stockConfig_publicKeyLabel(String key); + + /// Placeholder shown instead of the private key, which is never displayed (#568). + /// + /// In en, this message translates to: + /// **'Private Key: hidden'** + String get stockConfig_privateKeyHidden; + + /// Title of the stock config export screen (#574). + /// + /// In en, this message translates to: + /// **'Export Config'** + String get exportConfig_title; + + /// Banner instructing the user to pick sections to export (#574). + /// + /// In en, this message translates to: + /// **'Please select data to export.'** + String get exportConfig_instruction; + + /// Note that channels export as a whole, with no per-channel choice (#574). + /// + /// In en, this message translates to: + /// **'All channels will be exported.'** + String get exportConfig_allChannels; + + /// Note that contacts export as a whole, with no per-contact choice (#574). + /// + /// In en, this message translates to: + /// **'All contacts will be exported.'** + String get exportConfig_allContacts; + + /// Shown when the user tries to export with every section unchecked (#574). + /// + /// In en, this message translates to: + /// **'Select at least one section to export.'** + String get exportConfig_nothingSelected; + + /// Explains that an export drops Offband-only contact fields (#574). + /// + /// In en, this message translates to: + /// **'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'** + String get exportConfig_lossyNotice; + + /// Title of the dialog listing sections that could not be gathered (#574). + /// + /// In en, this message translates to: + /// **'Some sections could not be exported'** + String get exportConfig_omittedTitle; + + /// Reason line when firmware lacks the feature entirely, so retrying cannot help (#574). + /// + /// In en, this message translates to: + /// **'{section}: this radio\'s firmware was built without it.'** + String exportConfig_omittedUnsupported(String section); + + /// Reason line when the device did not answer in time (#574). + /// + /// In en, this message translates to: + /// **'{section}: the device did not respond.'** + String exportConfig_omittedNoReply(String section); + + /// Reason line when the device refused the request (#574). + /// + /// In en, this message translates to: + /// **'{section}: the device refused the request.'** + String exportConfig_omittedRejected(String section); + + /// Reason line when the value is not known to the app yet (#574). + /// + /// In en, this message translates to: + /// **'{section}: the device has not reported this yet.'** + String exportConfig_omittedUnavailable(String section); + + /// Confirms writing the file even though some sections are missing (#574). + /// + /// In en, this message translates to: + /// **'Export anyway'** + String get exportConfig_exportAnyway; + + /// Dismisses the export without writing a file (#574). + /// + /// In en, this message translates to: + /// **'Cancel'** + String get exportConfig_cancel; + + /// Subject line used when sharing an exported config file (#574). + /// + /// In en, this message translates to: + /// **'MeshCore config'** + String get exportConfig_shareSubject; + + /// Snackbar shown when writing the export file failed (#574). + /// + /// In en, this message translates to: + /// **'Export failed: {message}'** + String exportConfig_failed(String message); + + /// Shown when the export screen has no connected radio to read from (#574). + /// + /// In en, this message translates to: + /// **'Connect to a device before exporting.'** + String get exportConfig_notConnected; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 80e3466..d8b7832 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -4966,4 +4966,111 @@ class AppLocalizationsBg extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index fce03a9..818444a 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -4983,4 +4983,111 @@ class AppLocalizationsDe extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f8ecb85..150db01 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -4889,4 +4889,111 @@ class AppLocalizationsEn extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 4d82563..2942543 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -4971,4 +4971,111 @@ class AppLocalizationsEs extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ee474ca..aebcbcc 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -4999,4 +4999,111 @@ class AppLocalizationsFr extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index ec9b32c..6e5f857 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -4988,4 +4988,111 @@ class AppLocalizationsHu extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 4aaf0e2..cb04d0c 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -4976,4 +4976,111 @@ class AppLocalizationsIt extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 256d8eb..c990893 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -4742,4 +4742,111 @@ class AppLocalizationsJa extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 16cfb15..2fb163a 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -4743,4 +4743,111 @@ class AppLocalizationsKo extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e3ea207..16329c4 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -4951,4 +4951,111 @@ class AppLocalizationsNl extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index b3f47e7..2970d68 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -4988,4 +4988,111 @@ class AppLocalizationsPl extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 65a2002..877118a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -4964,4 +4964,111 @@ class AppLocalizationsPt extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 926ee26..1b11c28 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -4982,4 +4982,111 @@ class AppLocalizationsRu extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 74549d2..a1f93a7 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -4948,4 +4948,111 @@ class AppLocalizationsSk extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index a4e3abe..9783058 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -4946,4 +4946,111 @@ class AppLocalizationsSl extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 4badf0b..5093f3a 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -4921,4 +4921,111 @@ class AppLocalizationsSv extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 7b68e19..92c72ae 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -4983,4 +4983,111 @@ class AppLocalizationsUk extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 0206520..772b857 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -4616,4 +4616,111 @@ class AppLocalizationsZh extends AppLocalizations { @override String get batteryOptimizationOpenFailed => 'Couldn\'t open battery settings'; + + @override + String get stockConfig_selectAll => 'Select All'; + + @override + String get stockConfig_deselectAll => 'Deselect All'; + + @override + String get stockConfig_sectionName => 'Name'; + + @override + String get stockConfig_sectionIdentity => 'Private Identity Key'; + + @override + String get stockConfig_sectionRadio => 'Radio Settings'; + + @override + String get stockConfig_sectionPosition => 'Position Settings'; + + @override + String get stockConfig_sectionOther => 'Other Settings'; + + @override + String get stockConfig_sectionAutoAdd => 'Auto Add Settings'; + + @override + String stockConfig_sectionChannels(int count) { + return 'Channels ($count)'; + } + + @override + String stockConfig_sectionContacts(int count) { + return 'Contacts ($count)'; + } + + @override + String get stockConfig_identityWarning => + 'Your private key must be kept secret. It should only be exported as a backup.'; + + @override + String stockConfig_publicKeyLabel(String key) { + return 'Public Key: $key'; + } + + @override + String get stockConfig_privateKeyHidden => 'Private Key: hidden'; + + @override + String get exportConfig_title => 'Export Config'; + + @override + String get exportConfig_instruction => 'Please select data to export.'; + + @override + String get exportConfig_allChannels => 'All channels will be exported.'; + + @override + String get exportConfig_allContacts => 'All contacts will be exported.'; + + @override + String get exportConfig_nothingSelected => + 'Select at least one section to export.'; + + @override + String get exportConfig_lossyNotice => + 'This is the MeshCore stock format, so it cannot carry Offband-only data such as path overrides. Importing this file back into Offband will not restore that data.'; + + @override + String get exportConfig_omittedTitle => 'Some sections could not be exported'; + + @override + String exportConfig_omittedUnsupported(String section) { + return '$section: this radio\'s firmware was built without it.'; + } + + @override + String exportConfig_omittedNoReply(String section) { + return '$section: the device did not respond.'; + } + + @override + String exportConfig_omittedRejected(String section) { + return '$section: the device refused the request.'; + } + + @override + String exportConfig_omittedUnavailable(String section) { + return '$section: the device has not reported this yet.'; + } + + @override + String get exportConfig_exportAnyway => 'Export anyway'; + + @override + String get exportConfig_cancel => 'Cancel'; + + @override + String get exportConfig_shareSubject => 'MeshCore config'; + + @override + String exportConfig_failed(String message) { + return 'Export failed: $message'; + } + + @override + String get exportConfig_notConnected => + 'Connect to a device before exporting.'; } diff --git a/lib/screens/export_config_screen.dart b/lib/screens/export_config_screen.dart new file mode 100644 index 0000000..b8c6662 --- /dev/null +++ b/lib/screens/export_config_screen.dart @@ -0,0 +1,310 @@ +/// Stock-compatible config export (#574, epic #568). +/// +/// Mirrors the stock app's Export Config screen: a list of sections, Select All +/// and Deselect All, and **everything checked by default**. The asymmetry with +/// the import screen (which starts with nothing checked) is deliberate and +/// copied from stock: opt out on the way out, opt in on the way in. +library; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; +import '../services/stock_config_export_service.dart'; +import '../utils/stock_config_file.dart'; +import '../widgets/adaptive_app_bar_title.dart'; + +class ExportConfigScreen extends StatefulWidget { + const ExportConfigScreen({super.key}); + + @override + State createState() => _ExportConfigScreenState(); +} + +class _ExportConfigScreenState extends State { + /// Every section starts selected, matching stock. + final Set _selected = StockConfigSection.values.toSet(); + bool _busy = false; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final connector = context.watch(); + + return Scaffold( + appBar: AppBar( + title: AdaptiveAppBarTitle(l10n.exportConfig_title), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.check), + onPressed: _busy ? null : () => _export(connector), + tooltip: l10n.exportConfig_title, + ), + ], + ), + body: ListView( + children: [ + _Banner(text: l10n.exportConfig_instruction), + _Banner(text: l10n.exportConfig_lossyNotice, subdued: true), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () => + setState(() => _selected.addAll(StockConfigSection.values)), + child: Text(l10n.stockConfig_selectAll), + ), + TextButton( + onPressed: () => setState(_selected.clear), + child: Text(l10n.stockConfig_deselectAll), + ), + ], + ), + for (final section in StockConfigSection.values) + _SectionTile( + title: sectionLabel(context, section, connector), + subtitle: _subtitleFor(section, connector), + selected: _selected.contains(section), + onChanged: (value) => setState(() { + if (value) { + _selected.add(section); + } else { + _selected.remove(section); + } + }), + ), + ], + ), + ); + } + + String? _subtitleFor( + StockConfigSection section, + MeshCoreConnector connector, + ) { + final l10n = context.l10n; + switch (section) { + case StockConfigSection.name: + return connector.selfName; + case StockConfigSection.identity: + final key = connector.selfPublicKeyHex; + final abbreviated = key.length > 12 + ? '${key.substring(0, 6)}...${key.substring(key.length - 6)}' + : key; + // The private key is never rendered, not even abbreviated. + return '${l10n.stockConfig_identityWarning}\n' + '${l10n.stockConfig_publicKeyLabel(abbreviated)}\n' + '${l10n.stockConfig_privateKeyHidden}'; + case StockConfigSection.radioSettings: + final freq = connector.currentFreqHz; + final bw = connector.currentBwHz; + if (freq == null || bw == null) return null; + // currentFreqHz is kHz despite its name; currentBwHz really is Hz. + return '${l10n.settings_frequency}: ${freq / 1000} MHz\n' + '${l10n.settings_bandwidth}: ${bw / 1000} kHz\n' + '${l10n.settings_spreadingFactor}: ${connector.currentSf}\n' + '${l10n.settings_codingRate}: ${connector.currentCr}\n' + '${l10n.settings_txPower}: ${connector.currentTxPower}'; + case StockConfigSection.positionSettings: + return '${connector.selfLatitude ?? 0}, ${connector.selfLongitude ?? 0}'; + case StockConfigSection.otherSettings: + return null; + case StockConfigSection.autoAddSettings: + return null; + case StockConfigSection.channels: + return l10n.exportConfig_allChannels; + case StockConfigSection.contacts: + return l10n.exportConfig_allContacts; + } + } + + Future _export(MeshCoreConnector connector) async { + final l10n = context.l10n; + final messenger = ScaffoldMessenger.of(context); + + if (!connector.isConnected) { + messenger.showSnackBar( + SnackBar(content: Text(l10n.exportConfig_notConnected)), + ); + return; + } + if (_selected.isEmpty) { + messenger.showSnackBar( + SnackBar(content: Text(l10n.exportConfig_nothingSelected)), + ); + return; + } + + setState(() => _busy = true); + try { + final result = await StockConfigExportService( + connector, + ).build(sections: Set.of(_selected)); + + // A section the user asked for that could not be gathered is surfaced + // before the file is written, never dropped quietly. + if (!result.isComplete) { + if (!mounted) return; + final proceed = await _confirmOmissions(result); + if (proceed != true) return; + } + if (!mounted) return; + + await saveStockConfigFile( + context, + json: result.config.encode(), + fileName: stockConfigFileName(connector.selfName, DateTime.now()), + subject: l10n.exportConfig_shareSubject, + ); + } catch (e) { + messenger.showSnackBar( + SnackBar(content: Text(l10n.exportConfig_failed('$e'))), + ); + } finally { + if (mounted) setState(() => _busy = false); + } + } + + Future _confirmOmissions(StockConfigExportResult result) { + final l10n = context.l10n; + final connector = context.read(); + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.exportConfig_omittedTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final entry in result.omitted.entries) + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + _omissionText( + context, + sectionLabel(context, entry.key, connector), + entry.value, + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.exportConfig_cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.exportConfig_exportAnyway), + ), + ], + ), + ); + } +} + +String _omissionText( + BuildContext context, + String section, + StockConfigOmission reason, +) { + final l10n = context.l10n; + switch (reason) { + case StockConfigOmission.unsupported: + return l10n.exportConfig_omittedUnsupported(section); + case StockConfigOmission.noReply: + return l10n.exportConfig_omittedNoReply(section); + case StockConfigOmission.rejected: + return l10n.exportConfig_omittedRejected(section); + case StockConfigOmission.unavailable: + return l10n.exportConfig_omittedUnavailable(section); + } +} + +/// Section titles, shared with the import screen so the two cannot drift. +String sectionLabel( + BuildContext context, + StockConfigSection section, + MeshCoreConnector connector, +) { + final l10n = context.l10n; + switch (section) { + case StockConfigSection.name: + return l10n.stockConfig_sectionName; + case StockConfigSection.identity: + return l10n.stockConfig_sectionIdentity; + case StockConfigSection.radioSettings: + return l10n.stockConfig_sectionRadio; + case StockConfigSection.positionSettings: + return l10n.stockConfig_sectionPosition; + case StockConfigSection.otherSettings: + return l10n.stockConfig_sectionOther; + case StockConfigSection.autoAddSettings: + return l10n.stockConfig_sectionAutoAdd; + case StockConfigSection.channels: + return l10n.stockConfig_sectionChannels( + connector.channels.where((c) => !c.isEmpty).length, + ); + case StockConfigSection.contacts: + return l10n.stockConfig_sectionContacts(connector.contacts.length); + } +} + +class _SectionTile extends StatelessWidget { + const _SectionTile({ + required this.title, + required this.selected, + required this.onChanged, + this.subtitle, + }); + + final String title; + final String? subtitle; + final bool selected; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return CheckboxListTile( + value: selected, + onChanged: (value) => onChanged(value ?? false), + controlAffinity: ListTileControlAffinity.trailing, + isThreeLine: subtitle != null, + title: Text(title), + subtitle: subtitle == null ? null : Text(subtitle!), + ); + } +} + +class _Banner extends StatelessWidget { + const _Banner({required this.text, this.subdued = false}); + + final String text; + final bool subdued; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + width: double.infinity, + color: theme.colorScheme.surfaceContainerHighest, + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, size: 18, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Expanded( + child: Text( + text, + style: subdued ? theme.textTheme.bodySmall : null, + ), + ), + ], + ), + ); + } +} diff --git a/lib/utils/stock_config_file.dart b/lib/utils/stock_config_file.dart new file mode 100644 index 0000000..b21d887 --- /dev/null +++ b/lib/utils/stock_config_file.dart @@ -0,0 +1,70 @@ +/// Naming and writing of stock-compatible config files (#574, epic #568). +library; + +import 'dart:io' show File; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'log_export.dart'; +import 'log_web_download.dart'; + +/// Characters Windows forbids in a file name, plus the path separators. +final _illegalFileNameChars = RegExp(r'[<>:"/\\|?*\x00-\x1F]'); + +/// Builds the file name stock uses: `_meshcore_config_`. +/// +/// Real device names contain emoji and, in at least one observed case, a +/// trailing space, so the name is sanitized for the filesystem here. The name +/// written *inside* the file is never touched: it must round-trip exactly. +String stockConfigFileName(String? deviceName, DateTime at) { + final safeName = _sanitizeForFileName(deviceName ?? ''); + final stamp = [ + at.year.toString().padLeft(4, '0'), + at.month.toString().padLeft(2, '0'), + at.day.toString().padLeft(2, '0'), + ].join('-'); + final time = [ + at.hour.toString().padLeft(2, '0'), + at.minute.toString().padLeft(2, '0'), + at.second.toString().padLeft(2, '0'), + ].join(); + final prefix = safeName.isEmpty ? '' : '${safeName}_'; + return '${prefix}meshcore_config_$stamp-$time.json'; +} + +String _sanitizeForFileName(String name) { + final cleaned = name.replaceAll(_illegalFileNameChars, ''); + // Windows also rejects names ending in a space or a dot. + return cleaned.replaceAll(RegExp(r'[ .]+$'), '').trim(); +} + +/// Writes [json] out as [fileName], using each platform's normal mechanism: +/// the share sheet on mobile, a Save As dialog on desktop, a browser download +/// on web. Reuses [LogExport], which already owns that per-platform logic, so +/// there is only one place where export behavior can drift. +Future saveStockConfigFile( + BuildContext context, { + required String json, + required String fileName, + String? subject, +}) async { + if (kIsWeb) { + downloadTextFile(fileName, json); + return; + } + // A temp file is the handoff format LogExport works in. It holds the node's + // private key when the identity section was included, so it is written under + // the app's own temp directory and never anywhere shared. + final dir = await getTemporaryDirectory(); + final file = File('${dir.path}/$fileName'); + await file.writeAsString(json, flush: true); + if (!context.mounted) return; + await LogExport.shareFile( + context, + file, + subject: subject, + fileName: fileName, + ); +} diff --git a/test/utils/stock_config_file_test.dart b/test/utils/stock_config_file_test.dart new file mode 100644 index 0000000..ad798a0 --- /dev/null +++ b/test/utils/stock_config_file_test.dart @@ -0,0 +1,56 @@ +// #574 (epic #568): the export file name has to match stock's convention and +// survive real device names, which contain emoji and trailing spaces. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/utils/stock_config_file.dart'; + +void main() { + final at = DateTime(2026, 8, 12, 22, 16, 25); + + test('matches stock: name, then meshcore_config, then a stamp', () { + expect( + stockConfigFileName('Strycher-RAK-CAR', at), + 'Strycher-RAK-CAR_meshcore_config_2026-08-12-221625.json', + ); + }); + + test('single-digit months, days and times are zero padded', () { + expect( + stockConfigFileName('Node', DateTime(2026, 1, 2, 3, 4, 5)), + 'Node_meshcore_config_2026-01-02-030405.json', + ); + }); + + test('emoji in a device name are kept, as stock keeps them', () { + expect( + stockConfigFileName('Strycher T1000\u{1F6F0}️', at), + startsWith('Strycher T1000\u{1F6F0}️_meshcore_config_'), + ); + }); + + test('a trailing space is dropped, since Windows rejects it', () { + // Observed in a real export: the device name ended in a space. + expect( + stockConfigFileName('Strycher-Wio-L1 ', at), + 'Strycher-Wio-L1_meshcore_config_2026-08-12-221625.json', + ); + }); + + test('path separators and reserved characters are stripped', () { + expect( + stockConfigFileName(r'a/b\c:d*e?f"gi|j', at), + 'abcdefghij_meshcore_config_2026-08-12-221625.json', + ); + }); + + test('a device with no name still produces a usable file name', () { + expect( + stockConfigFileName(null, at), + 'meshcore_config_2026-08-12-221625.json', + ); + expect( + stockConfigFileName(' ', at), + 'meshcore_config_2026-08-12-221625.json', + ); + }); +} diff --git a/untranslated.json b/untranslated.json index b27cf49..6c84d40 100644 --- a/untranslated.json +++ b/untranslated.json @@ -143,7 +143,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "de": [ @@ -290,7 +319,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "es": [ @@ -437,7 +495,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "fr": [ @@ -584,7 +671,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "hu": [ @@ -731,7 +847,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "it": [ @@ -878,7 +1023,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "ja": [ @@ -1025,7 +1199,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "ko": [ @@ -1172,7 +1375,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "nl": [ @@ -1319,7 +1551,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "pl": [ @@ -1466,7 +1727,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "pt": [ @@ -1613,7 +1903,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "ru": [ @@ -1760,7 +2079,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "sk": [ @@ -1907,7 +2255,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "sl": [ @@ -2054,7 +2431,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "sv": [ @@ -2201,7 +2607,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "uk": [ @@ -2348,7 +2783,36 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ], "zh": [ @@ -2495,6 +2959,35 @@ "batteryOptimizationBody", "batteryOptimizationOpenSettings", "batteryOptimizationDismiss", - "batteryOptimizationOpenFailed" + "batteryOptimizationOpenFailed", + "stockConfig_selectAll", + "stockConfig_deselectAll", + "stockConfig_sectionName", + "stockConfig_sectionIdentity", + "stockConfig_sectionRadio", + "stockConfig_sectionPosition", + "stockConfig_sectionOther", + "stockConfig_sectionAutoAdd", + "stockConfig_sectionChannels", + "stockConfig_sectionContacts", + "stockConfig_identityWarning", + "stockConfig_publicKeyLabel", + "stockConfig_privateKeyHidden", + "exportConfig_title", + "exportConfig_instruction", + "exportConfig_allChannels", + "exportConfig_allContacts", + "exportConfig_nothingSelected", + "exportConfig_lossyNotice", + "exportConfig_omittedTitle", + "exportConfig_omittedUnsupported", + "exportConfig_omittedNoReply", + "exportConfig_omittedRejected", + "exportConfig_omittedUnavailable", + "exportConfig_exportAnyway", + "exportConfig_cancel", + "exportConfig_shareSubject", + "exportConfig_failed", + "exportConfig_notConnected" ] }